# 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,docs}

# 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
│
├── docs/                    # Module documentation (source of truth)
│   ├── index.md             # Module overview (REQUIRED)
│   └── ...                  # Additional docs as needed
│
├── locales/                 # Translations (auto-loaded)
│   ├── en.json
│   ├── de.json
│   ├── fr.json
│   └── lb.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, lb)
- [ ] Create `config.py` if module needs environment settings (optional)
- [ ] Create `migrations/versions/` with `__init__.py` files if module has database tables
- [ ] Create `docs/index.md` with module overview (see [Module Documentation](module-documentation.md))
- [ ] Create symlink: `ln -s ../../app/modules/$MODULE_NAME/docs docs/modules/$MODULE_NAME`
- [ ] 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
- [Module Documentation](module-documentation.md) - Documentation standards
- [Menu Management](../architecture/menu-management.md) - Sidebar integration
- [Architecture Rules](architecture-rules.md) - Validation rules
