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>
560 lines
15 KiB
Markdown
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
|