Subagent audit caught 11 live-Odoo callsites on the user-facing request path; this batch retires every one. (1) Subscription page orders-this-month gauge swapped from odoo.searchCount("pos.order", ...) to a cafe.orders aggregate (the only unconditional live call left in the audit). (2) openShift + closeShift skip odooOpenPosSession / odooClosePosSession when cafe.tenant_config.bypass_odoo_pos=true — pre-cutover blocker for R11.5 closed. (3) POST /api/odoo/customers fully async via the existing R6.4 cron push queue; cafe.customers row inserts immediately with odoo_partner_id=NULL, returns {id:0, cafeCustomerId:<uuid>}; listPendingCreates race-gate left-joins cafe.customers + filters orders whose customer push hasn't synced — never silently drops the customer to walk-in. (4) All 7 odoo-fallback branches retired across products / customers / team / pos / register pages + manager_live + /api/pos-configs (every prod tenant is cafe-master, dormant branches were dead code holding the Odoo client in the bundle). (5) /api/odoo/test deleted — diagnostic route hardcoded to "getcoffee" tenant, no UI nav. Zero live Odoo I/O on the cashier-critical path for cafe-master tenants.
ff09319, +219/-403 across 21 files, 1 deletion).
SELECT COUNT(*) FROM cafe.orders WHERE created_at >= date_trunc('month', NOW()) AND state IN ('paid','partial_refund','refunded') against Supabase prod — proves the Odoo round-trip is gone./api/odoo/customers returns {id:0, cafeCustomerId:<uuid>, ...}; DB-side row shows odoo_partner_id NULL, source='native', retries=0 — pending the cron drain.isNotNull(odoo_partner_id) filter).bypass_odoo_pos defaults to false everywhere; behavior unchanged for current tenants. R11.5 cutover now unblocked.| Suite | Result | Notes |
|---|---|---|
| test-no-live-odoo-prod.mjs | 12/12 | This batch |
| test-phase1-prod.mjs | 11/11 | SSO + Cafe routes |
| test-phase2-sso-outdoor-prod.mjs | 6/6 | Outdoor SSO bridge |
| test-phase2-cafe-multishop-prod.mjs | 6/6 | Cafe multi-shop (demo creds — parallel-safe) |
| test-m1-prod.mjs | 10/10 | Shop scoping |
| test-r7-prod.mjs | 14/14 | Dashboard + manager-live drawer |
| test-r8-prod.mjs | 4/4 | Auth/security trio |
| test-r9-prod.mjs | 16/16 | Open Orders + per-line refund (worker queue touch) |
| test-r10-prod.mjs | 16/16 | Product variants (Pro POS customer/order path) |
/api/odoo/test route returns 500 to authenticated requests on prod instead of 404. OpenNext's edge runtime treats vanished routes (deleted from the build manifest) as 500 rather than emitting Next.js's static 404 page. Unauth requests still 307 cleanly via middleware. The route's old behavior (returning the tenant's Odoo config + partner/order counts) is gone — verified by asserting the body contains no apiKeySet, odoo, or database fields. Followup if the 500 ever surfaces in error monitoring: add a tombstone route file that returns 410 Gone.
loading…
loading…






