From 00e08a86dda8d2e9f71421c386c42a708fa5d308 Mon Sep 17 00:00:00 2001 From: type-two Date: Sun, 5 Jul 2026 03:34:27 +1000 Subject: [PATCH] feat(sku): canonical 14-digit timestamp SKUs so RFID/EPC tagging works (new items) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RecordGod generated 'RG'+random-hex SKUs, which the SCANGOD EPC codec rejects (it requires 14 digits: SKU × 10^9 + release_id packed onto the UHF chip). New inventory now gets a 14-digit product-added timestamp (YYYYMMDDHHMMSS) via _fresh_sku(), which bumps a second on collision to stay unique against the sku PRIMARY KEY. release_id stays its own column (the sku+space+release_id string is only the physical chip encoding, which the codec already builds). New items only — existing 'RG' SKUs are untouched (they keep working; they just aren't RFID-taggable until re-SKU'd, a separate migration if ever wanted). Co-Authored-By: Claude Fable 5 --- app/admin_routes.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/app/admin_routes.py b/app/admin_routes.py index a34031d..87b483c 100644 --- a/app/admin_routes.py +++ b/app/admin_routes.py @@ -1,5 +1,6 @@ import re import secrets +from datetime import datetime, timedelta import httpx from fastapi import APIRouter, Depends, HTTPException, Query @@ -547,9 +548,24 @@ class BulkIn(BaseModel): value: float | int | str | None = None +async def _fresh_sku(db): + """Canonical SKU = 14-digit product-added timestamp (YYYYMMDDHHMMSS) — the format SCANGOD's EPC/RFID + codec requires (packs SKU 14 digits × 10^9 + release_id onto the UHF chip). Bumps a second on + collision so a same-second batch stays unique against the sku PRIMARY KEY. + ponytail: check-then-insert; a truly concurrent same-second add on another connection could still hit + the PK and be ON CONFLICT DO NOTHING'd. Fine for counter/admin stock entry; add retry-on-conflict if + bulk-import concurrency grows.""" + base = datetime.now() + for i in range(120): + sku = (base + timedelta(seconds=i)).strftime("%Y%m%d%H%M%S") + if not (await db.execute(text("SELECT 1 FROM inventory WHERE sku = :s"), {"s": sku})).first(): + return sku + return (base + timedelta(seconds=120)).strftime("%Y%m%d%H%M%S") + + @router.post("/inventory/add") async def inv_add(body: InvItemIn, ident=Depends(require_token), db=Depends(get_db)): - sku = (body.sku or "").strip() or ("RG" + secrets.token_hex(4).upper()) + sku = (body.sku or "").strip() or await _fresh_sku(db) title = body.title if body.release_id and not title: # pull the cached title if intaking by release r = (await db.execute(text("SELECT title FROM disc_cache WHERE release_id=:r"), @@ -629,7 +645,7 @@ async def inv_import(body: ImportIn, ident=Depends(require_token), db=Depends(ge if not rid: not_found.append(ln) continue - sku = "RG" + secrets.token_hex(4).upper() + sku = await _fresh_sku(db) await db.execute(text(""" INSERT INTO inventory (sku, store_id, kind, release_id, title, price, qty, in_stock, condition, crate_id, status, created_at, updated_at)