V2 Smoke-Test Plan¶
The step-by-step plan Stefan executes in the test environment to verify V2 behaves correctly before he flips V1 (XLSX + stringing.wagen.io) to V2 (RBO). One pass through every test case in this doc, then sign the launch-readiness checklist's go-decision.
Audience: Stefan (operator). The TCs are written so each one stands alone — Stefan can run them in any order and pause between them. Engineers triaging a TC failure read the linked design doc for that surface.
Companion docs.
- V2 launch readiness checklist — the go/no-go list. Every TC in this plan corresponds to one or more items there. Full pass here ⇒ Stefan ticks the launch-readiness items and signs.
- V1 → V2 upload spec — the dress-rehearsal upload that produces the test database this plan runs against.
- docs/architecture/i18n.md — UI + receipt locale rules underpinning TC8 (DE locale).
- docs/requirements/fadp-implementation-asks.md — DSAR portability spec underpinning TC10.
Preconditions for the whole plan¶
Before executing any TC:
- [ ] Test database is freshly populated from the most recent dress-rehearsal upload per V1 upload spec step 4 (test upload without
--dry-run). The reconciliation report Summary line isOK. - [ ] Test environment is up.
https://rbo-test.wagen.io/returns the FastAPI root JSON;https://rbo-test.wagen.io/healthzreturnsok. Per deploy verification. - [ ] Test stringer accounts exist. Two accounts, both with verified emails Stefan can read:
stringer-a@<test domain>— primary test stringer; default localeen.stringer-b@<test domain>— secondary stringer for the tenancy regression (TC9); default localeen.- [ ] Test admin account exists.
admin@<test domain>with theadminrole flag set on its Stringer record. Used by TC4 (catalogue moderation), TC7 (V1-upload), TC10 (DSAR). - [ ] Test client mailbox is reachable. Either a real shared inbox Stefan can open, or the Resend dashboard view filtered to the test environment. Used by TC2.
How to log in (test env)¶
https://rbo-test.wagen.io/login → enter email → magic-link arrives at the inbox → click the link → land on the dashboard at /dashboard. Per the magic-link contract in ADR-0006 — JWT session contract. If the magic-link inbox is unavailable, Stefan can log in by password directly (set during the onboarding password step per docs/design/stringer-onboarding.md).
Test cases¶
Each TC is a self-contained block. Stefan ticks each pass-criterion as he runs the TC. If a step fails, jump to What to do on failure at the bottom.
TC1 — Add Stringjob, happy path (client order)¶
Covers the core M9 flow. Maps to v2-launch-readiness § "Smoke test in test env" (Full happy path in test, end-to-end) — the Add Stringjob part of the chain.
Preconditions. Logged in as stringer-a. Dashboard renders.
Steps.
- Click Add Stringjob (header on md+, sticky-bottom CTA on sm).
- On
/orders/new, search for an existing client by last name — pick one from the migrated V1 dataset whose name Stefan recognises. (If no client matches, create a new Person inline per the new-client UX in docs/design/add-stringjob.md.) - Pick a racket from the client's catalogue history (or the catalogue picker for a first-time job).
- Fill the Main string group: pick a string, enter a tension in kg (e.g.
25.0), enter a price in CHF (e.g.35.00). - Leave Cross as
Same as main. - Set Labor (CHF) to a typical value (e.g.
15.00). - Click Save (NOT "Mark strung now" — TC2 covers that branch).
Expected outcome.
- HTTP 303 redirect to
/orders/<id>(the new order's detail page). - Order state badge reads Ordered (per
app/web/templates/orders/detail.htmllifecycle pills). - All entered values render verbatim on the detail page.
- Back on the dashboard at
/dashboard, the order appears under Today (today'sOrderedcount incremented by 1).
How to verify.
- [ ] Detail page URL contains a UUID.
- [ ] Lifecycle badge says
Ordered. - [ ] Dashboard
Todaychip count went up by 1.
TC2 — Mark Strung + email receipt¶
Covers M10 (lifecycle Strung → emit receipt) + M14 (receipt PDF + email dispatch). Maps to v2-launch-readiness § "Smoke test in test env" → "email receipt sends" and § "DE copy" → "DE PDF receipt template renders end-to-end" (when paired with a DE-locale client; see TC8 for the DE pass).
Preconditions. TC1 just completed; the order is open in /orders/<id> and is in state Ordered.
Steps.
- On the order detail page, click Mark strung.
- Wait for the page to reload; confirm the lifecycle badge flips to
Strungand the Strung date is today. - Open the test client mailbox (or the Resend dashboard view filtered to this test env).
Expected outcome.
- The order detail page now shows a Receipt card with a download link (per
orders/detail.htmlline ~221). - A receipt email arrives in the test inbox addressed to the order's client. Subject line is the receipt number (e.g.
Quittung 2026-0123for DE clients,Receipt 2026-0123for EN). Body contains a link to or attachment of the PDF. - The PDF totals (Labor + Strings subtotal + Total) match what was entered in TC1 within 0.01 CHF.
- BYO suppression: if the client is BYO on either side, that side's price line is suppressed in the PDF. Per docs/requirements/receipt-content.md.
How to verify.
- [ ] Lifecycle badge:
Strung. Strung date = today. - [ ] Test inbox shows one new email; subject line carries a receipt number.
- [ ] PDF opens; total is correct; locale matches the client's
default_locale. - [ ] Resend dashboard (or
EmailDispatchLogtable) shows the dispatch row with statusdelivered(oracceptedif Resend hasn't reported delivery yet).
If the client is a self-job: no email is dispatched (per receipt-delivery rules in docs/requirements/receipt-delivery.md); the Receipt card on the detail page still shows the PDF link. This is the deliberate self-job path; not a TC2 failure.
TC3 — Mark Paid¶
Covers the final lifecycle transition. Maps to v2-launch-readiness § "Smoke test in test env" → "mark Paid".
Preconditions. TC2 just completed; the order is in state Strung.
Steps.
- On the same order detail page, click Mark paid.
- Wait for the page to reload.
Expected outcome.
- Lifecycle badge unchanged at
Strung(Paid is a separate flag, not a state per ADR-0007 § Order lifecycle). - A Paid date pill appears next to the Strung date.
- The Mark paid action button is replaced by Clear paid (idempotent UX).
- No new receipt is emitted (paid is not a receipt-emitting transition).
How to verify.
- [ ] Paid date pill = today.
- [ ] Detail page action set switched:
Mark paidgone,Clear paidpresent. - [ ] No new email in the test inbox since TC2.
TC4 — Catalogue moderation¶
Covers M17 (admin catalogue moderation). Maps to v2-launch-readiness § "Admin tooling" → "Catalogue moderation UI functional".
Preconditions.
- Logged in as the admin account. (Sign out of
stringer-afirst, sign in asadmin.) - At least one catalogue submission exists in the queue. (If the test DB is empty here, log in as
stringer-a, create a Stringjob whose racket is a new model not in the shared catalogue — that fires the catalogue submission per the M17 design — then return as admin.)
Steps.
- From the dashboard, click the amber catalogue submission chip (or navigate to
/admin/catalogue/queuedirectly). - Verify the queue page lists the pending submission with the right metadata (manufacturer + model, submitter, date).
- Click into one submission via the row link.
- On the detail page (
/admin/catalogue/queue/<id>), in the Decide section, type a brief notes string into "Notes (optional)" and click Promote to shared catalogue. - Page reloads to show the decided state with a green Promoted chip.
- Optionally, repeat for another submission and click Reject — verify a reject reason is required, and that submitting flips the chip to red Rejected.
Expected outcome.
- Queue list updates: the promoted submission no longer appears under "Pending — oldest first".
- The catalogue racket is now visible to every stringer (test by signing back in as
stringer-aand confirming the model appears in the racket picker on/orders/new). - The submitter receives a notification email about the decision (test inbox under stringer-a's address). Per docs/design/admin-catalogue-moderation.md.
How to verify.
- [ ] Promote: queue list shrinks by 1; detail page shows green
Promotedchip with timestamp + reviewer name. - [ ] Reject: queue list shrinks by 1; detail page shows red
Rejectedchip with reason text echoed. - [ ] Submitter notification email arrives in the test inbox; copy reads correctly in the submitter's locale.
- [ ] Visibility flip: the promoted catalogue entry is selectable for new stringjobs.
TC5 — Reports¶
Covers M16 (reports rendering). Maps to v2-launch-readiness § "Admin tooling" → "Reports rendering".
Preconditions. Logged in as stringer-a. Test DB has at least one full year of migrated stringjobs.
Steps.
- From the dashboard, click the Reports link in the inbox column (or navigate to
/reports). - The page renders with a year selector chip row at the top.
- Click a recent year (e.g. last calendar year) — the URL becomes
/reports?year=<YYYY>and the page rerenders. - Verify the three KPI cards (Strung, Revenue, Mean turnaround) carry plausible values that match Stefan's memory of that year.
- Verify the Strung per month sparkline shows 12 bars; click any bar (e.g. month 6).
- URL becomes
/reports?year=<YYYY>&month=6; the Month / Year drilldown panel renders with three KPI cards for that month. - Verify Top clients lists the top 20 clients by order count for the selected year, with order-count + revenue chips.
Expected outcome.
- Yearly KPIs reconcile against the per-year revenue table from the V1 reconciliation report (within the 0.01 CHF tolerance per v1-upload-spec.md § Reconciliation report).
- Monthly drilldown numbers are smaller than (or equal to) the yearly numbers and add up across months.
- Top clients list is non-empty and the names are recognisable.
How to verify.
- [ ] Yearly Revenue KPI matches the same year's row in the reconciliation report.
- [ ] Sparkline renders 12 bars, bar heights look reasonable.
- [ ] Monthly drilldown panel appears after clicking a bar.
- [ ] Top-clients list non-empty.
TC6 — Self-service export¶
Covers M20 (per-stringer XLSX + JSON export). Maps to v2-launch-readiness § "Admin tooling" → "Self-service export working".
Preconditions. Logged in as stringer-a. Test DB has migrated stringjobs against this stringer.
Steps.
- On
/reports, scroll to the Export section. - Click Download XLSX — browser downloads
stringer-<id>-<date>.xlsx(or similar name per the M20 spec). - Open the file in Stefan's usual XLSX tool.
- Click Download JSON — browser downloads the JSON file.
- Open the JSON in a text editor (or
jq .).
Expected outcome — XLSX.
- All 4 sheets populated per the V1 format spec (
Client-Stringing-Orders,Self-Stringing-Orders,Lists,Stats). - Row counts match the V2 entity totals from the reconciliation report (after filtering to this stringer's data).
- Header rows match the V1 column names (the export round-trips the V1 format so a recipient can re-upload).
Expected outcome — JSON.
- Top-level shape: an object with keys
stringer,orders,clients,rackets,strings(or whatever the M20 schema names them). ordersarray length equals the V2 order count for this stringer.- Each order object includes the lifecycle dates, pricing block, comments, receipt-number, and an embedded reference to the client + racket + strings.
How to verify.
- [ ] XLSX opens; all 4 sheets present; row counts match.
- [ ] JSON parses cleanly (
jq .or equivalent);orderscount matches. - [ ] No PII leakage to other stringers (the export contains only this stringer's data; verified concretely in TC9).
TC7 — V1-upload (Round 6 deliverable)¶
Covers M15 admin V1-upload flow. Maps to v2-launch-readiness § "Data migration" — the test-env half. Run only after Round 6 lands (Pax-A backend + Juno frontend); until then, skip this TC and the v1-upload steps remain CLI-only per v1-upload-spec.md.
Preconditions. Logged in as admin. Stefan has a fresh XLSX export of his V1 sheet on disk.
Steps.
- Navigate to the admin V1-upload page (URL TBD — Round 6 design names it).
- Upload the XLSX, ticking the dry-run checkbox.
- Wait for the upload + parse + reconciliation to complete; the report renders inline.
- Read the report Summary line; if
OK, untick dry-run and re-upload to commit. - After commit, navigate to the past-runs index; verify the new run is listed with timestamp + summary status.
Expected outcome.
- Dry-run path leaves the database unchanged (verified by checking row counts before + after).
- Wet-run path commits the rows; the past-runs index shows the new run.
- The XLSX upload runs to completion in under a few minutes for a typical real-world XLSX (~330 client rows + ~600 self rows).
How to verify.
- [ ] Dry-run report renders with correct Summary line + Differences (when applicable).
- [ ] Wet-run commits; row counts after upload match the report's V2 entity totals.
- [ ] Past-runs index lists the run with the correct status.
- [ ] Re-uploading the same XLSX a second time produces all-zero insert counters and a non-zero
skipped_idempotent(per the idempotency contract in v1-upload-spec.md).
TC8 — DE locale¶
Covers DE copy + locale resolution. Maps to v2-launch-readiness § "DE copy" — all three rows. Depends on #125 being merged (Pax-B i18n runtime wiring); skip until that lands.
Preconditions. A test stringer account whose default_locale = "de" exists. (Either change stringer-a's default locale via the onboarding/settings page, or use a separate stringer-de@<test domain>.)
Steps.
- Sign in as the DE stringer.
- Walk through the dashboard,
/orders/new, an existing order detail, the onboarding-complete page (if not already onboarded), and the magic-link invite copy. - From
/orders/new, create a new stringjob for a client whose Persondefault_locale = "de". - Mark the order Strung; verify the receipt PDF rendered to the test inbox is
receipt_de.html-rendered (DE strings throughout, DE date formatting). - Sign out. From a fresh browser session with
Accept-Language: en-US,en;q=0.9, hit the landing page — verify the unauthenticated landing page renders in EN (browser fallback rule per i18n.md § UI locale-source locked rule). - Sign back in as the DE stringer from that EN browser; verify the dashboard now renders in DE (signed-in stringer locale wins over
Accept-Language).
Expected outcome.
- Every UI string in the signed-in DE-stringer surface reads as natural German (no machine-translation tells).
- DE PDF receipt totals + BYO suppression match the receipt-content spec; no EN strings leak through.
- Locale resolution: signed-in stringer locale beats
Accept-Language; unauthenticated falls back toAccept-Language.
How to verify.
- [ ] Dashboard, Add-Stringjob, order detail, onboarding, magic-link invite copy: all DE.
- [ ] Receipt PDF: DE-rendered; totals + BYO match spec.
- [ ] Unauthenticated landing in EN browser: EN.
- [ ] Signed-in DE stringer in EN browser: DE.
TC9 — Tenancy regression¶
Covers the per-stringer data-isolation contract. Maps to v2-launch-readiness § "Auth" → "No public exposure of customer data" + the row-level tenancy invariant from ADR-0004 — sharing model.
Preconditions. TC1 (or any earlier stringjob) created against stringer-a exists in the test DB. A second stringer stringer-b exists with their own (different) clients + orders.
Steps.
- Sign in as
stringer-b. - From
stringer-b's dashboard, scan theTodayandRecentlists — confirm none ofstringer-a's orders appear. - From
stringer-b's/orders/newracket picker, search for a client name onlystringer-ahas — confirm zero results. - Try direct-URL access to
stringer-a's order detail: paste/orders/<a-order-uuid>into the address bar. - Try
stringer-a's export URL:/stringers/<stringer-a-id>/export.json. - Try
stringer-a's reports page:/reportswhile signed in asstringer-b(this isstringer-b's own reports page, but verify the numbers reflectstringer-b's data only).
Expected outcome.
- Step 4: HTTP 404 (or 403 — either is acceptable; the row is invisible to
stringer-b's tenant scope). - Step 5: HTTP 404 / 403 (same).
- Step 6: Reports KPIs reflect
stringer-b's totals only — none ofstringer-a's revenue or top-clients leak in. - No cross-tenant data appears anywhere in
stringer-b's session.
How to verify.
- [ ] Dashboard Today + Recent: only
stringer-b's orders. - [ ] Client search: zero hits for
stringer-a-only client names. - [ ] Direct order-detail URL: 404 or 403.
- [ ] Direct export URL for the other stringer: 404 or 403.
- [ ] Reports KPIs: numbers are
stringer-b-only; cross-checked against the reconciliation report's per-stringer rollup.
TC10 — FADP — DSAR portability¶
Covers the DSAR portability obligation. Maps to v2-launch-readiness § "FADP" → "DSAR portability operational".
Preconditions. Logged in as admin. A test Person exists in the DB whose data Stefan can identify (e.g. a client with a known last name + at least one order).
Steps.
- Navigate to the admin DSAR queue (
/admin/dsaror the URL named indocs/design/admin-dsar-queue.md). - Trigger a DSAR export for the test Person — either by pasting their UUID, by searching by last name, or by clicking through from their order detail page (whichever surface the design provides).
- Wait for the export job to complete; download the resulting archive.
- Open the archive and inspect its contents.
Expected outcome.
- The archive is in the format named in fadp-implementation-asks.md (zipped JSON + receipt PDFs by current spec).
- The archive contains all data tied to the requesting Person: their
Personrow, all associatedClientProfiles, allOrders, all rendered receipts. - The archive contains only that Person's data — no rows from other clients, no global catalogue dumps.
- The export completes within the documented SLA (current target: 24h for prod; near-instant for the test single-Person case).
How to verify.
- [ ] Archive structure matches the FADP asks doc.
- [ ] All known orders for the Person are present (count cross-checked against
stringer-a's view of that Person's order history). - [ ] No rows from a different test Person appear in the archive (sample-check 5 random orders for the wrong-Person foreign-key).
- [ ] The audit log records the DSAR run with admin user + Person target + timestamp.
Sign-off¶
When every TC above is ticked (TC7 / TC8 may be deferred if their dependencies aren't merged yet — note the deferral and proceed):
Smoke test pass. I, Stefan Wagen, confirm the V2 test environment passed every applicable test case in this plan on the date below.
Signed: ____ Date: ______
Test env URL: ____ TCs deferred (and why): ____
After this signature, Stefan returns to the V2 launch-readiness checklist and ticks the Smoke test in test env items, plus any other rows whose verification this plan covers (Catalogue moderation, Reports, Self-service export, DE copy, FADP DSAR). When the launch-readiness checklist is fully ticked, Stefan signs the Final go-decision there.
What to do on failure¶
If a TC step doesn't produce the expected outcome:
- Don't proceed past the failing step. A failure here means a launch blocker; fixing it is more important than completing the rest of the plan.
- Capture the failure. Screenshot the page, copy any error text, note the URL and the steps to reproduce.
- File a blocking issue. Open a new GitLab issue with:
- Title:
V2 launch blocker: <one-line summary of the failure>. - Labels:
area::v2-launch-blocker,priority::high,status::needs-triage, plus the matchingarea::*for the surface (e.g.area::receipt,area::admin,area::reports,area::auth,area::fadp,area::migration,area::i18n). - Body: paste the failing TC heading from this plan, the screenshot / error text, and the reproducer steps.
- Notify Nora. Stefan messages Nora with the issue number; Nora dispatches the relevant specialist (Pax / Pax-A / Pax-B / Pax-C / Juno / Atlas / Vera / Iris) per the agent roster.
- Re-run the failing TC after the fix lands. Continue from the failed TC; don't restart the whole plan unless the fix touches an earlier surface.
A blocking issue is a hard stop on the V2 launch flip. Stefan does not sign the launch-readiness go-decision until every blocker is closed.
Related¶
- V2 launch readiness checklist — the go/no-go list this plan satisfies.
- V1 → V2 upload spec — produces the test database this plan runs against.
- Deploy pipeline verification — what
https://rbo-test.wagen.io/is and how it gets there. - docs/architecture/i18n.md — locale-resolution rule TC8 verifies.
- docs/requirements/fadp-implementation-asks.md — DSAR portability spec TC10 verifies.
- docs/requirements/receipt-content.md — receipt PDF content spec TC2 / TC8 verify.
- docs/design/admin-catalogue-moderation.md — catalogue moderation surface TC4 verifies.