# 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.