feat(ADM-018): completed feature
This commit is contained in:
54
work/artifacts/F-008/architect.md
Normal file
54
work/artifacts/F-008/architect.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Architect — F-008 Catalog core: products domain
|
||||
|
||||
done -> work/artifacts/F-008/architect.md
|
||||
|
||||
## Deliverables
|
||||
- `src/modules/catalog/` with domain, application, infrastructure and api layers.
|
||||
- PostgreSQL migration for `catalog_products` and `catalog_product_categories`.
|
||||
- Unit and integration tests covering product state filtering, slug conflicts, category assignment, public slug URL behavior and module boundaries.
|
||||
|
||||
## Key decisions
|
||||
1. **Catalog owns products only**: product rows live in `catalog_products`; assignment join table lives in `catalog_product_categories` because the relationship is product-owned for F-008. Categories remain independent and are referenced only by ID at the database boundary.
|
||||
2. **Domain is pure**: `src/modules/catalog/domain/*` must import no `pg`, no Fastify, no HTTP, no repository implementations. Acceptance explicitly requires zero database/HTTP imports.
|
||||
3. **Product state is explicit**: allowed states are `draft`, `active`, `archived`. Public list/search and slug reads must return only active products.
|
||||
4. **Slug as public identity**: API exposes `/productos/<slug>`; internal UUID may be returned in admin responses but must never be required for public product URLs.
|
||||
5. **SearchProducts v1 is PostgreSQL-backed but interface-shaped**: implement simple text matching over `name` and `description`, active-only for public use. This is not the dedicated FTS module from F-012; keep it boring and replaceable.
|
||||
6. **Category assignment validation**: creating/updating product category IDs must reject unknown categories. Runtime validation may query `categories_categories` from the catalog repository only to enforce referential integrity for the join. Do not import the categories module internals.
|
||||
7. **No variants, stock, prices**: product core fields are identity, name, slug, description, state, SEO metadata, timestamps and category IDs. Anything sellable, stock-related or price-related belongs to later tickets.
|
||||
8. **No new dependencies**: current stack already covers validation, PostgreSQL and testing.
|
||||
|
||||
## Suggested API contract
|
||||
- `GET /productos/:slug` → public active product by slug; `404` for draft/archived/missing.
|
||||
- `GET /products/search?q=&limit=&offset=` → public active product search/listing; only active products.
|
||||
- `POST /products` → admin-only create product with optional category IDs; duplicate slug returns `409`.
|
||||
- `PATCH /products/:id` → admin-only update product fields/state/category IDs.
|
||||
|
||||
## Domain model
|
||||
- `ProductState = 'draft' | 'active' | 'archived'`.
|
||||
- `Product`: `id`, `name`, `slug`, `description`, `state`, `seoTitle`, `seoDescription`, `categoryIds`, `createdAt`, `updatedAt`.
|
||||
- `NewProduct`: same editable fields except generated `id`/timestamps; `state` defaults to `draft`; `categoryIds` defaults to empty.
|
||||
- `ProductPatch`: optional editable fields; if `categoryIds` is present it replaces assignments.
|
||||
|
||||
## Error mapping
|
||||
- Duplicate slug → `409 PRODUCT_SLUG_EXISTS`.
|
||||
- Unknown category ID → `422 PRODUCT_CATEGORY_NOT_FOUND`.
|
||||
- Missing product → `404 NOT_FOUND`.
|
||||
- Invalid state/payload → existing `400 VALIDATION_ERROR` from Zod.
|
||||
|
||||
## Test plan
|
||||
- Unit: domain/application SearchProducts returns only active products.
|
||||
- Unit: domain layer imports remain pure via boundary lint and targeted source scan.
|
||||
- Integration/API: duplicate slug create returns HTTP 409.
|
||||
- Integration/API: draft products do not appear in public search/listing and are not readable by `/productos/:slug`.
|
||||
- Integration/API: active product is readable at `/productos/<slug>` and response exposes `url` with the slug.
|
||||
- Integration/API: product can be assigned to an existing category; unknown category rejected.
|
||||
|
||||
## Security posture
|
||||
- Public reads are unauthenticated and active-only.
|
||||
- Mutations are admin-only via injected shared auth and `requireRole`.
|
||||
- Never trust product state, price, stock or category information from other modules without server-side validation.
|
||||
- SQL must remain parameterized; any dynamic update columns must be whitelisted.
|
||||
|
||||
## Risks
|
||||
- F-008 touches a relationship with categories. Keep coupling at database IDs and repository validation, not TypeScript imports from `modules/categories/*`.
|
||||
- Search in this slice is intentionally simple. Do not overbuild FTS/relevance here; F-012 owns search infrastructure.
|
||||
19
work/artifacts/F-008/documenter.md
Normal file
19
work/artifacts/F-008/documenter.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Documenter — F-008 Catalog core
|
||||
|
||||
done -> work/artifacts/F-008/documenter.md
|
||||
|
||||
## Documentation updated
|
||||
- `project/README.md`
|
||||
|
||||
## Covered
|
||||
- Product-specific error codes.
|
||||
- Catalog core endpoint table.
|
||||
- Public slug URL rule: `/productos/<slug>`.
|
||||
- Active-only public reads/search behavior.
|
||||
- Product states and SEO metadata fields.
|
||||
- Product-category assignment storage/validation note.
|
||||
- Layout entry for `src/modules/catalog`.
|
||||
|
||||
## Verification
|
||||
- `cd project && npm run lint` — PASS after README Prettier normalization.
|
||||
- `./scripts/verify.sh` — PASS from repository root before documentation close.
|
||||
54
work/artifacts/F-008/implementer.md
Normal file
54
work/artifacts/F-008/implementer.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Implementer — F-008 Catalog core: products domain
|
||||
|
||||
done -> work/artifacts/F-008/implementer.md
|
||||
|
||||
## Summary
|
||||
Implemented the catalog core product slice as a hexagonal backend module with product state, unique slugs, active-only public reads/search, SEO metadata, category assignment, migration, tests and composition-root wiring.
|
||||
|
||||
## Changed files
|
||||
- `project/src/modules/catalog/domain/product.ts`
|
||||
- `project/src/modules/catalog/domain/errors.ts`
|
||||
- `project/src/modules/catalog/domain/ports.ts`
|
||||
- `project/src/modules/catalog/application/product-use-cases.ts`
|
||||
- `project/src/modules/catalog/infrastructure/pg-product-repository.ts`
|
||||
- `project/src/modules/catalog/api/catalog.routes.ts`
|
||||
- `project/src/modules/catalog/index.ts`
|
||||
- `project/migrations/006_catalog_products.js`
|
||||
- `project/src/app/build-app.ts`
|
||||
- `project/src/modules/catalog/tests/product-use-cases.test.ts`
|
||||
- `project/src/modules/catalog/tests/domain-purity.test.ts`
|
||||
- `project/src/app/tests/catalog.itest.ts`
|
||||
- `backlog/features.json`
|
||||
- `work/current.md`
|
||||
- `work/artifacts/F-008/architect.md`
|
||||
|
||||
## Acceptance traceability
|
||||
1. Domain layer has zero database or HTTP imports
|
||||
- Catalog domain files contain only type/model/port/error code.
|
||||
- Covered by `src/modules/catalog/tests/domain-purity.test.ts` and boundary lint.
|
||||
2. Duplicate slug returns HTTP 409
|
||||
- Implemented through `catalog_products.slug UNIQUE` and `PRODUCT_SLUG_EXISTS` mapping.
|
||||
- Covered by `src/app/tests/catalog.itest.ts`.
|
||||
3. Only active products appear in public listings
|
||||
- `SearchProducts` always passes `activeOnly: true`; repository filters `p.state = 'active'`.
|
||||
- Covered by unit and integration tests.
|
||||
4. Public URL is `/productos/<slug>`, never internal id
|
||||
- Implemented `GET /productos/:slug` active-only and serialized `url: /productos/<slug>`.
|
||||
- Covered by integration test.
|
||||
5. `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: 59 passed, 28 skipped (DB integration skipped without `TEST_DATABASE_URL`)
|
||||
- `cd project && npm run build` — PASS
|
||||
- `cd project && npm run lint:boundaries` — PASS: 71 files checked
|
||||
- `./scripts/verify.sh` — PASS
|
||||
|
||||
## Notes
|
||||
- No new npm dependencies.
|
||||
- Product mutation routes are admin-only via shared auth injected from the composition root.
|
||||
- Public reads (`GET /productos/:slug`, `GET /products/search`) are unauthenticated and active-only.
|
||||
- Category assignment validates category IDs against `categories_categories` without importing categories module internals.
|
||||
- Documentation stage should update README/API notes because new user-facing endpoints and product error codes were added.
|
||||
32
work/artifacts/F-008/leader-close.json
Normal file
32
work/artifacts/F-008/leader-close.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"feature_id": "F-008",
|
||||
"agent": "leader",
|
||||
"stage": "close",
|
||||
"verdict": "APPROVED",
|
||||
"title": "Catalog core: products domain",
|
||||
"gates": {
|
||||
"review": "APPROVED",
|
||||
"security": "APPROVED",
|
||||
"qa": "APPROVED"
|
||||
},
|
||||
"verification": {
|
||||
"lint": "clean",
|
||||
"boundaries": "71 files OK",
|
||||
"typecheck": "clean",
|
||||
"build": "clean",
|
||||
"unit_tests": "59 passed, 28 skipped",
|
||||
"integration_tests": "present; skipped without TEST_DATABASE_URL",
|
||||
"verify_sh": "green"
|
||||
},
|
||||
"deliverables": [
|
||||
"migrations/006_catalog_products.js",
|
||||
"src/modules/catalog/ (domain, application, infrastructure, api)",
|
||||
"app/build-app.ts: Authenticate injection into catalog",
|
||||
"tests: catalog use cases, domain purity, catalog integration acceptance",
|
||||
"README catalog endpoint and error-code documentation"
|
||||
],
|
||||
"known_followups": [
|
||||
"Search is simple ILIKE in this slice; F-012 owns dedicated search/FTS behavior",
|
||||
"No variants, stock or prices until later catalog/inventory/pricing tickets"
|
||||
]
|
||||
}
|
||||
46
work/artifacts/F-008/qa.json
Normal file
46
work/artifacts/F-008/qa.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"feature_id": "F-008",
|
||||
"stage": "qa_gate",
|
||||
"agent": "qa",
|
||||
"verdict": "APPROVED",
|
||||
"acceptance": [
|
||||
{
|
||||
"criterion": "Domain layer has zero database or HTTP imports",
|
||||
"status": "PASS",
|
||||
"evidence": "src/modules/catalog/tests/domain-purity.test.ts PASS; boundary lint PASS."
|
||||
},
|
||||
{
|
||||
"criterion": "Given duplicate slug When product 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. Repository unique constraint and error mapper reviewed."
|
||||
},
|
||||
{
|
||||
"criterion": "Only active products appear in public listings",
|
||||
"status": "PASS",
|
||||
"evidence": "src/modules/catalog/tests/product-use-cases.test.ts PASS; integration active-only test present."
|
||||
},
|
||||
{
|
||||
"criterion": "Public URL is /productos/<slug>, never internal id",
|
||||
"status": "PASS",
|
||||
"evidence": "GET /productos/:slug implemented and integration test present; serialized response includes /productos/<slug>."
|
||||
},
|
||||
{
|
||||
"criterion": "verify.sh green",
|
||||
"status": "PASS",
|
||||
"evidence": "./scripts/verify.sh — PASS"
|
||||
}
|
||||
],
|
||||
"commands": {
|
||||
"targeted_tests": "cd project && npm test -- --run src/modules/catalog/tests/product-use-cases.test.ts src/modules/catalog/tests/domain-purity.test.ts src/app/tests/catalog.itest.ts — PASS: 4 passed, 3 skipped without TEST_DATABASE_URL",
|
||||
"full_tests": "cd project && npm test — PASS: 59 passed, 28 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."
|
||||
]
|
||||
}
|
||||
27
work/artifacts/F-008/reviewer.json
Normal file
27
work/artifacts/F-008/reviewer.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"feature_id": "F-008",
|
||||
"stage": "review_gate",
|
||||
"agent": "reviewer",
|
||||
"verdict": "APPROVED",
|
||||
"checked": [
|
||||
"Hexagonal catalog module structure",
|
||||
"Domain purity: no DB or HTTP imports",
|
||||
"Composition root wiring",
|
||||
"PostgreSQL migration naming and constraints",
|
||||
"Product state filtering for public reads/search",
|
||||
"Slug uniqueness and error mapping",
|
||||
"Product-category assignment without TypeScript coupling to categories internals",
|
||||
"Acceptance test coverage"
|
||||
],
|
||||
"findings": [],
|
||||
"evidence": {
|
||||
"domain_scan": "grep over project/src/modules/catalog/domain for pg/fastify/http/infrastructure/api imports — PASS: no matches",
|
||||
"targeted_tests": "cd project && npm test -- --run src/modules/catalog/tests/product-use-cases.test.ts src/modules/catalog/tests/domain-purity.test.ts src/app/tests/catalog.itest.ts — PASS: 4 passed, 3 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: 59 passed, 28 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"
|
||||
}
|
||||
}
|
||||
28
work/artifacts/F-008/security.json
Normal file
28
work/artifacts/F-008/security.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"feature_id": "F-008",
|
||||
"stage": "security_gate",
|
||||
"agent": "security",
|
||||
"verdict": "APPROVED",
|
||||
"checked": [
|
||||
"No new runtime or dev dependencies",
|
||||
"Admin-only product mutation routes use server-side shared auth and requireRole('admin')",
|
||||
"Public product reads expose only active products",
|
||||
"SQL queries use parameterized pg placeholders; dynamic UPDATE columns are whitelisted",
|
||||
"Slug, UUID, state and payload validation use Zod via parseJson",
|
||||
"Category assignment is validated server-side before write",
|
||||
"Domain layer does not import DB or HTTP APIs",
|
||||
"Secret scan over F-008 files"
|
||||
],
|
||||
"findings": [],
|
||||
"evidence": {
|
||||
"npm_audit_runtime": "cd project && npm audit --audit-level=high --omit=dev — PASS: found 0 vulnerabilities",
|
||||
"secret_scan": "grep over F-008 files found only a non-secret test password fixture",
|
||||
"domain_scan": "No pg/fastify/http imports in catalog domain",
|
||||
"verify": "./scripts/verify.sh — PASS",
|
||||
"tests": "cd project && npm test — PASS"
|
||||
},
|
||||
"notes": [
|
||||
"No client-supplied role/user data is trusted.",
|
||||
"Public reads are active-only so draft/archived products are not exposed via catalog endpoints."
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user