# 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/`. 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/`. - `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=` → 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/` 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.