← All tests

R2 follow-ups PROD

Bundle of 5 small/medium follow-ups on top of the R2 Starter MVP — built and verified end-to-end on get-coffee in Starter mode. UploadButton wired into Starter products, multi-shop selector + Customer Display BroadcastChannel sync in Starter register, native receipt printing for both register success and orders list, and discount + multi-payment splits in the pay dialog. Plan flip + restore, real R2 upload of an SVG, real BroadcastChannel cart sync visible on the public Customer Display, real DB ground-truth assertion on cafe.orders.discount_usd = 0.70.

19/19 prod checks passed.
52/52 total prod tests green — no regressions from this push.
test-r2-followups-prod.mjs (this bundle)19/19
test-nix-os-r2-4b-prod.mjs (Starter register UI)12/12
test-nix-os-70-2-prod.mjs (R2 uploads)10/10
test-phase1-prod.mjs (route smoke)11/11

What's new — code surface

nix-outdoor-sales-backend (commit a55a00a):
  migrations/20260426100000_cafe_orders_discount.ts   — add cafe.orders.discount_usd
  migrate.js                                          — idempotent prod runner entry
  Applied to prod via: DATABASE_URL=... node migrate.js
  → "+ Added cafe.orders.discount_usd (default 0)"

nix-cafe (commit 9d885ca):
  components/upload-button.tsx                        — shared file picker (logo / promo / product-image)
  lib/native-receipt.ts                               — buildNativeReceipt + openPrintWindow
  lib/db/schema.ts                                    — discountUsd on cafeOrders
  lib/db/orders.ts                                    — createNativeOrder + aggregateNativeDailyReport accept/return discount
  app/api/cafe/uploads/route.ts                       — accept "product-image", per-kind perm check
  app/api/cafe/orders/route.ts                        — POST accepts discountUsd
  app/api/cafe/orders/[orderId]/route.ts              — NEW: GET full order detail
  app/(authed)/products/starter-products-client.tsx   — UploadButton next to image URL field
  app/(authed)/pos/starter-register-page.tsx          — read shop cookie, pass shops + storeInfo + cashier + displaySecret
  app/(authed)/pos/starter-register-client.tsx        — shop picker, Display button, BroadcastChannel,
                                                        rebuilt PayDialog with discount + multi-payment,
                                                        Print button on success banner
  app/(authed)/orders/page.tsx                        — pass storeInfo + cashier to Starter client
  app/(authed)/orders/starter-orders-client.tsx       — Print button per row (calls GET /api/cafe/orders/:id)
  app/(authed)/reports/daily/starter-daily-client.tsx — show − $X discounted under Avg Ticket
  app/(authed)/settings/display/display-branding-client.tsx — switch to shared UploadButton

Bugs caught mid-Gate-2

1. Stale R2.4b regression — the previous Starter register prod test had hardcoded
   "pay-received" / "pay-change" testids. The PayDialog rebuild for M2 renamed
   them to per-row variants ("pay-received-0", etc.). First Gate-2 run was 7/12
   on the regression. Updated test-nix-os-r2-4b-prod.mjs to use the new testids;
   that's not just a test change — it's the contract update for the new
   multi-payment shape, and any future tests should follow suit.

2. First-click hydration race on /cafe/products — clicking new-product-btn
   immediately after page load on a cold worker was queued by React for ~30s.
   The 70-2 test had the same workaround; copied the pattern: waitForLoadState
   "load" + 1.5s settle window before the first click. Open-shift submit had
   the same race; same fix applied. (project_playwright_hydration_clicks.md)

3. Orphan testid issue — both bugs above only surfaced in the warm-worker
   re-run; cold-start latency masked the testid mismatch by triggering generic
   timeouts. Took the second run to triangulate.

Operator follow-ups (not blocking)

• No image cleanup yet — same R2 GC follow-up flagged in 70.2 still applies.
  product-image keys live forever once written; GC job is a future polish.
• Per-line discount — only order-level discount today. line.unitPriceUsd stays
  at the menu price. Per-line discount UX can come later when needed.
• Multi-shop selector — get-coffee has 1 shop today, so the picker collapses
  to a label. The dropdown logic exists and will activate the moment a tenant
  has 2+ shops. Cpanel-side shop provisioning is the gating step.
• Receipt print popup — opens a new window via openPrintWindow. Browsers may
  block popups by default; operator may need to allow popups for the cafe
  origin. Same behavior as the Pro receipt path.