feat(ADM-018): completed feature
This commit is contained in:
55
work/artifacts/F-009/architect.md
Normal file
55
work/artifacts/F-009/architect.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# Architect — F-009 Brands module
|
||||
|
||||
done -> work/artifacts/F-009/architect.md
|
||||
|
||||
## Deliverables
|
||||
- `src/modules/brands/` with domain, application, infrastructure and api layers.
|
||||
- PostgreSQL migration for `brands_brands` and catalog product brand assignment.
|
||||
- Catalog search updated so products are filterable by brand.
|
||||
- Unit and integration tests covering duplicate brand slug, public brand slug URL, and product listing filter by brand.
|
||||
|
||||
## Key decisions
|
||||
1. **Brands is its own module**: brands owns `brands_brands`. This keeps brand SEO pages independent from catalog product internals and matches the backlog wording allowing a dedicated brands module.
|
||||
2. **Catalog owns the assignment field**: add nullable `brand_id` to `catalog_products` because product-brand assignment is product-owned. It references `brands_brands(id)` for integrity. Catalog may validate/query brand IDs/slugs at repository boundary, but must not import `modules/brands/*` internals.
|
||||
3. **Slug as public brand identity**: brands exposes `GET /marca/:slug`; internal UUID may exist but public URL must be `/marca/<slug>`.
|
||||
4. **Brand SEO metadata is first-class**: `brands_brands` stores `seo_title` and `seo_description` alongside `name` and `slug`.
|
||||
5. **Product filtering by brand**: extend catalog public search with `brandSlug` query parameter. Search remains active-only. Repository joins/filters via `brands_brands.slug` using parameterized SQL.
|
||||
6. **Admin-only mutations**: brand create/update routes require injected shared auth + `requireRole('admin')`.
|
||||
7. **No new dependencies**: existing Fastify/Zod/pg/Vitest stack is enough.
|
||||
|
||||
## Suggested API contract
|
||||
- `GET /marca/:slug` → public brand by slug; response includes `url: /marca/<slug>`.
|
||||
- `POST /brands` → admin-only create brand; duplicate slug returns `409 BRAND_SLUG_EXISTS`.
|
||||
- `PATCH /brands/:id` → admin-only update brand metadata/slug.
|
||||
- `GET /products/search?brandSlug=<slug>` → public active product listing filtered by brand.
|
||||
- `POST /products` / `PATCH /products/:id` accept optional nullable `brandId`.
|
||||
|
||||
## Domain model
|
||||
- `Brand`: `id`, `name`, `slug`, `seoTitle`, `seoDescription`, `createdAt`, `updatedAt`.
|
||||
- `NewBrand`: `name`, `slug`, optional SEO metadata.
|
||||
- `BrandPatch`: optional editable fields.
|
||||
- Extend catalog `Product`: nullable `brandId`.
|
||||
- Extend catalog `NewProduct`/`ProductPatch`: optional nullable `brandId`.
|
||||
- Extend `ProductSearch`: optional `brandSlug`.
|
||||
|
||||
## Error mapping
|
||||
- Duplicate brand slug → `409 BRAND_SLUG_EXISTS`.
|
||||
- Unknown brand assignment on product create/update → `422 PRODUCT_BRAND_NOT_FOUND`.
|
||||
- Missing brand/product → `404 NOT_FOUND`.
|
||||
|
||||
## Test plan
|
||||
- Unit: brand duplicate error mapping at repository/use-case boundary where practical.
|
||||
- Unit: product search passes brand filter and remains active-only.
|
||||
- Integration/API: duplicate brand slug returns HTTP 409.
|
||||
- Integration/API: `GET /marca/<slug>` returns brand response with slug URL.
|
||||
- Integration/API: product search filtered by `brandSlug` includes active matching products and excludes other brands.
|
||||
|
||||
## Security posture
|
||||
- Public brand reads are unauthenticated and expose only public SEO metadata.
|
||||
- Brand mutations and product brand assignment mutations are admin-only.
|
||||
- SQL must remain parameterized; dynamic update columns must be whitelisted.
|
||||
- Do not trust client-supplied brand names/slugs for product listing: filter server-side by stored brand relation.
|
||||
|
||||
## Risks
|
||||
- This feature crosses brands and catalog. Keep coupling at DB IDs/slugs in catalog infrastructure; TypeScript imports from catalog to brands internals remain forbidden.
|
||||
- `/marca/:slug` is a data endpoint in this backend slice, not a storefront-rendered page; storefront page remains out of scope.
|
||||
19
work/artifacts/F-009/documenter.md
Normal file
19
work/artifacts/F-009/documenter.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Documenter — F-009 Brands module
|
||||
|
||||
done -> work/artifacts/F-009/documenter.md
|
||||
|
||||
## Documentation updated
|
||||
- `project/README.md`
|
||||
|
||||
## Covered
|
||||
- Brand-specific error code `BRAND_SLUG_EXISTS`.
|
||||
- Product brand assignment error code `PRODUCT_BRAND_NOT_FOUND`.
|
||||
- Brands module endpoint table.
|
||||
- Public slug URL rule: `/marca/<slug>`.
|
||||
- Product brand filter: `GET /products/search?brandSlug=<slug>`.
|
||||
- Catalog product `brand_id` assignment and module-boundary note.
|
||||
- Layout entry for `src/modules/brands`.
|
||||
|
||||
## Verification
|
||||
- `cd project && npm run lint` — PASS after README Prettier normalization.
|
||||
- `./scripts/verify.sh` — PASS from repository root before documentation close.
|
||||
56
work/artifacts/F-009/implementer.md
Normal file
56
work/artifacts/F-009/implementer.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# Implementer — F-009 Brands module
|
||||
|
||||
done -> work/artifacts/F-009/implementer.md
|
||||
|
||||
## Summary
|
||||
Implemented brands as an independent backend module with slug URLs and SEO metadata, added product-brand assignment to catalog, and extended public product search with a brand filter.
|
||||
|
||||
## Changed files
|
||||
- `project/src/modules/brands/domain/brand.ts`
|
||||
- `project/src/modules/brands/domain/errors.ts`
|
||||
- `project/src/modules/brands/domain/ports.ts`
|
||||
- `project/src/modules/brands/application/brand-use-cases.ts`
|
||||
- `project/src/modules/brands/infrastructure/pg-brand-repository.ts`
|
||||
- `project/src/modules/brands/api/brands.routes.ts`
|
||||
- `project/src/modules/brands/index.ts`
|
||||
- `project/migrations/007_brands.js`
|
||||
- `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/app/build-app.ts`
|
||||
- `project/src/app/tests/brands.itest.ts`
|
||||
- `project/src/app/tests/catalog.itest.ts`
|
||||
- `work/artifacts/F-009/architect.md`
|
||||
- `backlog/features.json`
|
||||
- `work/current.md`
|
||||
|
||||
## Acceptance traceability
|
||||
1. Duplicate brand slug returns HTTP 409
|
||||
- Implemented through `brands_brands.slug UNIQUE` and `BRAND_SLUG_EXISTS` mapping.
|
||||
- Covered by `project/src/app/tests/brands.itest.ts`.
|
||||
2. Products list filterable by brand
|
||||
- `GET /products/search?brandSlug=<slug>` filters active products by stored `catalog_products.brand_id` joined to `brands_brands.slug`.
|
||||
- Covered by `project/src/app/tests/catalog.itest.ts`.
|
||||
3. Public URL is `/marca/<slug>`
|
||||
- Implemented `GET /marca/:slug` and serialized `url: /marca/<slug>`.
|
||||
- Covered by `project/src/app/tests/brands.itest.ts`.
|
||||
4. `verify.sh` green
|
||||
- Verified after implementation.
|
||||
|
||||
## Commands run
|
||||
- `cd project && npm run typecheck` — PASS
|
||||
- `cd project && npm test` — PASS: 59 passed, 31 skipped (DB integration skipped without `TEST_DATABASE_URL`)
|
||||
- `cd project && npm run lint` — PASS after Prettier normalization
|
||||
- `cd project && npm run build` — PASS
|
||||
- `cd project && npm run lint:boundaries` — PASS: 79 files checked
|
||||
- `./scripts/verify.sh` — PASS
|
||||
|
||||
## Notes
|
||||
- No new npm dependencies.
|
||||
- Brand mutation routes are admin-only via shared auth injected from the composition root.
|
||||
- Public brand reads are unauthenticated.
|
||||
- Catalog references `brands_brands` only at repository/SQL boundary; it does not import brands module internals.
|
||||
- Documentation stage should update README/API notes because new user-facing brand endpoints, product brand assignment, and product error code were added.
|
||||
33
work/artifacts/F-009/leader-close.json
Normal file
33
work/artifacts/F-009/leader-close.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"feature_id": "F-009",
|
||||
"agent": "leader",
|
||||
"stage": "close",
|
||||
"verdict": "APPROVED",
|
||||
"title": "Brands module",
|
||||
"gates": {
|
||||
"review": "APPROVED",
|
||||
"security": "APPROVED",
|
||||
"qa": "APPROVED"
|
||||
},
|
||||
"verification": {
|
||||
"lint": "clean",
|
||||
"boundaries": "79 files OK",
|
||||
"typecheck": "clean",
|
||||
"build": "clean",
|
||||
"unit_tests": "59 passed, 31 skipped",
|
||||
"integration_tests": "present; skipped without TEST_DATABASE_URL",
|
||||
"verify_sh": "green"
|
||||
},
|
||||
"deliverables": [
|
||||
"migrations/007_brands.js",
|
||||
"src/modules/brands/ (domain, application, infrastructure, api)",
|
||||
"catalog product brand_id assignment and brandSlug search filter",
|
||||
"app/build-app.ts: Authenticate injection into brands",
|
||||
"tests: brands integration, catalog brand filter integration",
|
||||
"README brands and catalog brand-filter documentation"
|
||||
],
|
||||
"known_followups": [
|
||||
"No rendered brand storefront page until frontend/catalog page tickets",
|
||||
"Search relevance remains simple; F-012 owns dedicated search/FTS behavior"
|
||||
]
|
||||
}
|
||||
41
work/artifacts/F-009/qa.json
Normal file
41
work/artifacts/F-009/qa.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"feature_id": "F-009",
|
||||
"stage": "qa_gate",
|
||||
"agent": "qa",
|
||||
"verdict": "APPROVED",
|
||||
"acceptance": [
|
||||
{
|
||||
"criterion": "Given duplicate brand slug When created Then HTTP 409",
|
||||
"status": "PASS",
|
||||
"evidence": "Covered by src/app/tests/brands.itest.ts; integration test is skipped automatically when TEST_DATABASE_URL is absent. Repository unique constraint and error mapper reviewed."
|
||||
},
|
||||
{
|
||||
"criterion": "Products list filterable by brand",
|
||||
"status": "PASS",
|
||||
"evidence": "GET /products/search?brandSlug=<slug> implemented; src/app/tests/catalog.itest.ts includes brandSlug filtering scenario."
|
||||
},
|
||||
{
|
||||
"criterion": "Public URL is /marca/<slug>",
|
||||
"status": "PASS",
|
||||
"evidence": "GET /marca/:slug implemented and integration test present; serialized response includes /marca/<slug>."
|
||||
},
|
||||
{
|
||||
"criterion": "verify.sh green",
|
||||
"status": "PASS",
|
||||
"evidence": "./scripts/verify.sh — PASS"
|
||||
}
|
||||
],
|
||||
"commands": {
|
||||
"targeted_tests": "cd project && npm test -- --run src/app/tests/brands.itest.ts src/app/tests/catalog.itest.ts src/modules/catalog/tests/product-use-cases.test.ts — PASS: 3 passed, 6 skipped without TEST_DATABASE_URL",
|
||||
"full_tests": "cd project && npm test — PASS: 59 passed, 31 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."
|
||||
]
|
||||
}
|
||||
26
work/artifacts/F-009/reviewer.json
Normal file
26
work/artifacts/F-009/reviewer.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"feature_id": "F-009",
|
||||
"stage": "review_gate",
|
||||
"agent": "reviewer",
|
||||
"verdict": "APPROVED",
|
||||
"checked": [
|
||||
"Hexagonal brands module structure",
|
||||
"Composition root wiring",
|
||||
"PostgreSQL migration naming and constraints",
|
||||
"Brand slug uniqueness and error mapping",
|
||||
"Public brand slug URL serialization",
|
||||
"Catalog product brand assignment without TypeScript imports from brands internals",
|
||||
"Product search brandSlug filtering",
|
||||
"Acceptance test coverage"
|
||||
],
|
||||
"findings": [],
|
||||
"evidence": {
|
||||
"targeted_tests": "cd project && npm test -- --run src/app/tests/brands.itest.ts src/app/tests/catalog.itest.ts src/modules/catalog/tests/product-use-cases.test.ts — PASS: 3 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: 59 passed, 31 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-009/security.json
Normal file
27
work/artifacts/F-009/security.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"feature_id": "F-009",
|
||||
"stage": "security_gate",
|
||||
"agent": "security",
|
||||
"verdict": "APPROVED",
|
||||
"checked": [
|
||||
"No new runtime or dev dependencies",
|
||||
"Admin-only brand mutation routes use server-side shared auth and requireRole('admin')",
|
||||
"Public brand reads expose only public SEO metadata",
|
||||
"SQL queries use parameterized pg placeholders; dynamic UPDATE columns are whitelisted",
|
||||
"Slug, UUID and payload validation use Zod via parseJson",
|
||||
"Catalog brand filtering uses server-side stored brand relation",
|
||||
"Catalog does not trust client brand data for product reads",
|
||||
"Secret scan over F-009 files"
|
||||
],
|
||||
"findings": [],
|
||||
"evidence": {
|
||||
"npm_audit_runtime": "cd project && npm audit --audit-level=high --omit=dev — PASS: found 0 vulnerabilities",
|
||||
"secret_scan": "grep over F-009 files found only non-secret test password fixtures",
|
||||
"verify": "./scripts/verify.sh — PASS",
|
||||
"tests": "cd project && npm test — PASS"
|
||||
},
|
||||
"notes": [
|
||||
"No client-supplied role/user data is trusted.",
|
||||
"Product brand assignment mutations are admin-only through product create/update routes."
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user