Files
mercadodevida/work/artifacts/F-009/architect.md
2026-08-17 22:23:10 +02:00

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