Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Dynamic Pricing 2.0 — Demand & Capacity: QA Test-Readiness Findings

Companion to:

OwnerQA Automation — qa-team@hungryhub.com
Source/grill-me interrogation session, 2026-07-03
Companion artifactTest Case Matrix — 43 cases, 7 categories — embedded in §7 Appendix (EN); ID version as a separate rendered artifact
StatusReconciled — §1 seat-scope contradiction resolved (aggregate time-window). §3 boundary semantics resolved ([start, end)). Ready for test-case authoring and review.

1. Resolved — seat-scope model aligned to aggregate time window

Status update (post-grilling, 2026-07-02): The knowledge-base Product PRD and Implementation Plan have been updated to the aggregate time-window model. The FE PRD has been aligned accordingly. This section is kept for audit history but the contradiction is now resolved.

SourceDatedSeat-scope claim
ClickUp PRD (“Dynamic Pricing 2.0”, Umam)2025-10-08Aggregate total capacity and total booked seats across the entire day or a specific time range (e.g., 09:00–12:00); dynamic price applies to all slots within that range.
kb Product PRD (Dynamic_Pricing_Capacity_Demand_20260701150000.md)2026-07-02 (updated)Per time window — capacity is aggregated across all 15-minute slots whose start_time falls within the rule’s configured time window.
kb FE PRD (FE_Dynamic_Pricing_Capacity_Demand_20260703093706.md)2026-07-03 (updated)scope (time) = per time window (aggregate across 15-minute slots).
kb Implementation Plan (PricingEngine/017-CAPACITY-DEMAND-IMPLEMENTATION-PLAN.md)2026-07-02 (updated)Kafka events carry full state per 15-minute slot; pricing-engine aggregates across the rule window for evaluation and applies the price to API slots whose start_time lies inside the window.

Canonical model:

  • Capacity is evaluated per rule time window (e.g., 09:00–12:00).
  • The window includes all 15-minute inventory slots whose start_time satisfies start_time >= window_start AND start_time < window_end ([start, end)).
  • The relevant capacity metric is summed across those 15-minute slots.
  • If the aggregate meets the rule threshold, the price adjustment applies to every API slot whose start_time lies inside the window.

Why this matters for QA:

  • Category B (exclusive-end-time boundary) is now valid and testable under the [start, end) semantics.
  • Category A scenarios built on the ClickUp PRD’s full-day/time-range examples (A1, A4, A6) are now the correct oracle.
  • Test cases should assert that all slots inside the matched window share the same adjusted price, while slots outside remain at base price.

2. v1 scope is narrower than either PRD implies

Per the FE PRD’s own “Team decision (2026-07-03),” v1 ships with:

In v1Deferred out of v1
total_seats_booked (absolute) onlyremaining_seats
Relations: >, >=, =, <, <=, betweenpct_seats_booked / pct_seats_remaining
Single threshold per ruleMulti-tier Action/Subaction structure
is_anytime / open-ended threshold semantics

This means the Test Case Matrix’s A4–A7 (Remaining Seats, % Seats Booked) and D1–D3 (multi-tier action selection) target v2 scope. They’re not wrong to have written, but shouldn’t be prioritized for the first round of execution — testing them against a v1 build produces false failures for functionality that was never meant to exist yet.

Recommendation: re-tag the matrix’s P1 cases against confirmed v1 scope before execution starts, so effort isn’t spent validating features that aren’t shipping first.

3. New defect found while reading the FE PRD

Not from this session’s original review — surfaced while cross-checking scope for this write-up.

hh-pegasus/src/components/search_filter/TimePickerButton2.vue:21 applies the low-price-font (green) class to every available slot, regardless of whether the price is actually below base price. Once Demand & Capacity rules go live, a normal slot and a surcharged slot render identically — both green — which is actively misleading to the diner. The FE PRD proposes two fix directions (engine-emitted isLowPrice/isHighPrice flag, or FE-computed comparison against basePrice) but leaves the choice open pending the backend’s final response shape.

