feat(ADM-018): completed feature
This commit is contained in:
53
work/artifacts/F-010/architect.md
Normal file
53
work/artifacts/F-010/architect.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# 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.
|
||||
19
work/artifacts/F-010/documenter.md
Normal file
19
work/artifacts/F-010/documenter.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Documenter — F-010 Variants, SKU/EAN and product rich data
|
||||
|
||||
done -> work/artifacts/F-010/documenter.md
|
||||
|
||||
## Documentation updated
|
||||
- `project/README.md`
|
||||
|
||||
## Covered
|
||||
- Product variant error code `PRODUCT_VARIANT_CODE_EXISTS`.
|
||||
- Variant endpoints under `/products/:id/variants`.
|
||||
- Rich data endpoint `/products/:id/rich-data`.
|
||||
- Variant SKU/EAN uniqueness.
|
||||
- Nutrition provenance sources: `manual`, `manufacturer`, `openfoodfacts`.
|
||||
- Manual nutrition protection from external-source overwrites.
|
||||
- Explicit out-of-scope note: no stock, prices, images, or external sync job.
|
||||
|
||||
## Verification
|
||||
- `cd project && npm run lint` — PASS after README Prettier normalization.
|
||||
- `./scripts/verify.sh` — PASS from repository root before documentation close.
|
||||
51
work/artifacts/F-010/implementer.md
Normal file
51
work/artifacts/F-010/implementer.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Implementer — F-010 Variants, SKU/EAN and product rich data
|
||||
|
||||
done -> work/artifacts/F-010/implementer.md
|
||||
|
||||
## Summary
|
||||
Implemented catalog product variants and rich product data with SKU/EAN uniqueness, nutrition provenance, and manual nutrition overwrite protection.
|
||||
|
||||
## Changed files
|
||||
- `project/src/modules/catalog/domain/variant.ts`
|
||||
- `project/src/modules/catalog/domain/errors.ts`
|
||||
- `project/src/modules/catalog/domain/ports.ts`
|
||||
- `project/src/modules/catalog/application/variant-use-cases.ts`
|
||||
- `project/src/modules/catalog/infrastructure/pg-variant-repository.ts`
|
||||
- `project/src/modules/catalog/infrastructure/pg-rich-data-repository.ts`
|
||||
- `project/src/modules/catalog/api/catalog.routes.ts`
|
||||
- `project/migrations/008_catalog_variants_rich_data.js`
|
||||
- `project/src/modules/catalog/tests/rich-data-use-cases.test.ts`
|
||||
- `project/src/app/tests/catalog.itest.ts`
|
||||
- `work/artifacts/F-010/architect.md`
|
||||
- `backlog/features.json`
|
||||
- `work/current.md`
|
||||
|
||||
## Acceptance traceability
|
||||
1. Duplicate SKU or EAN returns HTTP 409
|
||||
- Implemented via unique constraints on `catalog_product_variants.sku` and `catalog_product_variants.ean`.
|
||||
- Mapped to `PRODUCT_VARIANT_CODE_EXISTS`.
|
||||
- Covered by `project/src/app/tests/catalog.itest.ts`.
|
||||
2. Manual nutrition source blocks external overwrite
|
||||
- Implemented in `protectManualNutrition` inside application use-case logic.
|
||||
- Covered by `project/src/modules/catalog/tests/rich-data-use-cases.test.ts` and integration test scenario.
|
||||
3. Every nutrition payload stores provenance
|
||||
- API validation requires `nutritionSource` when `nutrition` is provided.
|
||||
- DB check enforces `nutrition IS NULL OR nutrition_source IS NOT NULL`.
|
||||
- Covered by integration test scenario.
|
||||
4. `verify.sh` green
|
||||
- Verified after implementation.
|
||||
|
||||
## Commands run
|
||||
- `cd project && npm run typecheck` — PASS
|
||||
- `cd project && npm run lint` — PASS after Prettier normalization
|
||||
- `cd project && npm test` — PASS: 61 passed, 33 skipped (DB integration skipped without `TEST_DATABASE_URL`)
|
||||
- `cd project && npm run build` — PASS
|
||||
- `cd project && npm run lint:boundaries` — PASS: 84 files checked
|
||||
- `./scripts/verify.sh` — PASS
|
||||
|
||||
## Notes
|
||||
- No new npm dependencies.
|
||||
- Variant and rich data mutations are admin-only via existing catalog route auth.
|
||||
- No OpenFoodFacts sync job, images, stock, or pricing added.
|
||||
- JSON payloads are bounded through Zod validation.
|
||||
- Documentation stage should update README/API notes because new variant and rich data endpoints/error codes were added.
|
||||
36
work/artifacts/F-010/leader-close.json
Normal file
36
work/artifacts/F-010/leader-close.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"feature_id": "F-010",
|
||||
"agent": "leader",
|
||||
"stage": "close",
|
||||
"verdict": "APPROVED",
|
||||
"title": "Variants, SKU/EAN and product rich data",
|
||||
"gates": {
|
||||
"review": "APPROVED",
|
||||
"security": "APPROVED",
|
||||
"qa": "APPROVED"
|
||||
},
|
||||
"verification": {
|
||||
"lint": "clean",
|
||||
"boundaries": "84 files OK",
|
||||
"typecheck": "clean",
|
||||
"build": "clean",
|
||||
"unit_tests": "61 passed, 33 skipped",
|
||||
"integration_tests": "present; skipped without TEST_DATABASE_URL",
|
||||
"verify_sh": "green"
|
||||
},
|
||||
"deliverables": [
|
||||
"migrations/008_catalog_variants_rich_data.js",
|
||||
"src/modules/catalog/domain/variant.ts",
|
||||
"src/modules/catalog/application/variant-use-cases.ts",
|
||||
"src/modules/catalog/infrastructure/pg-variant-repository.ts",
|
||||
"src/modules/catalog/infrastructure/pg-rich-data-repository.ts",
|
||||
"catalog API routes for variants and rich data",
|
||||
"tests: rich data provenance unit, catalog integration acceptance",
|
||||
"README variants/rich-data documentation"
|
||||
],
|
||||
"known_followups": [
|
||||
"No OpenFoodFacts sync job until a dedicated integration ticket",
|
||||
"No images until F-011",
|
||||
"No stock/pricing semantics until inventory/pricing tickets"
|
||||
]
|
||||
}
|
||||
41
work/artifacts/F-010/qa.json
Normal file
41
work/artifacts/F-010/qa.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"feature_id": "F-010",
|
||||
"stage": "qa_gate",
|
||||
"agent": "qa",
|
||||
"verdict": "APPROVED",
|
||||
"acceptance": [
|
||||
{
|
||||
"criterion": "Given duplicate SKU or EAN When variant created Then HTTP 409",
|
||||
"status": "PASS",
|
||||
"evidence": "Covered by src/app/tests/catalog.itest.ts; integration test is skipped automatically when TEST_DATABASE_URL is absent. DB unique constraints and error mapper reviewed."
|
||||
},
|
||||
{
|
||||
"criterion": "Given field with nutrition_source=manual When external source pushes same field Then internal value kept",
|
||||
"status": "PASS",
|
||||
"evidence": "src/modules/catalog/tests/rich-data-use-cases.test.ts PASS; integration scenario present."
|
||||
},
|
||||
{
|
||||
"criterion": "Every nutrition payload stores its provenance",
|
||||
"status": "PASS",
|
||||
"evidence": "API validation requires nutritionSource with nutrition; DB check constraint enforces source when nutrition is non-null; integration scenario present."
|
||||
},
|
||||
{
|
||||
"criterion": "verify.sh green",
|
||||
"status": "PASS",
|
||||
"evidence": "./scripts/verify.sh — PASS"
|
||||
}
|
||||
],
|
||||
"commands": {
|
||||
"targeted_tests": "cd project && npm test -- --run src/modules/catalog/tests/rich-data-use-cases.test.ts src/app/tests/catalog.itest.ts — PASS: 2 passed, 6 skipped without TEST_DATABASE_URL",
|
||||
"full_tests": "cd project && npm test — PASS: 61 passed, 33 skipped without TEST_DATABASE_URL",
|
||||
"typecheck": "cd project && npm run typecheck — PASS",
|
||||
"lint": "cd project && npm run lint — PASS",
|
||||
"build": "cd project && npm run build — PASS",
|
||||
"boundaries": "cd project && npm run lint:boundaries — PASS",
|
||||
"verify": "./scripts/verify.sh — PASS"
|
||||
},
|
||||
"findings": [],
|
||||
"notes": [
|
||||
"Real PostgreSQL acceptance tests are implemented but not executed in this environment because TEST_DATABASE_URL is not set. This matches existing project test behavior."
|
||||
]
|
||||
}
|
||||
24
work/artifacts/F-010/reviewer.json
Normal file
24
work/artifacts/F-010/reviewer.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"feature_id": "F-010",
|
||||
"stage": "review_gate",
|
||||
"agent": "reviewer",
|
||||
"verdict": "APPROVED",
|
||||
"checked": [
|
||||
"Catalog variant domain/application/infrastructure/API additions",
|
||||
"Migration constraints for SKU/EAN uniqueness and nutrition provenance",
|
||||
"Manual nutrition protection implemented in use-case layer",
|
||||
"Nutrition payload provenance validation in API and DB",
|
||||
"No out-of-scope OpenFoodFacts sync/images/pricing/stock",
|
||||
"Acceptance test coverage"
|
||||
],
|
||||
"findings": [],
|
||||
"evidence": {
|
||||
"targeted_tests": "cd project && npm test -- --run src/modules/catalog/tests/rich-data-use-cases.test.ts src/app/tests/catalog.itest.ts — PASS: 2 passed, 6 skipped without TEST_DATABASE_URL",
|
||||
"typecheck": "cd project && npm run typecheck — PASS",
|
||||
"lint": "cd project && npm run lint — PASS",
|
||||
"test": "cd project && npm test — PASS: 61 passed, 33 skipped without TEST_DATABASE_URL",
|
||||
"build": "cd project && npm run build — PASS",
|
||||
"boundaries": "cd project && npm run lint:boundaries — PASS",
|
||||
"verify": "./scripts/verify.sh — PASS"
|
||||
}
|
||||
}
|
||||
27
work/artifacts/F-010/security.json
Normal file
27
work/artifacts/F-010/security.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"feature_id": "F-010",
|
||||
"stage": "security_gate",
|
||||
"agent": "security",
|
||||
"verdict": "APPROVED",
|
||||
"checked": [
|
||||
"No new runtime or dev dependencies",
|
||||
"Variant and rich data mutation routes are admin-only",
|
||||
"SKU/EAN uniqueness enforced at database level",
|
||||
"Nutrition provenance enforced by API validation and database check constraint",
|
||||
"Manual nutrition overwrite protection lives in application use case",
|
||||
"JSON payloads are bounded by validation",
|
||||
"SQL queries use parameterized pg placeholders; dynamic UPDATE columns are whitelisted",
|
||||
"Secret scan over F-010 files"
|
||||
],
|
||||
"findings": [],
|
||||
"evidence": {
|
||||
"npm_audit_runtime": "cd project && npm audit --audit-level=high --omit=dev — PASS: found 0 vulnerabilities",
|
||||
"secret_scan": "grep over F-010 files found only a non-secret test password fixture",
|
||||
"verify": "./scripts/verify.sh — PASS",
|
||||
"tests": "cd project && npm test — PASS"
|
||||
},
|
||||
"notes": [
|
||||
"No external OpenFoodFacts job or remote call was added.",
|
||||
"No price, stock, or image behavior was introduced."
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user