← back to Dw Measure To Cart

SPEC.md

145 lines

# Measure-to-Cart — Read-Only Spec (Phase 0)

**Ticket:** TK-10233 · **Origin:** Officer Idea Council 2026-08-04 #1 (34/40, vp-dw-commerce), Steve "#1 BUILD dust2026" + "go dust2026" (2026-08-04 22:14 PT email)
**Status:** SPEC ONLY — no theme work, no PDP changes, no /measure code changes yet. Everything below is verified against live code + the local dw_unified mirror (read-only).

## 1. What exists today (verified 2026-08-04)

### NPH /measure (NationalPaperHangers)
- Route `routes/public.js:1073`, view `views/public/measure.ejs`, math `public/js/measure-kit.js`, AR capture `public/js/room-measure.js` (WebXR 3-tap on Android, gyro clinometer on iPhone).
- **Already has a product-context deep-link (P3):**
  `/measure?sku=&name=&from=&repeat=&panelw=&format=&shop=` — `shop` return-URL allowlisted to designerwallcoverings.com, wallco.ai, philipperomano.com, novasuede.com (`routes/public.js:1047-1065`).
- Entirely client-side, no server writes, no order handoff — the "Shop" CTA just links back to the PDP with **no quantity**.
- **Current math is a flat sq-ft shortcut** (`measure-kit.js:66-85`): net area × (1.15 or 1.20 waste) ÷ 28 ft² per US single roll (57 ft² Euro), then ceil to doubles. It knows nothing about the product's actual width, repeat, or roll length.

### dw_unified dimension data (local mirror, read-only)
All dimensions live in `shopify_products.metafields` (jsonb) — no dedicated columns. ACTIVE-product population:

| Field | Best keys (fallback order) | ACTIVE coverage |
|---|---|---|
| Width | `custom.width` → `dwc.width` → `global.width` | 76k / 73k / 57k of 86.5k |
| Vert. repeat | `custom.pattern_repeat` → `dwc.repeat` → `global.repeat` / `specs.repeat_v` | 45k / 40k / 21k |
| Roll length | `global.single_roll_length` + `global.double_roll_length` (D/R lines, ~5.7k) → `global.length` → `custom.roll_length` | 16k / 12k / 3.6k |
| Coverage sq-ft | `specs.coverage` etc. | only ~3-6% — **not** a reliable path |
| 2-roll minimum | `global.v_prods_quantity_order_min=2` + `v_prods_quantity_order_units=2` | 13.6k |

Values are messy mixed-unit strings (`"36 Inches"`, `"27\""`, `"0.52"` m, `"4 yd (3.66 m)"`, Kravet fuses width×length as `"20.5\" x 11 yards"`). **A unit-normalizing parser is a hard prerequisite** (in/cm/m/ft/yd; split Kravet fused strings; sanity-check Anna French lengths — some "11 Feet" are likely 11 yd).

Vendor staging tables (`*_catalog`, Mac2-canonical) carry clean columns (`width_inches`, `repeat_v`, `roll_length`…) and can seed/backfill the parser.

## 2. Roll math — the strip method (source of truth)

```
strip_h   = wall_h + 4in trim; if repeat > 0: strip_h = ceil(strip_h / repeat) × repeat
strips    = ceil(total_wall_width / material_width)
per_unit  = floor(unit_length / strip_h)          # unit = S/R, D/R, or bolt
units     = ceil(strips / per_unit)
# Double-roll lines (Thibaut, Malibu Wallpaper, Malibu Walls, York):
#   sell in D/R units; enforce 2-roll min; single-roll count always even
```
Openings are NOT deducted (standard conservative practice); the sq-ft shortcut may be shown as a sanity display only.

## 3. Pilot verification (5 SKUs, computed + cross-checked)

Wall A = 12ft × 9ft flat wall. Wall B = 14×16ft room, 8ft ceilings, 1 door + 1 window.

