feat(slabs): S2 CRUD + optimistic concurrency + soft delete + slab list UI
- backend/slabs.py: create (calibrated, client-derived stored as sent), list (excl soft-deleted, most recent), get, update (single compare-and-set on server_rev, 409 revision_conflict / 409 duplicate_sku / 422 stale_field, status server-derived), delete (soft, published rejected). Explicit column allowlists, parameterized SQL, JSON array fields, commits. - backend/app.py: wire slabs router. - frontend: src/api/slabs.ts client + create/slabs/SlabList.svelte + dev toggle. - tests: 9 slabs tests (create/dup/list/get/update/rev-conflict/dup-sku/soft-delete/ published-delete) with full taxonomy seeding. Verified: pytest 23 passed; check_requirements + check_coverage exit 0; npm build ok.
This commit is contained in:
+8
-1
@@ -90,7 +90,9 @@ def _enforce_startup_guards() -> None:
|
||||
_enforce_startup_guards()
|
||||
|
||||
|
||||
# --- Request ID middleware (threads X-Request-Id or generates) ---
|
||||
# --- Request ID middleware (threads X-Request-Id or generates) --
|
||||
|
||||
|
||||
class RequestIdMiddleware(BaseHTTPMiddleware):
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
rid = request.headers.get("X-Request-Id") or str(uuid.uuid4())
|
||||
@@ -178,5 +180,10 @@ async def put_settings(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
return settings_mod.update_settings(payload)
|
||||
|
||||
|
||||
# expose for tests
|
||||
|
||||
from .slabs import router as slabs_router
|
||||
app.include_router(slabs_router)
|
||||
|
||||
# expose for tests
|
||||
__all__ = ["app", "__version__"]
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
"""Slab CRUD service + FastAPI router (S2).
|
||||
|
||||
Client-derived numbers are stored as sent (never recomputed). Optimistic
|
||||
concurrency via a single compare-and-set UPDATE on server_rev. Soft delete
|
||||
sets deleted_at; published slabs cannot be deleted.
|
||||
"""
|
||||
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi.responses import JSONResponse, Response
|
||||
|
||||
from .db import get_connection
|
||||
|
||||
router = APIRouter(prefix="/api/v1/slabs")
|
||||
|
||||
# Client-settable slab columns (explicit allowlist; never build SQL from client keys).
|
||||
ALLOWED_FIELDS = [
|
||||
"id", "sku", "length_in", "thickness_in", "sqft", "bdft",
|
||||
"width_min_in", "width_max_in", "width_avg_in", "species_id",
|
||||
"wood_category_ids", "edge_type_term_id", "figure_term_ids", "grade_term_id",
|
||||
"thickness_term_id", "moisture_term_id", "fig_tag_ids", "feat_tag_ids",
|
||||
"price", "price_source", "title", "short_description", "description",
|
||||
"content_source", "species_confidence", "figure_confidence", "grade_confidence",
|
||||
"inference_status", "calibration_photo_id",
|
||||
]
|
||||
JSON_ARRAY_FIELDS = {"wood_category_ids", "figure_term_ids", "fig_tag_ids", "feat_tag_ids"}
|
||||
# Columns a client may set on update (id/status/revs/timestamps/deleted_at excluded).
|
||||
ALLOWED_UPDATE_FIELDS = set(ALLOWED_FIELDS) - {
|
||||
"id", "status", "client_rev", "server_rev", "deleted_at", "created_at", "updated_at",
|
||||
}
|
||||
TAXONOMY_TABLES = {
|
||||
"species_id": "woo_categories",
|
||||
"wood_category_ids": "woo_categories",
|
||||
"edge_type_term_id": "woo_attribute_terms",
|
||||
"figure_term_ids": "woo_attribute_terms",
|
||||
"grade_term_id": "woo_attribute_terms",
|
||||
"thickness_term_id": "woo_attribute_terms",
|
||||
"moisture_term_id": "woo_attribute_terms",
|
||||
"fig_tag_ids": "woo_tags",
|
||||
"feat_tag_ids": "woo_tags",
|
||||
}
|
||||
|
||||
|
||||
def _now_iso() -> str:
|
||||
return datetime.now(timezone.utc).replace(microsecond=0, tzinfo=None).isoformat()
|
||||
|
||||
|
||||
def _error(code: str, message: str, detail: Any = None) -> Dict[str, Any]:
|
||||
return {"error": {"code": code, "message": message, "detail": detail}}
|
||||
|
||||
|
||||
def _err(code: str, message: str, detail: Any = None, status: int = 400) -> JSONResponse:
|
||||
return JSONResponse(status_code=status, content=_error(code, message, detail))
|
||||
|
||||
|
||||
def _encode(payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
out = dict(payload)
|
||||
for f in JSON_ARRAY_FIELDS:
|
||||
if f in out and isinstance(out[f], list):
|
||||
out[f] = json.dumps(out[f])
|
||||
return out
|
||||
|
||||
|
||||
def _row_to_slab(row) -> Dict[str, Any]:
|
||||
d = dict(row)
|
||||
for f in JSON_ARRAY_FIELDS:
|
||||
if d.get(f) is not None and isinstance(d[f], str):
|
||||
try:
|
||||
d[f] = json.loads(d[f])
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
return d
|
||||
|
||||
|
||||
def _sku_taken(conn, sku: str, exclude_id: str | None = None) -> bool:
|
||||
if exclude_id:
|
||||
return conn.execute(
|
||||
"SELECT 1 FROM slabs WHERE sku=? AND deleted_at IS NULL AND id<>?", (sku, exclude_id)
|
||||
).fetchone() is not None
|
||||
return conn.execute(
|
||||
"SELECT 1 FROM slabs WHERE sku=? AND deleted_at IS NULL", (sku,)
|
||||
).fetchone() is not None
|
||||
|
||||
|
||||
def _check_taxonomy(conn, table: str, val: Any) -> bool:
|
||||
if isinstance(val, list):
|
||||
if not val:
|
||||
return True
|
||||
ph = ",".join(["?"] * len(val))
|
||||
rows = conn.execute(f"SELECT id FROM {table} WHERE id IN ({ph})", val).fetchall()
|
||||
return {r[0] for r in rows} == set(val)
|
||||
return conn.execute(f"SELECT 1 FROM {table} WHERE id=?", (val,)).fetchone() is not None
|
||||
|
||||
|
||||
@router.post("", status_code=201)
|
||||
async def create_slab(payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
missing = [f for f in ALLOWED_FIELDS if f not in payload]
|
||||
if missing:
|
||||
return _err("validation", "missing fields", {"fields": missing}, status=400)
|
||||
conn = get_connection()
|
||||
if _sku_taken(conn, payload["sku"]):
|
||||
return _err("duplicate_sku", "SKU already exists", {"sku": payload["sku"]}, status=409)
|
||||
now = _now_iso()
|
||||
data = _encode(payload)
|
||||
cols = [f for f in ALLOWED_FIELDS if f in data] + ["status", "client_rev", "server_rev", "deleted_at", "created_at", "updated_at"]
|
||||
vals = [data[f] for f in cols if f in data] + ["calibrated", 1, 1, None, now, now]
|
||||
ph = ",".join(["?"] * len(cols))
|
||||
conn.execute(f"INSERT INTO slabs ({', '.join(cols)}) VALUES ({ph})", vals)
|
||||
conn.commit()
|
||||
row = conn.execute("SELECT * FROM slabs WHERE id=?", (payload["id"],)).fetchone()
|
||||
return _row_to_slab(row)
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_slabs() -> List[Dict[str, Any]]:
|
||||
conn = get_connection()
|
||||
rows = conn.execute("SELECT * FROM slabs WHERE deleted_at IS NULL ORDER BY updated_at DESC, id DESC").fetchall()
|
||||
return [_row_to_slab(r) for r in rows]
|
||||
|
||||
|
||||
@router.get("/{id}")
|
||||
async def get_slab(id: str) -> Dict[str, Any]:
|
||||
conn = get_connection()
|
||||
row = conn.execute("SELECT * FROM slabs WHERE id=? AND deleted_at IS NULL", (id,)).fetchone()
|
||||
if row is None:
|
||||
return _err("not_found", "Slab not found", status=404)
|
||||
return _row_to_slab(row)
|
||||
|
||||
|
||||
@router.put("/{id}")
|
||||
async def update_slab(id: str, payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
if "client_rev" not in payload:
|
||||
return _err("validation", "client_rev required", status=400)
|
||||
conn = get_connection()
|
||||
current = conn.execute("SELECT * FROM slabs WHERE id=? AND deleted_at IS NULL", (id,)).fetchone()
|
||||
if current is None:
|
||||
return _err("not_found", "Slab not found", status=404)
|
||||
client_rev = payload["client_rev"]
|
||||
if client_rev != current["server_rev"]:
|
||||
return _err("revision_conflict", "client_rev mismatch",
|
||||
{"current_server_rev": current["server_rev"]}, status=409)
|
||||
if "sku" in payload and payload["sku"] != current["sku"]:
|
||||
if _sku_taken(conn, payload["sku"], exclude_id=id):
|
||||
return _err("duplicate_sku", "SKU already exists", {"sku": payload["sku"]}, status=409)
|
||||
for field, table in TAXONOMY_TABLES.items():
|
||||
if field in payload and payload[field] is not None:
|
||||
val = payload[field]
|
||||
if isinstance(val, str):
|
||||
try:
|
||||
val = json.loads(val)
|
||||
except (ValueError, TypeError):
|
||||
val = []
|
||||
if not _check_taxonomy(conn, table, val):
|
||||
return _err("stale_field", f"{field} not in current taxonomy",
|
||||
{"current_options": val}, status=422)
|
||||
data = _encode(payload)
|
||||
cols = [f for f in ALLOWED_UPDATE_FIELDS if f in data]
|
||||
if not cols:
|
||||
return _row_to_slab(current)
|
||||
set_clause = ", ".join(f"{f} = ?" for f in cols)
|
||||
vals = [data[f] for f in cols] + [client_rev + 1, _now_iso(), id, client_rev]
|
||||
cur = conn.execute(
|
||||
f"UPDATE slabs SET {set_clause}, server_rev=?, updated_at=? WHERE id=? AND server_rev=?",
|
||||
vals,
|
||||
)
|
||||
conn.commit()
|
||||
if cur.rowcount == 0:
|
||||
cur_server = conn.execute("SELECT server_rev FROM slabs WHERE id=?", (id,)).fetchone()
|
||||
return _err("revision_conflict", "client_rev mismatch",
|
||||
{"current_server_rev": cur_server[0] if cur_server else None}, status=409)
|
||||
row = conn.execute("SELECT * FROM slabs WHERE id=?", (id,)).fetchone()
|
||||
return _row_to_slab(row)
|
||||
|
||||
|
||||
@router.delete("/{id}", status_code=204)
|
||||
async def delete_slab(id: str):
|
||||
conn = get_connection()
|
||||
row = conn.execute("SELECT * FROM slabs WHERE id=? AND deleted_at IS NULL", (id,)).fetchone()
|
||||
if row is None:
|
||||
return _err("not_found", "Slab not found", status=404)
|
||||
if row["status"] == "published":
|
||||
return _err("publish_conflict", "Published slab cannot be deleted", status=409)
|
||||
conn.execute("UPDATE slabs SET deleted_at=? WHERE id=?", (_now_iso(), id))
|
||||
conn.commit()
|
||||
return Response(status_code=204)
|
||||
@@ -0,0 +1,192 @@
|
||||
import os
|
||||
import json
|
||||
import uuid
|
||||
import datetime
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
import backend.db as db_mod
|
||||
|
||||
|
||||
# Helper to set environment for tests
|
||||
|
||||
def _setup_env(monkeypatch, tmp):
|
||||
db_path = os.path.join(tmp, "test.db")
|
||||
img_dir = os.path.join(tmp, "images")
|
||||
os.mkdir(img_dir)
|
||||
monkeypatch.setenv("SLAB_DB_PATH", db_path)
|
||||
monkeypatch.setenv("SLAB_IMAGES_DIR", img_dir)
|
||||
monkeypatch.setenv("WOO_BASE_URL", "https://example.com")
|
||||
monkeypatch.setenv("SLAB_AES_KEY", "aGVsbGxvb2xvY2g=") # dummy base64
|
||||
monkeypatch.setenv("LOG_LEVEL", "INFO")
|
||||
db_mod._reset_for_tests()
|
||||
|
||||
# Taxonomy seeding helper
|
||||
|
||||
def _seed_taxonomy(conn):
|
||||
# create some categories
|
||||
conn.execute("INSERT OR REPLACE INTO woo_categories(id, name, slug, parent_id, woo_id) VALUES (1, 'Species A', 'species-a', NULL, 10)")
|
||||
conn.execute("INSERT OR REPLACE INTO woo_categories(id, name, slug, parent_id, woo_id) VALUES (2, 'Wood Cat A', 'wood-cat-a', NULL, 20)")
|
||||
# attribute terms
|
||||
conn.execute("INSERT OR REPLACE INTO woo_attributes(id, name, slug, woo_id) VALUES (1, 'Edge Type', 'edge-type', 30)")
|
||||
conn.execute("INSERT OR REPLACE INTO woo_attribute_terms(id, attribute_id, name, slug, woo_id) VALUES (1, 1, 'Edge A', 'edge-a', 40)")
|
||||
# tags
|
||||
conn.execute("INSERT OR REPLACE INTO woo_tags(id, name, slug, woo_id) VALUES (1, 'Fig Tag A', 'fig-tag-a', 50)")
|
||||
conn.execute("INSERT OR REPLACE INTO woo_tags(id, name, slug, woo_id) VALUES (2, 'Feat Tag A', 'feat-tag-a', 60)")
|
||||
conn.commit()
|
||||
|
||||
@pytest.fixture
|
||||
def db(tmp_path, monkeypatch):
|
||||
_setup_env(monkeypatch, tmp_path)
|
||||
conn = db_mod.get_connection()
|
||||
db_mod.init_db()
|
||||
_seed_taxonomy(conn)
|
||||
return conn
|
||||
_setup_env(monkeypatch, tmp_path)
|
||||
conn = db_mod.get_connection()
|
||||
db_mod.init_db()
|
||||
_seed_taxonomy(conn)
|
||||
return conn
|
||||
|
||||
# Helper to get client
|
||||
@pytest.fixture
|
||||
def client(db):
|
||||
from backend.app import app
|
||||
return TestClient(app)
|
||||
|
||||
# Utility to format now
|
||||
_now_iso = lambda: datetime.datetime.utcnow().replace(microsecond=0).isoformat()
|
||||
|
||||
# --- Tests ---
|
||||
|
||||
|
||||
def test_create_and_fetch(client, db):
|
||||
slab_id = str(uuid.uuid4())
|
||||
payload = {
|
||||
"id": slab_id,
|
||||
"sku": "SKU001",
|
||||
"length_in": 10.0,
|
||||
"thickness_in": 2.0,
|
||||
"sqft": 20.0,
|
||||
"bdft": 5.0,
|
||||
"width_min_in": 1.0,
|
||||
"width_max_in": 2.0,
|
||||
"width_avg_in": 1.5,
|
||||
"species_id": 1,
|
||||
"wood_category_ids": json.dumps([2]),
|
||||
"edge_type_term_id": 1,
|
||||
"figure_term_ids": json.dumps([1]),
|
||||
"grade_term_id": 1,
|
||||
"thickness_term_id": 1,
|
||||
"moisture_term_id": 1,
|
||||
"fig_tag_ids": json.dumps([1]),
|
||||
"feat_tag_ids": json.dumps([2]),
|
||||
"price": 100.0,
|
||||
"price_source": "recommendation",
|
||||
"title": "Test Slab",
|
||||
"short_description": "short",
|
||||
"description": "desc",
|
||||
"content_source": "manual",
|
||||
"species_confidence": 0.9,
|
||||
"figure_confidence": 0.8,
|
||||
"grade_confidence": 0.7,
|
||||
"inference_status": "inferring",
|
||||
"calibration_photo_id": None
|
||||
}
|
||||
resp = client.post("/api/v1/slabs", json=payload)
|
||||
assert resp.status_code == 201
|
||||
body = resp.json()
|
||||
assert body["status"] == "calibrated"
|
||||
assert body["client_rev"] == 1
|
||||
assert body["server_rev"] == 1
|
||||
for key in ["length_in","thickness_in","sqft","bdft","width_min_in","width_max_in","width_avg_in"]:
|
||||
assert body[key] == payload[key]
|
||||
|
||||
|
||||
def test_duplicate_sku(client, db):
|
||||
slab_id = str(uuid.uuid4())
|
||||
payload = {"id": slab_id, "sku": "SKU001", "length_in":10,"thickness_in":2,"sqft":20,"bdft":5,"width_min_in":1,"width_max_in":2,"width_avg_in":1.5,"species_id":1,"wood_category_ids":json.dumps([2]),"edge_type_term_id":1,"figure_term_ids":json.dumps([1]),"grade_term_id":1,"thickness_term_id":1,"moisture_term_id":1,"fig_tag_ids":json.dumps([1]),"feat_tag_ids":json.dumps([2]),"price":100,"price_source":"recommendation","title":"Test","short_description":"short","description":"desc","content_source":"manual","species_confidence":0.9,"figure_confidence":0.8,"grade_confidence":0.7,"inference_status":"inferring","calibration_photo_id":None}
|
||||
resp1 = client.post("/api/v1/slabs", json=payload)
|
||||
assert resp1.status_code == 201
|
||||
resp2 = client.post("/api/v1/slabs", json=payload)
|
||||
assert resp2.status_code == 409
|
||||
|
||||
|
||||
def test_list_and_exclude_deleted(client, db):
|
||||
# create two slabs
|
||||
for i in [1,2]:
|
||||
slab_id = str(uuid.uuid4())
|
||||
payload = {"id": slab_id, "sku": f"SKU{i}", "length_in":10,"thickness_in":2,"sqft":20,"bdft":5,"width_min_in":1,"width_max_in":2,"width_avg_in":1.5,"species_id":1,"wood_category_ids":json.dumps([2]),"edge_type_term_id":1,"figure_term_ids":json.dumps([1]),"grade_term_id":1,"thickness_term_id":1,"moisture_term_id":1,"fig_tag_ids":json.dumps([1]),"feat_tag_ids":json.dumps([2]),"price":100,"price_source":"recommendation","title":"Test","short_description":"short","description":"desc","content_source":"manual","species_confidence":0.9,"figure_confidence":0.8,"grade_confidence":0.7,"inference_status":"inferring","calibration_photo_id":None}
|
||||
client.post("/api/v1/slabs", json=payload)
|
||||
# delete first created
|
||||
resp_list = client.get("/api/v1/slabs")
|
||||
ids = [r["id"] for r in resp_list.json()]
|
||||
client.delete(f"/api/v1/slabs/{ids[0]}")
|
||||
# list again
|
||||
resp2 = client.get("/api/v1/slabs")
|
||||
body = resp2.json()
|
||||
assert len(body) == 1
|
||||
assert body[0]["id"] == ids[1]
|
||||
|
||||
|
||||
def test_get_not_found(client, db):
|
||||
resp = client.get("/api/v1/slabs/nonexistent")
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
def test_update_success(client, db):
|
||||
slab_id = str(uuid.uuid4())
|
||||
payload = {"id": slab_id, "sku": "SKU001", "length_in":10,"thickness_in":2,"sqft":20,"bdft":5,"width_min_in":1,"width_max_in":2,"width_avg_in":1.5,"species_id":1,"wood_category_ids":json.dumps([2]),"edge_type_term_id":1,"figure_term_ids":json.dumps([1]),"grade_term_id":1,"thickness_term_id":1,"moisture_term_id":1,"fig_tag_ids":json.dumps([1]),"feat_tag_ids":json.dumps([2]),"price":100,"price_source":"recommendation","title":"Test","short_description":"short","description":"desc","content_source":"manual","species_confidence":0.9,"figure_confidence":0.8,"grade_confidence":0.7,"inference_status":"inferring","calibration_photo_id":None}
|
||||
client.post("/api/v1/slabs", json=payload)
|
||||
# update
|
||||
update_payload = {"client_rev":1, "sku":"SKU002"}
|
||||
resp = client.put(f"/api/v1/slabs/{slab_id}", json=update_payload)
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["sku"] == "SKU002"
|
||||
assert body["server_rev"] == 2
|
||||
|
||||
|
||||
def test_update_revision_conflict(client, db):
|
||||
slab_id = str(uuid.uuid4())
|
||||
payload = {"id": slab_id, "sku": "SKU001", "length_in":10,"thickness_in":2,"sqft":20,"bdft":5,"width_min_in":1,"width_max_in":2,"width_avg_in":1.5,"species_id":1,"wood_category_ids":json.dumps([2]),"edge_type_term_id":1,"figure_term_ids":json.dumps([1]),"grade_term_id":1,"thickness_term_id":1,"moisture_term_id":1,"fig_tag_ids":json.dumps([1]),"feat_tag_ids":json.dumps([2]),"price":100,"price_source":"recommendation","title":"Test","short_description":"short","description":"desc","content_source":"manual","species_confidence":0.9,"figure_confidence":0.8,"grade_confidence":0.7,"inference_status":"inferring","calibration_photo_id":None}
|
||||
client.post("/api/v1/slabs", json=payload)
|
||||
# wrong client_rev
|
||||
resp = client.put(f"/api/v1/slabs/{slab_id}", json={"client_rev":99, "sku":"SKU002"})
|
||||
assert resp.status_code == 409
|
||||
|
||||
|
||||
def test_update_duplicate_sku(client, db):
|
||||
# create two slabs
|
||||
id1, id2 = str(uuid.uuid4()), str(uuid.uuid4())
|
||||
payload1 = {"id": id1, "sku": "SKU001", "length_in":10,"thickness_in":2,"sqft":20,"bdft":5,"width_min_in":1,"width_max_in":2,"width_avg_in":1.5,"species_id":1,"wood_category_ids":json.dumps([2]),"edge_type_term_id":1,"figure_term_ids":json.dumps([1]),"grade_term_id":1,"thickness_term_id":1,"moisture_term_id":1,"fig_tag_ids":json.dumps([1]),"feat_tag_ids":json.dumps([2]),"price":100,"price_source":"recommendation","title":"Test","short_description":"short","description":"desc","content_source":"manual","species_confidence":0.9,"figure_confidence":0.8,"grade_confidence":0.7,"inference_status":"inferring","calibration_photo_id":None}
|
||||
payload2 = {"id": id2, "sku": "SKU002", "length_in":10,"thickness_in":2,"sqft":20,"bdft":5,"width_min_in":1,"width_max_in":2,"width_avg_in":1.5,"species_id":1,"wood_category_ids":json.dumps([2]),"edge_type_term_id":1,"figure_term_ids":json.dumps([1]),"grade_term_id":1,"thickness_term_id":1,"moisture_term_id":1,"fig_tag_ids":json.dumps([1]),"feat_tag_ids":json.dumps([2]),"price":100,"price_source":"recommendation","title":"Test","short_description":"short","description":"desc","content_source":"manual","species_confidence":0.9,"figure_confidence":0.8,"grade_confidence":0.7,"inference_status":"inferring","calibration_photo_id":None}
|
||||
client.post("/api/v1/slabs", json=payload1)
|
||||
client.post("/api/v1/slabs", json=payload2)
|
||||
# update first to sku of second
|
||||
resp = client.put(f"/api/v1/slabs/{id1}", json={"client_rev":1, "sku":"SKU002"})
|
||||
assert resp.status_code == 409
|
||||
|
||||
|
||||
def test_soft_delete(client, db):
|
||||
slab_id = str(uuid.uuid4())
|
||||
payload = {"id": slab_id, "sku": "SKU001", "length_in":10,"thickness_in":2,"sqft":20,"bdft":5,"width_min_in":1,"width_max_in":2,"width_avg_in":1.5,"species_id":1,"wood_category_ids":json.dumps([2]),"edge_type_term_id":1,"figure_term_ids":json.dumps([1]),"grade_term_id":1,"thickness_term_id":1,"moisture_term_id":1,"fig_tag_ids":json.dumps([1]),"feat_tag_ids":json.dumps([2]),"price":100,"price_source":"recommendation","title":"Test","short_description":"short","description":"desc","content_source":"manual","species_confidence":0.9,"figure_confidence":0.8,"grade_confidence":0.7,"inference_status":"inferring","calibration_photo_id":None}
|
||||
client.post("/api/v1/slabs", json=payload)
|
||||
client.delete(f"/api/v1/slabs/{slab_id}")
|
||||
resp = client.get("/api/v1/slabs")
|
||||
body = resp.json()
|
||||
assert all(r["id"] != slab_id for r in body)
|
||||
resp_get = client.get(f"/api/v1/slabs/{slab_id}")
|
||||
assert resp_get.status_code == 404
|
||||
|
||||
|
||||
def test_delete_published_conflict(client, db):
|
||||
slab_id = str(uuid.uuid4())
|
||||
payload = {"id": slab_id, "sku": "SKU001", "length_in":10,"thickness_in":2,"sqft":20,"bdft":5,"width_min_in":1,"width_max_in":2,"width_avg_in":1.5,"species_id":1,"wood_category_ids":json.dumps([2]),"edge_type_term_id":1,"figure_term_ids":json.dumps([1]),"grade_term_id":1,"thickness_term_id":1,"moisture_term_id":1,"fig_tag_ids":json.dumps([1]),"feat_tag_ids":json.dumps([2]),"price":100,"price_source":"recommendation","title":"Test","short_description":"short","description":"desc","content_source":"manual","species_confidence":0.9,"figure_confidence":0.8,"grade_confidence":0.7,"inference_status":"inferring","calibration_photo_id":None}
|
||||
client.post("/api/v1/slabs", json=payload)
|
||||
# manually set status to published
|
||||
conn = db_mod.get_connection()
|
||||
conn.execute("UPDATE slabs SET status = 'published' WHERE id = ?", (slab_id,))
|
||||
conn.commit()
|
||||
resp = client.delete(f"/api/v1/slabs/{slab_id}")
|
||||
assert resp.status_code == 409
|
||||
Reference in New Issue
Block a user