← All tasks

H4-P4 — POS read-path cutover PROD · GATE 2

Fourth phase of the H4 Product/Variant rework. POS terminal now resolves variant attributes from the new Odoo-shape singular tables (cafe.product_*) instead of R10 plural tables. Schema bridge + backfill v2 + Cartesian generator safeguard + in-place rewrite of buildShellAttributePairs. cafe.products stays authoritative through P4; H4-P5 retires R10 next slice.

7/7 prod test · 51/51 regression green · 58/58 total. Backend 14ef245 + nix-cafe a5788c8. Migration applied to prod Supabase via node migrate.js from local; backfill ran clean on all 3 tenants with the dup-name multi-variant materialization landing 8 new product_product rows on get-coffee.

Backfill results on prod

[1995 — empty tenant] [demo] [get-coffee] [lumiere-coffee] templates: 0 templates: 0 templates: 9 templates: 25 variants: 0 variants: 0 variants: 17 variants: 25 backreffed:0 backreffed:0 backreffed:17 backreffed:25 + 9 back-refs filled + 25 back-refs filled + 8 new variants for 0 new variants dup-named groups (5× "Odoo Polo" + 3× "ESPRESSO" + 2× "Ice Latte" + 2× "Test Product" all materialized as separate SKUs)

Every cafe.products row on get-coffee + lumiere-coffee now has exactly 1 cafe.product_product row pointing back to it. The H4-P3 dup-name regression (17 → 9) is reversed: 17 products → 9 templates + 17 product_products on get-coffee.

POS read-path verification on prod

Login + /cafe/pos on lumiere-coffee renders without 5xx. Then end-to-end attribute resolution proven via probe: seeded a TAG-prefixed R10 attribute + 2 values + assignment for one lumiere product → re-ran both H4-P3 + H4-P4 backfills from local → asserted singular tables received the mirror (attribute, values, line with is_required=true + default_value_id backfilled) → called buildShellAttributePairs directly against the live prod DB → confirmed the returned ShellProductAttribute[] matches the seeded shape:

Seeded R10 (cafe.product_attributes + values + assignments): name="H4P4PROBE_Sweetness", values=["Sweet","Mild"], isRequired=true, defaultValueId=Sweet Backfill propagated to singular tables (after H4-P3 + H4-P4 re-run): cafe.product_attribute .name="H4P4PROBE_Sweetness" cafe.product_attribute_value .value="Sweet" / .value="Mild" cafe.product_template_attribute_line .is_required=true + .default_value_id → Sweet (mapped) cafe.product_template_attribute_line_value_rel ← both values selected buildShellAttributePairs(tenantId, {cafeProductId → shellId}) returned: [["shell-${""}", [ { attributeId, name: "H4P4PROBE_Sweetness", nameKh: null, isRequired: true, defaultValueId: ..., values: [Sweet, Mild] } ]]]

Screenshots

lumiere /cafe/pos rendered post-cutover
01 · lumiere-coffee /cafe/pos rendered cleanly post-cutover

Checks — 7/7 prod

Raw: result.json

51/51 regression green — no behavioural regressions from this push. POS terminal read-path swap survived all existing suites; products list + auth + tenant boundary + PIN flow + Reports + 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)7/7
Total58/58

What's next in the H4 arc

H4-P5 — retire R10 + cafe.products consolidation. Pure-cleanup slice: drop cafe.product_attributes, cafe.product_attribute_values, cafe.product_attribute_assignments. Decision pending: keep cafe.products as a real table (admin still writes there), or migrate to a view over (product_template JOIN product_product). Delete legacy /cafe/settings/modifiers admin route + R10 DAO + sync hooks (none added in P4).

Files

New: nix-outdoor-sales-backend/migrations/20260520100000_h4_p4_pos_readpath_bridge.ts · nix-cafe/scripts/backfill-h4-p4-pos-readpath.ts
Modified: nix-outdoor-sales-backend/migrate.js · nix-cafe/lib/db/schema.ts · nix-cafe/lib/db/product_attribute.ts · nix-cafe/lib/pos/attribute-pairs.ts (in-place rewrite) · nix-cafe/lib/actions/product_attribute.ts · nix-cafe/app/(authed)/settings/attributes/attributes-client.tsx
Migrations: 1 (3 column adds, idempotent).