← All tasks

H4-X — All 5 H4-adjacent items bundled PROD · GATE 2

Closes the H4 arc completely. View-ifies cafe.products; migrates order_lines.product_id FK; refactors all DAOs that wrote to cafe.products into singular-table writes; adds name_kh on attribute model; adds per-variant edit UI; adds global branded 404.

🎉 H4 arc fully done. Six slices shipped 2026-05-19 → 2026-05-20: P1 schema → P2+P3 admin UI + R10 backfill + Cartesian generator → P4 POS read-path cutover + multi-variant backfill → P5 retire R10 + cafe.products sync hooks → X view-ify cafe.products + all H4-adjacent polish. NIX Cafe product model is now end-to-end Odoo-shape singular tables with cafe.products as a backwards-compat VIEW.
8/8 prod test · 51/51 regression green · 59/59 total. Backend 0b8aaa7 + nix-cafe 0482893. Migration applied to prod Supabase via node migrate.js — 5 phases A-E completed atomically. 35,084 cafe.order_lines rows had their product_id retargeted from cafe.products → cafe.product_product. DAO probe verified createProduct/updateProduct/deleteProduct round-trip through the new view + per-variant updateVariant + Khmer Unicode name_kh round-trip (កម្តៅ / ក្តៅ).

Prod state post-H4-X

cafe.products VIEW ← (was BASE TABLE) cafe.product_template BASE TABLE (canonical, holds per-template metadata) cafe.product_product BASE TABLE (canonical, holds per-variant metadata + cafe_product_id back-ref) cafe.product_attribute BASE TABLE (+ name_kh) cafe.product_attribute_value BASE TABLE (+ name_kh) cafe.order_lines.product_id FOREIGN KEY → cafe.product_product(id) ON DELETE SET NULL (was → cafe.products(id); migrated 35,084 rows) Row counts: lumiere-coffee 25 templates / 25 variants (unchanged from H4-P4) get-coffee 9 templates / 17 variants (unchanged from H4-P4) demo 0 / 0

DAO probe on prod (full round-trip)

On prod lumiere-coffee, TAG-prefixed throughout, cleaned up at end: 1. createProduct("H4XPROBE_Espresso", $3.50) → cafe.product_template "H4XPROBE_Espresso" created → cafe.product_product with priceUsd=3.50 created ✓ create 2. SELECT FROM cafe.products WHERE id = (new product_product.id) → view returns the joined row (name=H4XPROBE_Espresso, priceUsd=3.50) ✓ view read-back 3. updateProduct(rename to "H4XPROBE_Macchiato") → new template "H4XPROBE_Macchiato" created → variant.productTmplId migrated to new template → old empty template deleted ✓ rename 4. updateProduct(price="4.25") → product_product.priceUsd updated → view reflects new price ✓ price update 5. updateVariant({defaultCode: "MAC-PROD-001", priceUsd: "4.75"}) → both fields updated on product_product row ✓ per-variant edit 6. deleteProduct(...) → product_product row deleted → empty template auto-cleaned ✓ delete + cleanup 7. createAttribute({name: "H4XPROBE_Temp", nameKh: "កម្តៅ", createVariant: "never"}) createAttributeValue({value: "Hot", nameKh: "ក្តៅ", priceExtraUsd: 0}) → Khmer Unicode round-trips cleanly through Postgres ✓ nameKh Steady-state check: lumiere template count = 25 (unchanged from pre-probe). Cleanup ran clean.

Screenshots

/cafe/settings/attributes post-H4-X with name_kh fields
02 · /cafe/settings/attributes — Attribute modal now has Khmer name field

Checks — 8/8 prod

Raw: 01-prod-dao-probe.json · result.json

51/51 regression green — no behavioural regressions from this push. Order history reads work through the view; POS + Reports + auth boundary + R8 all clean.
test-phase1-prod.mjs11/11
test-phase2-sso-outdoor-prod.mjs6/6
test-phase2-cafe-multishop-prod.mjs (parallel on demo)6/6
test-m1-prod.mjs10/10
test-r7-prod.mjs14/14
test-r8-prod.mjs4/4
Plus prod feature test (above)8/8
Total59/59

Mid-Gate-2 note (resolved)

First prod-test run had 7/8 — the /cafe/settings/attributes attr-form check timed out waiting for the new attr-name-kh testid. Re-ran after a 60-second delay (CF Pages deploy was still propagating the new client bundle) and got 8/8. The backfill + FK migration + view recreation completed on the migrate.js run; only the client bundle deploy lagged.

H4 arc — complete

P1 (2026-05-19) — schema (7 dark singular tables)
P2+P3 (2026-05-19→20) — Attributes admin + R10/products backfill + Cartesian generator
P4 (2026-05-20) — POS read-path cutover + multi-variant backfill
P5 (2026-05-20) — Retire R10 + cafe.products sync hooks
X (2026-05-20) — View-ify cafe.products + name_kh + per-variant edit + not-found + reverse sync via direct singular writes

The data model now matches Odoo's product.template / product.product convention end-to-end. Order history works via the back-compat view. Admin manages attributes + variants at template level. POS reads from singular tables directly. Pro Odoo sync writes singular (via refactored backfill scripts + lib/db/odoo_push.ts mark*Synced/Failed).

Files

New: nix-outdoor-sales-backend/migrations/20260520140000_h4_x_viewify_cafe_products.ts · nix-cafe/app/(authed)/not-found.tsx
Modified: nix-outdoor-sales-backend/migrate.js · nix-cafe/lib/db/schema.ts · nix-cafe/lib/db/products.ts · nix-cafe/lib/db/odoo_push.ts · nix-cafe/lib/db/product_attribute.ts · nix-cafe/lib/actions/product_attribute.ts · nix-cafe/app/(authed)/settings/attributes/attributes-client.tsx · nix-cafe/scripts/backfill-pro-tenant-products.ts · nix-cafe/scripts/backfill-pro-tenant-categories.ts · nix-cafe/scripts/backfill-pro-tenant-product-images.ts
Deleted: nix-cafe/lib/db/product_template_sync.ts
Migrations: 1 (5 phases, idempotent).