54 lines
4.2 KiB
Markdown
54 lines
4.2 KiB
Markdown
# 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.
|