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.
/cafe/uploads/... route serves the SVG bytes back./cafe/display/{sessionId}?secret=... as a separate tab and watches it flip from idle → cart in real time as the register tab adds a product to the cart./orders row; GET /api/cafe/orders/{id} returns full lines + payments + discount.subtotal_usd=7, discount_usd=0.70, total_usd=6.30, two order_payments; daily report renders the discount metric.| 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 |
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
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.
• 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.