Two unrelated unblocked-list items shipped as one Gate 2 bundle on 2026-05-11. Outdoor local auth was broken on docker since R3.6δ.3 added refresh_tokens.tenant_user_id NOT NULL but RefreshTokenDAO.create never inserted it — push-to-prod-then-screenshot was the workaround. Cafe's session-move aggregator was preventively miscounting same-session partial refunds (partialRefundOrder doesn't adjust cafe.orders.total_usd or write to cafe.order_payments; the refund lives only on cafe.order_refunds) — would have overstated Sales credit by the refund amount the moment bypass_odoo_pos=true on R11.5 cutover. Both verified end-to-end locally; cafe additionally verified via read-only diagnostic probe against Supabase prod.
d167ff1 (nix-outdoor-sales-backend) — RefreshTokenDAO.create accepts tenantUserId + cached hasColumn probe + outdoor_employees fallback. Login + refresh paths thread (user as any).tenant_user_id. No migration.581f2bb (nix-cafe) — Same-session partial-refund reconciliation in aggregateSalesAndPaymentsForSession: subtract refund total from gross + first cash bucket when order in this session AND state='partial_refund'. New diagnostic counters on SessionMoveAggregate. Two misleading pre-existing comments corrected. No migration.POST /auth/login owner@demo.com (X-Tenant-Code: demo) → 200 with accessToken + refreshToken → refresh_tokens row 4869 carries tenant_user_id = c975561b-aa50-4556-bd85-09c6e9d3ebf7 ✓ POST /auth/refresh (with issued refreshToken) → 200 with new pair → old row 4869 revoked, new row 4870 carries tenant_user_id ✓ → rotation works correctly
scripts/test-same-session-partial-refund-local.ts baseline aggregate on lumiere session 50cc3010 → inject $20 paid order + $20 cash payment + $5 partial refund → re-aggregate, diff against baseline gross delta: +15.00 (expect +15.00) ✓ cash bucket delta: +15.00 (expect +15.00) ✓ sspr.count delta: +1 (expect +1) ✓ sspr.total delta: +5.00 (expect +5.00) ✓ PASS — same-session partial-refund aggregator deltas all correct.
| Tenant | Session | Closed | Refund rows | Raw $ | Aggregator picked up |
|---|---|---|---|---|---|
| get-coffee | ec65b1b1… | 2026-05-10T14:05 | 1 | $0.00 | 0 / $0.00 |
| get-coffee | c6b695da… | 2026-05-10T13:39 | 1 | $0.00 | 0 / $0.00 |
| get-coffee | 7a56036e… | 2026-05-09T15:08 | 1 | $0.00 | 0 / $0.00 |
| get-coffee | f15affc8… | 2026-05-09T14:49 | 1 | $0.00 | 0 / $0.00 |
| get-coffee | d7e236ae… | 2026-05-09T13:57 | 1 | $0.00 | 0 / $0.00 |
All 5 hits are R10 modifier-line voids — e.g. POS07-1004: $2.50 bread order with a free "50% Sugar" modifier (qty=2, subtotal=$0). Cashier voided the modifier → refund_total=$0, order stays partial_refund because the bread line is still unrefunded. Aggregator correctly drops them via the amount <= 0 guard. Zero $ would have been mishandled on cutover. Useful sample for the accountant review: expect state='partial_refund' rows with refund.total_usd = $0 in the data; that's the normal modifier-void pattern, not a reconciliation issue.
| Suite | Result | Tenant | Notes |
|---|---|---|---|
| test-phase1-prod.mjs | 11/11 | get-coffee | 10/11 first run (SSO landing race) → 11/11 solo retry |
| test-phase2-sso-outdoor-prod.mjs | 6/6 | get-coffee | Outdoor SSO bridge |
| test-phase2-cafe-multishop-prod.mjs | 6/6 | demo | Parallel-safe (distinct tenant) |
| test-m1-prod.mjs | 10/10 | get-coffee | Shop scoping |
| test-r7-prod.mjs | 14/14 | get-coffee + lumiere | Dashboard + manager-live |
| test-r8-prod.mjs | 4/4 | get-coffee | Auth/security trio |
51/51 total prod checks — no regressions from this push.
bypass_odoo_pos=true on get-coffee, the session-move aggregator runs for every session close. Without this fix, a cashier ringing a paid order and refunding a line during the same shift would have caused the resulting account.move to credit Sales for the full original gross + debit AR/PoS for the full original cash — silently overstating both by the refund amount. With this fix, the move correctly nets to the post-refund cash actually in the drawer. Pre-cutover the bug was latent (no tenant in bypass mode); post-cutover it would have been first-day-visible.