Narong 2026-07-21 — the prerequisite for concurrent promotions. Verified on lumiere.staging
6/6 + DAO probe 8/8 + staging smoke 5/5.
product_template.category_id stays the primary category — it still drives the POS
menu tab, the sales-by-category report, and the Odoo mirror, so nothing on the live menu changes. A new join
table cafe.product_template_categories (mirroring promotion_categories, backfilled from
the current primary) holds the full membership set (primary + extras). Promotion eligibility now
reads the union and tests set intersection instead of a single equality. Odoo is read-only for
categories (its pos_categ_ids is already an array), so Pro sync is unaffected. One migration.
The product form gains an Additional categories field (searchable picker + removable chips, reusing the inline-create combobox). The primary category picker above is unchanged. Adding a category persists to the join table; the membership read runs in a transaction so a reload reflects the save immediately (past Hyperdrive's cache).


Cart = 2 Latte $5 + 1 Tea $2 (gross $12). The Latte is in MC Coffee (primary) and
BOGO Drinks; the Tea is in MC Coffee only. A BOGO scoped to BOGO Drinks:
| Item | Primary | Also in | Eligible for BOGO(BOGO Drinks)? |
|---|---|---|---|
| BOGO Test Latte $5 ×2 | MC Coffee | BOGO Drinks | ✓ — 1 freed |
| BOGO Test Tea $2 | MC Coffee | — | ✗ — not eligible |
Result: total $7 (one Latte freed via its secondary tag), the Tea charged. Old single-category logic would find nothing eligible (Latte's primary ∉ scope) → $12.
| DAO probe (real DAO vs staging DB) — membership set + intersection eligibility | 8/8 |
| Editor: add additional category → save → persists on reload (+ DB row) | pass |
| Editor: remove additional category → save → gone (+ DB row removed) | pass |
| POS: Latte freed via secondary "BOGO Drinks" tag → total $7 (not $12) | pass |
| POS: Coffee-only Tea NOT eligible | pass |
Migration on nix_db_staging + backfill (0 primaries missing) | pass |
| Staging smoke | 5/5 |
nix-cafe staging 3611cb0 + nix-OS-backend
staging af47134 (migration 20260721120000, backfilled). Starter tenant —
BOGO eligibility resolves cafe-native product UUIDs. Parked for the scheduled prod promote (needs
node migrate.js on prod).