fix(review): #90 thickness band omitted entirely (not read-only)
This commit is contained in:
@@ -45,6 +45,21 @@ def env(monkeypatch):
|
|||||||
conn.execute("INSERT INTO woo_categories(id,name,slug,parent_id,woo_id) VALUES (2,'Live edge wood','live-edge-wood',NULL,101)")
|
conn.execute("INSERT INTO woo_categories(id,name,slug,parent_id,woo_id) VALUES (2,'Live edge wood','live-edge-wood',NULL,101)")
|
||||||
conn.execute("INSERT INTO woo_attributes(id,name,slug,woo_id) VALUES (8,'Edge Type','edge-type',200)")
|
conn.execute("INSERT INTO woo_attributes(id,name,slug,woo_id) VALUES (8,'Edge Type','edge-type',200)")
|
||||||
conn.execute("INSERT INTO woo_attribute_terms(id,attribute_id,name,slug,woo_id) VALUES (1,8,'Flat','flat',300)")
|
conn.execute("INSERT INTO woo_attribute_terms(id,attribute_id,name,slug,woo_id) VALUES (1,8,'Flat','flat',300)")
|
||||||
|
# Thickness bands, names as synced (the derivation parses them) — QUALITY-95.
|
||||||
|
conn.execute("INSERT INTO woo_attributes(id,name,slug,woo_id) VALUES (9,'Thickness','pa_thk',201)")
|
||||||
|
for tid, name, slug in [
|
||||||
|
(347, 'Under 1/2"', 'xthin'), (348, '1/2" - 3/4"', '2-3qtr'),
|
||||||
|
(349, '3/4" - 1"', '3-4qtr'), (350, '1"- 1 1/2"', '4-6qtr'),
|
||||||
|
(351, '1 1/2" - 2"', '6-8qtr'), (352, '2" - 3"', '8-12qtr'),
|
||||||
|
(353, 'Over 3"', '12qtr'),
|
||||||
|
]:
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO woo_attribute_terms(id,attribute_id,name,slug,woo_id) VALUES (?,9,?,?,?)",
|
||||||
|
(tid, name, slug, tid),
|
||||||
|
)
|
||||||
|
# Moisture keeps its own attribute so the five locked attributes stay distinct.
|
||||||
|
conn.execute("INSERT INTO woo_attributes(id,name,slug,woo_id) VALUES (10,'Moisture','pa_mst',202)")
|
||||||
|
conn.execute("INSERT INTO woo_attribute_terms(id,attribute_id,name,slug,woo_id) VALUES (326,10,'Kiln dried','kd',326)")
|
||||||
# creds
|
# creds
|
||||||
conn.execute("INSERT OR REPLACE INTO settings(key,value,value_enc) VALUES ('woo_wp_username','admin',NULL)")
|
conn.execute("INSERT OR REPLACE INTO settings(key,value,value_enc) VALUES ('woo_wp_username','admin',NULL)")
|
||||||
conn.execute("INSERT OR REPLACE INTO settings(key,value,value_enc) VALUES ('woo_app_password',NULL,?)", (encrypt("secret"),))
|
conn.execute("INSERT OR REPLACE INTO settings(key,value,value_enc) VALUES ('woo_app_password',NULL,?)", (encrypt("secret"),))
|
||||||
@@ -119,6 +134,62 @@ def test_publish_success(env, monkeypatch):
|
|||||||
assert conn.execute("SELECT processed_blob FROM slab_photos WHERE id='p1'").fetchone()["processed_blob"] is None
|
assert conn.execute("SELECT processed_blob FROM slab_photos WHERE id='p1'").fetchone()["processed_blob"] is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_publish_sends_derived_thickness_band(env, monkeypatch):
|
||||||
|
"""#90 / QUALITY-95: the band is off the Review screen but must still reach Woo.
|
||||||
|
|
||||||
|
The owner has no thickness-band control, so nothing client-side carries it: the
|
||||||
|
server derives it from the measurement on create/update, and publish must send the
|
||||||
|
live synced term name on the product payload. This pins that chain end to end.
|
||||||
|
"""
|
||||||
|
client, conn = env
|
||||||
|
create = client.post("/api/v1/slabs", json={
|
||||||
|
"id": "s1", "sku": "SLAB-BAND", "length_in": 114.0, "thickness_in": 1.75,
|
||||||
|
"sqft": 30.0, "bdft": 18.4, "width_min_in": 18.0, "width_max_in": 24.3, "width_avg_in": 24.3,
|
||||||
|
})
|
||||||
|
assert create.status_code == 201, create.text
|
||||||
|
# 1.75 in bands into the live '1 1/2" - 2"' term — never sent by the client.
|
||||||
|
assert create.json()["thickness_term_id"] == 351
|
||||||
|
|
||||||
|
put = client.put("/api/v1/slabs/s1", json={
|
||||||
|
"client_rev": create.json()["server_rev"], "species_id": 1, "wood_category_ids": [2],
|
||||||
|
"edge_type_term_id": 1, "figure_term_ids": [1], "grade_term_id": 1, "moisture_term_id": 326,
|
||||||
|
"price": 184.0, "price_source": "recommendation", "title": "T", "short_description": "S",
|
||||||
|
"description": "D", "content_source": "manual",
|
||||||
|
})
|
||||||
|
assert put.status_code == 200, put.text
|
||||||
|
conn.execute("UPDATE slabs SET status='ready' WHERE id='s1'")
|
||||||
|
conn.execute("INSERT INTO slab_photos(id,slab_id,seq,is_calibration,processed_blob,created_at) VALUES ('p1','s1',1,1,X'89504E47','t')")
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
products = []
|
||||||
|
|
||||||
|
def fake_request(url, method, data, headers):
|
||||||
|
if "/products?sku=" in url:
|
||||||
|
return 200, {}, b"[]"
|
||||||
|
if url.endswith("/wp-json/wp/v2/media") and method == "POST":
|
||||||
|
return 201, {}, json.dumps({"id": 42}).encode()
|
||||||
|
if url.endswith("/wp-json/wc/v3/products") and method == "POST":
|
||||||
|
products.append(json.loads(data))
|
||||||
|
return 201, {}, json.dumps({"id": 999}).encode()
|
||||||
|
return 200, {}, b"[]"
|
||||||
|
|
||||||
|
from backend.woo import WooClient
|
||||||
|
|
||||||
|
orig_init = WooClient.__init__
|
||||||
|
|
||||||
|
def patched_init(self, *a, **k):
|
||||||
|
orig_init(self, *a, **k)
|
||||||
|
self._request = fake_request
|
||||||
|
|
||||||
|
monkeypatch.setattr(WooClient, "__init__", patched_init)
|
||||||
|
r = client.post("/api/v1/slabs/s1/publish")
|
||||||
|
assert r.status_code == 200, r.text
|
||||||
|
assert len(products) == 1, "expected exactly one product create"
|
||||||
|
attrs = {a["id"]: a for a in products[0].get("attributes", [])}
|
||||||
|
assert 9 in attrs, f"Thickness attribute missing from the Woo payload: {products[0].get('attributes')}"
|
||||||
|
assert attrs[9]["options"] == ['1 1/2" - 2"']
|
||||||
|
|
||||||
|
|
||||||
def test_publish_woo_image_order_by_role(env, monkeypatch):
|
def test_publish_woo_image_order_by_role(env, monkeypatch):
|
||||||
# PUBLISH-45: Woo order is clean cal, dimension-overlay, then batch — by
|
# PUBLISH-45: Woo order is clean cal, dimension-overlay, then batch — by
|
||||||
# role, not by stored seq (the Review strip stores the owner's keep order).
|
# role, not by stored seq (the Review strip stores the owner's keep order).
|
||||||
|
|||||||
@@ -121,9 +121,10 @@ hard stop. Switch to a supported browser.
|
|||||||
3. **Wood categories.** Pick at least one from the synced list.
|
3. **Wood categories.** Pick at least one from the synced list.
|
||||||
4. **Edge type, figure, grade.** Pick from the synced lists. One edge type, at least one figure,
|
4. **Edge type, figure, grade.** Pick from the synced lists. One edge type, at least one figure,
|
||||||
exactly one grade.
|
exactly one grade.
|
||||||
5. **Thickness band.** Derived, not picked: the server bands the measured thickness up into
|
5. **Thickness band.** Not on the screen and not picked: the server bands the measured thickness
|
||||||
the synced Woo band and Review shows it read-only. Verify the shown band is the one the
|
up into the synced Woo band, stores it, and publish sends it to the store. Verify after publish
|
||||||
captured thickness rounds up into (`QUALITY-95`).
|
that the Woo product's Thickness attribute is the band the captured thickness rounds up into
|
||||||
|
(`QUALITY-95`).
|
||||||
6. **Moisture.** Keep the default (kiln-dried) or change it.
|
6. **Moisture.** Keep the default (kiln-dried) or change it.
|
||||||
7. **fig-* tags.** Optional; pick any from the synced list.
|
7. **fig-* tags.** Optional; pick any from the synced list.
|
||||||
8. **feat-* tags.** Zero or more. Only pick existing `feat-*` tags.
|
8. **feat-* tags.** Zero or more. Only pick existing `feat-*` tags.
|
||||||
|
|||||||
@@ -59,9 +59,9 @@ Cites REVIEW-5, REVIEW-15, REVIEW-20, METADATA-45.
|
|||||||
optional; price; title, short_description, and description.
|
optional; price; title, short_description, and description.
|
||||||
- **Primary category.** The owner declares one primary wood category and any secondary ones; the
|
- **Primary category.** The owner declares one primary wood category and any secondary ones; the
|
||||||
default is the first selected.
|
default is the first selected.
|
||||||
- **Thickness band is derived, not picked.** The measured thickness bands up into the synced Woo
|
- **Thickness band is derived and not shown.** The measured thickness bands up into the synced Woo
|
||||||
band server-side (`METADATA-15`, `QUALITY-95`); Review shows the assigned band read-only and
|
band server-side (`METADATA-15`, `QUALITY-95`); Review carries no thickness-band control and no
|
||||||
offers no owner control for it.
|
read-only echo of it. The derived band is stored on the slab and sent to the store at publish.
|
||||||
- **Inline nudges.** Every empty required field shows an inline nudge as the owner works; there is
|
- **Inline nudges.** Every empty required field shows an inline nudge as the owner works; there is
|
||||||
no single submit-time gap list.
|
no single submit-time gap list.
|
||||||
- **Calculated fields.** Widths, sqft, and bdft are read-only and not overridable.
|
- **Calculated fields.** Widths, sqft, and bdft are read-only and not overridable.
|
||||||
|
|||||||
@@ -39,7 +39,6 @@
|
|||||||
let edgeOpts: Opt[] = [];
|
let edgeOpts: Opt[] = [];
|
||||||
let figureOpts: Opt[] = [];
|
let figureOpts: Opt[] = [];
|
||||||
let gradeOpts: Opt[] = [];
|
let gradeOpts: Opt[] = [];
|
||||||
let thicknessOpts: Opt[] = [];
|
|
||||||
let moistureOpts: Opt[] = [];
|
let moistureOpts: Opt[] = [];
|
||||||
let figTagOpts: Opt[] = [];
|
let figTagOpts: Opt[] = [];
|
||||||
let featTagOpts: Opt[] = [];
|
let featTagOpts: Opt[] = [];
|
||||||
@@ -76,7 +75,8 @@
|
|||||||
edgeOpts = termsOf('edge type');
|
edgeOpts = termsOf('edge type');
|
||||||
figureOpts = termsOf('figure');
|
figureOpts = termsOf('figure');
|
||||||
gradeOpts = termsOf('grade');
|
gradeOpts = termsOf('grade');
|
||||||
thicknessOpts = termsOf('thickness');
|
// No thickness options: the band is derived server-side and never shown here
|
||||||
|
// (METADATA-15 / QUALITY-95).
|
||||||
moistureOpts = termsOf('moisture');
|
moistureOpts = termsOf('moisture');
|
||||||
// fig-* / feat-* product tags (METADATA-30): pulled from the live synced set,
|
// fig-* / feat-* product tags (METADATA-30): pulled from the live synced set,
|
||||||
// filtered by the family the backend keeps. Never invent a prefix — if the
|
// filtered by the family the backend keeps. Never invent a prefix — if the
|
||||||
@@ -346,11 +346,9 @@
|
|||||||
.map((o) => ({ ...o, ai: aiRecommendations.includes(o.id) }));
|
.map((o) => ({ ...o, ai: aiRecommendations.includes(o.id) }));
|
||||||
|
|
||||||
// METADATA-15 / QUALITY-95: the thickness band is derived server-side from the
|
// METADATA-15 / QUALITY-95: the thickness band is derived server-side from the
|
||||||
// measured thickness against the live synced Woo bands. There is no owner
|
// measured thickness against the live synced Woo bands. It is deliberately absent
|
||||||
// control for it — this is the read-only display of the band the server assigned.
|
// from this screen — no owner control, no read-only echo. The only thickness the
|
||||||
$: thicknessBandName =
|
// owner sees here is the measured value in the summary above.
|
||||||
thicknessOpts.find((o) => o.id === (slab?.thickness_term_id ?? -1))?.name ?? '';
|
|
||||||
|
|
||||||
const aiIn = (ids: number[]) => (id: number) => ids.includes(id);
|
const aiIn = (ids: number[]) => (id: number) => ids.includes(id);
|
||||||
// Persist taxonomy/price/sku edits to the slab (PUT), then recompute ready.
|
// Persist taxonomy/price/sku edits to the slab (PUT), then recompute ready.
|
||||||
async function commitReview(): Promise<void> {
|
async function commitReview(): Promise<void> {
|
||||||
@@ -730,12 +728,6 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
|
||||||
<label class="lbl">Thickness band <span class="sug">set from the measurement</span></label>
|
|
||||||
<p class="ro" data-testid="thickness-band">
|
|
||||||
{#if thicknessBandName}{thicknessBandName}{:else}— not yet resolved{/if}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="lbl" for="moisture">Moisture</label>
|
<label class="lbl" for="moisture">Moisture</label>
|
||||||
<select id="moisture" class="sel" data-testid="moisture" bind:value={moistureId} on:change={commitReview}>
|
<select id="moisture" class="sel" data-testid="moisture" bind:value={moistureId} on:change={commitReview}>
|
||||||
@@ -862,9 +854,6 @@
|
|||||||
.field { margin-bottom: 12px; }
|
.field { margin-bottom: 12px; }
|
||||||
.nudge { color: #e8a; font-size: .74rem; margin: 4px 0 0; }
|
.nudge { color: #e8a; font-size: .74rem; margin: 4px 0 0; }
|
||||||
.sug { color: #aaa; font-size: .74rem; font-weight: 400; margin-left: 6px; }
|
.sug { color: #aaa; font-size: .74rem; font-weight: 400; margin-left: 6px; }
|
||||||
/* Read-only derived value (thickness band): reads as data, not as a control. */
|
|
||||||
.ro { margin: 0; padding: 9px; background: #161616; border: 1px solid #2f2f2f; border-radius: 3px;
|
|
||||||
color: #cfcfcf; font-size: .9rem; font-variant-numeric: tabular-nums; }
|
|
||||||
.gen { margin-top: 8px; } .gen-btn { padding: 8px 12px; border: 0; border-radius: 3px; background: #00e5ff; color: #002; font-weight: 600; font-size: .82rem; }
|
.gen { margin-top: 8px; } .gen-btn { padding: 8px 12px; border: 0; border-radius: 3px; background: #00e5ff; color: #002; font-weight: 600; font-size: .82rem; }
|
||||||
/* Lock chrome matches agreed #90 wireframe: muted dark green, not jarring neon. */
|
/* Lock chrome matches agreed #90 wireframe: muted dark green, not jarring neon. */
|
||||||
.lockbox { margin: 14px 0; padding: 12px; border: 1px solid #2a3a2a; border-radius: 3px; background: #141a14; }
|
.lockbox { margin: 14px 0; padding: 12px; border: 1px solid #2a3a2a; border-radius: 3px; background: #141a14; }
|
||||||
|
|||||||
Reference in New Issue
Block a user