Files
mercadodevida/work/artifacts/F-010/architect.md
2026-08-17 22:23:10 +02:00

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_variants and catalog_product_rich_data.
  • Unit and integration tests for duplicate SKU/EAN, nutrition provenance persistence, and manual nutrition protection.

Key decisions

  1. 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.
  2. Variants are product-scoped sellable identities: catalog_product_variants has product_id, sku, ean, attributes jsonb, timestamps. sku and ean are globally unique when present. ean can be nullable if a product lacks barcode, but duplicate non-null EAN must conflict.
  3. Rich data is product-level in v1: ingredients, allergens, nutrition, organic/ecological certification live in catalog_product_rich_data keyed by product_id. Variant-specific rich data can come later if needed.
  4. Provenance is explicit and stored with nutrition: allowed nutrition sources are manual, manufacturer, openfoodfacts. Store nutrition_source plus nutrition jsonb together. Every nutrition payload must have a source.
  5. Trusted internal data wins: when current nutrition_source = 'manual', updates from manufacturer or openfoodfacts must not overwrite nutrition or nutrition_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.
  6. No OpenFoodFacts sync job: this slice exposes an explicit update use case/API that can receive external-source payloads, but no scheduled import.
  7. No images, no prices, no stock: those remain out of scope. Do not add sellable pricing/inventory semantics.
  8. No new dependencies: use existing Zod/pg/Vitest.

Suggested API contract

  • POST /products/:id/variants → admin-only create variant; duplicate SKU/EAN returns 409 PRODUCT_VARIANT_CODE_EXISTS.
  • PATCH /products/:id/variants/:variantId → admin-only update variant attributes/SKU/EAN.
  • GET /productos/:slug should include ordered variants and rich data summary if available.
  • PATCH /products/:id/rich-data → admin-only upsert rich data. Payload includes optional ingredients, allergens, certification fields, and nutrition + required nutritionSource when nutrition is present.

Domain model additions

  • ProductVariant: id, productId, sku, ean, attributes, createdAt, updatedAt.
  • NewProductVariant: sku, optional nullable ean, optional attributes record.
  • 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.