3.6 KiB
3.6 KiB
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_brandsand 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
- 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. - Catalog owns the assignment field: add nullable
brand_idtocatalog_productsbecause product-brand assignment is product-owned. It referencesbrands_brands(id)for integrity. Catalog may validate/query brand IDs/slugs at repository boundary, but must not importmodules/brands/*internals. - Slug as public brand identity: brands exposes
GET /marca/:slug; internal UUID may exist but public URL must be/marca/<slug>. - Brand SEO metadata is first-class:
brands_brandsstoresseo_titleandseo_descriptionalongsidenameandslug. - Product filtering by brand: extend catalog public search with
brandSlugquery parameter. Search remains active-only. Repository joins/filters viabrands_brands.slugusing parameterized SQL. - Admin-only mutations: brand create/update routes require injected shared auth +
requireRole('admin'). - No new dependencies: existing Fastify/Zod/pg/Vitest stack is enough.
Suggested API contract
GET /marca/:slug→ public brand by slug; response includesurl: /marca/<slug>.POST /brands→ admin-only create brand; duplicate slug returns409 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/:idaccept optional nullablebrandId.
Domain model
Brand:id,name,slug,seoTitle,seoDescription,createdAt,updatedAt.NewBrand:name,slug, optional SEO metadata.BrandPatch: optional editable fields.- Extend catalog
Product: nullablebrandId. - Extend catalog
NewProduct/ProductPatch: optional nullablebrandId. - Extend
ProductSearch: optionalbrandSlug.
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
brandSlugincludes 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/:slugis a data endpoint in this backend slice, not a storefront-rendered page; storefront page remains out of scope.