← All tests

Odoo pos.config rename/deactivate round-trip (local)

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.

9/9 local probe PASS. Both repos typecheck clean. No data migration — one index widen.

State machine

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.)

Create-flight divergence check

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.

Local probe — 9/9

✓ 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 can't round-trip to Odoo — Gate 2 plan

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).

Files touched

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)