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>
9.8 KiB
9.8 KiB
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_idis 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:
- Direct Product Creation - Create without marketplace source
- Digital Product Inventory - Verify unlimited inventory for digital
- is_digital Column - Verify it's an independent column, not derived
- Source Comparison - Verify read-only source info display
See:
tests/unit/models/database/test_product.pytests/integration/api/v1/admin/test_store_products.py