Second + third phases of the H4 Product/Variant rework, bundled into one Gate cycle. Lights up the
seven new cafe.product_* tables that Slice P1 created dark (2026-05-19 earlier same
day). The R10 plural tables stay authoritative for POS reads until H4-P4 ships the read-path cutover.
028ed27 on
nix-cafe. New /cafe/settings/attributes admin with attribute + value +
template + per-template attribute-line CRUD + Instant/Never create_variant picker.
Cartesian variant generator verified end-to-end on prod (lumiere-coffee created an attribute
via UI, wired a template line with 2 values, asserted 2 product_product rows +
2 combo rows in the DB).
Code (nix-cafe 028ed27) — 5 new files (DAO, actions, page,
client, backfill script) + 1 modified (settings-nav adds "Attributes (new)" entry). No
migration in this slice (Slice P1 already created the 7 tables). No new dependencies.
Data backfill — scripts/backfill-h4-p3-product-variant.ts ran
against prod Supabase from local. Four idempotent passes mirror R10 + cafe.products
into the singular tables, matched on natural keys:
Final row counts on prod after backfill (verified via direct SQL probe):
Playwright drove the full create-attribute → add-values → create-template → wire-line →
assert-variants flow with TAG-prefixed test data. All in-place visibility (Slice K pattern
extended to this new client) — no page.goto() fallback needed.
Duplicate product names on get-coffee → 9 templates from 17 products.
The backfill collapses by (tenant, name). get-coffee's
cafe.products has these dup-named rows:
These duplicates are presumably real Odoo variant rows that today share a name. The H4-P4
read-path cutover will need to either: (a) recognise duplicate-named products as variants of
one template + emit multiple product_product rows; (b) treat them as distinct
templates and de-dup by Odoo product.product.id instead of name; or (c) merge
admin-side. Tracked as a P4 prereq, not a regression for P2+P3. POS reads still
come from R10 plural until P4, so customer-facing menus are unaffected.
/cafe/settings/attributes renders both sections (attributes + templates)createVariant='instant' + Instant badge visiblehas_variant=true/cafe/settings/attributes renders, templates section shows 9 cardsRaw: result.json
/cafe/settings/attributes route is affected.
| test-phase1-prod.mjs | 11/11 |
| test-phase2-sso-outdoor-prod.mjs | 6/6 |
| test-phase2-cafe-multishop-prod.mjs (parallel on demo) | 6/6 |
| test-m1-prod.mjs | 10/10 |
| test-r7-prod.mjs | 14/14 |
| test-r8-prod.mjs | 4/4 |
| Plus prod feature test (above) | 8/8 |
| Total | 59/59 |
H4-P4 — POS read-path cutover. POS picker reads from
cafe.product_template + product_product + product_variant_combo
instead of R10 tables. cafe.products becomes a view or aliased. Must resolve the
dup-name collision on get-coffee before this ships (otherwise 8 products disappear from the menu).
H4-P5 — drop the R10 plural tables once nothing reads them. Pure cleanup slice.
New:
nix-cafe/lib/db/product_attribute.ts ·
nix-cafe/lib/actions/product_attribute.ts ·
nix-cafe/app/(authed)/settings/attributes/page.tsx ·
nix-cafe/app/(authed)/settings/attributes/attributes-client.tsx ·
nix-cafe/scripts/backfill-h4-p3-product-variant.ts
Modified:
nix-cafe/app/(authed)/settings/_components/settings-nav.tsx
(one entry added)
Migrations: none (Slice P1 already created the 7 tables).
Dependencies: none.