| SKU | Vendor / packaging | Width | Repeat | Unit length | Wall A | Wall B |
|---|---|---|---|---|---|---|
| DWTT-73129 | Thibaut, D/R | 36" | 25.25" | D/R 8 yd | 2 D/R (4 S/R) | 10 D/R |
| DWQW-60067 | Malibu Wallpaper (D/R) | 27" | 12.6" | D/R 9 yd | 3 D/R | 9 D/R |
| DWKK-133346 | Cole & Son, Euro bolt | 20.5" | 24" | 33 ft | 3 bolts | 12 bolts |
| DWKK-115581 | Kravet (2-roll order min) | 20.5" | 7" | 11 yd | 3 rolls | 12 rolls |
| DWPT-200024-S | 1838, Euro roll | 20.47" | 20.87" | 32.97 ft | 3 rolls | 12 rolls |

**Cross-check vs the vendor-calculator sq-ft shortcut (÷0.73 waste):** matches on the flat wall for 4/5, but on the room it **under-buys 1 unit on Cole & Son, Kravet, and 1838** (big repeats / narrow widths force real waste >27%), under-buys 1 D/R on Thibaut, and over-buys 2 D/R on Malibu. This is exactly the "computes rolls correctly, competitors can't" edge — the strip method over the product's real metafields is the differentiator.

## 4. Handoff design (target, not yet built)

1. **PDP → /measure:** DW PDP button builds the existing P3 deep-link, adding the missing physical params: `width` (in), `unitlen` (in), `unit` (`sr|dr|bolt`), `ordermin`. NPH `/measure` clamps + prefills as it already does for `repeat`/`panelw`.
2. **/measure upgrade:** add strip-method engine alongside `rollsForArea()`; when width+unitlen are present use strip method as source of truth (fall back to sq-ft shortcut when absent).
3. **/measure → cart:** "Add N rolls + a $4.25 sample" CTA using a Shopify **cart permalink**: `https://designerwallcoverings.com/cart/{roll_variant_id}:{qty},{sample_variant_id}:1` — no theme quantity-param support needed, works today. Variant ids ride the deep-link (`vid`, `svid` params) so /measure stays stateless. D/R lines pass qty in the store's selling unit (even S/R count per `order_units=2`).
4. **Sample upsell** is always the second permalink line (every pilot SKU has the sample variant).

## 5. Gaps / risks before build

- **Unit parser** (§1) — build + test against the ~10 observed formats before anything user-facing.
- Mirror `status`/price columns are stale (standing memory) — variant ids + metafields for the deep-link generator must be read **live** from Admin API at build time, not from the mirror.
- ~10k ACTIVE products lack any roll length → strip method unavailable; fall back to shortcut + "estimate" label.
- /measure is on nationalpaperhangers.com — cross-domain cart handoff is a plain permalink (no cookies needed), but analytics attribution should carry `utm_source=measure-kit`.
- Theme work on the DW PDP (the button) is **customer-facing → Steve-gated** at build time.

## 6. Proposed build order (next phases, each gated as appropriate)

1. Unit parser + metafield extractor (local lib + tests) — reversible, ungated.
2. Strip-method engine in `measure-kit.js` + unit tests mirroring §3 — NPH deploy gated.
3. Deep-link generator (live Admin API reads) for the 5 pilots — ungated dry-run.
4. PDP button on the 5 pilot SKUs only (theme change) — **Steve gate**.
5. Measure → cart permalink CTA — **Steve gate** (customer-facing).

---

# Appendix A — Independent verification pass (TK-10232, 2026-08-04)

A second read-only pass ran in parallel under TK-10232 and converged on the same architecture
(strip method over real metafields; NPH sq-ft shortcut demoted to fallback). Deltas + extra
evidence worth keeping:

## A1. NPH handoff data shape (device → page), for the record

`RoomMeasure.measure()` (public/js/room-measure.js) resolves
`{ width_ft, height_ft, method: 'webxr'|'gyro' }` — **feet**, rounded to 0.5 ft, client-side only;
gyro (iPhone) returns `width_ft: null` (manual-width fallback). Multi-wall calc input
(`measure-kit.js`): `wall = { width_ft, height_ft, deductions:[{w_ft,h_ft}] }`. The `/book` flow's
`lib/room-captures.js` sanitizer already carries a per-room wallpaper sub-object
`{ image_url, width_in, repeat_in, match_type:'straight'|'half_drop' }` — the exact product params
the strip method needs, with **inches** as the product-side unit (matches the §4 deep-link params).

## A2. PDP-render fallback chains (live theme, authoritative for what customers see)

`theme-LIVE-pull-20260728-colorbar/snippets/product-description-meta.liquid` (live theme 144396058675):