Recommendation: file as its own ticket, independent of the Demand & Capacity rollout — it’s a pre-existing bug this feature will expose, not one it introduces. Add an explicit visual-regression case (three states: unpriced / low-price / high-demand) once the fix direction is chosen.

4. Findings carried over from the interrogation

FindingStatus
Rule type hidden (show: false) and payload serializer unimplemented in book-biteVerified fact — now confirmed as a tracked FE to-do in FE PRD §3.1, not an oversight
Condition-evaluation logic owned by the external pricing-engine service, not hh-serverVerified fact
Zero automated test coverage for demand_capacity in hh-server todayVerified fact
Boundary at exactly 12:00 (“Exclusive End Times”)Resolved — Product PRD and Implementation Plan confirm [start, end) semantics: a slot whose start_time == window_end is excluded. Test case B3 should assert exclusion.
Creator can approve their own rule (no segregation of duties)Open — flagged to business
5 unspecified edge cases in checkout price-lock (exact expiry, refresh-restarts-lock, concurrent seat hold, abandonment, payment-gateway delay)Open, unresolved
Race-condition coverage for concurrent bookings crossing a thresholdOpen — harness designed, execution blocked on pricing-engine staging access

5. Open questions for the team

Numbering continues from the FE PRD’s own §8, since these compound on top of it.

  1. Which seat-scope model is correct — aggregated day/time-range (ClickUp PRD) or per-slot independent (both kb PRDs)? RESOLVED: aggregate time-window model, documented in the updated Product PRD and Implementation Plan. Category A/B can proceed.
  2. 12:00 boundary inclusivity RESOLVED: [start, end) semantics documented in Product PRD, Implementation Plan, and FE PRD. Test case B3 asserts exclusion.
  3. Composes-with-date/time relationship — documented in Product PRD §“Composition with Date & Time Rules”: within a Demand & Capacity rule, date_time_conditions_attributes gates eligibility and capacity threshold gates firing (AND); across separate rule records, pricing-engine returns up to 2 rules per slot (1 percentage + 1 fixed) by priority/value, then hh-pegasus getDynamicPricingV2Rule picks exactly one (biggest discount, or smallest increase). Not full multiplicative stacking. Pending formal BE/FE sign-off.
  4. isLowPrice/isHighPrice ownership (engine-emitted vs. FE-computed) — affects both the bug fix in §3 and any QA test on price-direction styling. FE PRD §5.2 proposes FE-computed as default; pending backend response shape confirmation.
  5. Wire-format mismatches — documented in FE PRD §6 as a canonical contract: book-bite maps internal greater_than/less_than/… to gt/lt/… and raw percent/currency to basis-points/cents at the API boundary. Pending BE + FE sign-off before integration testing is meaningful.
  • ClickUp PRD: “Dynamic Pricing 2.0” (Umam, 2025-10-08)
  • knowledge-base PR #292 — “docs(pricing): add Capacity & Demand PRD for Dynamic Pricing 2.0” (draft)
    • src/PRD/Dynamic_Pricing_Capacity_Demand_20260701150000.md
    • src/PRD/FE_Dynamic_Pricing_Capacity_Demand_20260703093706.md
  • PricingEngine/013-DEMAND-CAPACITY-CONDITIONS.md
  • Test Case Matrix (this session) — EN / ID artifacts

7. Appendix — Test Case Matrix (43 cases, 7 categories)

Every expected result below cites its ClickUp PRD page so the oracle stays auditable. The §1 seat-scope conflict is now resolved (aggregate time-window). Category A rows A1, A4, A6 and all of Category B should be verified against the documented [start, end) aggregate-window semantics before being treated as final. A4–A7 and D1–D3 target v2 scope per §2 and should not block v1 sign-off.

A · Condition type & relation evaluation

Four condition types crossed with six relation operators, with explicit boundary cases at the threshold itself.

