4.2 KiB
4.2 KiB
Architect — F-008 Catalog core: products domain
done -> work/artifacts/F-008/architect.md
Deliverables
src/modules/catalog/with domain, application, infrastructure and api layers.- PostgreSQL migration for
catalog_productsandcatalog_product_categories. - Unit and integration tests covering product state filtering, slug conflicts, category assignment, public slug URL behavior and module boundaries.
Key decisions
- Catalog owns products only: product rows live in
catalog_products; assignment join table lives incatalog_product_categoriesbecause the relationship is product-owned for F-008. Categories remain independent and are referenced only by ID at the database boundary. - Domain is pure:
src/modules/catalog/domain/*must import nopg, no Fastify, no HTTP, no repository implementations. Acceptance explicitly requires zero database/HTTP imports. - Product state is explicit: allowed states are
draft,active,archived. Public list/search and slug reads must return only active products. - Slug as public identity: API exposes
/productos/<slug>; internal UUID may be returned in admin responses but must never be required for public product URLs. - SearchProducts v1 is PostgreSQL-backed but interface-shaped: implement simple text matching over
nameanddescription, active-only for public use. This is not the dedicated FTS module from F-012; keep it boring and replaceable. - Category assignment validation: creating/updating product category IDs must reject unknown categories. Runtime validation may query
categories_categoriesfrom the catalog repository only to enforce referential integrity for the join. Do not import the categories module internals. - No variants, stock, prices: product core fields are identity, name, slug, description, state, SEO metadata, timestamps and category IDs. Anything sellable, stock-related or price-related belongs to later tickets.
- No new dependencies: current stack already covers validation, PostgreSQL and testing.
Suggested API contract
GET /productos/:slug→ public active product by slug;404for draft/archived/missing.GET /products/search?q=&limit=&offset=→ public active product search/listing; only active products.POST /products→ admin-only create product with optional category IDs; duplicate slug returns409.PATCH /products/:id→ admin-only update product fields/state/category IDs.
Domain model
ProductState = 'draft' | 'active' | 'archived'.Product:id,name,slug,description,state,seoTitle,seoDescription,categoryIds,createdAt,updatedAt.NewProduct: same editable fields except generatedid/timestamps;statedefaults todraft;categoryIdsdefaults to empty.ProductPatch: optional editable fields; ifcategoryIdsis present it replaces assignments.
Error mapping
- Duplicate slug →
409 PRODUCT_SLUG_EXISTS. - Unknown category ID →
422 PRODUCT_CATEGORY_NOT_FOUND. - Missing product →
404 NOT_FOUND. - Invalid state/payload → existing
400 VALIDATION_ERRORfrom Zod.
Test plan
- Unit: domain/application SearchProducts returns only active products.
- Unit: domain layer imports remain pure via boundary lint and targeted source scan.
- Integration/API: duplicate slug create returns HTTP 409.
- Integration/API: draft products do not appear in public search/listing and are not readable by
/productos/:slug. - Integration/API: active product is readable at
/productos/<slug>and response exposesurlwith the slug. - Integration/API: product can be assigned to an existing category; unknown category rejected.
Security posture
- Public reads are unauthenticated and active-only.
- Mutations are admin-only via injected shared auth and
requireRole. - Never trust product state, price, stock or category information from other modules without server-side validation.
- SQL must remain parameterized; any dynamic update columns must be whitelisted.
Risks
- F-008 touches a relationship with categories. Keep coupling at database IDs and repository validation, not TypeScript imports from
modules/categories/*. - Search in this slice is intentionally simple. Do not overbuild FTS/relevance here; F-012 owns search infrastructure.