← All tests

No-live-Odoo cleanup — request path audit PROD

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.

12/12 prod checks PASS + 67/67 regression green. 1 cafe commit (ff09319, +219/-403 across 21 files, 1 deletion).

Regression sweep — 67/67 — sequential per R8.2 sid-rotation rule

SuiteResultNotes
test-no-live-odoo-prod.mjs12/12This batch
test-phase1-prod.mjs11/11SSO + Cafe routes
test-phase2-sso-outdoor-prod.mjs6/6Outdoor SSO bridge
test-phase2-cafe-multishop-prod.mjs6/6Cafe multi-shop (demo creds — parallel-safe)
test-m1-prod.mjs10/10Shop scoping
test-r7-prod.mjs14/14Dashboard + manager-live drawer
test-r8-prod.mjs4/4Auth/security trio
test-r9-prod.mjs16/16Open Orders + per-line refund (worker queue touch)
test-r10-prod.mjs16/16Product variants (Pro POS customer/order path)
Known cosmetic, not a regression: the deleted /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.

POST /api/odoo/customers — async response shape (probe output)

loading…

cafe.customers row — pending state in DB (probe output)

loading…
logged in
01-logged-in
subscription
02-subscription (gauge from cafe.orders)
products
03-products
customers
04-customers
team
05-team
pos
06-pos

What ships in one commit

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

Why this matters

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.

Followups (not in this ship)

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.