IDCaseSetupExpected resultPriority
A1Total Seats Booked, full-day, > threshold — metJan 1–2, all day · > 50 pax · +20%51 pax booked → price = 120 THB for all Jan 1 slots (PRD p.29, Scenario A)P1
A2Total Seats Booked, exact threshold — not metsame rule · pax = 50 exactly“>” is strict — 50 is not > 50 → price stays 100 THBP1
A3Total Seats Booked, “greater than or equal” at exact thresholdrelation = ≥ 50 · pax = 50 exactlycondition met → price adjusted (confirms ≥ behaves differently from > at the boundary)P1
A4Remaining Seats, full-day, < threshold — metcapacity 100/day · < 20 remaining · +30%81 booked → 19 remaining → price = 130 THB (PRD p.30, Scenario B)P1
A5Remaining Seats, exact threshold — not met80 booked → 20 remaining exactly“<” is strict — 20 is not < 20 → base price holdsP1
A6% Seats Booked, time-range, > threshold — met17:00–20:00 · 3 slots × 50 = 150 cap · > 80% · +25%121/150 = 80.67% → price = 125 THB (PRD p.30, Scenario D)P1
A7% Seats Booked, exact 80% — not met120/150 booked exactly80.0% is not > 80% → base price holdsP1
A8Equal To — no false positive on adjacent valuescondition = Total Seats Booked equal to 5049 and 51 both leave condition unmet; only exactly 50 firesP2
A9Between — inclusive lower boundcondition: Total Seats Between 10 and 20 · actual = 10met — “Between” is inclusive on both ends (PRD p.28)P1
A10Between — inclusive upper boundsame rule · actual = 20met, by the same inclusivity ruleP1
A11Between — just outside both sidesactual = 9, then actual = 21neither fires the ruleP2
A12Between — invalid range at creationsecond number ≤ first number, e.g. Between 20 and 10form blocks save: “The second number must be greater than the first.” (PRD p.28)P2

B · Time-window boundary (exclusive end time)

✅ Per §1 above, the aggregate time-window model is now confirmed. Test cases below assume [start, end) boundary semantics and 15-minute slot granularity.

IDCaseSetupExpected resultStatus
B1Booking at window startrange 09:00–12:00 · booking at 09:00:00included in aggregationReady
B2Booking at last valid minuterange 09:00–12:00 · booking at 11:59included — last aggregated minute per PRD (PRD p.27, “Exclusive End Times”)Ready
B3Booking at exact end timerange 09:00–12:00 · booking at 12:00:00 exactlyexcluded — slot whose start_time == window_end is outside [start, end) per Product PRD, Implementation Plan, and FE PRD.Ready
B4Booking just past the windowbooking at 12:01excluded (control case, uncontested)Ready
B5Booking just before the windowbooking at 08:59excludedReady

C · Real-time re-evaluation & data integrity

IDCaseSetupExpected resultPriority
C1New booking crosses threshold mid-dayrule at >50 pax · running total 49 → new booking of 2 paxprice updates immediately on the next read, no delay/cache lag (PRD p.29)P1
C2Cancellation drops total below thresholdrunning total 52 (rule active) → cancel a 3-pax booking → 49price reverts immediately to the previous/base price (PRD p.48, “Cancellation & Price Revert Behavior”)P1
C3Already-confirmed booking is unaffected by a later revertguest confirmed at the dynamic (120 THB) price → a different booking cancels, dropping the rule below thresholdthe already-confirmed booking keeps its locked 120 THB; only new bookings after the revert see the reverted price (PRD p.48, “New Bookings Only”)P1
C4Manual allotment change recalculates remaining-seats rulesadmin increases daily capacity mid-day on a “Remaining Seats < 20” ruleremaining seats recalculated against new capacity; rule may de-activate if no longer below threshold (PRD p.29)P2
C5Concurrent bookings jointly cross the thresholdrunning total 49 · two bookings of 1 pax each submitted within the same second, from two different sessionsboth bookings persist, final count is exactly 51, and the rule fires exactly once — no lost update, no double-fire, no stale readP1 · non-functional
C6Concurrent cancel + book racing at the thresholdtotal = 50 (rule active, threshold >49) · Request A cancels 1 pax, Request B books 1 pax, submitted within the same secondfinal state is deterministic from the actual final DB total (50, unchanged) regardless of which request’s evaluation runs firstP1 · non-functional
C7Timing sweep, not a single runrepeat C5 with submission gaps of 0ms, 50ms, 500ms, ×30 iterations eachfailure rate across iterations is tracked and reported, not a single pass/fail — a race that only shows up 1-in-30 runs is still a real defectP1 · non-functional

