SETTINGS-35: species $/bdft in Settings + taxonomy parent fix (#76)
Add Settings species pricing UI (children of Species grouper by slug, never hardcoded ids). Map Woo REST parent→parent_id on taxonomy sync; upsert categories so slab FKs survive re-sync. Tests: parent map, FK-safe sync, TV-8 0.96×12.50→12.00, Playwright mock e2e. UAT verified on 5060.
This commit is contained in:
+45
-5
@@ -20,6 +20,23 @@ def _scoped_tags(tags: list[dict]) -> list[dict]:
|
||||
return [t for t in tags if t.get("name", "").startswith(("fig-", "feat-"))]
|
||||
|
||||
|
||||
def _normalize_category(c: dict) -> tuple:
|
||||
"""Map a Woo REST category row into woo_categories insert values.
|
||||
|
||||
WooCommerce REST v3 returns `parent` (0 = top-level). Older fixtures may
|
||||
already use `parent_id`. Never invent ids — only store what the payload carries.
|
||||
"""
|
||||
raw_parent = c.get("parent_id", c.get("parent"))
|
||||
if raw_parent in (0, "0", None, ""):
|
||||
parent_id = None
|
||||
else:
|
||||
parent_id = int(raw_parent)
|
||||
woo_id = c.get("woo_id", c.get("id"))
|
||||
if woo_id is not None:
|
||||
woo_id = int(woo_id)
|
||||
return (c["id"], c["name"], c["slug"], parent_id, woo_id)
|
||||
|
||||
|
||||
def _subset_hash(categories: list[dict], attributes: list[dict],
|
||||
terms_by_attr: dict, scoped_tags: list[dict]) -> str:
|
||||
subset = {
|
||||
@@ -40,14 +57,37 @@ def sync(client: WooClient, conn) -> dict:
|
||||
terms_by_attr = {a["id"]: client.get_attribute_terms(a["id"]) for a in attributes}
|
||||
scoped_tags = _scoped_tags(tags)
|
||||
|
||||
# Attributes/tags/terms are not referenced by slabs FKs — safe to rebuild.
|
||||
conn.execute("DELETE FROM woo_attribute_terms")
|
||||
conn.execute("DELETE FROM woo_attributes")
|
||||
conn.execute("DELETE FROM woo_categories")
|
||||
conn.execute("DELETE FROM woo_tags")
|
||||
conn.executemany(
|
||||
"INSERT INTO woo_categories(id, name, slug, parent_id, woo_id) VALUES (?,?,?,?,?)",
|
||||
[(c["id"], c["name"], c["slug"], c.get("parent_id"), c.get("woo_id")) for c in categories],
|
||||
)
|
||||
|
||||
# Categories: upsert then drop stale rows. A full DELETE fails when slabs or
|
||||
# pricing_rules still reference species_id (live UAT IntegrityError on test-woo).
|
||||
cat_rows = [_normalize_category(c) for c in categories]
|
||||
cat_ids = [r[0] for r in cat_rows]
|
||||
for row in cat_rows:
|
||||
conn.execute(
|
||||
"INSERT INTO woo_categories(id, name, slug, parent_id, woo_id) VALUES (?,?,?,?,?) "
|
||||
"ON CONFLICT(id) DO UPDATE SET "
|
||||
"name=excluded.name, slug=excluded.slug, parent_id=excluded.parent_id, woo_id=excluded.woo_id",
|
||||
row,
|
||||
)
|
||||
if cat_ids:
|
||||
placeholders = ",".join("?" * len(cat_ids))
|
||||
conn.execute(
|
||||
f"DELETE FROM woo_categories WHERE id NOT IN ({placeholders}) "
|
||||
"AND id NOT IN (SELECT species_id FROM slabs WHERE species_id IS NOT NULL) "
|
||||
"AND id NOT IN (SELECT species_id FROM pricing_rules)",
|
||||
cat_ids,
|
||||
)
|
||||
else:
|
||||
conn.execute(
|
||||
"DELETE FROM woo_categories "
|
||||
"WHERE id NOT IN (SELECT species_id FROM slabs WHERE species_id IS NOT NULL) "
|
||||
"AND id NOT IN (SELECT species_id FROM pricing_rules)"
|
||||
)
|
||||
|
||||
conn.executemany(
|
||||
"INSERT INTO woo_attributes(id, name, slug, woo_id) VALUES (?,?,?,?)",
|
||||
[(a["id"], a["name"], a["slug"], a.get("woo_id")) for a in attributes],
|
||||
|
||||
@@ -87,6 +87,47 @@ def test_price_suggestion_from_rate(env):
|
||||
assert body["price"] == 184.0
|
||||
|
||||
|
||||
def test_price_suggestion_tv8_quality_90(env):
|
||||
"""QUALITY-90 / TV-8: 0.96 bdft × $12.50 → 12.00."""
|
||||
client, conn = env
|
||||
conn.execute(
|
||||
"INSERT INTO pricing_rules(species_id,price_per_bdft,created_at,updated_at) VALUES (1,12.50,'t','t')"
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO slabs(id,sku,length_in,thickness_in,sqft,bdft,width_min_in,width_max_in,width_avg_in,species_id,wood_category_ids,figure_term_ids,price,price_source,title,short_description,description,content_source,status,server_rev,client_rev,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
(
|
||||
"s1",
|
||||
"SLAB-01",
|
||||
114.0,
|
||||
1.75,
|
||||
30.0,
|
||||
0.96,
|
||||
18.0,
|
||||
24.3,
|
||||
24.3,
|
||||
1,
|
||||
json.dumps([2]),
|
||||
json.dumps([1]),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
"A slab",
|
||||
"manual",
|
||||
"calibrated",
|
||||
1,
|
||||
1,
|
||||
"t",
|
||||
"t",
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
r = client.post("/api/v1/slabs/s1/review", json={})
|
||||
body = r.json()
|
||||
assert body["price_suggestion"] == 12.00
|
||||
assert body["price"] == 12.00
|
||||
|
||||
|
||||
def test_knife_blank_carries_dims(env):
|
||||
client, conn = env
|
||||
conn.execute("INSERT INTO woo_categories(id,name,slug,parent_id,woo_id) VALUES (3,'Knife blanks','knife-blanks',NULL,102)")
|
||||
|
||||
@@ -141,6 +141,75 @@ def test_anchor_stable_across_identical_syncs(db_dir):
|
||||
assert r1["taxonomy_anchor"] == r2["taxonomy_anchor"]
|
||||
|
||||
|
||||
def test_sync_maps_woo_parent_field_to_parent_id(db_dir):
|
||||
"""Woo REST categories use `parent` (0 = top-level), not `parent_id`.
|
||||
|
||||
Live UAT lost hierarchy when sync only read parent_id (SETTINGS-35 / METADATA-10).
|
||||
Fixture ids are local to this test — not production store ids.
|
||||
"""
|
||||
cats = (
|
||||
200,
|
||||
{"X-WP-TotalPages": "1"},
|
||||
json.dumps(
|
||||
[
|
||||
{"id": 10, "name": "Species", "slug": "species", "parent": 0},
|
||||
{"id": 11, "name": "Cedar", "slug": "cedar", "parent": 10},
|
||||
{"id": 20, "name": "Wood category", "slug": "wcat", "parent": 0},
|
||||
{"id": 21, "name": "Live edge wood", "slug": "liveedge", "parent": 20},
|
||||
]
|
||||
).encode(),
|
||||
)
|
||||
tags = (200, {"X-WP-TotalPages": "1"}, b"[]")
|
||||
attrs = (200, {"X-WP-TotalPages": "1"}, b"[]")
|
||||
c, _ = _client([cats, tags, attrs])
|
||||
taxonomy_mod.sync(c, db_mod.get_connection())
|
||||
conn = db_mod.get_connection()
|
||||
rows = {
|
||||
r[0]: {"name": r[1], "slug": r[2], "parent_id": r[3], "woo_id": r[4]}
|
||||
for r in conn.execute("SELECT id, name, slug, parent_id, woo_id FROM woo_categories")
|
||||
}
|
||||
assert rows[10]["parent_id"] is None
|
||||
assert rows[11]["parent_id"] == 10
|
||||
assert rows[20]["parent_id"] is None
|
||||
assert rows[21]["parent_id"] == 20
|
||||
# woo_id falls back to Woo category id when the payload has no woo_id field
|
||||
assert rows[11]["woo_id"] == 11
|
||||
|
||||
|
||||
def test_sync_with_slab_species_fk_does_not_fail(db_dir):
|
||||
"""Live UAT: DELETE woo_categories fails when a slab still points at species_id."""
|
||||
conn = db_mod.get_connection()
|
||||
conn.execute(
|
||||
"INSERT INTO woo_categories(id,name,slug,parent_id,woo_id) VALUES (11,'Old Cedar','old-cedar',NULL,11)"
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO slabs(id, status, species_id, server_rev, created_at, updated_at) "
|
||||
"VALUES ('slab-fk-1', 'draft', 11, 1, 't', 't')"
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
cats = (
|
||||
200,
|
||||
{"X-WP-TotalPages": "1"},
|
||||
json.dumps(
|
||||
[
|
||||
{"id": 10, "name": "Species", "slug": "species", "parent": 0},
|
||||
{"id": 11, "name": "Cedar", "slug": "cedar", "parent": 10},
|
||||
]
|
||||
).encode(),
|
||||
)
|
||||
tags = (200, {"X-WP-TotalPages": "1"}, b"[]")
|
||||
attrs = (200, {"X-WP-TotalPages": "1"}, b"[]")
|
||||
c, _ = _client([cats, tags, attrs])
|
||||
taxonomy_mod.sync(c, conn)
|
||||
rows = {
|
||||
r[0]: r[3]
|
||||
for r in conn.execute("SELECT id, name, slug, parent_id FROM woo_categories")
|
||||
}
|
||||
assert rows[11] == 10 # parent mapped; row kept for FK
|
||||
assert conn.execute("SELECT species_id FROM slabs WHERE id='slab-fk-1'").fetchone()[0] == 11
|
||||
|
||||
|
||||
def test_403_raises_forbidden_error(db_dir):
|
||||
def fake_request(url, method, data, headers):
|
||||
raise WooForbiddenError("request forbidden (403)")
|
||||
|
||||
Reference in New Issue
Block a user