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

3.8 KiB

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.