Notion test-run #3 doc item #15 — three intertwined complaints on the per-product attribute editor: (1) only show values added to THIS template; (2) replace pill-chip-row picker with multi-select dropdown; (3) per-template extra-price override so editing FRAPPE=+$1.99 on Café Latte stops bleeding to every other template using "Drink Type". Karou picked bundled — all three in one slice. Narong's locked design (in the Notion note): "The extra price is always configured in Product Template › Variant/Modifier › Configure. Configuration › Attributes can be used to change the Name of the attribute or the Default Extra Price, but it's global whereas the change in each Product stays local."
cafe.product_attribute_value.price_extra_usd (existing)/cafe/settings/attributes (U15-C admin)cafe.product_template_attribute_line_value_rel.price_extra_usd_override/cafe/products/[id]/attributes/[lineId]Effective extra price in every read path: COALESCE(rel.price_extra_usd_override, av.price_extra_usd). Null override = use global default.
/cafe/products/[id]. Native <details>/<summary> open/close + scrollable checkbox list. Summary header reads "N of M selected"./cafe/products/[id]/attributes/[lineId]. Disabled with a "Configure (save first)" placeholder for unsaved new lines.Value | Exclude For | Extra Price | [Save] table. Extra Price column is inline-editable; placeholder shows the global default like 1.99 (default); blank input clears the override. Per-row Save calls setLineValueExtraPriceOverrideAction (gated on nix_cafe.settings.edit; validates non-negative ≤999 with 2dp). On success: local state patches; "Override saved · global default $X.XX" hint appears.lib/pos/attribute-pairs.ts resolves override ?? global per row → POS variant picker now shows the per-template effective price.lib/db/product_import.ts writes the per-row diff to rel.priceExtraUsdOverride instead of mutating av.price_extra_usd globally. Two templates sharing the same attribute keep their own per-template extras.setTemplateAttributeLines reads existing overrides keyed by ${attributeId}:${valueId} BEFORE the wipe-and-reinsert, then re-applies them when surviving (attr, value) pairs land in the new rel rows. Without this, every "Save attributes" click would clear all per-template overrides./cafe/settings/attributes (U15-C admin); per-template overrides happen on the new Configure page. One obvious way to do each thing.| ✓ | Pre-flight: seed 2 throwaway templates sharing 1 attribute + 1 value (global default $1.00) on lumiere via raw SQL |
| ✓ | SSO login |
| ✓ | Product detail (/cafe/products/[A]): multi-select picker rendered · summary chip for the seeded value visible · Configure link to /products/[A]/attributes/[lineA] visible |
| ✓ | Configure page: table + Extra Price input visible · placeholder shows 1.00 (default) |
| ✓ | Type override 2.50 → Save → "Override saved · global default $1.00" flag appears |
| ✓ | Reload Configure page → input pre-populated with 2.50 |
| ✓ | Load-bearing cross-template isolation: DB probe confirms template A override = 2.50, template B override = NULL (template B still resolves to the global default) |
| ✓ | Clear override → blank input → Save → "Override saved" flag gone |
| ✓ | DB: template A override now NULL after clear → effective price reverts to global default |
| ✓ | No HTTP 5xx during the U17 flow |
| ✓ | Cleanup: delete seeded templates + attribute + value |
cafe.product_template has no list_price_usd column. First prod test attempt INSERT failed with "column list_price_usd does not exist." Schema mirror in schema.ts doesn't have it either; the local probe inserted a key Drizzle silently dropped (the .mjs runtime path doesn't enforce values() type strictness). Base price actually lives on product_product.price_usd (variant-level). Trivial fix.prod-u17 dir. Existing durable rule covers it.wrangler deployments list; just ran the prod test directly — the testid assertions would have failed if the deploy wasn't live, but they passed first attempt on the warm Worker.getTemplateWithLinesById, getLineWithEditableValues) are already db.transaction-wrapped. The Configure page round-trip read-after-write also benefits from dynamic = "force-dynamic" + revalidate = 0.price_extra_usd on the value row. Two reasons: (a) "null = use default" semantics is explicit; (b) Configuration › Attributes admin keeps editing the global default without surprise behavior across all products using the same value.— in the Configure page. Odoo's per-combo exclusion model (e.g. "ICE excluded when paired with FRAPPE") is in scope for a v2 follow-up./cafe/settings/attributes (U15-C admin) where they belong.| test-phase1-prod.mjs | 11/11 |
| test-phase2-sso-outdoor-prod.mjs | 6/6 |
| test-m1-prod.mjs | 10/10 |
| test-r7-prod.mjs | 14/14 |
| test-r8-prod.mjs | 4/4 |
| test-phase2-cafe-multishop-prod.mjs | 6/6 |
feedback_phase2_cafe_multishop_solo_retry.