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

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

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

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

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

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

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