fix(backend): create accepts capture-only payload; fix INSERT binding count
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.
This commit is contained in:
+18
-3
@@ -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
|
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)
|
@router.post("", status_code=201)
|
||||||
async def create_slab(payload: Dict[str, Any]) -> Dict[str, Any]:
|
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:
|
if missing:
|
||||||
return _err("validation", "missing fields", {"fields": missing}, status=400)
|
return _err("validation", "missing fields", {"fields": missing}, status=400)
|
||||||
conn = get_connection()
|
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)
|
return _err("duplicate_sku", "SKU already exists", {"sku": payload["sku"]}, status=409)
|
||||||
now = _now_iso()
|
now = _now_iso()
|
||||||
data = _encode(payload)
|
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"]
|
allowed_cols = [f for f in ALLOWED_FIELDS if f in data]
|
||||||
vals = [data[f] for f in cols if f in data] + ["calibrated", 1, 1, None, now, now]
|
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))
|
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.execute(f"INSERT INTO slabs ({', '.join(cols)}) VALUES ({ph})", vals)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
row = conn.execute("SELECT * FROM slabs WHERE id=?", (payload["id"],)).fetchone()
|
row = conn.execute("SELECT * FROM slabs WHERE id=?", (payload["id"],)).fetchone()
|
||||||
|
|||||||
Reference in New Issue
Block a user