Concurrency harness (how C5/C6/C7 actually get built):

  • Why manual QA can’t do this — clicking “Book” twice in two browser tabs does not produce true concurrency; network jitter alone spreads requests by tens of milliseconds, usually enough for the race to never trigger.
  • Approach — script the booking-creation request directly (bypass the UI) with a load tool that supports precise scheduling (k6 or Gatling). Fire N parallel POST requests at the reservation-creation endpoint for the same package/date/time-window, sized so no single request crosses the threshold alone but the sum does. Confirm the exact endpoint and payload shape with backend first.
  • Isolation — every run needs a fresh, disposable date/package combination; reusing one test restaurant’s live counters makes results non-reproducible.
  • Assertions per run — (1) final aggregated pax count equals the arithmetic sum of submitted bookings; (2) Price Change History shows exactly one activated action, not a duplicate; (3) a price read immediately after all requests resolve reflects the correct post-threshold price; (4) any booking already checkout-locked before the threshold was crossed keeps its locked price (Category G).
  • Blocked on — same blocker as the rest of this matrix: evaluation runs inside the external pricing-engine service. This harness can only point at a real environment once a staging/contract endpoint is confirmed with engineering.

D · Multiple action tiers within one rule

Targets v2 scope per §2 — the FE PRD’s v1 team decision explicitly defers the multi-tier Action/Subaction structure. Keep these written but do not block v1 sign-off on them.

IDCaseSetupExpected resultPriority
D1Two tiers both technically match — closer tier winsItem 1: Total Seats 10 (Jan 1–30, +100%) · Item 2: Total Seats 20 (Jan 15–30, +50%) · actual = 20Item 2 applies (+50%), not Item 1, even though 20 ≥ 10 also matches Item 1 (PRD p.21, worked example)P1
D2Only the lower tier matchessame two items · actual = 12 (below Item 2’s Jan-15 start, or below its threshold)Item 1 applies (+100%)P2
D3Neither tier matchesactual = 5, below both thresholdsbase price holds, no action recorded in Price Change HistoryP2

E · Rule stacking & conflict validation

IDCaseSetupExpected resultPriority
E1Same metric, overlapping range — blocked at creationtwo Demand & Capacity rules, both on “Total Seats Booked”, overlapping datessave is blocked; modal “Duplicate rule detected” (PRD p.25)P1
E2Different metric, overlapping range — allowedone rule on “Total Seats Booked”, another on “% Seats Remaining”, same overlapping datesboth save successfully, no conflict errorP2
E3Cross-type conflict at runtime — only priority winner firesa Demand & Capacity rule and a Past Performance-Based rule both match the same booking at the same moment; Demand & Capacity has higher priorityonly the Demand & Capacity action applies; the other is fully suppressed, not blended (PRD p.27, “Rule Stacking”)P1
E4Rule’s own range exceeds the parent General Setting periodGeneral Setting: Jan 1–31 · rule’s “When” block: Feb 1–15non-blocking warning: “Some rules are outside the main period” with Save anyway / Cancel (PRD p.25–26)P2

F · Approval workflow

