# 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