From c8d6d5eda009cf8f8d1c789abd0ab63d8f970ed9 Mon Sep 17 00:00:00 2001 From: Ty Date: Sun, 20 Sep 2026 14:45:32 -0700 Subject: [PATCH] fix(backend): create accepts capture-only payload; fix INSERT binding count MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit create_slab required ALL ALLOWED_FIELDS present, but the Review screen fills taxonomy/content after create (CREATE.md §1). Now requires only the capture fields (id, sku, dims, widths); the rest stay nullable for Review. Also fixed a pre-existing off-by-one: vals re-filtered cols by 'in data', dropping the 6 server-managed columns and under-counting bindings (19 vs 18). Split allowed_cols/server_cols so vals always matches cols. Verified: 9/9 slabs tests pass; live POST /slabs with the app's capture-math payload returns 201. --- backend/slabs.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/backend/slabs.py b/backend/slabs.py index 814c5b3..e3ca79e 100644 --- a/backend/slabs.py +++ b/backend/slabs.py @@ -95,9 +95,19 @@ def _check_taxonomy(conn, table: str, val: Any) -> bool: return conn.execute(f"SELECT 1 FROM {table} WHERE id=?", (val,)).fetchone() is not None +# Capture-only fields required to create a slab. The Review screen fills the +# taxonomy/content fields after create (CREATE.md §1: "every other field stays +# editable until create"), so create must accept a capture-math payload. +# client_rev is server-managed (set to 1 at create) — never required from the client. +REQUIRED_CREATE_FIELDS = [ + "id", "sku", "length_in", "thickness_in", "sqft", "bdft", + "width_min_in", "width_max_in", "width_avg_in", +] + + @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] + missing = [f for f in REQUIRED_CREATE_FIELDS if f not in payload] if missing: return _err("validation", "missing fields", {"fields": missing}, status=400) conn = get_connection() @@ -105,9 +115,14 @@ async def create_slab(payload: Dict[str, Any]) -> Dict[str, Any]: 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] + allowed_cols = [f for f in ALLOWED_FIELDS if f in data] + server_cols = ["status", "client_rev", "server_rev", "deleted_at", "created_at", "updated_at"] + server_vals = ["calibrated", 1, 1, None, now, now] + cols = allowed_cols + server_cols + # One value per col, in cols order: data values for the allowed fields, then the 6 server values. + vals = [data[f] for f in allowed_cols] + server_vals ph = ",".join(["?"] * len(cols)) + assert len(vals) == len(cols), f"create_slab binding mismatch: {len(vals)} vals vs {len(cols)} 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()