Skip to content

Migrated from Notion Scenario matrix — https://app.notion.com/p/3d31f8eaa3ad81eab20aea346d9cad7f

Scenario matrix

Callout Who should read: QA / engineers deciding unit vs E2E coverage for a class. Callout As of 2026-09-24 IST — Project 10 coverage status (tips: API 94f64f5, browser 4a53eab). Full audit: TEST coverage recommendations (2026-09-24). Covered: S-NOTIMESLOT (#162 Done), S-IDOR (#163 Done — orphaned from ci-quality path). Still gaps: S-GOLDEN-CREATE (#146), ADR-0008 / S-ADR-0008 (#145), concurrency-exhaust / k6 (#166–#169 packs on develop, board still Backlog). Board Done vs Open (Epics A–H): Done ≈ #129–#139, Epic A #155, Epic B #156, #162, #163. Open: Epic C #157 (#140 buyout + nightly BA Midway red), Epic D #158 (#141/#143/#144), Epic E #159 (#153/#154), Epic F #160 (#145–#152), Epic G #161 (#142 Datacap manual), Epic H #166–#169 (evidence, not new scenarios). How to use: config changes update the snapshot — they do not add rows. One smoke journey per class. Do not add a fourth smoke for another SKU in the same class. Next: Test approach · E2E features · Overview / Domain brief · TEST coverage recommendations (2026-09-24). Callout Class-bound matrix: new Outlet of an existing class = zero new tests. Config changes update the snapshot — they do not add rows.

Scenario matrix — unit vs E2E, bound to config

How a Business or ServiceItem change is handled: it does not add a row. It changes the snapshot. The same scenario runs with new numbers, or it is N/A.

class_id  ──►  oracle snapshot (live doctypes)  ──►  scenario uses snapshot fields
                    │
                    ├ duration.booking_minutes
                    ├ guests.min / max / smoke_count
                    ├ deposit.amount
                    ├ pwa.path
                    ├ shape: plan | package | buyout
                    └ schedule.live
If snapshot says Scenario
guests.min == 1 N-CLAMP-BELOW is N/A (US Plans). Still run N-CLAMP-ABOVE if max is finite
deposit.amount == 0 N-DEPOSIT is N/A; N-FULL-PAY still runs
shape == buyout S-GP smoke uses a different golden (Midway), not Buyout. N-BUYOUT is its own nightly
schedule.live == false oracle must pick another row or fail the class (catalog dead)
addons == [] N-ADDON N/A
promos_live == [] N-PROMO N/A
new Outlet, same class_id zero new rows (SYS-005)
new SKU, same shape zero new rows — stay on the golden

Unit tests use checked-in example JSON (no DEV). Karate smoke refreshes the snapshot from Frappe at run, then fails if shape drifted (buffer 30→0, duration rule changed).

Classes in play:

class_id Golden Fixture
VP-FRAPPE-PACKAGE-VENUE-DATACAP-PWA The Midway Play examples/vp-midway-play.json
US-FRAPPE-PLAN-DATACAP-PWA Super Pass (Sat+Sun) examples/us-super-pass.json
US-FRAPPE-PACKAGE-VENUE-DATACAP-PWA ULTIMATE examples/us-ultimate-package.json
FS-CE-PRODUCT-CUSTOMERWEB — blocked on Outlet ID

Unit tests (code level — pytest / Jest)

These must exist in reservation-api-server and venuplus-app. Fixtures = the three example snapshots plus a few smell rows (Buyout Monday-only, duration=0, dead Saturday 10am).

ID Name Assert Fixture / config input Lives in
U-CLASSIFY Classify item package / plan / buyout ATP PartyEvent+package; US BuyNow session; Buyout is_buyout API pytest
U-CLASS-ID Fingerprint same class_id for Boardwalk vs Midway; Plan vs Package differ ATP 4 packages; US Super Pass vs ULTIMATE API pytest
U-END-ITEM Booking end from item duration 10:30 + 120 → 12:30, not 11:00 Midway booking_duration_minutes=120, slot grid 30 API + PWA
U-END-SESSION Booking end from schedule when item duration=0 does not use 0; uses session length Super Pass / ULTIMATE booking_duration_minutes=0 API pytest
U-GRID-NE Slot grid ≠ party length grid 30 is start tile only ATP 16 starts 10:30–18:00 API + PWA
U-CLAMP Schedule min/max wins 5 → reject; 8 → 8; 30 → reject; item 1–100 ignored Buyout item 1–100 vs schedule 8–25 API pytest
U-CLAMP-US US plan guests smoke_count=1, max=1 Super Pass API pytest
U-LIVE Live schedule filter Fri Super Pass expired; Sat+Sun live; Sat 10am dead census 2026-09-02 API pytest
U-HOLD Lock window guest ± buffer ATP buffer 30; US buffer 0 API pytest
U-PATH PWA path /plans-and-games vs /plans date-first VP vs US snapshots PWA Jest
U-BUYOUT-SELECT Golden picker skip base_price=0 / immediate_pay=false Birthday Buyout vs Midway API pytest
U-ADVANCE Same-day / window past start hidden; min 1h / max 90d outlet fields API pytest
U-PROMO-100 100OFF/TEST100 100% Midway 8×17.99 → discount=baseline, total 0, skip card ATP checkout API pytest
U-PROMO-FALLBACK rejected 100OFF baseline total; deposit 15.11; block production Datacap ATP API pytest
U-PROMO-CAP cap 1000 2000 @100% → total 1000 ATP promo spec API pytest
U-PROMO-NA US no live 100% code Super Pass / ULTIMATE promos_live=[] US fixtures API pytest
U-DEPOSIT-CALC Karate deposit math $0 total → $0; flat 15.11; percent 10% of 200=20; none → full snapshot.deposit API pytest
U-PAY-ROUTE skip vs sandbox vs block 100% skip; production block; sandbox charge checkout API pytest

That is the full unit list for SYS-005 W1. Not one test per SKU.


E2E @smoke (Karate, PR gate, write when approved)

One journey per class. Parameters all from snapshot.

ID Class Steps Pass (DocType) Skip / N/A
S-GP-VP VP package catalog → live tile → cart → create → (pay later) Pending then Successful; end=start+120; guests=8; deposit/total from pricing_engine and snapshot.deposit pay step off until sandbox Datacap
S-GP-US-PLAN US plan date-first /plans → Super Pass Sat/Sun Successful; guests=1; duration rule=schedule.session Fri Super Pass
S-GP-US-PKG US package same PWA, ULTIMATE Thu–Sun Successful; guests in 1–40; deposit 50 if charged Barbie (expired)
S-NOTIMESLOT VP, US get_timeslots on a date with no live schedule 200 + empty list, no create —
S-IDOR all Frappe classes get_booking as other guest 403/404 —

FiveStar S-GP-FS is a row to add when Outlet ID exists, same pattern.


E2E @nightly (Karate, not PR)

Each row is one feature. Examples: | class_id | and N/A when snapshot field missing.

ID From old pack When it runs Assert N/A unless
N-CLAMP-BELOW ATP #4 try 5 guests.min > 1 create rejected US Plans (min=1)
N-CLAMP-ABOVE ATP #4 try 30 guests.max < 999 create rejected —
N-ADDON ATP #5 addons[] nonempty line = price × guests; 8→12 reprices US if we omit addons on smoke
N-DEPOSIT ATP #6 deposit.amount > 0 charged amount = snapshot.deposit not grand total; status Initiated Super Pass deposit none
N-FULL-PAY ATP #13 / DEV #13 pay approved Successful; balance 0 Datacap production
N-SLOT-TAKEN ATP #8 after S-GP same start gone; next start ≥ end+buffer —
N-PROMO Karate 100OFF promos_live nonempty discount > 0 or documented skip code expired / min purchase
N-BUYOUT ATP #9 #15 shape=buyout no online pay; admin custom hours; xfail Monday-only Sunday row not on Midway golden
N-ADMIN-CREATE ATP #14 / DEV #16 admin JWT calendar slot matches oracle end —
N-ADMIN-PATCH DEV #19 Successful booking patch_booking in-place; not update_booking —
N-CANCEL ATP #16 own booking only status Cancelled; slot free —
N-HOLDING DEV #7 #18 admin unpaid Holding; slot held US if unused
N-SAME-DAY ATP #18 U-ADVANCE already unit past tile absent; later today books —
N-WAIVER ATP #20 digital waiver count=0 N/A expected for ATP+US Test if waiver on, must collect
N-TZ ATP #17 always start string equal API / booking / (UI later) —

Not nightly until asked: S-883 / update_booking, split pay, gift card, kiosk, CE/FiveStar, CSV import, game cards (ATP #21 = record gap only).


Playwright (not PR)

ID Why API is not enough
P-GREY occupied start tile looks disabled
P-CAL-BAR admin bar stretches to oracle end (90/120/150), not 30 min
P-EMAIL confirmation copy (if mailbox readable)

Traceability (old 21 ATP rows → this list)

ATP checklist Layer
1 Package list S-GP-VP catalog step
2 Date / Sunday fewer U-LIVE + S-NOTIMESLOT
3 30-min starts / hold U-GRID-NE + S-GP cart
4 Child 5 / 30 / 8 U-CLAMP + N-CLAMP-*
5 Add-ons N-ADDON
6 Deposit $15.11 N-DEPOSIT (amount from snapshot, not the sheet)
7 Email/SMS P-EMAIL
8 Slot taken + 12:30 next N-SLOT-TAKEN (end+buffer)
9 Buyout call-only N-BUYOUT
10 Calendar length P-CAL-BAR
11 Admin child reprice N-ADDON admin variant
12 Collect balance N-FULL-PAY
13 Payment link later
14 Phone booking N-ADMIN-CREATE
15 Buyout custom hours N-BUYOUT xfail
16 Cancel N-CANCEL
17 Local time N-TZ
18 Same-day U-ADVANCE + N-SAME-DAY
19 Birthday child nightly optional
20 Waiver N-WAIVER N/A
21 Game cards gap, not a test

United Skates DEV list (date-first, guest, Cognito) = S-GP-US-PLAN / PKG + N-FULL-PAY. Outlet admin tabs 20–27 are config tools, not booking SYS-005.


Count (what “present in the code” means)

Layer How many test functions/features How many DEV SKUs they cover
Unit 12 (U-*) all 4 ATP + 97 US via fixtures, not 101 files
Smoke E2E 5 (3 goldens + empty slots + IDOR) 3 classes
Nightly E2E 15 (N-*), most parameterized by class N/A when snapshot says so
Playwright 3 UI only

If someone adds Saturday Public Skate 12:30 as a fourth smoke, that is a SYS-005 miss — it is the same Plan class as Super Pass.


Test framework (finalize) — BookingZone SYS-005

Where tests live, what they protect, and how they raise code quality. No execution in this note. No Datacap charges. No GitHub write until you ask.

Decision

Layer Tool Lives in PR gate? TDD or BDD
Unit (oracle + domain math) pytest (API), Jest/Vitest (PWA) reservation-api-server, venuplus-app yes — must be green TDD
API journey (class smoke) Karate @smoke keep Karate, rebind to snapshot yes — <10 min, no \|\| true BDD
Pairwise / nightly Karate @nightly + PICT same suite no (nightly) BDD
UI only-what-API-cannot-see Playwright venuplus-app or a thin qa folder no BDD
Styling / 120 viewports existing visual suite stays never on PR —

Do not add a third E2E framework. Keep Karate. Do not TDD the checkout click-path.

Pyramid (what a failing test means)

        Playwright          email body, greyed slot, tz string on calendar
     Karate @nightly        deposit vs full, add-on, promo, admin patch
   Karate @smoke            1 live golden × class → BZBooking matches snapshot
 pytest / Jest              duration, guest clamp, live?, classify, lock window

A red unit test = the rule is wrong (end = start+30 instead of start+120). A red smoke = DEV booking no longer satisfies that class’s doctypes. A red Playwright = UI lied; DocType may still be fine.

If smoke is the only layer, we will keep encoding $79.92 in Gherkin. That does not improve the codebase.

1. Unit — this is the code-quality lever

reservation-api-server (pytest, TDD)

Pure functions, fixtures from the knowledge-base dumps, no Frappe HTTP.

Module (intent) Examples from DEV
classify_item PartyEvent+package → VP package; BuyNow+session → US plan; is_buyout → buyout journey
expected_end(start, item, schedule) Midway 10:30 + 120m → 12:30, not 11:00. US item duration 0 → session length on schedule
clamp_guests(n, item, schedule) schedule 8–25 wins over item 1–100 (Buyout smell)
live_schedule(as_of, row) Super Pass Fri expired; Sat+Sun live; Saturday 10am dead
hold_window(start, end, buffer) ATP ±30; US 0
class_id(outlet, item) fingerprint row; new Outlet of same class → same id

This is also where reservation-api-server #849 / #850 belong: lock confirm at create_booking, guest/duration rules next to production code, not only in QA.

TDD loop: write test_expected_end_uses_item_duration_not_slot_grid (red) → extract helper from booking_transaction / timeslot mapper (green) → callers use the helper.

venuplus-app (Jest/Vitest, TDD)

The Karate comment already documents the bug class:

API row is a start tile; booking end is start + BZServiceItem.duration_minutes, not the row’s 30-minute endTime24.

Lift that addMinutes out of Karate Java and unit-test it in the app that paints the tile. Catalog path by class: /plans-and-games vs /plans vs date-first.

What unit tests are not

  • Not 77 United Skates plan files
  • Not Datacap
  • Not get_list of 1261 resources

2. Karate — BDD journeys, data from the oracle

Keep the existing features. Change how they get data.

karate-tests/
  karate-config.js          env, tokens — no Mac .env path
  oracle/
    load-snapshot.feature   GET item+schedules → expected-snapshot v1
  smoke/
    book-class.feature      Scenario Outline Examples: | class_id |
  nightly/
    deposit-vs-full.feature
    addon-reprice.feature
    no-timeslot.feature
    admin-patch.feature     patch_booking only; never update_booking until asked

book-class.feature (one):

Given oracle snapshot for <class_id>
When guest books smoke_count on a live start tile
Then BZBooking.start/end/guests/status match snapshot
And booking end is start + duration.booking_minutes

Examples: VP-FRAPPE-PACKAGE-VENUE-DATACAP-PWA | US-FRAPPE-PLAN-DATACAP-PWA | US-FRAPPE-PACKAGE-VENUE-DATACAP-PWA

Delete: hardcoded Arnot outletId in every Background, selectablePackages[1], baked 100OFF as required, treating SKIPPED cart as green.

Cleanup: every run cancels only bookings it created.

3. Playwright — thin, not the daily gate

Only:

  • Greyed occupied start tile
  • Confirmation email/SMS copy (if we can read it)
  • Admin calendar bar stretches to oracle end, not 30 min (ATP checklist #10 / #15)

Not: full guest checkout (Karate already hits the RPCs the PWA uses).

4. How this improves the product code

Tests that only live in karate-tests/ never force a refactor. The unit layer does:

  1. Timeslot 30-min grid vs party length — one helper, used by PWA + create_booking.
  2. Guest min/max — schedule wins; Buyout 1–100 vs 8–25 becomes a visible rule, not a surprise.
  3. Live schedule filter — catalog can still list dead SKUs; booking path must not pick them.
  4. Class fingerprint — docs/booking-config-fingerprint.md (#112) is the contract; CI fails if a golden’s shape changes (buffer 30→0, Datacap→CE).
  5. Dual oracle — if pricing_engine.calculate_total ≠ item.basePrice × guests (PerPerson), that is a product bug (INR vs USD), not a reason to trust the RPC.

SYS-005 success criteria stay: PR to develop fails if @smoke fails; smoke <10 min; assert DocType state; new Outlet of existing class = zero tests.

5. CI sketch

PR to develop
  pytest reservation-api-server/tests/unit     required
  jest venuplus-app duration/catalog helpers   required
  karate @smoke (3 class goldens, DEV)         required, <10 min, no || true

Nightly
  karate @nightly
  PICT pairwise (W3)
  optional Playwright occupied-slot + calendar duration

Never on PR
  120 styling
  cartesian SKU × day × pay
  #883 update_booking

Datacap: do not run @smoke pay against payment_environment=production. Block the job until a sandbox/cert outlet is named — or smoke stops at Pending + Confirmed lock (create without capture) until you approve a pay pass.

6. Mapping old artifacts

Old Becomes
21-row ATP checklist 1 VP smoke + nightly: clamp 5/30, add-on reprice, deposit vs total, buyout call-only (xfail Monday-only)
Booking Scenarios taxonomy PICT dimensions, not 80 features
Karate zip smoke/book-class.feature + oracle load; rest tagged @nightly
97 US items 2 class goldens (Super Pass, ULTIMATE)

7. Order of work (still no GitHub until you ask)

  1. Oracle schema v1 — done (expected-snapshot.schema.json + 3 examples)
  2. pytest fixtures from those examples (TDD the four helpers)
  3. Rebind Karate @smoke to snapshots
  4. Docs PR to bz-platform-context #112
  5. Extract helpers into reservation-api-server / venuplus-app so unit tests sit next to production code
  6. Write-pass smoke on DEV (create only, then pay) when you approve

BZServiceItem field coverage

Honest answer: the scenario matrix did not cover all 120 DocType fields. It covered the ones that already differ between ATP and United Skates goldens. This file is the rest.

Source: DocType BZServiceItem (120 fields) + GET Midway bz-3702289d + Super Pass bz-19d6fa9b on DEV 2026-09-02.

How a field is treated

Kind When the field changes What we add
A. Class shape Would make two goldens need different journeys (venue lock, provider, item_type, session vs package, payment gateway on outlet) New class_id row + one smoke. SYS-005.
B. Snapshot / N/A Changes numbers or turns a scenario on/off (min/max, duration, buffer, deposit, waiver, buyout, immediate pay) Extend expected-snapshot schema. Same test IDs. New unit for the rule. E2E N/A flips.
C. Child journey Table/check that adds a step (addons, questions, processing fee, game card, waitlist, kitchen) Nightly feature, N/A when empty/0
D. Ignore CMS / ops (images, copy, report URLs, S3 keys, HTML) Zero tests
E. New field later Unknown See protocol at the bottom

Do not add a test per SKU that merely sets the same field to another value (another PerPerson package).

Midway vs Super Pass (proof the classes differ)

Field Midway (ATP package) Super Pass (US plan) Kind
item_type PartyEvent BuyNow A
plan_type PerPerson PerHour A
session_status / package_status 0 / 1 1 / 0 A
duration_minutes 120 0 (time on schedule) A/B
buffer_minutes 30 0 A/B
min/max_guests 8 / 25 1 / 1 B
is_buyout 0 0 B (Buyout SKU is 1)
enable_immediate_payment 1 0 B
require_deposit / deposit_amount 0 / 0 (deposit lives on schedule 15.11) 0 / 0 B — item deposit is a trap
waiver_status / waivers[] 0 / [] 1 / 6 rows C — matrix miss
pwa_status 1 1 B
no_venue_allocation 0 0 A if ever 1
addons[] 3 optional 1 C
fivestarparks_game_card_status 1 + 1 child 0 C — ATP #21 gap
processing_fees[] 0 1 C
rules[] 2 0 C
waitlist_status 0 0 C
allow_customer_edit 0 0 C
walkin_status 0 0 C
kitchen_status 0 0 C
hide_single_time 0 0 B
disable_lock_window_resolver 0 0 A
pwa_auto_assign_status 1 1 B
reserve_cloud_tag / mappings empty empty A if set (RC class)
start_date / end_date 2026-04-29 → 2027-04-29 (empty on Super) B — must not override live schedule
min/max_adv_time 0 (outlet 1h / 90d wins) 0 B
pricing[] child 1 (currency smell INR on ATP) 1 B dual oracle
questions[] 0 0 C
status / is_deleted Active / 0 Active / 0 B skip Inactive
max_guest (singular) vs max_guests both exist; Midway max_guest=0, max_guests=25 max_guest=0, max_guests=1 B — unit: which field wins

Kind D — do not test (CMS)

image_url*, video_url, show_img_as_banner, description, rich_description, top_rich_description, images[], guest_report_* (6+ fields), guest_list_report_*, enable_guest_report, enable_second_editor, is_profile_pic_required, is_guest_pic_required, accounting_html, schedules_html, idx, owner, creation, modified, naming.

Fields we had in the matrix

duration, buffer, min/max guests, session vs package, buyout, PWA path, schedule deposit, addons as nightly.

Fields we did not have — add these IDs

ID Field Layer Rule
U-DEPOSIT-SOURCE require_deposit, deposit_amount vs schedule deposit unit schedule upfront wins for ATP; item 0 is not “no deposit”
U-MAX-GUEST-FIELD max_guest vs max_guests unit document winner; Midway max_guest=0 must not clamp to 0
U-ADV-ITEM min_adv_* vs outlet window unit 0 on item → outlet 1h/90d
N-WAIVER waiver_status, waivers[] nightly N/A on Midway; ON for Super Pass (6 waivers) — old ATP #20 does not apply to US
N-FEE processing_fees[] nightly N/A if empty; Super Pass has 1
N-GAMECARD fivestarparks_game_card_* nightly / gap ATP Midway status=1; assert booking lines vs copy (ATP #21)
N-WAITLIST waitlist_status nightly N/A while 0
N-QUESTIONS questions[] nightly N/A while 0; ATP #19 if they appear
N-IMMEDIATE enable_immediate_payment nightly 0 → do not require full capture on PWA (Buyout, Super Pass)
N-RULES rules[] unit first Midway has 2; encode as oracle constraints
U-NO-VENUE no_venue_allocation unit if 1, smoke must not require BZBookingAvailability
U-LOCK-RESOLVER disable_lock_window_resolver unit if 1, hold_window rule off
N-WALKIN walkin_status / is_walk_in_enabled nightly admin-only SKU
N-EDIT allow_customer_edit nightly 0 → guest cannot patch
N-RC reserve_cloud_tag class A nonempty → not these goldens
N-HIDE-TIME hide_single_time UI Playwright if 1
N-INACTIVE status, is_deleted, pwa_status=0 smoke filter never pick as golden
N-ITEM-DATES start_date/end_date vs schedule unit item end 2027-04-29 vs schedule 2027-12-31 — which wins

Protocol when someone adds a new field on BZServiceItem

  1. Classify A/B/C/D (above). If unsure, it is B until proven CMS.
  2. A — new fingerprint column + one new class smoke. Not a copy of Midway.
  3. B — bump expected-snapshot schema_version; add the field to oracle; add one unit that states the rule (like U-END-ITEM). Existing S-GP-* keep working with new numbers.
  4. C — add N-* with N/A when default/empty. Do not turn it on for every class.
  5. D — changelog only.
  6. CI: a small schema-drift unit test lists DocType fieldnames and fails if a new Check/Select/Int/Currency field is not in this table (A/B/C/D). That is how we do not miss the next waiver_status.

No cartesian: waiver_status × 97 US SKUs is still one N-WAIVER parameterized by snapshot.