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.
20260514110000_cafe_pos_configs_push_queue.ts): adds odoo_sync_retries INTEGER NOT NULL DEFAULT 0 + odoo_next_attempt_at TIMESTAMPTZ NULL on cafe.pos_configs; partial index cafe_pos_configs_pending_push_idx on the drain selector.lib/odoo/pos_config.ts): fetchTemplatePosConfig + buildPosConfigCreatePayload (pure-fn — clones m2o/m2m from template, overrides name) + createPosConfigForRegister (search_read + create).lib/db/pos_configs_push.ts): tenantsWithPendingPosConfigPushes, listPendingPosConfigPushes, markPosConfigPushed, markPosConfigPushFailed (exponential backoff 1/5/15/60/240 min, dead-letter at 5 retries)./api/cafe/cron/odoo-sync alongside the orders / partial-refunds / session-move drains. Per-tenant batch size 10. Existing tenant union extended with pos-config push tenants. TenantResult + totals reducer extended with pending/synced/failedPosConfigPushes counts.cafePosConfigs table gains odooSyncRetries + odooNextAttemptAt.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).
✓ 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 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.
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)