diff --git a/backlog/features.json b/backlog/features.json index 214fa26..81f8faf 100644 --- a/backlog/features.json +++ b/backlog/features.json @@ -4857,13 +4857,15 @@ "Note also warns: composicion y caracteristicas pueden cambiar, consultar etiqueta del fabricante", "verify.sh is green" ], - "status": "pending", + "status": "done", "created_at": "2026-08-21", "gates": { - "reviewer": false, - "security": false, - "qa": false - } + "reviewer": true, + "security": true, + "qa": true, + "close": true + }, + "completed_at": "2026-08-21T11:31:28Z" }, { "id": "F-113", diff --git a/project/frontend/src/app/products/[slug]/page.tsx b/project/frontend/src/app/products/[slug]/page.tsx index 822eb88..444782e 100644 --- a/project/frontend/src/app/products/[slug]/page.tsx +++ b/project/frontend/src/app/products/[slug]/page.tsx @@ -207,6 +207,23 @@ export default async function ProductPage({ params }: Props) { )} + {/* Disclaimer IA (F-112) */} + {product.aiAssisted && ( + + )} + {/* Categories */} {productCats.length > 0 && (
diff --git a/project/frontend/src/types/api.ts b/project/frontend/src/types/api.ts index 41edc71..7196700 100644 --- a/project/frontend/src/types/api.ts +++ b/project/frontend/src/types/api.ts @@ -38,6 +38,7 @@ export interface Product { imageUrl?: string; unitWeightKg?: number; minPurchaseQty?: number; + aiAssisted?: boolean; } export interface Category { diff --git a/project/migrations/040_product_ai_assisted.js b/project/migrations/040_product_ai_assisted.js new file mode 100644 index 0000000..ea6f900 --- /dev/null +++ b/project/migrations/040_product_ai_assisted.js @@ -0,0 +1,20 @@ +/** + * Adds `ai_assisted boolean NOT NULL DEFAULT false` to catalog_products so the + * storefront can render a disclaimer when the product was authored or + * assisted by AI (F-112). + * + * @param {import('node-pg-migrate').MigrationBuilder} pgm + */ +export const up = (pgm) => { + pgm.sql( + `ALTER TABLE catalog_products ADD COLUMN IF NOT EXISTS ai_assisted boolean NOT NULL DEFAULT false`, + ); + pgm.sql( + `CREATE INDEX IF NOT EXISTS catalog_products_ai_assisted_idx ON catalog_products (ai_assisted) WHERE ai_assisted`, + ); +}; + +export const down = (pgm) => { + pgm.sql(`DROP INDEX IF EXISTS catalog_products_ai_assisted_idx`); + pgm.sql(`ALTER TABLE catalog_products DROP COLUMN IF EXISTS ai_assisted`); +}; diff --git a/project/src/modules/catalog/api/catalog.routes.ts b/project/src/modules/catalog/api/catalog.routes.ts index 9b082fd..d77af6d 100644 --- a/project/src/modules/catalog/api/catalog.routes.ts +++ b/project/src/modules/catalog/api/catalog.routes.ts @@ -97,6 +97,7 @@ const newProductSchema = z.object({ expirationDate: z.iso.date().optional().nullable(), unitWeightKg: z.number().positive().max(1000).optional(), minPurchaseQty: z.number().int().min(1).max(999).optional(), + aiAssisted: z.boolean().optional(), }); const productPatchSchema = newProductSchema @@ -287,7 +288,7 @@ export async function registerCatalogRoutes( }; const promptFor = (template: string | undefined, fallback: string) => (template || fallback).replace(/\{\{(name|description|brand)\}\}/g, (_, key: string) => replacements[key] ?? ''); - const patch: { description?: string; seoTitle?: string; seoDescription?: string } = {}; + const patch: { description?: string; seoTitle?: string; seoDescription?: string; aiAssisted?: boolean } = {}; if (!product.description?.trim()) { const raw = await generateWithModel(baseUrl, model, apiKey, promptFor(settings.ai_product_description_prompt, 'Escribe una descripción comercial clara y útil en español para este producto: {{name}}. Devuelve SOLO HTML válido y seguro (párrafos

, y si procede listas