```liquid
width  = custom.width → specs.width → global.Width → global.width
repeat = custom.pattern_repeat → custom.repeat → specs.pattern_repeat
         → specs.repeat_v → dwc.repeat → global.repeat → global['Vert-Rpt']
```

Note vs §1's coverage-ordered list: the THEME consults `specs.*` before `global.*` and never
`dwc.width` — the build's extractor should implement the theme's chain first (so the kit always
agrees with the number printed on the PDP), then extend with the §1 coverage keys as backfill.
Mirror value shape: each key holds `{"type":"single_line_text_field","value":"..."}` — always
free text. Double-roll packaging is stated verbatim in `custom.unit_of_measure`, e.g. Thibaut:
"Priced per single roll — packaged in double rolls only. Single roll: 27 in × 4.5 yd (4.11 m).
Double roll: 27 in × 9 yd (8.22 m)." — confirms D/R = 2× S/R.

## A3. Second pilot set (role-spanning) — strip method vs the shipped NPH sq-ft shortcut

Wall: 12 ft × 8 ft (144" × 96"), straight match, 4" trim (drop rounded up to repeat multiple).

| Role | SKU (mfr) | Vendor | W | V-rpt | Unit len | Strip method | NPH shortcut (28/57 ft², +20%) | Verdict |
|---|---|---|---|---|---|---|---|---|
| Double-roll line | DWTT-70021 (T16253) "Emily" | Thibaut | 27" | 27" | S/R 13.5 ft, D/R 27 ft | drop 108"; 6 strips; 3/DR → **2 D/R** (4 singles) | 5 singles → **3 D/R** | ⚠️ shortcut over-buys 1 D/R |
| Single roll (Euro bolt) | DWKK-133288 (100/10047.CS.0) | Cole & Son | 20.5" | 21.1" | 33 ft bolt | drop 105.5"; 8 strips; 3/bolt → **3 bolts** | **3 rolls** | ✅ match |
| Wide-width 54" | DWKK-128734 (W3649-16) | Kravet commercial | 54" | 9" | yard goods | drop 108"=3 yd; 3 strips → **9 lin yd** | n/a — **no yard-goods format in measure-kit.js** | ⚠️ gap (same as §5) |
| Large repeat | handle `cloud-9-grey-on-pearl-dwtt-73109` | Thibaut | 27" | **79"** | S/R 13.5 ft | drop 158"; 6 strips; 2/DR → **3 D/R** (6 singles) | 5 singles → 3 D/R | ✅ D/R count matches, but singles differ (6 vs 5) — even-rounding masked the shortcut's under-count |
| Plain / no repeat | DWJP-10936 (3533) Linen Weaves | Phillip Jeffries | 36" pre-trimmed | None | yard goods | drop 100"; 4 strips → **11.2 → 12 lin yd** (matches catalog "12 Yards") | n/a — yard-goods gap | ⚠️ gap |

Same conclusion as §3 from an independent SKU set: the flat sq-ft constant diverges exactly where
the product's real geometry bites (tall-drop-efficient formats, giant repeats, yard goods).

## A4. Public-methodology citation (checked, $0)

**yorkwallcoverings.com/pages/wallpaper-calculator** (fetched 2026-08-04): "Wallpaper is packaged in
bolts. 1 Bolt = 56 FT² {20.5-in × 33-ft}. 1 Bolt covers about 6-ft of wall {up to 9-ft ceilings} …
larger pattern repeats will require more wallpaper rolls. Deduct 1 bolt for every 4 ordinary size
door or window." Their "≈6 lf of wall per bolt" is the strip method (4 drops × 20.5" = 6.8 lf at
8-ft ceilings; 3 drops = 5.1 lf at 9-ft), and their 12×12-room/8-ft chart value (7 bolts) matches
§2/§A3 arithmetic. Pilot #2 above (Cole & Son 20.5"×33 ft = York's exact bolt format) computes
3 bolts for 12 lf of wall — consistent with their chart's coverage rate.

## A5. Sample upsell datum

`price` on the Thibaut pilot's sample variant row = **$3.50** in the mirror vs the $4.25 standing
memo price — mirror scalar-price staleness (standing memory) reconfirmed; the $4.25 sample variant
id must be read live at deep-link-generation time.