Files
orion/docs/development/creating-modules.md
Samir Boulahtit e9253fbd84 refactor: rename Wizamart to Orion across entire codebase
Replace all ~1,086 occurrences of Wizamart/wizamart/WIZAMART/WizaMart
with Orion/orion/ORION across 184 files. This includes database
identifiers, email addresses, domain references, R2 bucket names,
DNS prefixes, encryption salt, Celery app name, config defaults,
Docker configs, CI configs, documentation, seed data, and templates.

Renames homepage-wizamart.html template to homepage-orion.html.
Fixes duplicate file_pattern key in api.yaml architecture rule.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 16:46:56 +01:00

560 lines
15 KiB
Markdown

# Creating Modules
This guide explains how to create new **plug-and-play modules** for the Orion platform. Modules are fully self-contained and automatically discovered - no changes to framework files required.
## Quick Start (5 Minutes)
Creating a new module requires **zero changes** to `main.py`, `registry.py`, or any other framework file.
### Step 1: Create Directory Structure
```bash
# Create module with all directories
MODULE_NAME=mymodule
mkdir -p app/modules/$MODULE_NAME/{routes/{api,pages},services,models,schemas,templates/$MODULE_NAME/store,static/store/js,locales,tasks}
# Create required files
touch app/modules/$MODULE_NAME/__init__.py
touch app/modules/$MODULE_NAME/definition.py
touch app/modules/$MODULE_NAME/exceptions.py
touch app/modules/$MODULE_NAME/routes/__init__.py
touch app/modules/$MODULE_NAME/routes/api/__init__.py
touch app/modules/$MODULE_NAME/routes/pages/__init__.py
touch app/modules/$MODULE_NAME/services/__init__.py
touch app/modules/$MODULE_NAME/models/__init__.py
touch app/modules/$MODULE_NAME/schemas/__init__.py
touch app/modules/$MODULE_NAME/tasks/__init__.py
```
### Step 2: Create Module Definition
```python
# app/modules/mymodule/definition.py
from app.modules.base import ModuleDefinition
from models.database.admin_menu_config import FrontendType
mymodule_module = ModuleDefinition(
code="mymodule",
name="My Module",
description="Description of what this module does",
version="1.0.0",
# Classification
is_core=False, # True = always enabled
is_internal=False, # True = admin-only
is_self_contained=True,
# Features for tier-based gating
features=["mymodule_feature"],
# Menu items
menu_items={
FrontendType.STORE: ["mymodule"],
},
# Paths (for self-contained modules)
services_path="app.modules.mymodule.services",
models_path="app.modules.mymodule.models",
schemas_path="app.modules.mymodule.schemas",
exceptions_path="app.modules.mymodule.exceptions",
templates_path="templates",
locales_path="locales",
)
```
### Step 3: Create Routes (Auto-Discovered)
```python
# app/modules/mymodule/routes/api/store.py
from fastapi import APIRouter, Depends
from app.api.deps import get_current_store_api, get_db
router = APIRouter() # MUST be named 'router'
@router.get("")
def get_mymodule_data(current_user=Depends(get_current_store_api)):
return {"message": "Hello from mymodule"}
```
```python
# app/modules/mymodule/routes/pages/store.py
from fastapi import APIRouter, Request, Path
from fastapi.responses import HTMLResponse
from app.api.deps import get_current_store_from_cookie_or_header, get_db, Depends
from app.templates_config import templates
router = APIRouter() # MUST be named 'router'
@router.get("/{store_code}/mymodule", response_class=HTMLResponse)
async def mymodule_page(
request: Request,
store_code: str = Path(...),
current_user=Depends(get_current_store_from_cookie_or_header),
db=Depends(get_db),
):
return templates.TemplateResponse(
"mymodule/store/index.html",
{"request": request, "store_code": store_code},
)
```
### Step 4: Done!
That's it! The framework automatically:
- Discovers and registers the module
- Mounts API routes at `/api/v1/store/mymodule`
- Mounts page routes at `/store/{code}/mymodule`
- Loads templates from `templates/`
- Mounts static files at `/static/modules/mymodule/`
- Loads translations from `locales/`
## Complete Module Structure
```
app/modules/mymodule/
├── __init__.py # Package marker
├── definition.py # ModuleDefinition (REQUIRED)
├── config.py # Environment config (optional, auto-discovered)
├── exceptions.py # Module exceptions
├── routes/ # Auto-discovered routes
│ ├── __init__.py
│ ├── api/
│ │ ├── __init__.py
│ │ ├── admin.py # /api/v1/admin/mymodule
│ │ └── store.py # /api/v1/store/mymodule
│ └── pages/
│ ├── __init__.py
│ ├── admin.py # /admin/mymodule
│ └── store.py # /store/{code}/mymodule
├── services/ # Business logic
│ ├── __init__.py
│ └── mymodule_service.py
├── models/ # SQLAlchemy models
│ ├── __init__.py
│ └── mymodel.py
├── schemas/ # Pydantic schemas
│ ├── __init__.py
│ └── myschema.py
├── templates/ # Jinja2 templates (auto-loaded)
│ └── mymodule/
│ ├── admin/
│ │ └── index.html
│ └── store/
│ └── index.html
├── static/ # Static files (auto-mounted)
│ ├── admin/
│ │ └── js/
│ │ └── mymodule.js
│ └── store/
│ └── js/
│ └── mymodule.js
├── locales/ # Translations (auto-loaded)
│ ├── en.json
│ ├── de.json
│ ├── fr.json
│ └── lu.json
├── tasks/ # Celery tasks (auto-discovered)
│ ├── __init__.py # REQUIRED for discovery
│ └── background_tasks.py
└── migrations/ # Alembic migrations (auto-discovered)
├── __init__.py # REQUIRED for discovery
└── versions/
├── __init__.py # REQUIRED for discovery
└── mymodule_001_initial.py
```
## Auto-Discovery Details
### What Gets Auto-Discovered
| Component | Location | Discovered By | Notes |
|-----------|----------|---------------|-------|
| Module Definition | `definition.py` | `app/modules/discovery.py` | Must export ModuleDefinition |
| Configuration | `config.py` | `app/modules/config.py` | Must export `config` or `config_class` |
| API Routes | `routes/api/*.py` | `app/modules/routes.py` | Must export `router` |
| Page Routes | `routes/pages/*.py` | `app/modules/routes.py` | Must export `router` |
| Templates | `templates/` | `app/templates_config.py` | Use namespace prefix |
| Static Files | `static/` | `main.py` | Mounted at `/static/modules/{code}/` |
| Locales | `locales/*.json` | `app/utils/i18n.py` | Keyed by module code |
| Tasks | `tasks/` | `app/modules/tasks.py` | Needs `__init__.py` |
| Migrations | `migrations/versions/` | `app/modules/migrations.py` | Use naming convention |
### Route File Requirements
Route files MUST export a `router` variable:
```python
# CORRECT - will be discovered
router = APIRouter()
@router.get("/")
def my_endpoint():
pass
# WRONG - won't be discovered
my_router = APIRouter() # Variable not named 'router'
```
### Template Namespacing
Templates must be namespaced under the module code:
```
templates/
└── mymodule/ # Module code as namespace
└── store/
└── index.html
```
Reference in code:
```python
templates.TemplateResponse("mymodule/store/index.html", {...})
```
### Static File URLs
Static files are mounted at `/static/modules/{module_code}/`:
```html
<!-- In template -->
<script src="{{ url_for('mymodule_static', path='store/js/mymodule.js') }}"></script>
```
### Translation Keys
Translations are namespaced under the module code:
```json
// locales/en.json
{
"mymodule": {
"title": "My Module",
"description": "Module description"
}
}
```
Use in templates:
```html
{{ _('mymodule.title') }}
```
## Module Classification
### Core Modules
Always enabled, cannot be disabled.
```python
ModuleDefinition(
code="mymodule",
is_core=True,
# ...
)
```
### Optional Modules (Default)
Can be enabled/disabled per platform.
```python
ModuleDefinition(
code="mymodule",
is_core=False,
is_internal=False,
# ...
)
```
### Internal Modules
Admin-only tools, not visible to stores.
```python
ModuleDefinition(
code="mymodule",
is_internal=True,
# ...
)
```
## Module Dependencies
Declare dependencies in the definition:
```python
ModuleDefinition(
code="orders",
requires=["payments"], # Must have payments enabled
# ...
)
```
**Rules:**
- Enabling a module auto-enables its dependencies
- Disabling a module auto-disables modules that depend on it
- Core modules cannot depend on optional modules
## Services Pattern
```python
# app/modules/mymodule/services/mymodule_service.py
from sqlalchemy.orm import Session
class MyModuleService:
def get_data(self, db: Session, store_id: int):
# Business logic here
pass
# Singleton instance
mymodule_service = MyModuleService()
```
```python
# app/modules/mymodule/services/__init__.py
from app.modules.mymodule.services.mymodule_service import (
mymodule_service,
MyModuleService,
)
__all__ = ["mymodule_service", "MyModuleService"]
```
## Exceptions Pattern
```python
# app/modules/mymodule/exceptions.py
from app.exceptions import OrionException
class MyModuleException(OrionException):
"""Base exception for mymodule."""
pass
class MyModuleNotFoundError(MyModuleException):
"""Resource not found."""
def __init__(self, resource_id: int):
super().__init__(f"Resource {resource_id} not found")
```
## Configuration Pattern
Modules can have their own environment-based configuration using Pydantic Settings.
```python
# app/modules/mymodule/config.py
from pydantic import Field
from pydantic_settings import BaseSettings
class MyModuleConfig(BaseSettings):
"""Configuration for mymodule."""
# Settings are loaded from environment with MYMODULE_ prefix
api_timeout: int = Field(default=30, description="API timeout in seconds")
max_retries: int = Field(default=3, description="Maximum retry attempts")
batch_size: int = Field(default=100, description="Batch processing size")
enable_feature_x: bool = Field(default=False, description="Enable feature X")
model_config = {"env_prefix": "MYMODULE_"}
# Export for auto-discovery
config_class = MyModuleConfig
config = MyModuleConfig()
```
Access configuration in your code:
```python
# In services or routes
from app.modules.config import get_module_config
mymodule_config = get_module_config("mymodule")
timeout = mymodule_config.api_timeout
```
Or import directly:
```python
from app.modules.mymodule.config import config
timeout = config.api_timeout
```
Environment variables:
```bash
# .env
MYMODULE_API_TIMEOUT=60
MYMODULE_MAX_RETRIES=5
MYMODULE_ENABLE_FEATURE_X=true
```
## Background Tasks
```python
# app/modules/mymodule/tasks/__init__.py
from app.modules.mymodule.tasks.background import process_data
__all__ = ["process_data"]
```
```python
# app/modules/mymodule/tasks/background.py
from app.modules.task_base import DatabaseTask
from celery_config import celery_app
@celery_app.task(base=DatabaseTask, bind=True)
def process_data(self, data_id: int):
"""Process data in background."""
db = self.get_db()
# Use db session
pass
```
## Migrations
Modules can have their own migrations that are auto-discovered by Alembic.
### Directory Structure
```
app/modules/mymodule/migrations/
├── __init__.py # REQUIRED for discovery
└── versions/
├── __init__.py # REQUIRED for discovery
├── mymodule_001_initial.py
└── mymodule_002_add_feature.py
```
### Naming Convention
```
{module_code}_{sequence}_{description}.py
```
Example: `mymodule_001_create_tables.py`
### Creating the Migrations Directory
```bash
# Create migrations directory structure
mkdir -p app/modules/mymodule/migrations/versions
# Create required __init__.py files
echo '"""Module migrations."""' > app/modules/mymodule/migrations/__init__.py
echo '"""Migration versions."""' > app/modules/mymodule/migrations/versions/__init__.py
```
### Migration Template
```python
# app/modules/mymodule/migrations/versions/mymodule_001_create_tables.py
"""Create mymodule tables.
Revision ID: mymodule_001
Create Date: 2026-01-28
"""
from alembic import op
import sqlalchemy as sa
revision = "mymodule_001"
down_revision = None
branch_labels = ("mymodule",)
def upgrade() -> None:
op.create_table(
"mymodule_items",
sa.Column("id", sa.Integer(), primary_key=True),
sa.Column("store_id", sa.Integer(), sa.ForeignKey("stores.id")),
sa.Column("name", sa.String(200), nullable=False),
)
def downgrade() -> None:
op.drop_table("mymodule_items")
```
### Running Module Migrations
Module migrations are automatically discovered. Run all migrations:
```bash
alembic upgrade head
```
The `alembic/env.py` automatically discovers module migrations via `app/modules/migrations.py`.
### Note on Existing Modules
Existing modules (CMS, billing, marketplace, etc.) currently have their migrations in the central `alembic/versions/` directory. Moving these to module-specific directories is an optional migration task that can be done incrementally.
**New modules should create their own `migrations/versions/` directory.**
## Validation
Run the architecture validator to check your module:
```bash
python scripts/validate/validate_architecture.py
```
### Validation Rules
| Rule | What It Checks |
|------|----------------|
| MOD-001 | Required directories exist |
| MOD-002 | Services contain actual code |
| MOD-003 | Schemas contain actual code |
| MOD-004 | Routes import from module |
| MOD-005 | Templates and static exist |
| MOD-006 | Locales directory exists |
| MOD-007 | Definition paths are valid |
| MOD-008 | exceptions.py exists |
| MOD-009 | definition.py exists |
| MOD-010 | Routes export `router` |
| MOD-011 | Tasks has `__init__.py` |
| MOD-012 | All locale files exist |
| MOD-013 | config.py exports config |
| MOD-014 | Migrations follow naming convention |
| MOD-015 | Migrations have `__init__.py` |
## Checklist
### New Module Checklist
- [ ] Create module directory with structure
- [ ] Create `definition.py` with ModuleDefinition
- [ ] Set `is_self_contained=True`
- [ ] Create `__init__.py` in all directories
- [ ] Create `exceptions.py`
- [ ] Create routes with `router` variable
- [ ] Create templates with namespace prefix
- [ ] Create locales (en, de, fr, lu)
- [ ] Create `config.py` if module needs environment settings (optional)
- [ ] Create `migrations/versions/` with `__init__.py` files if module has database tables
- [ ] Run `python scripts/validate/validate_architecture.py`
- [ ] Test routes are accessible
### No Framework Changes Needed
You do NOT need to:
- Edit `main.py`
- Edit `registry.py`
- Edit any configuration files
- Register routes manually
- Import the module anywhere
The framework discovers everything automatically!
## Related Documentation
- [Module System](../architecture/module-system.md) - Architecture overview
- [Menu Management](../architecture/menu-management.md) - Sidebar integration
- [Architecture Rules](architecture-rules.md) - Validation rules