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:
291
app/modules/catalog/docs/architecture.md
Normal file
291
app/modules/catalog/docs/architecture.md
Normal 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`
|
||||
105
app/modules/catalog/docs/data-model.md
Normal file
105
app/modules/catalog/docs/data-model.md
Normal 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
|
||||
57
app/modules/catalog/docs/index.md
Normal file
57
app/modules/catalog/docs/index.md
Normal 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
|
||||
Reference in New Issue
Block a user