# Catalog Data Model

Entity relationships and database schema for the catalog module.

## Entity Relationship Overview

```
Store 1──* Product 1──* ProductTranslation
                   │
                   ├──* ProductMedia *──1 MediaFile
                   │
                   ├──? MarketplaceProduct (source)
                   │
                   └──* Inventory (from inventory module)
```

## Models

### Product

Store-specific product with independent copy pattern from marketplace imports. All monetary values stored as integer cents.

| Field | Type | Constraints | Description |
|-------|------|-------------|-------------|
| `id` | Integer | PK | Primary key |
| `store_id` | Integer | FK, not null | Store ownership |
| `marketplace_product_id` | Integer | FK, nullable | Optional marketplace source |
| `store_sku` | String | indexed | Store's internal SKU |
| `gtin` | String(50) | indexed | EAN/UPC barcode |
| `gtin_type` | String(20) | nullable | gtin13, gtin14, gtin12, gtin8, isbn13, isbn10 |
| `price_cents` | Integer | nullable | Gross price in cents |
| `sale_price_cents` | Integer | nullable | Sale price in cents |
| `currency` | String(3) | default "EUR" | Currency code |
| `brand` | String | nullable | Product brand |
| `condition` | String | nullable | Product condition |
| `availability` | String | nullable | Availability status |
| `primary_image_url` | String | nullable | Main product image URL |
| `additional_images` | JSON | nullable | Array of additional image URLs |
| `download_url` | String | nullable | Digital product download URL |
| `license_type` | String(50) | nullable | Digital product license |
| `tax_rate_percent` | Integer | not null, default 17 | VAT rate (LU: 0, 3, 8, 14, 17) |
| `supplier` | String(50) | nullable | codeswholesale, internal, etc. |
| `supplier_product_id` | String | nullable | Supplier's product reference |
| `cost_cents` | Integer | nullable | Cost to acquire in cents |
| `margin_percent_x100` | Integer | nullable | Markup × 100 (2550 = 25.5%) |
| `is_digital` | Boolean | default False, indexed | Digital vs physical |
| `product_type` | String(20) | default "physical" | physical, digital, service, subscription |
| `is_featured` | Boolean | default False | Featured flag |
| `is_active` | Boolean | default True | Active flag |
| `display_order` | Integer | default 0 | Sort order |
| `min_quantity` | Integer | default 1 | Min purchase quantity |
| `max_quantity` | Integer | nullable | Max purchase quantity |
| `fulfillment_email_template` | String | nullable | Template for digital delivery |
| `created_at` | DateTime | tz-aware | Record creation time |
| `updated_at` | DateTime | tz-aware | Record update time |

**Unique Constraint**: `(store_id, marketplace_product_id)`
**Composite Indexes**: `(store_id, is_active)`, `(store_id, is_featured)`, `(store_id, store_sku)`, `(supplier, supplier_product_id)`

**Key Properties**: `price`, `sale_price`, `cost` (euro converters), `net_price_cents` (gross minus VAT), `vat_amount_cents`, `profit_cents`, `profit_margin_percent`, `total_inventory`, `available_inventory`

### ProductTranslation

Store-specific multilingual content with SEO fields. Independent copy from marketplace translations.

| Field | Type | Constraints | Description |
|-------|------|-------------|-------------|
| `id` | Integer | PK | Primary key |
| `product_id` | Integer | FK, not null, cascade | Parent product |
| `language` | String(5) | not null | en, fr, de, lb |
| `title` | String | nullable | Product title |
| `description` | Text | nullable | Full description |
| `short_description` | String(500) | nullable | Abbreviated description |
| `meta_title` | String(70) | nullable | SEO title |
| `meta_description` | String(160) | nullable | SEO description |
| `url_slug` | String(255) | nullable | URL-friendly slug |
| `created_at` | DateTime | tz-aware | Record creation time |
| `updated_at` | DateTime | tz-aware | Record update time |

**Unique Constraint**: `(product_id, language)`

### ProductMedia

Association between products and media files with usage tracking.

| Field | Type | Constraints | Description |
|-------|------|-------------|-------------|
| `id` | Integer | PK | Primary key |
| `product_id` | Integer | FK, not null, cascade | Product reference |
| `media_id` | Integer | FK, not null, cascade | Media file reference |
| `usage_type` | String(50) | default "gallery" | main_image, gallery, variant, thumbnail, swatch |
| `display_order` | Integer | default 0 | Sort order |
| `variant_id` | Integer | nullable | Variant reference |
| `created_at` | DateTime | tz-aware | Record creation time |
| `updated_at` | DateTime | tz-aware | Record update time |

**Unique Constraint**: `(product_id, media_id, usage_type)`

## Design Patterns

- **Independent copy pattern**: Products are copied from marketplace sources, not linked. Store-specific data diverges independently.
- **Money as cents**: All prices, costs, margins stored as integer cents
- **Luxembourg VAT**: Supports all LU rates (0%, 3%, 8%, 14%, 17%)
- **Multi-type products**: Physical, digital, service, subscription with type-specific fields
- **SEO per language**: Meta title and description in each translation
