# Architect — F-007 Categories module

done -> work/artifacts/F-007/architect.md

## Deliverables
- `src/modules/categories/` with domain, application, infrastructure and api layers.
- PostgreSQL migration for `categories_categories`.
- Unit and integration tests covering slug uniqueness, tree hierarchy, cycle prevention and public slug URL behavior.

## Key decisions
1. **Module ownership**: categories owns `categories_categories`; no catalog/product dependency in this slice. Product assignment is explicitly deferred to F-008.
2. **Hexagonal shape**: domain model + repository port in `domain/`; use cases in `application/`; PostgreSQL adapter in `infrastructure/`; Fastify routes in `api/`. Match existing module style instead of inventing a parallel architecture.
3. **Tree model**: adjacency list via nullable `parent_id` FK to the same table. It is boring, queryable, and enough for v1. Add recursive reads only where needed for cycle validation/tree response.
4. **Slug as public identity**: API must expose `/categoria/<slug>` for public reads; internal UUID can exist but must not be required for public URLs. Slug is globally unique for the slice, not sibling-scoped, because backlog says unique slug per category.
5. **Cycle prevention in application + DB-safe query**: create/update parent operations must reject self-parenting, unknown parent IDs, and descendant-as-parent. Use a recursive CTE in the repository to detect descendants before updating `parent_id`.
6. **SEO metadata is first-class**: store `seo_title` and `seo_description` columns on the category row, not a side table.
7. **No dependencies new**: current stack already has Fastify, Zod, pg and Vitest.

## Suggested API contract
- `GET /categories/tree` → `{ items: CategoryTreeNode[] }` for navigation/admin tree consumers.
- `GET /categoria/:slug` → public category read by slug; URL uses slug, never internal ID.
- `POST /categories` → create category; admin-only once auth is wired into the route.
- `PATCH /categories/:id` → update metadata/slug/parent; admin-only; rejects cycles.
- `DELETE /categories/:id` → delete only leaf categories for v1, or return `409` when children exist.

## Domain model
- `Category`: `id`, `parentId`, `name`, `slug`, `seoTitle`, `seoDescription`, `createdAt`, `updatedAt`.
- `NewCategory`: `name`, `slug`, optional `parentId`, optional SEO metadata.
- `CategoryPatch`: optional editable fields, including `parentId` where `null` means move to root.

## Error mapping
- Duplicate slug → `409 CONFLICT` with stable code such as `CATEGORY_SLUG_EXISTS`.
- Unknown parent → `422 VALIDATION_ERROR` or `404 NOT_FOUND`; choose one and keep tests explicit.
- Cycle/self-parent → `422 VALIDATION_ERROR` with stable code such as `CATEGORY_TREE_CYCLE`.
- Delete category with children → `409 CONFLICT`.

## Test plan
- Unit: slug duplicate conflict mapping in create/update use cases.
- Unit: update parent rejects self-parent and descendant-as-parent.
- Integration: migration up/down/fresh verify remains green.
- Integration/API: duplicate create returns HTTP 409.
- Integration/API: tree supports parent/child and returns nested structure.
- Integration/API: `GET /categoria/<slug>` succeeds without internal id in request path.

## Security posture
- Public slug reads can be unauthenticated.
- Mutations should be admin-only using shared auth (`Authenticate`, `requireRole`) if this feature exposes mutation routes now. Do not accept role/user data from the body.
- Validate all route inputs with Zod through existing `parseJson` pattern.

## Risks
- If admin auth is injected into categories routes, composition root must pass the existing authenticator just like users. Keep this cross-module dependency in `src/app`, not inside the categories module.
- Recursive CTE bugs can silently allow invalid trees; tests must cover root → child → grandchild then moving root under grandchild.
