# 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_products` and `catalog_product_categories`. - Unit and integration tests covering product state filtering, slug conflicts, category assignment, public slug URL behavior and module boundaries. ## Key decisions 1. **Catalog owns products only**: product rows live in `catalog_products`; assignment join table lives in `catalog_product_categories` because the relationship is product-owned for F-008. Categories remain independent and are referenced only by ID at the database boundary. 2. **Domain is pure**: `src/modules/catalog/domain/*` must import no `pg`, no Fastify, no HTTP, no repository implementations. Acceptance explicitly requires zero database/HTTP imports. 3. **Product state is explicit**: allowed states are `draft`, `active`, `archived`. Public list/search and slug reads must return only active products. 4. **Slug as public identity**: API exposes `/productos/`; internal UUID may be returned in admin responses but must never be required for public product URLs. 5. **SearchProducts v1 is PostgreSQL-backed but interface-shaped**: implement simple text matching over `name` and `description`, active-only for public use. This is not the dedicated FTS module from F-012; keep it boring and replaceable. 6. **Category assignment validation**: creating/updating product category IDs must reject unknown categories. Runtime validation may query `categories_categories` from the catalog repository only to enforce referential integrity for the join. Do not import the categories module internals. 7. **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. 8. **No new dependencies**: current stack already covers validation, PostgreSQL and testing. ## Suggested API contract - `GET /productos/:slug` → public active product by slug; `404` for 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 returns `409`. - `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 generated `id`/timestamps; `state` defaults to `draft`; `categoryIds` defaults to empty. - `ProductPatch`: optional editable fields; if `categoryIds` is 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_ERROR` from 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/` and response exposes `url` with 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.