← All tests

POS × Loyalty — member scan, earn on close, coupon redeem STAGING

The gap this closes: nix-cafe had no call to the loyalty backend’s /pos/* routes, so a purchase never earned points — a member only gained points from the signup bonus and spin rewards, and coupons could not be redeemed at the till. Narong asked for the scan to live in the existing Select Customer dialog, and for the Customers page to list registered app users with Points and Chances. Both are here.

Gate 2 (staging) 15/15 — Pro flow on get-coffee.staging.nixtech.app, Starter on lumiere-coffee.staging.nixtech.app, against the staging loyalty backend. Scan → attach → apply coupon ($2 → $1) → Pay → points 13 → 14, coupon used, order row loyalty_earn_status=earned. Staging smoke 5/5 (no regression). nix-cafe 4f36c63, loyalty backend ee50e93, nix-db migration 20260710120000, app 8f9ffc5. Parked on staging for the scheduled prod promote.

Checks:

loyalty staging: member resolves with points + spendable chances (a processing chance does not count)
Customers page lists App Members with Points and Chances columns
Pro register opens with products · item added to cart
Select Customer dialog opens from the cart’s Scan member CTA
typing/scanning the 32-char member QR + Enter attaches the member (wedge-scanner path)
the member’s coupon is offered and applies as a 100% discount line ($2 → $1)
Pay closes the order · no 5xx anywhere in the flow
points earned on the closed order (13 → 14) — the thing that never happened before
coupon burned after commit (status used, delisted) — never before, since redeem has no un-redeem
cafe.orders row records member id, coupon id, points_earned=1, earn_status=earned
Starter: Odoo customer attach stays Pro-locked, but Scan member works (loyalty is not Odoo-coupled)
Starter member attaches by scanned QR (25 pts) · staging smoke 5/5

Narong’s two asks

Customers page App Members section
Customers › App Members — registered app users with Points and Chances
Member attached at the till
Member QR scanned into the Select Customer search box — attaches with points + chances

Coupon as a 100%-discount line, then paid

Coupon applied to the cart
The member’s Free Latte applied — one unit discounted to zero, cart total $2 → $1
Order paid
Order closed — points earned and the coupon burned after the transaction commits

Starter tier — loyalty is not a Pro feature

Starter cart
Starter: “Add customer” stays Pro-locked, “Scan member” is available
Starter member attached
Starter member attached (25 pts) — Starter tenants earn points too

Earn and redeem run after the order transaction commits and are not awaited: the loyalty backend is scale-to-zero on Fly, so a cold call must never delay a cashier or fail a paid sale. The order carries loyalty_earn_status='pending' until the call reports back, so points lost to a timeout are recoverable (a retry sweep is not yet built). Coupon redeem has no un-redeem, which is why it fires post-commit.

nix-cafe customer-picker.tsx, member-attach-row.tsx, qr-camera-scanner.tsx, register-shell.tsx, api/cafe/orders/route.ts, api/loyalty/members/route.ts (4f36c63) · nix-loyalty-backend routes/pos.ts + routes/admin.ts (ee50e93) · nix-db 20260710120000_cafe_orders_loyalty_columns · nix-loyalty-app coupon QR (8f9ffc5) · harness test-pos-loyalty-staging.mjs.
Not covered: the camera scanner (no camera in headless) — the wedge/typed path is what the tests drive.