# Architect — F-011 Product images

## Decision
Keep product images inside the catalog module. Product and variant images are catalog-owned presentation metadata, not a separate bounded context yet.

## Data model
Add `catalog_product_images`:
- `id uuid primary key default gen_random_uuid()`
- `product_id uuid not null references catalog_products(id) on delete cascade`
- `variant_id uuid null references catalog_product_variants(id) on delete cascade`
- `url text not null`
- `alt_text text not null`
- `position integer not null`
- `role text not null check (role in ('main', 'gallery'))`
- timestamps

Rules:
- `position >= 0`
- `variant_id` is optional; when present it must belong to the same `product_id`.
- Exactly one `main` image per product-level set and per variant-level set. Implement with partial unique indexes:
  - `(product_id) WHERE variant_id IS NULL AND role = 'main'`
  - `(variant_id) WHERE variant_id IS NOT NULL AND role = 'main'`
- Stable ordering by `position`, then `created_at`, then `id`.

## Domain/application
Add image domain types in catalog:
- `ProductImageRole = 'main' | 'gallery'`
- `ProductImage` with productId, optional variantId, url, altText, position, role.
- `NewProductImage` and `ProductImagePatch`.

Add `ProductImageRepository` port. It is the boundary that makes storage swappable:
- API/application depend only on the port.
- Local-first storage is represented as URLs accepted by the adapter/repository path for this slice.
- Future CDN/image processing must touch infrastructure only.

Use cases:
- `ListProductImages(productId, variantId?)`
- `AttachProductImage(productId, input)`; verify product exists, and if `variantId` is passed verify it belongs to that product.
- `DetachProductImage(productId, imageId)`
- `ReorderProductImages(productId, items)`; validates all listed images belong to the same product/scope.

## API
Admin mutations, public read through product serialization:
- `GET /productos/:slug` includes ordered `images` list for product-level images.
- `GET /products/search` may continue returning product core only unless cheap to include images; acceptance only requires product exposes ordered image list.
- `GET /products/:id/images` returns ordered product-level images.
- `POST /products/:id/images` attaches product or variant image. Body: `url`, `altText`, `role`, optional `variantId`, optional `position`.
- `DELETE /products/:id/images/:imageId` detaches.
- `PATCH /products/:id/images/reorder` accepts ordered `{ imageId, position }[]`.

Validation:
- URL must be a bounded string and should be parseable URL or local absolute path beginning with `/`.
- `altText` is required, trimmed, max 300.
- `role` uses the domain enum.

## Tests/evidence
- Unit test use cases with fake repositories:
  - ordered list preserves repository ordering.
  - attach rejects missing product.
  - attach rejects variant from another product.
- Repository/migration shape can be covered by existing migration integration test when `TEST_DATABASE_URL` exists.
- `npm test`, `npm run typecheck`, and root `./scripts/verify.sh` must be green.

## Out of scope guardrails
No binary upload, CDN, resizing, optimization pipeline, or external object-store SDK in F-011. No new runtime dependency expected.
