2026-05-14 Gate 1 local. Multi-register arc follow-up: NIX → Odoo write-back for register renames and deactivations. The create connector already pushes new Pro registers to pos.config.create; this adds the missing update half. When an admin renames or deactivates a register that already lives in Odoo, the DAO flips sync_state='pending_update'; the existing odoo-sync cron tick drains it via pos.config.write({name, active}) within ~60s. Same retry/backoff/dead-letter shape as the create queue. Sequence-prefix changes stay NIX-only — Odoo's pos.config has no such field and NIX already overrides order names in the R5 push payload.
20260514130000_cafe_pos_configs_pending_update_index.ts): widens the partial drain index cafe_pos_configs_pending_push_idx from sync_state = 'pending_create' to sync_state IN ('pending_create','pending_update'). No column changes — sync_state is already varchar(32) and accepts the new value.lib/odoo/pos_config.ts): adds buildPosConfigUpdatePayload (pure-fn — name / active, drops empties) + updatePosConfigInOdoo (pos.config.write([id], payload)).lib/db/pos_configs_push.ts): tenantsWithPendingPosConfigUpdates, listPendingPosConfigUpdates, markPosConfigUpdateSynced, markPosConfigUpdateFailed (shared backoff 1/5/15/60/240 min, dead-letter at 5). Plus a divergence check baked into markPosConfigPushed.lib/db/pos_configs.ts): renameRegister + setRegisterActive now run in a transaction — local write, then queueOdooUpdate flips sync_state='pending_update' for rows that have an odoo_pos_config_id and aren't pending_create. setRegisterPrefix deliberately unchanged./api/cafe/cron/odoo-sync gains a pending_update drain loop right after the pending_create one. Tenant union + TenantResult + totals reducer all extended with the update counts.synced ──(rename / deactivate, has odoo id)──▶ pending_update
│
cron tick: pos.config.write │
┌──────────────────────┤
success failure
│ │
▼ ▼
synced retries++ , backoff
│
retries == 5 ?
┌────────┴────────┐
no yes
│ │
pending_update sync_error
(re-queued if the
admin edits again)
Guard rails on the action-side flip (queueOdooUpdate):
• odoo_pos_config_id IS NULL → no-op. Starter rows + Pro registers
whose create hasn't landed have nothing to write back to.
• sync_state = 'pending_create' → left alone. The create connector
reads the live name itself, and markPosConfigPushed's divergence
check re-queues an update if name/active drifted mid-flight.
• sync_state = 'sync_error' WITH an odoo id → re-queued. A dead-lettered
*update* gets a fresh 5 attempts when the admin edits again. (A
dead-lettered *create* has a NULL odoo id, so it's left for the
operator.)
Race: admin clicks "Add register" (→ pending_create), then renames or deactivates it before the cron tick pushes the create. The rename DAO leaves pending_create alone (mustn't clobber the create signal), so the name/active change would otherwise be lost on the Odoo side. Fix: markPosConfigPushed now takes the name it actually pushed. After the create lands it re-reads the row inside the same transaction: diverged = row.name !== pushedName || row.isActive === false sync_state = diverged ? 'pending_update' : 'synced' So a mid-flight edit is caught and reconciled on the next tick. The connector always creates an *active* pos.config, so a NIX-side is_active=false counts as divergence too.
✓ Partial drain index covers sync_state='pending_update' ✓ renameRegister: synced+pushed register → pending_update, retries reset, name written ✓ setRegisterActive: synced+pushed register → pending_update, is_active written ✓ setRegisterPrefix: NIX-only — does NOT flip sync_state ✓ renameRegister: pending_create + Starter rows (no odoo id) left untouched ✓ renameRegister: dead-lettered update (sync_error WITH odoo id) is re-queued ✓ update-queue DAO round-trip — list/tenants finders, markUpdateSynced, markUpdateFailed dead-letters at 5 ✓ markPosConfigPushed divergence — rename/deactivate during create-flight re-queues as pending_update ✓ buildPosConfigUpdatePayload — pure-fn: name only / active only / both / neither Local diagnostic: pos_configs_total = 6 (all Starter, no Pro Odoo present locally) pending_create = 0 pending_update = 0 sync_error = 0 synced = 6
Local dev DB has no Pro tenant with Odoo creds, so the actual
pos.config.write against a live Odoo instance is unverified here.
The NIX-side queue mechanics are fully covered by the probe; the Odoo
I/O round-trip happens at Gate 2 against get-coffee's real Odoo.
Gate 2 doubles as the PUSH-TEST cleanup. get-coffee's Odoo still has
the stale pos.config.id=11 ("PUSH-TEST-…") left over from the create
connector's end-to-end verification. The prod test uses it as the
fixture:
1. Apply prod migrate (index widen) + push code.
2. Open wrangler tail on the main Cafe Worker.
3. Find the NIX cafe.pos_configs row for Odoo id 11; rename it to
"z-archived-test-register-2026-05-14" via /cafe/settings/registers.
4. Wait for the cron tick (≤60s). Verify:
a. Worker tail shows the pos.config.write attempt.
b. cafe.pos_configs row flipped back to sync_state='synced'.
c. Odoo backoffice shows pos.config.id=11 renamed.
5. Deactivate the same register in NIX.
6. Wait for the next tick. Verify Odoo's pos.config.id=11 now has
active=false (archived).
7. End state: get-coffee's Odoo has a clearly-disposable archived
register; operator can hard-delete in backoffice if desired.
8. Full regression sweep (51/51).
nix-outdoor-sales-backend: migrations/20260514130000_cafe_pos_configs_pending_update_index.ts (new) migrate.js (entry added) nix-cafe: lib/odoo/pos_config.ts (+ buildPosConfigUpdatePayload, updatePosConfigInOdoo) lib/db/pos_configs_push.ts (+ update-queue DAOs, markPosConfigPushed divergence check) lib/db/pos_configs.ts (renameRegister + setRegisterActive flip sync_state) app/api/cafe/cron/odoo-sync/route.ts (+ pending_update drain loop)