feat(ADM-018): completed feature

This commit is contained in:
chattie
2026-08-17 22:23:10 +02:00
parent cf1c69fc8b
commit d595b4871f
871 changed files with 47411 additions and 281 deletions

View 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.

View 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.

View 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.

View 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"
]
}

View 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."
]
}

View 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"
}
}

View 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."
]
}