Files
orion/docs/architecture/multi-platform-cms.md
Samir Boulahtit aad18c27ab
Some checks failed
CI / ruff (push) Successful in 11s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has started running
refactor: remove all backward compatibility code across 70 files
Clean up 28 backward compatibility instances identified in the codebase.
The app is not live, so all shims are replaced with the target architecture:

- Remove legacy Inventory.location column (use bin_location exclusively)
- Remove dashboard _extract_metric_value helper (use flat metrics dict)
- Remove legacy stat field duplicates (total_stores, total_imports, etc.)
- Remove 13 re-export shims and class aliases across modules
- Remove module-enabling JSON fallback (use PlatformModule junction table)
- Remove menu_to_legacy_format() conversion (return dataclasses directly)
- Remove title/description from MarketplaceProductBase schema
- Clean billing convenience method docstrings
- Clean test fixtures and backward-compat comments
- Add PlatformModule seeding to init_production.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 13:20:29 +01:00

290 lines
14 KiB
Markdown

# Multi-Platform CMS Architecture
## Overview
The Multi-Platform CMS enables Orion to serve multiple business offerings (OMS, Loyalty, Site Builder) from a single codebase, each with its own marketing site and store ecosystem.
## Three-Tier Content Hierarchy
Content pages follow a three-tier inheritance model:
```
┌─────────────────────────────────────────────────────────────────────┐
│ TIER 1: Platform Marketing │
│ Public pages for the platform (homepage, pricing, features) │
│ is_platform_page=TRUE, store_id=NULL │
│ NOT inherited by stores │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ TIER 2: Store Defaults │
│ Default pages all stores inherit (about, terms, privacy) │
│ is_platform_page=FALSE, store_id=NULL │
│ Inherited by ALL stores on the platform │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ TIER 3: Store Overrides │
│ Custom pages created by individual stores │
│ is_platform_page=FALSE, store_id=<store_id> │
│ Overrides store defaults for specific store │
└─────────────────────────────────────────────────────────────────────┘
```
## Content Resolution Flow
When a customer visits a store page (e.g., `/stores/shopname/about`):
```
Customer visits: /stores/shopname/about
┌─────────────────────────────────────────────────────────────────────┐
│ Step 1: Check Store Override │
│ SELECT * FROM content_pages │
│ WHERE platform_id=1 AND store_id=123 AND slug='about' │
│ Found? → Return store's custom "About" page │
└─────────────────────────────────────────────────────────────────────┘
│ Not found
┌─────────────────────────────────────────────────────────────────────┐
│ Step 2: Check Store Default │
│ SELECT * FROM content_pages │
│ WHERE platform_id=1 AND store_id IS NULL │
│ AND is_platform_page=FALSE AND slug='about' │
│ Found? → Return platform's default "About" template │
└─────────────────────────────────────────────────────────────────────┘
│ Not found
Return 404
```
## Database Schema
### platforms
```sql
CREATE TABLE platforms (
id SERIAL PRIMARY KEY,
code VARCHAR(50) UNIQUE NOT NULL, -- 'oms', 'loyalty', 'sitebuilder'
name VARCHAR(100) NOT NULL, -- 'Order Management System'
description TEXT,
domain VARCHAR(255), -- 'oms.orion.lu'
path_prefix VARCHAR(50), -- '/oms'
logo VARCHAR(255),
logo_dark VARCHAR(255),
favicon VARCHAR(255),
theme_config JSONB DEFAULT '{}',
default_language VARCHAR(10) DEFAULT 'fr',
supported_languages JSONB DEFAULT '["fr", "de", "en"]',
is_active BOOLEAN DEFAULT TRUE,
is_public BOOLEAN DEFAULT TRUE,
settings JSONB DEFAULT '{}',
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
```
### store_platforms (Junction Table)
```sql
CREATE TABLE store_platforms (
store_id INTEGER REFERENCES stores(id) ON DELETE CASCADE,
platform_id INTEGER REFERENCES platforms(id) ON DELETE CASCADE,
joined_at TIMESTAMP DEFAULT NOW(),
is_active BOOLEAN DEFAULT TRUE,
settings JSONB DEFAULT '{}',
PRIMARY KEY (store_id, platform_id)
);
```
### content_pages (Extended)
```sql
ALTER TABLE content_pages ADD COLUMN platform_id INTEGER REFERENCES platforms(id);
ALTER TABLE content_pages ADD COLUMN is_platform_page BOOLEAN DEFAULT FALSE;
-- Platform marketing pages: is_platform_page=TRUE, store_id=NULL
-- Store defaults: is_platform_page=FALSE, store_id=NULL
-- Store overrides: is_platform_page=FALSE, store_id=<id>
```
## Request Flow
### URL Routing Structure
The system uses different URL patterns for development vs production:
**Development (localhost):**
- Main marketing site: `localhost:9999/` (no prefix) → `main` platform
- Platform sites: `localhost:9999/platforms/{code}/` → specific platform
**Production (custom domains):**
- Main marketing site: `orion.lu/``main` platform
- Platform sites: `omsflow.lu/`, `rewardflow.lu/` → specific platform
### Request Processing
```
Request: GET /platforms/oms/stores/shopname/about
┌─────────────────────────────────────────────────────────────────────┐
│ PlatformContextMiddleware │
│ - Detects platform from /platforms/{code}/ prefix or domain │
│ - Rewrites path: /platforms/oms/stores/shopname/about │
│ → /stores/shopname/about │
│ - Sets request.state.platform = Platform(code='oms') │
│ - Sets request.state.platform_clean_path = /stores/shopname/about │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ StoreContextMiddleware │
│ - Uses rewritten path for store detection │
│ - Sets request.state.store = Store(subdomain='shopname') │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ Route Handler (shop_pages.py) │
│ - Gets platform_id from request.state.platform │
│ - Calls content_page_service.get_page_for_store( │
│ platform_id=1, store_id=123, slug='about' │
│ ) │
│ - Service handles three-tier resolution │
└─────────────────────────────────────────────────────────────────────┘
```
### Main Marketing Site (No Platform Prefix)
For requests without the `/platforms/` prefix (e.g., `localhost:9999/about`):
```
Request: GET /about
┌─────────────────────────────────────────────────────────────────────┐
│ PlatformContextMiddleware │
│ - No /platforms/ prefix detected │
│ - Uses DEFAULT_PLATFORM_CODE = 'main' │
│ - Sets request.state.platform = Platform(code='main') │
│ - Path unchanged: /about │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ Route Handler (platform_pages.py) │
│ - Gets platform_id from request.state.platform (main) │
│ - Loads CMS content for main marketing site │
└─────────────────────────────────────────────────────────────────────┘
```
## Admin Interface
### Platform Management (`/admin/platforms`)
- Lists all platforms with statistics
- Shows store count, marketing pages, store defaults
- Links to platform detail and edit pages
### Content Pages (`/admin/content-pages`)
- Platform filter dropdown
- Four-tab view:
- **All Pages**: Complete list
- **Platform Marketing**: Public platform pages (is_platform_page=TRUE)
- **Store Defaults**: Inherited by stores (is_platform_page=FALSE, store_id=NULL)
- **Store Overrides**: Store-specific (store_id set)
- Color-coded tier badges:
- Blue: Platform Marketing
- Teal: Store Default
- Purple: Store Override
## API Endpoints
### Platform Management
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/v1/admin/platforms` | List all platforms |
| GET | `/api/v1/admin/platforms/{code}` | Get platform details |
| PUT | `/api/v1/admin/platforms/{code}` | Update platform |
| GET | `/api/v1/admin/platforms/{code}/stats` | Platform statistics |
### Content Pages
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/v1/admin/content-pages/` | List all pages (supports `platform` filter) |
| GET | `/api/v1/admin/content-pages/platform` | Platform default pages only |
| POST | `/api/v1/admin/content-pages/platform` | Create platform page |
| POST | `/api/v1/admin/content-pages/store` | Create store page |
## Key Files
### Models
- `models/database/platform.py` - Platform model
- `models/database/store_platform.py` - Junction table
- `models/database/content_page.py` - Extended with platform_id
### Middleware
- `middleware/platform_context.py` - Platform detection and context
### Services
- `app/services/content_page_service.py` - Three-tier content resolution
### Routes
- `app/routes/platform_pages.py` - Platform marketing pages
- `app/routes/shop_pages.py` - Store shop pages with inheritance
### Admin
- `app/api/v1/admin/platforms.py` - Platform management API
- `app/templates/admin/platforms.html` - Platform admin UI
- `static/admin/js/platforms.js` - Alpine.js component
## CMS Tier Limits (Subscription-Based)
| Tier | Total Pages | Custom Pages |
|------|-------------|--------------|
| Essential | 3 | 0 |
| Professional | 10 | 5 |
| Business | 30 | 20 |
| Enterprise | Unlimited | Unlimited |
## Adding a New Platform
1. Insert platform record:
```sql
INSERT INTO platforms (code, name, description, domain, path_prefix)
VALUES ('loyalty', 'Loyalty Program', 'Customer loyalty and rewards', 'rewardflow.lu', 'loyalty');
```
2. Create platform-specific content pages:
```sql
INSERT INTO content_pages (platform_id, slug, title, content, is_platform_page)
VALUES (2, 'home', 'Loyalty Program', '<h1>Welcome</h1>', TRUE);
```
3. Configure routing:
- **Development:** Access at `localhost:9999/platforms/loyalty/`
- **Production:** Access at `rewardflow.lu/`
- Platform detected automatically by `PlatformContextMiddleware`
- No additional route configuration needed
4. Assign stores to platform:
```sql
INSERT INTO store_platforms (store_id, platform_id)
VALUES (1, 2);
```
### Platform URL Summary
| Platform | Code | Dev URL | Prod URL |
|----------|------|---------|----------|
| Main Marketing | `main` | `localhost:9999/` | `orion.lu/` |
| OMS | `oms` | `localhost:9999/platforms/oms/` | `omsflow.lu/` |
| Loyalty | `loyalty` | `localhost:9999/platforms/loyalty/` | `rewardflow.lu/` |