Skip to content

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.

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 is OK.
  • [ ] Test environment is up. https://rbo-test.wagen.io/ returns the FastAPI root JSON; https://rbo-test.wagen.io/healthz returns ok. Per deploy verification.
  • [ ] Test stringer accounts exist. Two accounts, both with verified emails Stefan can read:
  • stringer-a@<test domain> — primary test stringer; default locale en.
  • stringer-b@<test domain> — secondary stringer for the tenancy regression (TC9); default locale en.
  • [ ] Test admin account exists. admin@<test domain> with the admin role 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.

  1. Click Add Stringjob (header on md+, sticky-bottom CTA on sm).
  2. 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.)
  3. Pick a racket from the client's catalogue history (or the catalogue picker for a first-time job).
  4. 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).
  5. Leave Cross as Same as main.
  6. Set Labor (CHF) to a typical value (e.g. 15.00).
  7. 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.html lifecycle pills).
  • All entered values render verbatim on the detail page.
  • Back on the dashboard at /dashboard, the order appears under Today (today's Ordered count incremented by 1).

How to verify.

  • [ ] Detail page URL contains a UUID.
  • [ ] Lifecycle badge says Ordered.
  • [ ] Dashboard Today chip 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.

  1. On the order detail page, click Mark strung.
  2. Wait for the page to reload; confirm the lifecycle badge flips to Strung and the Strung date is today.
  3. 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.html line ~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-0123 for DE clients, Receipt 2026-0123 for 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 EmailDispatchLog table) shows the dispatch row with status delivered (or accepted if 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.

  1. On the same order detail page, click Mark paid.
  2. 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 paid gone, Clear paid present.
  • [ ] 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-a first, sign in as admin.)
  • 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.

  1. From the dashboard, click the amber catalogue submission chip (or navigate to /admin/catalogue/queue directly).
  2. Verify the queue page lists the pending submission with the right metadata (manufacturer + model, submitter, date).
  3. Click into one submission via the row link.
  4. 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.
  5. Page reloads to show the decided state with a green Promoted chip.
  6. 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-a and 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 Promoted chip with timestamp + reviewer name.
  • [ ] Reject: queue list shrinks by 1; detail page shows red Rejected chip 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.

  1. From the dashboard, click the Reports link in the inbox column (or navigate to /reports).
  2. The page renders with a year selector chip row at the top.
  3. Click a recent year (e.g. last calendar year) — the URL becomes /reports?year=<YYYY> and the page rerenders.
  4. Verify the three KPI cards (Strung, Revenue, Mean turnaround) carry plausible values that match Stefan's memory of that year.
  5. Verify the Strung per month sparkline shows 12 bars; click any bar (e.g. month 6).
  6. URL becomes /reports?year=<YYYY>&month=6; the Month / Year drilldown panel renders with three KPI cards for that month.
  7. 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.

  1. On /reports, scroll to the Export section.
  2. Click Download XLSX — browser downloads stringer-<id>-<date>.xlsx (or similar name per the M20 spec).
  3. Open the file in Stefan's usual XLSX tool.
  4. Click Download JSON — browser downloads the JSON file.
  5. 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).
  • orders array 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); orders count 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.

  1. Navigate to the admin V1-upload page (URL TBD — Round 6 design names it).
  2. Upload the XLSX, ticking the dry-run checkbox.
  3. Wait for the upload + parse + reconciliation to complete; the report renders inline.
  4. Read the report Summary line; if OK, untick dry-run and re-upload to commit.
  5. 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.

  1. Sign in as the DE stringer.
  2. Walk through the dashboard, /orders/new, an existing order detail, the onboarding-complete page (if not already onboarded), and the magic-link invite copy.
  3. From /orders/new, create a new stringjob for a client whose Person default_locale = "de".
  4. Mark the order Strung; verify the receipt PDF rendered to the test inbox is receipt_de.html-rendered (DE strings throughout, DE date formatting).
  5. 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).
  6. 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 to Accept-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.

  1. Sign in as stringer-b.
  2. From stringer-b's dashboard, scan the Today and Recent lists — confirm none of stringer-a's orders appear.
  3. From stringer-b's /orders/new racket picker, search for a client name only stringer-a has — confirm zero results.
  4. Try direct-URL access to stringer-a's order detail: paste /orders/<a-order-uuid> into the address bar.
  5. Try stringer-a's export URL: /stringers/<stringer-a-id>/export.json.
  6. Try stringer-a's reports page: /reports while signed in as stringer-b (this is stringer-b's own reports page, but verify the numbers reflect stringer-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 of stringer-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.

  1. Navigate to the admin DSAR queue (/admin/dsar or the URL named in docs/design/admin-dsar-queue.md).
  2. 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).
  3. Wait for the export job to complete; download the resulting archive.
  4. 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 Person row, all associated ClientProfiles, all Orders, 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:

  1. 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.
  2. Capture the failure. Screenshot the page, copy any error text, note the URL and the steps to reproduce.
  3. File a blocking issue. Open a new GitLab issue with:
  4. Title: V2 launch blocker: <one-line summary of the failure>.
  5. Labels: area::v2-launch-blocker, priority::high, status::needs-triage, plus the matching area::* for the surface (e.g. area::receipt, area::admin, area::reports, area::auth, area::fadp, area::migration, area::i18n).
  6. Body: paste the failing TC heading from this plan, the screenshot / error text, and the reproducer steps.
  7. 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.
  8. 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.