fix(m3): Option A must-fix #79 #82 #83 #84 #89 #98

- #79 publish SKU/create via WooClient (User-Agent + typed 403 unrecoverable)
- #82 stale_field current_options from live taxonomy + owner labels
- #83 create allowlist/extra-key safe; taxonomy+enum 4xx on create
- #84 health woo_reachable real probe (non-failing); version 0.5.0-m3
- #89 review persists assembled title/short after taxonomy lock; UI applies
- #98 live-edge from synced term; attrs resolved by slug then name
This commit is contained in:
Ty
2026-09-21 12:49:53 -07:00
parent bdd03afce4
commit f6eb9e352d
8 changed files with 408 additions and 94 deletions
+81 -15
View File
@@ -5,6 +5,7 @@ METADATA-80/85/90, SETTINGS-35.
import json
import re
from datetime import datetime, timezone
from typing import Any
from fastapi import APIRouter
@@ -66,14 +67,16 @@ def _dims(length_in: float, width_avg_in: float, thickness_in: float) -> str:
return f"{_fmt1(length_in)} x {_fmt1(width_avg_in)} x {_fmt2(thickness_in)} in"
def build_title(slab: dict, figure: str | None, species_name: str, primary_leaf: dict) -> str:
def build_title(slab: dict, figure: str | None, species_name: str, primary_leaf: dict, terms: dict | None = None) -> str:
pattern, noun, carries_dims = _LEAF_PATTERNS.get(
primary_leaf.get("slug"), ("{figure}{species} Slab", "Slab", False)
)
figure = figure.strip() if figure else ""
live_edge = ""
if primary_leaf["slug"] in ("dimensional-wood", "bowl-blanks"):
live_edge = "Live Edge " if slab.get("edge_type_term_id") else ""
if primary_leaf.get("slug") in ("dimensional-wood", "bowl-blanks"):
# "Live Edge" is a product-form flag driven by the *synced* edge term
# (METADATA-5 / #98) — never treat any non-null edge id as Live Edge.
live_edge = "Live Edge " if _edge_is_live(slab.get("edge_type_term_id"), terms or {}) else ""
title = pattern.format(
figure=figure + " " if figure else "",
species=species_name,
@@ -84,8 +87,17 @@ def build_title(slab: dict, figure: str | None, species_name: str, primary_leaf:
return re.sub(r"\s+", " ", title).strip()
def build_short_description(slab: dict, figure: str | None, species_name: str, primary_leaf: dict) -> str:
title = build_title(slab, figure, species_name, primary_leaf)
def _edge_is_live(edge_term_id: Any, terms: dict) -> bool:
"""True when the synced edge term name/slug indicates live edge."""
if not edge_term_id:
return False
term = terms.get(edge_term_id) or {}
blob = f"{term.get('name', '')} {term.get('slug', '')}".lower()
return "live" in blob
def build_short_description(slab: dict, figure: str | None, species_name: str, primary_leaf: dict, terms: dict | None = None) -> str:
title = build_title(slab, figure, species_name, primary_leaf, terms)
ft, rem = _split_length(slab["length_in"])
length_str = f"{ft}ft {_fmt1(rem)}in"
bdft = round(slab["bdft"], 1)
@@ -142,6 +154,13 @@ def price_suggestion(conn, slab: dict) -> float | None:
@router.post("/{slab_id}/review")
async def review_slab(slab_id: str, payload: dict[str, Any] | None = None) -> dict:
"""Compute ready + deterministic title/short_description from *locked* taxonomy.
METADATA-80 / REVIEW-10 / #89: title and short_description are assembled only
from the confirmed (persisted) listing facts. Owner-supplied title/short_desc
win (METADATA-50). Assembled values are written back to the slab so the lock
sticks for publish.
"""
payload = payload or {}
conn = get_connection()
row = conn.execute(
@@ -151,7 +170,16 @@ async def review_slab(slab_id: str, payload: dict[str, Any] | None = None) -> di
return JSONResponse(status_code=404, content=_error("not_found", "Slab not found"))
slab = _slab_dict(row)
# Apply any review edits (primary category declaration + prose).
editable = {"primary_wood_category_id", "title", "short_description", "description", "price", "price_source", "sku", "content_source"}
editable = {
"primary_wood_category_id",
"title",
"short_description",
"description",
"price",
"price_source",
"sku",
"content_source",
}
for k in editable:
if k in payload and payload[k] is not None:
if k == "primary_wood_category_id":
@@ -163,18 +191,30 @@ async def review_slab(slab_id: str, payload: dict[str, Any] | None = None) -> di
wood_cats = slab.get("wood_category_ids") or []
if not primary_id:
primary_id = wood_cats[0] if wood_cats else None
primary_leaf = cats.get(primary_id, {})
species_name = cats.get(slab.get("species_id"), {}).get("name", "")
primary_leaf = cats.get(primary_id, {}) if primary_id else {}
species_name = cats.get(slab.get("species_id"), {}).get("name", "") if slab.get("species_id") else ""
figure_term_ids = slab.get("figure_term_ids") or []
figure = terms.get(figure_term_ids[0], {}).get("name") if figure_term_ids else None
# Deterministic copy (only if not already overridden by owner).
if not payload.get("title"):
if primary_leaf and species_name:
slab["title"] = build_title(slab, figure, species_name, primary_leaf)
if not payload.get("short_description"):
if primary_leaf and species_name:
slab["short_description"] = build_short_description(slab, figure, species_name, primary_leaf)
# Taxonomy lock gate (#89): only assemble title/short_desc when the owner has
# confirmed the facts that patterns need (species + primary wood category).
taxonomy_locked = bool(primary_leaf and species_name and primary_leaf.get("slug"))
# Owner override: non-empty title/short_description in the payload wins.
# Empty/absent means "assemble from locked taxonomy".
raw_title = payload.get("title")
raw_short = payload.get("short_description")
want_title = not (isinstance(raw_title, str) and raw_title.strip())
want_short = not (isinstance(raw_short, str) and raw_short.strip())
if taxonomy_locked:
if want_title:
slab["title"] = build_title(slab, figure, species_name, primary_leaf, terms)
if want_short:
slab["short_description"] = build_short_description(
slab, figure, species_name, primary_leaf, terms
)
# Price suggestion (REVIEW-25): bdft * species rate, empty if no rate; owner override sticks.
suggested = price_suggestion(conn, slab)
if payload.get("price") is not None:
@@ -183,7 +223,32 @@ async def review_slab(slab_id: str, payload: dict[str, Any] | None = None) -> di
price = slab["price"]
else:
price = suggested
if price is not None:
slab["price"] = price
if not slab.get("price_source"):
slab["price_source"] = "recommendation"
# Persist lock: write assembled title/short_desc/price onto the slab so publish
# and subsequent loads see the confirmed set (#89).
now = datetime.now(timezone.utc).replace(microsecond=0, tzinfo=None).isoformat()
conn.execute(
"UPDATE slabs SET title=?, short_description=?, price=?, price_source=COALESCE(price_source, ?), "
"updated_at=? WHERE id=?",
(
slab.get("title"),
slab.get("short_description"),
price,
"recommendation",
now,
slab_id,
),
)
# Server-derived ready status on the row when the full set is present.
ready, missing = compute_ready({**slab, "price": price})
if ready:
conn.execute("UPDATE slabs SET status='ready', updated_at=? WHERE id=? AND status NOT IN ('published','failed')", (now, slab_id))
conn.commit()
return {
"ready": ready,
"missing": missing,
@@ -192,4 +257,5 @@ async def review_slab(slab_id: str, payload: dict[str, Any] | None = None) -> di
"price": price,
"price_suggestion": suggested,
"primary_wood_category_id": primary_id,
"taxonomy_locked": taxonomy_locked,
}