# 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/<slug>`; 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/<slug>` 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.
