# Product Architecture ## Overview The product management system uses an **independent copy pattern** where store products (`Product`) are fully independent entities that can optionally reference a marketplace source (`MarketplaceProduct`) for display purposes only. ## Core Principles | Principle | Description | |-----------|-------------| | **Full Independence** | Store products have all their own fields - no inheritance or fallback to marketplace | | **Optional Source Reference** | `marketplace_product_id` is nullable - products can be created directly | | **No Reset Functionality** | No "reset to source" - products are independent from the moment of creation | | **Source for Display Only** | Source comparison info is read-only, used for "view original" display | --- ## Architecture Diagram ``` ┌─────────────────────────────────────────────────────────────────────┐ │ MarketplaceProduct │ │ (Central Repository - raw imported data from marketplaces) │ │ │ │ - marketplace_product_id (unique) │ │ - gtin, mpn, sku │ │ - brand, price_cents, sale_price_cents │ │ - is_digital, product_type_enum │ │ - translations (via MarketplaceProductTranslation) │ └──────────────────────────────────────────────────────────────────────┘ ╳ No runtime dependency │ │ Optional FK (for "view source" display only) │ marketplace_product_id (nullable) ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ Product │ │ (Store's Independent Product - fully standalone) │ │ │ │ === IDENTIFIERS === │ │ - store_id (required) │ │ - store_sku │ │ - gtin, gtin_type │ │ │ │ === PRODUCT TYPE (own columns) === │ │ - is_digital (Boolean) │ │ - product_type (String: physical, digital, service, subscription) │ │ │ │ === PRICING === │ │ - price_cents, sale_price_cents │ │ - currency, tax_rate_percent │ │ │ │ === CONTENT === │ │ - brand, condition, availability │ │ - primary_image_url, additional_images │ │ - translations (via ProductTranslation) │ │ │ │ === STATUS === │ │ - is_active, is_featured │ │ │ │ === SUPPLIER === │ │ - supplier, cost_cents │ └─────────────────────────────────────────────────────────────────────┘ ``` --- ## Product Creation Patterns ### 1. From Marketplace Source (Import) When copying from a marketplace product: - All fields are **copied** at creation time - `marketplace_product_id` is set for source reference - No ongoing relationship - product is immediately independent ```python # Service copies all fields at import time product = Product( store_id=store.id, marketplace_product_id=marketplace_product.id, # Source reference # All fields copied - no inheritance brand=marketplace_product.brand, price=marketplace_product.price, is_digital=marketplace_product.is_digital, product_type=marketplace_product.product_type_enum, # ... all other fields ) ``` ### 2. Direct Creation (No Marketplace Source) Stores can create products directly without a marketplace source: ```python product = Product( store_id=store.id, marketplace_product_id=None, # No source store_sku="DIRECT_001", brand="MyBrand", price=29.99, is_digital=True, product_type="digital", is_active=True, ) ``` --- ## Key Fields ### Product Type Fields | Field | Type | Default | Description | |-------|------|---------|-------------| | `is_digital` | Boolean | `False` | Whether product is digital (no physical shipping) | | `product_type` | String(20) | `"physical"` | Product type: physical, digital, service, subscription | These are **independent columns** on Product, not derived from MarketplaceProduct. ### Source Reference | Field | Type | Nullable | Description | |-------|------|----------|-------------| | `marketplace_product_id` | Integer FK | **Yes** | Optional reference to source MarketplaceProduct | --- ## Inventory Handling Digital and physical products have different inventory behavior: ```python @property def has_unlimited_inventory(self) -> bool: """Digital products have unlimited inventory.""" return self.is_digital @property def total_inventory(self) -> int: """Get total inventory across all locations.""" if self.is_digital: return Product.UNLIMITED_INVENTORY # 999999 return sum(inv.quantity for inv in self.inventory_entries) ``` --- ## Source Comparison (Display Only) For products with a marketplace source, we provide comparison info for display: ```python def get_source_comparison_info(self) -> dict: """Get current values with source values for comparison. Used for "view original source" display feature. """ mp = self.marketplace_product return { "price": self.price, "price_source": mp.price if mp else None, "brand": self.brand, "brand_source": mp.brand if mp else None, # ... other fields } ``` This is **read-only** - there's no mechanism to "reset" to source values. --- ## UI Behavior ### Detail Page | Product Type | Source Info Card | Edit Button Text | |-------------|------------------|------------------| | Marketplace-sourced | Shows source info with "View Source" link | "Edit Overrides" | | Directly created | Shows "Direct Creation" badge | "Edit Product" | ### Info Banner - **Marketplace-sourced**: Purple banner - "Store Product Catalog Entry" - **Directly created**: Blue banner - "Directly Created Product" --- ## Database Schema ### Product Table Key Columns ```sql CREATE TABLE products ( id INTEGER PRIMARY KEY, store_id INTEGER NOT NULL REFERENCES stores(id), marketplace_product_id INTEGER REFERENCES marketplace_products(id), -- Nullable! -- Product Type (independent columns) is_digital BOOLEAN DEFAULT FALSE, product_type VARCHAR(20) DEFAULT 'physical', -- Identifiers store_sku VARCHAR, gtin VARCHAR, gtin_type VARCHAR(10), brand VARCHAR, -- Pricing (in cents) price_cents INTEGER, sale_price_cents INTEGER, currency VARCHAR(3) DEFAULT 'EUR', tax_rate_percent INTEGER DEFAULT 17, availability VARCHAR, -- Media primary_image_url VARCHAR, additional_images JSON, -- Status is_active BOOLEAN DEFAULT TRUE, is_featured BOOLEAN DEFAULT FALSE, -- Timestamps created_at TIMESTAMP, updated_at TIMESTAMP ); -- Index for product type queries CREATE INDEX idx_product_is_digital ON products(is_digital); ``` --- ## Migration History | Migration | Description | |-----------|-------------| | `x2c3d4e5f6g7` | Made `marketplace_product_id` nullable | | `y3d4e5f6g7h8` | Added `is_digital` and `product_type` columns to products | --- ## API Endpoints ### Create Product (Admin) ``` POST /api/v1/admin/store-products { "store_id": 1, "translations": { "en": {"title": "Product Name", "description": "..."}, "fr": {"title": "Nom du produit", "description": "..."} }, "store_sku": "SKU001", "brand": "BrandName", "price": 29.99, "is_digital": false, "is_active": true } ``` ### Update Product (Admin) ``` PATCH /api/v1/admin/store-products/{id} { "is_digital": true, "price": 39.99, "translations": { "en": {"title": "Updated Name"} } } ``` --- ## Testing Key test scenarios: 1. **Direct Product Creation** - Create without marketplace source 2. **Digital Product Inventory** - Verify unlimited inventory for digital 3. **is_digital Column** - Verify it's an independent column, not derived 4. **Source Comparison** - Verify read-only source info display See: - `tests/unit/models/database/test_product.py` - `tests/integration/api/v1/admin/test_store_products.py`