Files
orion/app/modules/catalog/docs/architecture.md
Samir Boulahtit f141cc4e6a docs: migrate module documentation to single source of truth
Move 39 documentation files from top-level docs/ into each module's
docs/ folder, accessible via symlinks from docs/modules/. Create
data-model.md files for 10 modules with full schema documentation.
Replace originals with redirect stubs. Remove empty guide stubs.

Modules migrated: tenancy, billing, loyalty, marketplace, orders,
messaging, cms, catalog, inventory, hosting, prospecting.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 23:38:37 +01:00

9.8 KiB
Raw Blame History

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

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:

@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:

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

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