Dynamic Pricing 2.0 — Demand & Capacity: QA Test-Readiness Findings
Companion to:
- Product PRD (ClickUp): “Dynamic Pricing 2.0” (Umam, 2025-10-08)
- Product PRD (knowledge-base):
Dynamic_Pricing_Capacity_Demand_20260701150000.md- Frontend PRD (knowledge-base):
FE_Dynamic_Pricing_Capacity_Demand_20260703093706.md(PR #292, draft)- Engineering spec:
PricingEngine/013-DEMAND-CAPACITY-CONDITIONS.md
| Owner | QA Automation — qa-team@hungryhub.com |
| Source | /grill-me interrogation session, 2026-07-03 |
| Companion artifact | Test Case Matrix — 43 cases, 7 categories — embedded in §7 Appendix (EN); ID version as a separate rendered artifact |
| Status | Reconciled — §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.
| Source | Dated | Seat-scope claim |
|---|---|---|
| ClickUp PRD (“Dynamic Pricing 2.0”, Umam) | 2025-10-08 | Aggregate 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_timesatisfiesstart_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_timelies 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 v1 | Deferred out of v1 |
|---|---|
total_seats_booked (absolute) only | remaining_seats |
Relations: >, >=, =, <, <=, between | pct_seats_booked / pct_seats_remaining |
| Single threshold per rule | Multi-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
| Finding | Status |
|---|---|
Rule type hidden (show: false) and payload serializer unimplemented in book-bite | Verified 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-server | Verified fact |
Zero automated test coverage for demand_capacity in hh-server today | Verified 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 threshold | Open — 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.
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.12:00 boundary inclusivityRESOLVED:[start, end)semantics documented in Product PRD, Implementation Plan, and FE PRD. Test case B3 asserts exclusion.- Composes-with-date/time relationship — documented in Product PRD §“Composition with Date & Time Rules”: within a Demand & Capacity rule,
date_time_conditions_attributesgates 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-pegasusgetDynamicPricingV2Rulepicks exactly one (biggest discount, or smallest increase). Not full multiplicative stacking. Pending formal BE/FE sign-off. isLowPrice/isHighPriceownership (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.- Wire-format mismatches — documented in FE PRD §6 as a canonical contract: book-bite maps internal
greater_than/less_than/… togt/lt/… and raw percent/currency to basis-points/cents at the API boundary. Pending BE + FE sign-off before integration testing is meaningful.
6. Related reading
- ClickUp PRD: “Dynamic Pricing 2.0” (Umam, 2025-10-08)
knowledge-basePR #292 — “docs(pricing): add Capacity & Demand PRD for Dynamic Pricing 2.0” (draft)src/PRD/Dynamic_Pricing_Capacity_Demand_20260701150000.mdsrc/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.
| ID | Case | Setup | Expected result | Priority |
|---|---|---|---|---|
| A1 | Total Seats Booked, full-day, > threshold — met | Jan 1–2, all day · > 50 pax · +20% | 51 pax booked → price = 120 THB for all Jan 1 slots (PRD p.29, Scenario A) | P1 |
| A2 | Total Seats Booked, exact threshold — not met | same rule · pax = 50 exactly | “>” is strict — 50 is not > 50 → price stays 100 THB | P1 |
| A3 | Total Seats Booked, “greater than or equal” at exact threshold | relation = ≥ 50 · pax = 50 exactly | condition met → price adjusted (confirms ≥ behaves differently from > at the boundary) | P1 |
| A4 | Remaining Seats, full-day, < threshold — met | capacity 100/day · < 20 remaining · +30% | 81 booked → 19 remaining → price = 130 THB (PRD p.30, Scenario B) | P1 |
| A5 | Remaining Seats, exact threshold — not met | 80 booked → 20 remaining exactly | “<” is strict — 20 is not < 20 → base price holds | P1 |
| A6 | % Seats Booked, time-range, > threshold — met | 17: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 met | 120/150 booked exactly | 80.0% is not > 80% → base price holds | P1 |
| A8 | Equal To — no false positive on adjacent values | condition = Total Seats Booked equal to 50 | 49 and 51 both leave condition unmet; only exactly 50 fires | P2 |
| A9 | Between — inclusive lower bound | condition: Total Seats Between 10 and 20 · actual = 10 | met — “Between” is inclusive on both ends (PRD p.28) | P1 |
| A10 | Between — inclusive upper bound | same rule · actual = 20 | met, by the same inclusivity rule | P1 |
| A11 | Between — just outside both sides | actual = 9, then actual = 21 | neither fires the rule | P2 |
| A12 | Between — invalid range at creation | second number ≤ first number, e.g. Between 20 and 10 | form 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.
| ID | Case | Setup | Expected result | Status |
|---|---|---|---|---|
| B1 | Booking at window start | range 09:00–12:00 · booking at 09:00:00 | included in aggregation | Ready |
| B2 | Booking at last valid minute | range 09:00–12:00 · booking at 11:59 | included — last aggregated minute per PRD (PRD p.27, “Exclusive End Times”) | Ready |
| B3 | Booking at exact end time | range 09:00–12:00 · booking at 12:00:00 exactly | excluded — slot whose start_time == window_end is outside [start, end) per Product PRD, Implementation Plan, and FE PRD. | Ready |
| B4 | Booking just past the window | booking at 12:01 | excluded (control case, uncontested) | Ready |
| B5 | Booking just before the window | booking at 08:59 | excluded | Ready |
C · Real-time re-evaluation & data integrity
| ID | Case | Setup | Expected result | Priority |
|---|---|---|---|---|
| C1 | New booking crosses threshold mid-day | rule at >50 pax · running total 49 → new booking of 2 pax | price updates immediately on the next read, no delay/cache lag (PRD p.29) | P1 |
| C2 | Cancellation drops total below threshold | running total 52 (rule active) → cancel a 3-pax booking → 49 | price reverts immediately to the previous/base price (PRD p.48, “Cancellation & Price Revert Behavior”) | P1 |
| C3 | Already-confirmed booking is unaffected by a later revert | guest confirmed at the dynamic (120 THB) price → a different booking cancels, dropping the rule below threshold | the 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 |
| C4 | Manual allotment change recalculates remaining-seats rules | admin increases daily capacity mid-day on a “Remaining Seats < 20” rule | remaining seats recalculated against new capacity; rule may de-activate if no longer below threshold (PRD p.29) | P2 |
| C5 | Concurrent bookings jointly cross the threshold | running total 49 · two bookings of 1 pax each submitted within the same second, from two different sessions | both bookings persist, final count is exactly 51, and the rule fires exactly once — no lost update, no double-fire, no stale read | P1 · non-functional |
| C6 | Concurrent cancel + book racing at the threshold | total = 50 (rule active, threshold >49) · Request A cancels 1 pax, Request B books 1 pax, submitted within the same second | final state is deterministic from the actual final DB total (50, unchanged) regardless of which request’s evaluation runs first | P1 · non-functional |
| C7 | Timing sweep, not a single run | repeat C5 with submission gaps of 0ms, 50ms, 500ms, ×30 iterations each | failure 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 defect | P1 · 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-engineservice. 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.
| ID | Case | Setup | Expected result | Priority |
|---|---|---|---|---|
| D1 | Two tiers both technically match — closer tier wins | Item 1: Total Seats 10 (Jan 1–30, +100%) · Item 2: Total Seats 20 (Jan 15–30, +50%) · actual = 20 | Item 2 applies (+50%), not Item 1, even though 20 ≥ 10 also matches Item 1 (PRD p.21, worked example) | P1 |
| D2 | Only the lower tier matches | same two items · actual = 12 (below Item 2’s Jan-15 start, or below its threshold) | Item 1 applies (+100%) | P2 |
| D3 | Neither tier matches | actual = 5, below both thresholds | base price holds, no action recorded in Price Change History | P2 |
E · Rule stacking & conflict validation
| ID | Case | Setup | Expected result | Priority |
|---|---|---|---|---|
| E1 | Same metric, overlapping range — blocked at creation | two Demand & Capacity rules, both on “Total Seats Booked”, overlapping dates | save is blocked; modal “Duplicate rule detected” (PRD p.25) | P1 |
| E2 | Different metric, overlapping range — allowed | one rule on “Total Seats Booked”, another on “% Seats Remaining”, same overlapping dates | both save successfully, no conflict error | P2 |
| E3 | Cross-type conflict at runtime — only priority winner fires | a Demand & Capacity rule and a Past Performance-Based rule both match the same booking at the same moment; Demand & Capacity has higher priority | only the Demand & Capacity action applies; the other is fully suppressed, not blended (PRD p.27, “Rule Stacking”) | P1 |
| E4 | Rule’s own range exceeds the parent General Setting period | General Setting: Jan 1–31 · rule’s “When” block: Feb 1–15 | non-blocking warning: “Some rules are outside the main period” with Save anyway / Cancel (PRD p.25–26) | P2 |
F · Approval workflow
| ID | Case | Setup | Expected result | Priority |
|---|---|---|---|---|
| F1 | Auto-approve activates immediately on creation | create rule, Approval = Auto-approve | rule is live the moment it’s saved — no pending state (PRD p.20) | P1 |
| F2 | Auto-approve rule is visible in both places | same rule, condition later met by real bookings | rule appears in the Pricing Rule table, action appears directly in Price Change History | P1 |
| F3 | Require-approval rule stays out of the pricing rule table | create rule, Approval = Always require approval | rule does not appear in the Pricing Rule list; only the pending action shows in Alert (PRD p.20) | P1 |
| F4 | Approve confirms and activates | click Approve → confirm in modal | removed from Pending list, toast “approved successfully”, entry shown in Price Change History (PRD p.5) | P1 |
| F5 | Reject discards permanently | click Reject → confirm in modal | removed from Pending list, toast “rejected”, price change never applied, shown as Rejected in history (PRD p.5) | P1 |
| F6 | Cancel on either modal is a true no-op | open Approve modal, then Reject modal, click Cancel on each | modal closes, item stays in Pending Approval unchanged in both cases | P2 |
| F7 | Multiple qualifying actions shown individually, not merged | a require-approval rule with two action tiers, both triggered on different dates in the same window | each action appears as its own line item (PRD p.20, “show them 1 on 1”) | P1 |
| F8 | Creator can approve their own rule | same staff account creates a require-approval rule, then opens the pending queue | per 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 business | Flag to business |
| F9 | Approving an action that now exceeds the Global Price Limit | rule 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 |
| F10 | Notification counter — singular/plural and zero-state | 1 pending item, then 2, then approve/reject down to 0 | singular/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.
| ID | Case | Setup | Expected result | Status |
|---|---|---|---|---|
| G1 | Price holds for the full 10-minute window | guest enters checkout at 110 THB · a new booking crosses the rule’s threshold 3 minutes later | guest still sees and can confirm at 110 THB (PRD p.48) | Ready |
| G2 | Behavior at the exact 10-minute expiry | guest still on checkout page at the 10:00 mark, hasn’t submitted payment | not specified in the PRD — does the lock extend, re-fetch, or fail? Needs a written answer before pass/fail can be defined | Spec gap |
| G3 | Price changes before checkout is entered | guest 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 |
| G4 | Does clicking Refresh itself start the 10-minute lock? | guest clicks Refresh on the Price Changed modal | not specified — affects whether the guest gets a full fresh 10 minutes or a race window before checkout | Spec gap |
| G5 | Two guests lock the last available seats simultaneously | 2 seats remain · two guests both reach checkout for the same slot within the same second | not specified — confirm whether entering checkout also provisionally holds the seat count, since that feeds this whole matrix’s Demand & Capacity aggregate | Spec gap |
| G6 | Guest abandons checkout before the lock expires | guest closes the tab 2 minutes into the 10-minute lock | not specified — confirm whether temporary_reservation releases immediately or only after 10 minutes | Spec gap |
| G7 | Payment gateway delay pushes confirmation past the lock window | guest submits payment at 9:58 into the lock, gateway callback returns at 10:15 | expected to honor the locked price, consistent with “New Bookings Only” — but not explicit in the PRD; confirm with backend | Spec 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).