feat(review): S5 pricing rules + deterministic title/short desc + server-derived ready + Review UI
- backend/pricing.py: /admin/pricing CRUD (one flat $/bdft per species leaf, SETTINGS-35).
- backend/review.py: POST /slabs/{id}/review -> deterministic title + short description
(7 leaf patterns, length ft/in split, thickness 1/8-in precision, METADATA-80/85/90),
price suggestion (bdft x species rate, REVIEW-25), server-derived ready + missing set
(REVIEW-5).
- frontend: create/review/Review.svelte (slab id, primary category, price override,
ready state + generated copy) + dev toggle.
- tests: 3 pricing + 4 review tests (title no-dims, ready set, price suggestion,
knife-blank dims).
Verified: pytest 43 passed; gates exit 0; npm build ok.
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
"""S5 pricing rules tests (SETTINGS-35, REVIEW-25)."""
|
||||
|
||||
import base64
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
import backend.db as db_mod
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def env():
|
||||
tmp = tempfile.mkdtemp(prefix="pricing_")
|
||||
os.environ["SLAB_DB_PATH"] = os.path.join(tmp, "p.db")
|
||||
os.environ["SLAB_IMAGES_DIR"] = tmp
|
||||
os.environ["WOO_BASE_URL"] = "https://example.com"
|
||||
os.environ["SLAB_AES_KEY"] = base64.b64encode(b"0123456789abcdef0123456789abcdef").decode()
|
||||
db_mod._reset_for_tests()
|
||||
from backend.app import app
|
||||
|
||||
conn = db_mod.init_db()
|
||||
for cid in (1, 7):
|
||||
conn.execute("INSERT OR IGNORE INTO woo_categories(id,name,slug,parent_id,woo_id) VALUES (?,?,?,NULL,?)", (cid, f"Sp{cid}", f"sp{cid}", cid))
|
||||
conn.commit()
|
||||
yield TestClient(app)
|
||||
db_mod._reset_for_tests()
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
|
||||
|
||||
def test_crud_cycle(env):
|
||||
r = env.post("/api/v1/admin/pricing", json={"species_id": 1, "price_per_bdft": 12.5})
|
||||
assert r.status_code == 201, r.text
|
||||
rule = r.json()
|
||||
rid = rule["id"]
|
||||
assert rule["price_per_bdft"] == 12.5
|
||||
|
||||
r = env.get("/api/v1/admin/pricing")
|
||||
assert r.status_code == 200
|
||||
assert [x["species_id"] for x in r.json()] == [1]
|
||||
|
||||
r = env.put(f"/api/v1/admin/pricing/{rid}", json={"price_per_bdft": 15.0})
|
||||
assert r.status_code == 200
|
||||
assert r.json()["price_per_bdft"] == 15.0
|
||||
|
||||
r = env.delete(f"/api/v1/admin/pricing/{rid}")
|
||||
assert r.status_code == 204
|
||||
assert env.get("/api/v1/admin/pricing").json() == []
|
||||
|
||||
|
||||
def test_duplicate_species(env):
|
||||
env.post("/api/v1/admin/pricing", json={"species_id": 7, "price_per_bdft": 1.0})
|
||||
r = env.post("/api/v1/admin/pricing", json={"species_id": 7, "price_per_bdft": 2.0})
|
||||
assert r.status_code == 409
|
||||
|
||||
|
||||
def test_validation(env):
|
||||
r = env.post("/api/v1/admin/pricing", json={"species_id": 1})
|
||||
assert r.status_code == 400
|
||||
r = env.post("/api/v1/admin/pricing", json={"species_id": 1, "price_per_bdft": -5})
|
||||
assert r.status_code == 400
|
||||
@@ -0,0 +1,97 @@
|
||||
"""S5 deterministic copy + ready derivation tests (METADATA-80/85/90, REVIEW-5/25)."""
|
||||
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
import backend.db as db_mod
|
||||
from backend import review as review_mod
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def env():
|
||||
tmp = tempfile.mkdtemp(prefix="review_")
|
||||
os.environ["SLAB_DB_PATH"] = os.path.join(tmp, "r.db")
|
||||
os.environ["SLAB_IMAGES_DIR"] = tmp
|
||||
os.environ["WOO_BASE_URL"] = "https://example.com"
|
||||
os.environ["SLAB_AES_KEY"] = base64.b64encode(b"0123456789abcdef0123456789abcdef").decode()
|
||||
db_mod._reset_for_tests()
|
||||
conn = db_mod.init_db()
|
||||
# taxonomy: species + a live-edge-wood leaf + figure term
|
||||
conn.execute("INSERT INTO woo_categories(id,name,slug,parent_id,woo_id) VALUES (1,'Black Walnut','black-walnut',NULL,100)")
|
||||
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 (1,'Figure','figure',200)")
|
||||
conn.execute("INSERT INTO woo_attribute_terms(id,attribute_id,name,slug,woo_id) VALUES (1,1,'Figured','figured',300)")
|
||||
conn.commit()
|
||||
from backend.app import app
|
||||
|
||||
yield TestClient(app), conn
|
||||
db_mod._reset_for_tests()
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
|
||||
|
||||
def _slab(sku="SLAB-01", **over):
|
||||
base = {
|
||||
"id": "s1", "sku": sku, "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,
|
||||
"species_id": 1, "wood_category_ids": json.dumps([2]), "edge_type_term_id": None,
|
||||
"figure_term_ids": json.dumps([1]), "grade_term_id": None, "thickness_term_id": None,
|
||||
"moisture_term_id": None, "fig_tag_ids": "[]", "feat_tag_ids": "[]",
|
||||
"price": None, "price_source": None, "title": None, "short_description": None,
|
||||
"description": "A fine slab.", "content_source": "manual", "status": "calibrated",
|
||||
"client_rev": 1, "server_rev": 1, "deleted_at": None, "created_at": "t", "updated_at": "t",
|
||||
}
|
||||
base.update(over)
|
||||
return base
|
||||
|
||||
|
||||
def test_title_live_edge_no_dims(env):
|
||||
_, conn = env
|
||||
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,18.4,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 = env[0].post("/api/v1/slabs/s1/review", json={})
|
||||
assert r.status_code == 200, r.text
|
||||
body = r.json()
|
||||
assert body["title"] == "Figured Black Walnut Live Edge Slab"
|
||||
assert body["short_description"].startswith("Figured Black Walnut Live Edge Slab, 9ft 6in x 24.3in x 1.75in")
|
||||
assert "18.4 bdft" in body["short_description"]
|
||||
|
||||
|
||||
def test_ready_requires_full_set(env):
|
||||
client, conn = env
|
||||
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,18.4,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["ready"] is False
|
||||
# price is empty (no rate) and title/short_description generated -> missing includes price, price_source, grade/thickness/moisture terms
|
||||
assert "price" in body["missing"]
|
||||
|
||||
|
||||
def test_price_suggestion_from_rate(env):
|
||||
client, conn = env
|
||||
conn.execute("INSERT INTO pricing_rules(species_id,price_per_bdft,created_at,updated_at) VALUES (1,10.0,'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,18.4,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"] == 184.0 # 18.4 bdft * $10
|
||||
assert body["price"] == 184.0
|
||||
|
||||
|
||||
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)")
|
||||
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",12.0,1.5,6.0,0.7,6.0,6.5,6.5,1,json.dumps([3]),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={})
|
||||
assert r.json()["title"] == "Figured Black Walnut Knife Blank (12 x 6.5 x 1.5 in)"
|
||||
Reference in New Issue
Block a user