{ "feature_id": "F-137", "agent": "qa", "stage": "qa_gate", "verdict": "APPROVED", "reviewed_at": "2026-08-21T19:04:00Z", "summary": "Acceptance criteria trazados contra evidencia de diff + comandos. Todos los AC pasan a nivel código. AC4 (flujo create) y AC5 (failure surface) requieren smoke manual en navegador con monolith reiniciado.", "acceptance_traceability": [ { "criterion": "AC-1: 'Guardar precio' button is gone from the UI", "evidence": "grep -c 'Guardar precio' project/apps/admin/src/features/products/components/sections/PriceStockSection.tsx → 0", "ok": true }, { "criterion": "AC-2: Stock, EAN, peso, compra mínima inputs do NOT auto-save on blur or Enter", "evidence": "grep -nE 'onBlur=\\{save' PriceStockSection.tsx → 0 matches; grep onKeyDown saveStock/saveEan/saveProductMeta → 0 matches", "ok": true }, { "criterion": "AC-3: Clicking 'Guardar cambios' persists price + stock + EAN + meta in a single logical action", "evidence": "ProductEditor.tsx:188 llama priceStockRef.current?.saveAll() tras productsApi.create/update. saveAll hace Promise.all([savePrice, saveStock, saveEan, saveProductMeta]).", "ok": true }, { "criterion": "AC-4: Create flow persists variant price/stock/EAN/meta after 'Crear producto'", "evidence": "saveAll invoca ensureVariant() que llama productsApi.getVariants(productId) si la sección aún no tiene variant cargado. Cubre el caso create donde el useEffect de carga aún no corrió.", "ok": true, "requires_manual_smoke": true }, { "criterion": "AC-5: Per-group failure surfacing (which group(s) failed)", "evidence": "ProductEditor.tsx:191-202 mapea Object.entries(result).filter(not ok) a etiquetas (precio, stock, EAN, peso/compra mínima) y construye mensaje 'Revisa X'.", "ok": true, "requires_manual_smoke": true }, { "criterion": "AC-6: Build green, lint green, typecheck green", "evidence": "npx tsc --noEmit → exit 0; npx eslint (ambos archivos) → 0 errors / 7 warnings pre-existentes; npm run build → 'Compiled successfully' con BUILD_ID regenerado", "ok": true }, { "criterion": "AC-7: Existing dirty indicator still works for product payload", "evidence": "getSnap() en ProductEditor.tsx no se tocó; sigue cubriendo name/slug/desc/brandId/categoryIds/channels/featured/attributes/state/seoTitle/seoDesc/expirationDate. snapRef.current = getSnap() se setea después del saveAll con éxito.", "ok": true } ], "checks": [ { "item": "Diff size y scope razonables", "ok": true, "evidence": "git diff --stat: ProductEditor.tsx +34 lines, PriceStockSection.tsx +110/-86 lines. Sin churn excesivo." }, { "item": "Patrón forwardRef + useImperativeHandle consistente con el resto del codebase", "ok": true, "evidence": "LexicalEditor (mismo dir features/cms) ya usa forwardRef. Patrón familiar." }, { "item": "Sin console.log ni debugger statements", "ok": true, "evidence": "grep -E 'console\\.(log|debug)|debugger' en archivos modificados → 0" }, { "item": "Mensajes de error visibles al usuario sin filtrar stack traces", "ok": true, "evidence": "Todos los catch hacen `error instanceof Error ? error.message : 'Error genérico'` antes de pasarlo al state. Sin stack traces en UI." } ], "issues": [], "notes": "Smoke test manual requerido:\n1. cd project && ./scripts/monolith.sh prod restart\n2. Login admin → /products/new → rellenar nombre, PVP, stock, EAN, peso, compra mínima → Crear producto → verificar que tras recarga los valores persisten.\n3. /products/ → modificar PVP → Guardar cambios → verificar que se persiste (sin parpadeo, sin doble save).\n4. Probar escenario de fallo: introducir un EAN que ya exista en otro producto → Guardar cambios → verificar mensaje 'Revisa EAN'.\n\nSin e2e automatizado disponible para esta vista (sin playwright instalado en este repo). El comando smoke mínimo es el restart del monolito + recarga manual." }