← All tests

Odoo pos.config push connector (local)

2026-05-14 Gate 1 local. Multi-register arc follow-up: NIX → Odoo push for new Pro registers. Bundle 2 created Pro pos_configs in NIX with sync_state='pending_create'; this connector drains them via the existing odoo-sync cron tick (~1min). On success it stamps odoo_pos_config_id back + flips to 'synced'; on failure it dead-letters at 5 retries with 'sync_error'. Uses clone-from-existing for Odoo field defaults (journal / picking_type / pricelist / payment_methods / company copied from the tenant's first existing pos.config), so a new register is functional in Odoo immediately with no backoffice setup.

6/6 local probe PASS. Typecheck clean.

Drain flow

1. Admin creates a register via /cafe/settings/registers on a Pro tenant.
   Bundle 2's createRegister inserts cafe.pos_configs with sync_state='pending_create'.

2. Cron fires every minute → /api/cafe/cron/odoo-sync.

3. Worker fetches tenants with pending pos.config pushes; for each, lists
   pending rows (up to 10 per tick) where retries < 5 AND
   (next_attempt_at IS NULL OR next_attempt_at <= NOW()).

4. For each pending row:
     a. fetchTemplatePosConfig(odoo) — pull the first existing active
        pos.config to clone settings from.
     b. buildPosConfigCreatePayload(template, register.name) — clone the
        Odoo many2one fields (journal_id, picking_type_id, pricelist_id,
        company_id, currency_id, invoice_journal_id, stock_location_id)
        + many2many fields (available_pricelist_ids, payment_method_ids)
        with the [(6, 0, [ids])] Odoo command shape. Override `name`.
     c. odoo.call({ model:'pos.config', method:'create', args:[payload] })
        → returns the new Odoo id.
     d. markPosConfigPushed(id, odooId) → flips sync_state='synced',
        sets odoo_pos_config_id, clears retries + error.

5. On any failure (no template; create rejected; transient Odoo timeout):
   markPosConfigPushFailed(id, retries, errorMessage) → bumps retries,
   sets next_attempt_at with backoff, stores error. At retries=5,
   flips sync_state='sync_error' + clears next_attempt_at (dead-letter,
   operator inspects + fixes).

Local probe — 6/6

✓ cafe.pos_configs.odoo_sync_retries column exists (NOT NULL default 0)
✓ cafe.pos_configs.odoo_next_attempt_at column exists (NULL)
✓ cafe_pos_configs_pending_push_idx partial index exists
✓ DAO round-trip — insert pending row, listPending finds it, markPushed flips
  state, markFailed bumps retries + dead-letters at 5
✓ buildPosConfigCreatePayload pure-fn check (clones m2o + m2m, overrides name)
✓ Sparse-template fallback (handles missing many2one fields gracefully)

Local diagnostic:
  pos_configs_total = 6   (all Starter, no Pro Odoo present locally)
  pending_create   = 0
  sync_error       = 0

⚠ Local can't round-trip to Odoo

Local dev DB has no Pro tenant with Odoo creds, so the actual
search_read + create against a live Odoo instance is unverified here.
DAO + payload logic is fully covered by the probe; the Odoo I/O round-
trip happens at Gate 2 against get-coffee's real Odoo.

Gate 2 plan:
  1. Apply prod migrate.
  2. Push code.
  3. Open wrangler tail on the main Cafe Worker.
  4. Create a register named "PUSH-TEST" on get-coffee via
     /cafe/settings/registers (Pro tenant with live Odoo).
  5. Wait for next cron tick (≤60s).
  6. Verify:
       a. Worker tail shows the pos.config push attempt + new Odoo id.
       b. cafe.pos_configs row flipped to sync_state='synced' with
          odoo_pos_config_id set.
       c. Odoo backoffice (get-coffee's instance) shows the new
          pos.config with cloned settings from the template register.
  7. Cleanup: archive the test pos.config in Odoo OR keep it (operator
     decides).
  8. Full regression sweep.

Files touched

nix-outdoor-sales-backend:
  migrations/20260514110000_cafe_pos_configs_push_queue.ts  (new)
  migrate.js                                                 (entry added)

nix-cafe:
  lib/db/schema.ts                  (cafePosConfigs: 2 new columns)
  lib/db/pos_configs_push.ts        (new — queue DAOs)
  lib/odoo/pos_config.ts            (new — pos.config.create wrapper)
  app/api/cafe/cron/odoo-sync/route.ts (drain folded into per-tenant loop)