IDCaseSetupExpected resultPriority
F1Auto-approve activates immediately on creationcreate rule, Approval = Auto-approverule is live the moment it’s saved — no pending state (PRD p.20)P1
F2Auto-approve rule is visible in both placessame rule, condition later met by real bookingsrule appears in the Pricing Rule table, action appears directly in Price Change HistoryP1
F3Require-approval rule stays out of the pricing rule tablecreate rule, Approval = Always require approvalrule does not appear in the Pricing Rule list; only the pending action shows in Alert (PRD p.20)P1
F4Approve confirms and activatesclick Approve → confirm in modalremoved from Pending list, toast “approved successfully”, entry shown in Price Change History (PRD p.5)P1
F5Reject discards permanentlyclick Reject → confirm in modalremoved from Pending list, toast “rejected”, price change never applied, shown as Rejected in history (PRD p.5)P1
F6Cancel on either modal is a true no-opopen Approve modal, then Reject modal, click Cancel on eachmodal closes, item stays in Pending Approval unchanged in both casesP2
F7Multiple qualifying actions shown individually, not mergeda require-approval rule with two action tiers, both triggered on different dates in the same windoweach action appears as its own line item (PRD p.20, “show them 1 on 1”)P1
F8Creator can approve their own rulesame staff account creates a require-approval rule, then opens the pending queueper written PRD, currently allowed for all roles including the creator (PRD p.20) — confirm intended, else flag as a segregation-of-duties gap for the businessFlag to business
F9Approving an action that now exceeds the Global Price Limitrule proposes +40% · Global Pricing Setting max increase lowered to 20% after rule creation, before approval“Price Limit Exceeded” modal blocks the change (PRD p.6)P1
F10Notification counter — singular/plural and zero-state1 pending item, then 2, then approve/reject down to 0singular/plural phrasing correct; counter decrements each time; section hides at 0 (PRD p.4, p.10)P2

G · Price lock at checkout

Five of seven cases below are spec gaps, not testable defects — the PRD states the 10-minute lock exists but never specifies its edges. Route these to the PO/backend before writing automation against them.

IDCaseSetupExpected resultStatus
G1Price holds for the full 10-minute windowguest enters checkout at 110 THB · a new booking crosses the rule’s threshold 3 minutes laterguest still sees and can confirm at 110 THB (PRD p.48)Ready
G2Behavior at the exact 10-minute expiryguest still on checkout page at the 10:00 mark, hasn’t submitted paymentnot specified in the PRD — does the lock extend, re-fetch, or fail? Needs a written answer before pass/fail can be definedSpec gap
G3Price changes before checkout is enteredguest browsing package detail when the rule fires in the background, then proceeds to checkout“Price Changed” modal appears, old price never silently carried into checkout (PRD p.48)Ready
G4Does clicking Refresh itself start the 10-minute lock?guest clicks Refresh on the Price Changed modalnot specified — affects whether the guest gets a full fresh 10 minutes or a race window before checkoutSpec gap
G5Two guests lock the last available seats simultaneously2 seats remain · two guests both reach checkout for the same slot within the same secondnot specified — confirm whether entering checkout also provisionally holds the seat count, since that feeds this whole matrix’s Demand & Capacity aggregateSpec gap
G6Guest abandons checkout before the lock expiresguest closes the tab 2 minutes into the 10-minute locknot specified — confirm whether temporary_reservation releases immediately or only after 10 minutesSpec gap
G7Payment gateway delay pushes confirmation past the lock windowguest submits payment at 9:58 into the lock, gateway callback returns at 10:15expected to honor the locked price, consistent with “New Bookings Only” — but not explicit in the PRD; confirm with backendSpec gap

Adjacent scope — tracked, not detailed here

Sits next to condition evaluation but belongs to a separate matrix once core logic is confirmed testable: global price-limit enforcement outside the approval flow, percentage rounding behavior (“Standard Rounded (Nearest) Using the Library” — confirm which library and rounding mode), price-floor-at-zero for decrease actions, and the wire-format mismatches noted in open question 5 of §5 (relation-type naming, basis-point/cent unit conversion).