nix-cafe / 21 files changed, 219 insertions(+), 403 deletions(-) — 1 deletion
lib/tenant.ts
+ TenantConfig.bypassOdooPos: boolean
+ getTenant() maps cafeConfig.bypassOdooPos
lib/db/orders.ts
+ countOrdersThisMonth(tenantId) — UTC month-start aggregate
app/(authed)/subscription/page.tsx
- odoo.searchCount("pos.order", ...) (CRITICAL — fired on every render)
+ countOrdersThisMonth(tenant.id)
lib/actions/register.ts
openShiftAction: skip odooOpenPosSession when bypassOdooPos=true
closeShiftAction: skip odooClosePosSession when bypassOdooPos=true
Telegram-notify path: drop fetchPosConfigs fallback, always read mirror
app/api/odoo/customers/route.ts
POST: drop synchronous Odoo res.partner create — return
{ id: 0, cafeCustomerId: <uuid>, name, email, phone }; cron worker
drains via existing R6.4 customer push queue
GET: drop fetchCustomers fallback, always read mirror
lib/db/customers.ts
MirrorCustomerSearchRow: + cafeCustomerId
searchCustomersFromMirror: drop isNotNull(odooPartnerId) filter so
pending rows surface in the picker (key = cafeCustomerId)
app/(authed)/pos/customer-picker.tsx
Customer interface: + cafeCustomerId; React key swap
app/(authed)/pos/_components/types.ts
ShellCustomer / OrderPayload.customer / ShellDraft.customer: + cafeCustomerId
app/(authed)/pos/_components/pro-register-mount.tsx
CustomerPicker bridge: thread cafeCustomerId through
app/(authed)/pos/_components/register-shell.tsx
Park/Draft customer object: round-trip cafeCustomerId
app/(authed)/pos/_adapters/pro-handlers.ts
submitOrder: send cafeCustomerId + odooPartnerId (null when push pending)
saveDraft / normalizeProDrafts: round-trip cafeCustomerId
lib/db/draft_orders.ts
DraftPayload.customer: + cafeCustomerId
lib/db/odoo_sync.ts
listPendingCreates:
+ leftJoin cafe.customers ON cafe.orders.cafe_customer_id
+ WHERE: skip rows whose customer is still pending
(cafe_customer_id IS NOT NULL AND cafe.customers.odoo_partner_id IS NULL)
Mapper prefers cafe.customers.odoo_partner_id over cafe.orders snapshot
app/(authed)/products/page.tsx
Drop ProPath (legacy Odoo-direct render); collapse to ProCafeMasterPath
app/(authed)/customers/page.tsx
Drop customersMaster=='odoo' fallback (fetchCustomers)
app/(authed)/team/page.tsx
Drop posMaster=='odoo' fallback (fetchPosConfigs)
app/(authed)/pos/page.tsx
Drop productsMaster + posMaster Odoo branches; collapse types
app/(pos-fullscreen)/pos/register/[configId]/page.tsx
Same — drop both Odoo branches
lib/db/manager_live.ts
loadManagerLiveData + buildLandingFromConfigs: drop posMaster=='odoo'
app/api/pos-configs/route.ts
Drop posMaster=='odoo' branch
app/api/odoo/test/route.ts
DELETED — diagnostic route hardcoded to "getcoffee" tenant
Pre-batch: every Pro+cafe-master /cafe/subscription page load fired a live Odoo searchCount RPC. Worst case Odoo RTT (Singapore → KH ~250ms + the Odoo query) added a quarter-second to every subscription render on top of the regular page weight. The 7 conditional fallbacks (products/customers/team/pos x2 + manager_live + 1 API route) were dormant for prod tenants — all three (get-coffee, Lumiere, demo) already flipped to *_master='cafe'. But the branches still typed against PosConfig from "@/lib/odoo/queries", forcing the Odoo client into every page import. Removal also shrinks the future audit surface (fewer files referencing lib/odoo). POST /api/odoo/customers was the last cafe-master write that still blocked the user on Odoo. For a New Customer save during checkout, the cashier waited on a synchronous res.partner create + customer_rank flag. Now the cafe.customers row inserts immediately, the picker returns, the cashier proceeds. The existing R6.4 cron worker drains the partner create in the background; the order's odoo_partner_id is resolved at order-push time via LEFT JOIN on cafe.customers in listPendingCreates. Worst-case effect: if the order push runs before the customer push has synced (both fire every 60s), the order holds back one tick (~60s) until odoo_partner_id is populated — never silently drops the customer to walk-in. bypass_odoo_pos honoring on openShift/closeShift was a pre-cutover blocker for R11.5: tenants flipped to bypass mode would otherwise still create a stray pos.session in Odoo on every shift open AND attempt pos.session.close on shift close — wasted RPC and inconsistent with the account.move-only posture the R11 arc was built for. With this in place, R11.5 cutover is now a clean SQL flip + verify-during-Narong's-shift sequence.
None blocking.
Operational:
- The *_master='odoo' flag columns + their plumbing in lib/tenant.ts +
the worker push queues can be dropped once Narong + accountant
confirm no tenant will ever revert. Kept this round for rollback
safety. Cost: ~3 columns + ~50 LOC. Best to bundle with R11.5
cutover review.
- Tombstone route at app/api/odoo/test/route.ts returning 410 Gone if
the OpenNext 500 ever surfaces in error monitoring. Currently
unreachable (no UI nav), but a curious authed user could trigger it.
- Picker draft round-trip preserves cafeCustomerId across park/resume.
Edge case still unhandled: cashier creates a customer, parks the
draft, hard-refreshes the browser, customer push lands in the
meantime. On resume, the draft's cafe_customer_id still points at
the now-mirrored row — the worker's LEFT JOIN finds odoo_partner_id
and the order pushes correctly. No follow-up needed; mentioning for
cutover review.