4.2 KiB
4.2 KiB
Architect — F-010 Variants, SKU/EAN and product rich data
done -> work/artifacts/F-010/architect.md
Deliverables
- Extend
src/modules/catalog/with product variants and rich product data. - PostgreSQL migration for
catalog_product_variantsandcatalog_product_rich_data. - Unit and integration tests for duplicate SKU/EAN, nutrition provenance persistence, and manual nutrition protection.
Key decisions
- Catalog owns variants and rich data: variants are part of the product aggregate in this slice. Do not create a new module yet; inventory/pricing later reference variant IDs through catalog public contracts.
- Variants are product-scoped sellable identities:
catalog_product_variantshasproduct_id,sku,ean,attributes jsonb, timestamps.skuandeanare globally unique when present.eancan be nullable if a product lacks barcode, but duplicate non-null EAN must conflict. - Rich data is product-level in v1: ingredients, allergens, nutrition, organic/ecological certification live in
catalog_product_rich_datakeyed byproduct_id. Variant-specific rich data can come later if needed. - Provenance is explicit and stored with nutrition: allowed nutrition sources are
manual,manufacturer,openfoodfacts. Storenutrition_sourceplusnutrition jsonbtogether. Every nutrition payload must have a source. - Trusted internal data wins: when current
nutrition_source = 'manual', updates frommanufactureroropenfoodfactsmust not overwritenutritionornutrition_source. Return the existing row unchanged for those fields. Manual can overwrite any source; manufacturer/openfoodfacts can overwrite each other unless current source is manual. - No OpenFoodFacts sync job: this slice exposes an explicit update use case/API that can receive external-source payloads, but no scheduled import.
- No images, no prices, no stock: those remain out of scope. Do not add sellable pricing/inventory semantics.
- No new dependencies: use existing Zod/pg/Vitest.
Suggested API contract
POST /products/:id/variants→ admin-only create variant; duplicate SKU/EAN returns409 PRODUCT_VARIANT_CODE_EXISTS.PATCH /products/:id/variants/:variantId→ admin-only update variant attributes/SKU/EAN.GET /productos/:slugshould include orderedvariantsand rich data summary if available.PATCH /products/:id/rich-data→ admin-only upsert rich data. Payload includes optionalingredients,allergens, certification fields, andnutrition+ requirednutritionSourcewhen nutrition is present.
Domain model additions
ProductVariant:id,productId,sku,ean,attributes,createdAt,updatedAt.NewProductVariant:sku, optional nullableean, optionalattributesrecord.ProductRichData:productId,ingredients,allergens,nutrition,nutritionSource,isOrganic,organicCertification,createdAt,updatedAt.NutritionSource = 'manual' | 'manufacturer' | 'openfoodfacts'.
Error mapping
- Duplicate SKU or EAN →
409 PRODUCT_VARIANT_CODE_EXISTS. - Missing product/variant →
404 NOT_FOUND. - Nutrition payload without source →
400 VALIDATION_ERROR.
Test plan
- Unit: manual nutrition source blocks external overwrite and preserves existing manual nutrition.
- Unit: external source can update when current source is not manual.
- Integration/API: duplicate SKU returns 409.
- Integration/API: duplicate EAN returns 409.
- Integration/API: nutrition rich data stores payload and provenance.
- Integration/API: external update after manual source leaves manual nutrition unchanged.
Security posture
- All mutations are admin-only via injected shared auth and
requireRole. - Public product reads expose only catalog data intended for storefront display.
- Variant attributes and nutrition JSON are validated as bounded records/objects; no arbitrary huge payloads.
- SQL remains parameterized; dynamic update columns must be whitelisted.
Risks
- JSONB payloads can become a dumping ground. Keep fields explicit and bounded at API validation.
- Manual-source protection must happen in application/use-case logic, not only in route code, so future import jobs cannot bypass it.