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>
This commit is contained in:
2026-03-08 23:38:37 +01:00
parent 2287f4597d
commit f141cc4e6a
140 changed files with 19921 additions and 17723 deletions

View File

@@ -0,0 +1,291 @@
# 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`

View File

@@ -0,0 +1,105 @@
# 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

View File

@@ -0,0 +1,57 @@
# Product Catalog
Product catalog browsing and search for storefronts.
## Overview
| Aspect | Detail |
|--------|--------|
| Code | `catalog` |
| Classification | Optional |
| Dependencies | None |
| Status | Active |
## Features
- `product_catalog` — Product catalog browsing
- `product_search` — Product search and filtering
- `product_variants` — Product variant management
- `product_categories` — Category hierarchy
- `product_attributes` — Custom product attributes
- `product_import_export` — Bulk product import/export
## Permissions
| Permission | Description |
|------------|-------------|
| `products.view` | View products |
| `products.create` | Create products |
| `products.edit` | Edit products |
| `products.delete` | Delete products |
| `products.import` | Import products |
| `products.export` | Export products |
## Data Model
See [Data Model](data-model.md) for full entity relationships and schema.
- **Product** — Store-specific product with pricing, VAT, and supplier fields
- **ProductTranslation** — Multilingual content with SEO fields
- **ProductMedia** — Product-media associations with usage types
## API Endpoints
| Method | Path | Description |
|--------|------|-------------|
| `*` | `/api/v1/admin/catalog/*` | Admin product management |
| `*` | `/api/v1/store/catalog/*` | Store product management |
| `GET` | `/api/v1/storefront/catalog/*` | Public product browsing |
## Configuration
No module-specific configuration.
## Additional Documentation
- [Data Model](data-model.md) — Entity relationships and database schema
- [Architecture](architecture.md) — Independent product copy pattern and API design