feat(ADM-018): completed feature

This commit is contained in:
chattie
2026-08-17 22:23:10 +02:00
parent cf1c69fc8b
commit d595b4871f
871 changed files with 47411 additions and 281 deletions

View File

@@ -19,6 +19,17 @@ npm run lint # eslint + prettier check
npm run lint:boundaries # module boundary check
```
Frontend shell commands live in `storefront/`:
```bash
cd storefront
npm install
npm run dev # Next.js development server
npm run lint # storefront formatting check
npm run typecheck # frontend TypeScript check
npm run build # production Next.js build
```
## Configuration
Startup is fail-fast: `src/infrastructure/config` parses env once and refuses to boot
@@ -38,6 +49,11 @@ is separate from deployment (no redeploy). Copy `.env.example` to `.env` to star
- Errors always use one envelope:
`{ "error": { "statusCode", "code", "message", "details?" }, "requestId" }`
Codes: `NOT_FOUND`, `VALIDATION_ERROR`, `BAD_REQUEST`/Fastify 4xx codes, `INTERNAL_ERROR`.
Category-specific codes: `CATEGORY_SLUG_EXISTS` (409), `CATEGORY_PARENT_NOT_FOUND` (422),
`CATEGORY_TREE_CYCLE` (422), `CATEGORY_HAS_CHILDREN` (409). Product-specific codes:
`PRODUCT_SLUG_EXISTS` (409), `PRODUCT_CATEGORY_NOT_FOUND` (422),
`PRODUCT_BRAND_NOT_FOUND` (422), `PRODUCT_VARIANT_CODE_EXISTS` (409).
Brand-specific codes: `BRAND_SLUG_EXISTS` (409).
5xx messages are always generic; stack traces stay in server logs only.
- Input validation is explicit per route: `parseJson(schema, body)` (zod) in the handler.
- Auth codes: `UNAUTHORIZED` (401, missing/invalid/revoked session),
@@ -96,6 +112,111 @@ operation in this slice (no admin API yet).
- The users module never imports identity: session resolution arrives as an
injected `Authenticate` function from the composition root.
## Categories (categories module)
Categories provide the public taxonomy for SEO-friendly catalog URLs. The module
owns `categories_categories`; products are not assigned to categories until the
catalog slice.
| Route | Access | Result |
| ---------------------- | ---------- | ------------------------------------------- |
| GET /categories/tree | public | `200` + `{ items: [categoryTreeNode] }` |
| GET /categoria/:slug | public | `200` category by slug, `404` if missing |
| POST /categories | admin only | `201` category, `409` on duplicate slug |
| PATCH /categories/:id | admin only | `200` category, `422` on invalid tree move |
| DELETE /categories/:id | admin only | `204`, `409` when the category has children |
- Public URLs are slug-based: `/categoria/<slug>`, never internal ids.
- Slugs are globally unique for this slice.
- Category hierarchy uses `parentId`; self-parenting and descendant-as-parent moves
are rejected with `CATEGORY_TREE_CYCLE`.
- Category rows include `seoTitle` and `seoDescription`.
## Catalog core (catalog module)
Catalog owns product identity, variants, rich product data, product images and public product discovery.
Stock, prices and external sync jobs are intentionally outside this slice.
| Route | Access | Result |
| --------------------------------------- | ---------- | ------------------------------------------------------- |
| GET /productos/:slug | public | `200` active product by slug, `404` otherwise |
| GET /products/search | public | `200` + `{ items: [activeProduct] }` via PostgreSQL FTS |
| POST /products | admin only | `201` product, `409` on duplicate slug |
| PATCH /products/:id | admin only | `200` product, `422` on unknown category |
| GET /products/:id/variants | public | `200` + `{ items: [variant] }` |
| POST /products/:id/variants | admin only | `201` variant, `409` on duplicate SKU/EAN |
| PATCH /products/:id/variants/:variantId | admin only | `200` variant, `404` if missing |
| PATCH /products/:id/rich-data | admin only | `200` rich data with nutrition provenance |
| GET /products/:id/images | public | `200` + `{ items: [image] }` |
| POST /products/:id/images | admin only | `201` image, `404` on missing product |
| DELETE /products/:id/images/:imageId | admin only | `204`, `404` if missing |
| PATCH /products/:id/images/reorder | admin only | `200` + reordered `{ items: [image] }` |
- Public URLs are slug-based: `/productos/<slug>`, never internal ids.
- Product states are `draft`, `active` and `archived`; public reads/search return
only `active` products.
- Product rows include `seoTitle` and `seoDescription`.
- Category assignment is stored in `catalog_product_categories` and validates
category ids against `categories_categories` without importing categories internals.
- Product search is behind a `ProductSearchRepository` port so future engines can replace PostgreSQL
without changing the HTTP API. `GET /products/search?q=<term>` uses PostgreSQL full-text search over
product, brand and category text with stable pagination/relevance ordering.
- Product reads can be filtered by brand with `GET /products/search?brandSlug=<slug>` and by category
with `GET /products/search?categorySlug=<slug>`.
- Product brand assignment is stored as `catalog_products.brand_id` and validates
brand ids against `brands_brands` without importing brands internals.
- Variants live in `catalog_product_variants`; non-null SKU and optional EAN are globally unique.
- Images live in `catalog_product_images`; product pages expose ordered image metadata with `url`,
`altText`, `position` and `role` (`main` or `gallery`). Images can be product-level or variant-level.
Storage is behind a catalog infrastructure adapter; this slice stores URLs only, with no binary upload,
CDN or processing pipeline.
- Rich data lives in `catalog_product_rich_data`; nutrition payloads require `nutritionSource`
(`manual`, `manufacturer`, `openfoodfacts`).
- Manual nutrition is trusted: external-source updates cannot overwrite existing manual nutrition.
- Search logs structured `catalog_search` telemetry with duration, result count and sanitized bounded
query metadata for later popular-search/cache work.
## Brands (brands module)
Brands provide public SEO-friendly brand identity and product filtering support.
The module owns `brands_brands`; product assignment remains catalog-owned.
| Route | Access | Result |
| ----------------- | ---------- | ------------------------------------- |
| GET /brands | public | `200` brand list for sitemap/catalog |
| GET /marca/:slug | public | `200` brand by slug, `404` if missing |
| POST /brands | admin only | `201` brand, `409` on duplicate slug |
| PATCH /brands/:id | admin only | `200` brand, `404` if missing |
- Public URLs are slug-based: `/marca/<slug>`, never internal ids.
- Slugs are globally unique for this slice.
- Brand rows include `seoTitle` and `seoDescription`.
## Storefront shell (Next.js)
The customer-facing shell lives in `storefront/` as a separate frontend package. It uses
Next.js App Router, React, TypeScript and Tailwind CSS.
- Home renders as a server component with global layout, navigation and footer.
- Product detail pages live at `/productos/<slug>`; category pages at `/categoria/<slug>`; brand pages at
`/marca/<slug>`; search results at `/products/search`.
- Product, category and brand pages use ISR (`revalidate = 300`) and generate metadata with canonical URLs
and OpenGraph fields. Search results use `revalidate = 120`.
- Product pages embed Product JSON-LD; product/category/brand pages embed BreadcrumbList JSON-LD; the root
layout embeds Organization JSON-LD.
- `sitemap.xml` and `robots.txt` are generated by Next.js metadata routes. Sitemap includes static public URLs,
active products, categories and brands, and degrades to static URLs if the API is unavailable.
- Permanent SEO redirects are configured with `REDIRECTS_JSON`, an array of local same-origin
`{ "from": "/old", "to": "/new" }` path entries handled by `src/proxy.ts` with HTTP 301.
- On-demand catalog revalidation is available at `POST /api/revalidate` with `REVALIDATE_SECRET` and
`x-revalidate-secret`; accepted paths are catalog public paths only.
- The frontend consumes backend public endpoints only through `src/lib/api.ts` typed DTOs.
- `src/lib/api.ts` imports `server-only`, so API calls are not accidentally bundled into client components.
- Backend internals under `src/` are never imported by the storefront.
- Configure API origin with `API_BASE_URL` for server-side rendering or `NEXT_PUBLIC_API_BASE_URL` when needed;
the local default is `http://localhost:3000`.
- Cart and checkout UI are intentionally outside this slice.
## Database (local dev)
```bash
@@ -129,7 +250,10 @@ src/
│ ├── health/ # exemplar module: public API only via index.ts
│ ├── flags/ # feature flags (unknown default OFF, runtime flip)
│ ├── identity/ # register/login/logout, argon2, sessions, rate limit
── users/ # profile + address CRUD, owner-or-admin RBAC
── users/ # profile + address CRUD, owner-or-admin RBAC
│ ├── categories/ # category tree, slugs, SEO metadata
│ ├── catalog/ # products, states, slugs, category/brand assignment
│ └── brands/ # brands, slugs, SEO metadata
└── shared/ # cross-cutting helpers (error envelope, input parsing)
```