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, browser4a53eab). Full audit: TEST coverage recommendations (2026-09-24). Covered: S-NOTIMESLOT (#162 Done), S-IDOR (#163 Done — orphaned fromci-qualitypath). 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-minuteendTime24.
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_listof 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:
- Timeslot 30-min grid vs party length — one helper, used by PWA +
create_booking. - Guest min/max — schedule wins; Buyout 1–100 vs 8–25 becomes a visible rule, not a surprise.
- Live schedule filter — catalog can still list dead SKUs; booking path must not pick them.
- Class fingerprint —
docs/booking-config-fingerprint.md(#112) is the contract; CI fails if a golden’s shape changes (buffer 30→0, Datacap→CE). - 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)¶
- Oracle schema v1 — done (
expected-snapshot.schema.json+ 3 examples) - pytest fixtures from those examples (TDD the four helpers)
- Rebind Karate
@smoketo snapshots - Docs PR to
bz-platform-context#112 - Extract helpers into
reservation-api-server/venuplus-appso unit tests sit next to production code - 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¶
- Classify A/B/C/D (above). If unsure, it is B until proven CMS.
- A — new fingerprint column + one new class smoke. Not a copy of Midway.
- B — bump
expected-snapshotschema_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. - C — add
N-*with N/A when default/empty. Do not turn it on for every class. - D — changelog only.
- 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.