← All tests

Multi-category products STAGING

Narong 2026-07-21 — the prerequisite for concurrent promotions. Verified on lumiere.staging 6/6 + DAO probe 8/8 + staging smoke 5/5.

Why. To run a BOGO on some drinks and a % Discount on others at the same time, a product must be able to belong to more than one category at once. Narong: "please allow Product/Menu Item to be assigned to Multiple Categories — an item can be in Coffee Category and under BOGO Drinks."

The model

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.

1 · Editor — assign additional categories

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

editor additional category persisted
"BOGO Drinks" added as an additional category and persisted across a reload — the primary category is untouched.
POS latte freed via secondary tag
POS: a BOGO scoped to "BOGO Drinks" frees a Latte whose primary is "MC Coffee" — total $7 (would be $12 if only the primary counted).

2 · POS — eligibility via a secondary category

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:

ItemPrimaryAlso inEligible for BOGO(BOGO Drinks)?
BOGO Test Latte $5 ×2MC CoffeeBOGO Drinks✓ — 1 freed
BOGO Test Tea $2MC 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.

Checks

DAO probe (real DAO vs staging DB) — membership set + intersection eligibility8/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 eligiblepass
Migration on nix_db_staging + backfill (0 primaries missing)pass
Staging smoke5/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).