From 4081462cc4a37168a4748a1954a70f8df1b6a045 Mon Sep 17 00:00:00 2001 From: chattie Date: Sat, 22 Aug 2026 18:49:33 +0200 Subject: [PATCH] feat(F-176): completed feature --- backlog/features.json | 32 +++++++++ project/apps/pos/src/app/(terminal)/page.tsx | 13 ++-- project/src/modules/pos/api/pos.routes.ts | 48 ++++++++++---- work/artifacts/F-176/architect.md | 3 + work/artifacts/F-176/documenter.md | 3 + work/artifacts/F-176/implementer.md | 19 ++++++ work/artifacts/F-176/leader-close.json | 1 + work/artifacts/F-176/qa.json | 1 + work/artifacts/F-176/reviewer.json | 1 + work/artifacts/F-176/security.json | 1 + work/current.md | 4 +- work/runtime-status.json | 69 ++++++++++++++++++-- 12 files changed, 170 insertions(+), 25 deletions(-) create mode 100644 work/artifacts/F-176/architect.md create mode 100644 work/artifacts/F-176/documenter.md create mode 100644 work/artifacts/F-176/implementer.md create mode 100644 work/artifacts/F-176/leader-close.json create mode 100644 work/artifacts/F-176/qa.json create mode 100644 work/artifacts/F-176/reviewer.json create mode 100644 work/artifacts/F-176/security.json diff --git a/backlog/features.json b/backlog/features.json index d9bf0c0..09c9a7b 100644 --- a/backlog/features.json +++ b/backlog/features.json @@ -6988,6 +6988,38 @@ "close": true }, "completed_at": "2026-08-22T16:35:10Z" + }, + { + "id": "F-176", + "type": "fix", + "title": "Fix POS existing product search", + "description": "POS returns no results for existing barcode 590123412345", + "priority": "high", + "risk": "med", + "status": "done", + "created_at": "2026-08-22", + "gates": { + "reviewer": true, + "security": true, + "qa": true, + "close": true + }, + "completed_at": "2026-08-22T16:49:33Z" + }, + { + "id": "F-177", + "type": "feature", + "title": "Add stale order notifications to admin header", + "description": "Create admin header notification module for pending and shipped orders unchanged for more than 24 hours", + "priority": "high", + "risk": "med", + "status": "pending", + "created_at": "2026-08-22", + "gates": { + "reviewer": false, + "security": false, + "qa": false + } } ] } diff --git a/project/apps/pos/src/app/(terminal)/page.tsx b/project/apps/pos/src/app/(terminal)/page.tsx index fa06822..926f73f 100644 --- a/project/apps/pos/src/app/(terminal)/page.tsx +++ b/project/apps/pos/src/app/(terminal)/page.tsx @@ -50,6 +50,7 @@ export default function RegisterPage() { const [search, setSearch] = useState(''); const [searchResults, setSearchResults] = useState([]); const [searching, setSearching] = useState(false); + const [searchError, setSearchError] = useState(''); const [selectedItem, setSelectedItem] = useState(null); const [showDiscountPanel, setShowDiscountPanel] = useState(false); const [customer, setCustomer] = useState(null); @@ -112,13 +113,16 @@ export default function RegisterPage() { }; const doSearch = useCallback(async (q: string) => { - if (q.trim().length < 2) { setSearchResults([]); return; } + if (q.trim().length < 2) { setSearchResults([]); setSearchError(''); return; } setSearching(true); + setSearchError(''); try { const res = await posApi.searchProducts(q, config?.session?.storeId) as { items: SearchResult[] }; setSearchResults(res.items ?? []); - } catch { setSearchResults([]); } - finally { setSearching(false); } + } catch (err) { + setSearchResults([]); + setSearchError(err instanceof Error ? err.message : 'No se pudo buscar productos'); + } finally { setSearching(false); } }, [config]); useEffect(() => { @@ -324,7 +328,8 @@ export default function RegisterPage() { )} {searching &&

Buscando…

} - {search && searchResults.length === 0 && !searching && ( + {searchError && !searching &&

{searchError}

} + {search && searchResults.length === 0 && !searching && !searchError && (

Sin resultados para "{search}"

)} diff --git a/project/src/modules/pos/api/pos.routes.ts b/project/src/modules/pos/api/pos.routes.ts index 4bc22e0..1ec3a1b 100644 --- a/project/src/modules/pos/api/pos.routes.ts +++ b/project/src/modules/pos/api/pos.routes.ts @@ -454,17 +454,29 @@ export async function registerPosRoutes(app: FastifyInstance, deps: PosRouteDeps if (!q || q.trim().length < 2) throw new AppError(400, 'INVALID_QUERY', 'Query must be at least 2 characters'); const result = await pool.query( - `SELECT v.id AS variant_id, v.product_id, v.name, v.sku, v.ean, - COALESCE(s.quantity, 0) AS stock, pp.price_cents, c.name AS category, b.name AS brand + `SELECT v.id AS variant_id, v.product_id, p.name, v.sku, v.ean, + COALESCE(s.quantity, 0) AS stock, + COALESCE(pp.offer_cents, pp.net_unit_amount_cents, 0) AS price_cents, + c.name AS category, b.name AS brand FROM catalog_product_variants v JOIN catalog_products p ON p.id = v.product_id - LEFT JOIN LATERAL (SELECT quantity FROM inventory_stock WHERE variant_id = v.id AND store_id = $3 LIMIT 1) s ON true + LEFT JOIN LATERAL ( + SELECT COALESCE(SUM(available), 0)::int AS quantity + FROM inventory_stock + WHERE variant_id = v.id AND ($3::uuid IS NULL OR store_id = $3) + ) s ON true LEFT JOIN pricing_variant_prices pp ON pp.variant_id = v.id AND pp.currency = 'EUR' - LEFT JOIN catalog_product_categories pc ON pc.variant_id = v.id AND pc.is_primary = true - LEFT JOIN categories_categories c ON c.id = pc.category_id + LEFT JOIN LATERAL ( + SELECT category.name + FROM catalog_product_categories pc + JOIN categories_categories category ON category.id = pc.category_id + WHERE pc.product_id = p.id + ORDER BY category.name + LIMIT 1 + ) c ON true LEFT JOIN brands_brands b ON b.id = p.brand_id - WHERE (v.name ILIKE $1 OR v.sku ILIKE $1 OR v.ean = $1) AND v.active = true AND p.active = true - ORDER BY v.name LIMIT $2`, + WHERE (p.name ILIKE $1 OR v.sku ILIKE $1 OR v.ean ILIKE $1) AND p.state = 'active' + ORDER BY p.name LIMIT $2`, [`%${q.trim()}%`, limit, storeId ?? null], ); return reply.send({ @@ -498,12 +510,17 @@ export async function registerPosRoutes(app: FastifyInstance, deps: PosRouteDeps requireAnyRole(user, ['admin', 'pos_manager', 'pos_cashier'] as ReadonlyArray); const { ean } = request.params; const result = await pool.query( - `SELECT v.id AS variant_id, v.product_id, v.name, v.sku, v.ean, COALESCE(s.quantity, 0) AS stock, pp.price_cents + `SELECT v.id AS variant_id, v.product_id, p.name, v.sku, v.ean, + COALESCE(s.quantity, 0) AS stock, + COALESCE(pp.offer_cents, pp.net_unit_amount_cents, 0) AS price_cents FROM catalog_product_variants v JOIN catalog_products p ON p.id = v.product_id - LEFT JOIN LATERAL (SELECT quantity FROM inventory_stock WHERE variant_id = v.id LIMIT 1) s ON true + LEFT JOIN LATERAL ( + SELECT COALESCE(SUM(available), 0)::int AS quantity + FROM inventory_stock WHERE variant_id = v.id + ) s ON true LEFT JOIN pricing_variant_prices pp ON pp.variant_id = v.id AND pp.currency = 'EUR' - WHERE v.ean = $1 AND v.active = true AND p.active = true LIMIT 1`, + WHERE v.ean = $1 AND p.state = 'active' LIMIT 1`, [ean], ); if (!result.rows[0]) throw new AppError(404, 'NOT_FOUND', 'Product not found'); @@ -535,12 +552,17 @@ export async function registerPosRoutes(app: FastifyInstance, deps: PosRouteDeps requireAnyRole(user, ['admin', 'pos_manager', 'pos_cashier'] as ReadonlyArray); const { sku } = request.params; const result = await pool.query( - `SELECT v.id AS variant_id, v.product_id, v.name, v.sku, v.ean, COALESCE(s.quantity, 0) AS stock, pp.price_cents + `SELECT v.id AS variant_id, v.product_id, p.name, v.sku, v.ean, + COALESCE(s.quantity, 0) AS stock, + COALESCE(pp.offer_cents, pp.net_unit_amount_cents, 0) AS price_cents FROM catalog_product_variants v JOIN catalog_products p ON p.id = v.product_id - LEFT JOIN LATERAL (SELECT quantity FROM inventory_stock WHERE variant_id = v.id LIMIT 1) s ON true + LEFT JOIN LATERAL ( + SELECT COALESCE(SUM(available), 0)::int AS quantity + FROM inventory_stock WHERE variant_id = v.id + ) s ON true LEFT JOIN pricing_variant_prices pp ON pp.variant_id = v.id AND pp.currency = 'EUR' - WHERE v.sku = $1 AND v.active = true AND p.active = true LIMIT 1`, + WHERE v.sku = $1 AND p.state = 'active' LIMIT 1`, [sku], ); if (!result.rows[0]) throw new AppError(404, 'NOT_FOUND', 'Product not found'); diff --git a/work/artifacts/F-176/architect.md b/work/artifacts/F-176/architect.md new file mode 100644 index 0000000..6c11af9 --- /dev/null +++ b/work/artifacts/F-176/architect.md @@ -0,0 +1,3 @@ +# F-176 — Design + +Use `p.name`, `p.state='active'`, `v.sku/ean`, `inventory_stock.available`, `pricing_variant_prices.net_unit_amount_cents`, and categories linked by `product_id`. Search EAN with `ILIKE` so scanner input and partial manual barcode searches both work. Keep all input parameterized. diff --git a/work/artifacts/F-176/documenter.md b/work/artifacts/F-176/documenter.md new file mode 100644 index 0000000..c7533bd --- /dev/null +++ b/work/artifacts/F-176/documenter.md @@ -0,0 +1,3 @@ +# POS product search + +The POS search field accepts product names, SKU, full EAN and partial EAN text. Lookup failures now display an explicit API error instead of the misleading “Sin resultados”. diff --git a/work/artifacts/F-176/implementer.md b/work/artifacts/F-176/implementer.md new file mode 100644 index 0000000..5b1f894 --- /dev/null +++ b/work/artifacts/F-176/implementer.md @@ -0,0 +1,19 @@ +# F-176 — Implementer + +## Cause +POS lookup SQL was written for an obsolete schema and referenced nonexistent columns: `v.name`, `v.active`, `p.active`, `inventory_stock.quantity`, `pricing_variant_prices.price_cents`, plus category links by variant. PostgreSQL returned 500; the UI swallowed it and rendered “Sin resultados”. + +## Changes +- Search uses product name/state and current variant SKU/EAN. +- Inventory uses summed `available`, scoped to the active store. +- Price uses offer or current net amount. +- Category lookup uses `catalog_product_categories.product_id`. +- Manual partial EAN search uses parameterized `ILIKE`; exact EAN/SKU endpoints were repaired too. +- UI now exposes API search errors rather than reporting false empty results. + +## Evidence +- Project TypeScript PASS. +- POS Next build PASS. +- `git diff --check` PASS. +- Authenticated runtime search `q=590123412345`: HTTP 200, 12 existing products. +- Exact EAN `59012341234501`: HTTP 200, Almendras Crudas Ecologicas, stock 25, price 895. diff --git a/work/artifacts/F-176/leader-close.json b/work/artifacts/F-176/leader-close.json new file mode 100644 index 0000000..452eb12 --- /dev/null +++ b/work/artifacts/F-176/leader-close.json @@ -0,0 +1 @@ +{"feature_id":"F-176","agent":"leader","stage":"close","verdict":"APPROVED","checks":[{"item":"reviewer/security/qa approved","ok":true},{"item":"runtime partial and exact barcode acceptance","ok":true},{"item":"verify.sh green","ok":true}],"issues":[]} diff --git a/work/artifacts/F-176/qa.json b/work/artifacts/F-176/qa.json new file mode 100644 index 0000000..cfdb9cf --- /dev/null +++ b/work/artifacts/F-176/qa.json @@ -0,0 +1 @@ +{"feature_id":"F-176","agent":"qa","stage":"qa_gate","verdict":"APPROVED","checks":[{"item":"partial barcode 590123412345 returns HTTP 200 and 12 products","ok":true},{"item":"exact EAN 59012341234501 returns correct product","ok":true},{"item":"stock and price returned","ok":true},{"item":"POS build and TypeScript","ok":true},{"item":"verify.sh","ok":true}],"issues":[]} diff --git a/work/artifacts/F-176/reviewer.json b/work/artifacts/F-176/reviewer.json new file mode 100644 index 0000000..ae71de3 --- /dev/null +++ b/work/artifacts/F-176/reviewer.json @@ -0,0 +1 @@ +{"feature_id":"F-176","agent":"reviewer","stage":"review_gate","verdict":"APPROVED","checks":[{"item":"all lookup fields match current database schema","ok":true},{"item":"search UI no longer hides server errors","ok":true},{"item":"TypeScript and POS build","ok":true},{"item":"authenticated runtime search","ok":true}],"issues":[]} diff --git a/work/artifacts/F-176/security.json b/work/artifacts/F-176/security.json new file mode 100644 index 0000000..0e3127b --- /dev/null +++ b/work/artifacts/F-176/security.json @@ -0,0 +1 @@ +{"feature_id":"F-176","agent":"security","stage":"security_gate","verdict":"APPROVED","checks":[{"item":"role authorization retained","ok":true},{"item":"query and limits remain parameterized","ok":true},{"item":"store scope supplied by active cash session","ok":true}],"issues":[]} diff --git a/work/current.md b/work/current.md index efd0602..6da83d6 100644 --- a/work/current.md +++ b/work/current.md @@ -1,3 +1,3 @@ -# F-174 — Fix missing almagro.ttf +# F-176 — Fix POS existing product search -CSS solicita `/fonts/almagro.ttf`, pero archivo está como `Almagro.ttf`. Normalizar asset a minúsculas para filesystem case-sensitive. +POS search currently executes stale SQL against nonexistent variant fields (`v.name`, `v.active`, `p.active`, `s.quantity`, `pp.price_cents`, category-by-variant), then hides HTTP 500 as an empty result. Align search/EAN/SKU queries with current catalog, pricing, inventory and category schemas. Barcode text must support exact and partial EAN lookup. diff --git a/work/runtime-status.json b/work/runtime-status.json index faf58b9..857287d 100644 --- a/work/runtime-status.json +++ b/work/runtime-status.json @@ -1,11 +1,68 @@ { - "feature_id": null, - "stage": "idle", + "feature_id": "F-176", + "stage": "close", "agent": "leader", - "action": "Sin ejecución activa", - "state": "waiting", + "action": "Close POS existing product search fix", + "state": "running", "next_agent": "leader", "waiting_for": "Seleccionar una feature pending y actualizar este estado", - "updated_at": "2026-08-22T16:39:53Z", - "timeline": [] + "updated_at": "2026-08-22T16:49:23Z", + "timeline": [ + { + "ts": "2026-08-22T16:45:39Z", + "agent": "leader", + "stage": "intake", + "state": "running", + "message": "Diagnose POS product search for existing barcode" + }, + { + "ts": "2026-08-22T16:46:57Z", + "agent": "architect", + "stage": "design", + "state": "running", + "message": "Align POS lookup SQL with current catalog schema" + }, + { + "ts": "2026-08-22T16:47:05Z", + "agent": "implementer", + "stage": "build", + "state": "running", + "message": "Repair POS product lookup queries and visible errors" + }, + { + "ts": "2026-08-22T16:48:42Z", + "agent": "reviewer", + "stage": "review_gate", + "state": "running", + "message": "Review current-schema POS search repair" + }, + { + "ts": "2026-08-22T16:48:51Z", + "agent": "security", + "stage": "security_gate", + "state": "running", + "message": "Audit POS search authorization and SQL parameters" + }, + { + "ts": "2026-08-22T16:49:02Z", + "agent": "qa", + "stage": "qa_gate", + "state": "running", + "message": "Validate partial barcode and exact EAN searches" + }, + { + "ts": "2026-08-22T16:49:12Z", + "agent": "documenter", + "stage": "document", + "state": "running", + "message": "Document repaired POS lookup behavior" + }, + { + "ts": "2026-08-22T16:49:23Z", + "agent": "leader", + "stage": "close", + "state": "running", + "message": "Close POS existing product search fix" + } + ] }