Files
orion/docs/development/creating-modules.md
Samir Boulahtit 7dbdbd4c7e docs: add observability, creating modules guide, and unified migration plan
- Add observability framework documentation (health checks, metrics, Sentry)
- Add developer guide for creating modules
- Add comprehensive module migration plan with Celery task integration
- Update architecture overview with module system and observability sections
- Update module-system.md with links to new docs

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 22:41:19 +01:00

12 KiB

Creating Modules

This guide explains how to create new modules for the Wizamart platform, including both simple wrapper modules and self-contained modules with their own services, models, and migrations.

Module Types

Type Complexity Use Case
Wrapper Module Simple Groups existing routes and features under a toggleable module
Self-Contained Module Complex Full isolation with own services, models, templates, migrations

Quick Start: Wrapper Module

For a simple module that wraps existing functionality:

# app/modules/analytics/definition.py
from app.modules.base import ModuleDefinition
from models.database.admin_menu_config import FrontendType

analytics_module = ModuleDefinition(
    code="analytics",
    name="Analytics & Reports",
    description="Business analytics, reports, and dashboards",
    version="1.0.0",
    features=[
        "analytics_dashboard",
        "sales_reports",
        "customer_insights",
    ],
    menu_items={
        FrontendType.ADMIN: ["analytics-dashboard", "reports"],
        FrontendType.VENDOR: ["analytics", "sales-reports"],
    },
    is_core=False,
    is_internal=False,
)

Module Definition Fields

Required Fields

Field Type Description
code str Unique identifier (e.g., "billing", "analytics")
name str Display name (e.g., "Billing & Subscriptions")

Optional Fields

Field Type Default Description
description str "" What the module provides
version str "1.0.0" Semantic version
requires list[str] [] Module codes this depends on
features list[str] [] Feature codes for tier gating
menu_items dict {} Menu items per frontend type
permissions list[str] [] Permission codes defined
is_core bool False Cannot be disabled if True
is_internal bool False Admin-only if True

Configuration Fields

Field Type Description
config_schema type[BaseModel] Pydantic model for validation
default_config dict Default configuration values

Lifecycle Hooks

Field Signature Description
on_enable (platform_id: int) -> None Called when enabled
on_disable (platform_id: int) -> None Called when disabled
on_startup () -> None Called on app startup
health_check () -> dict Returns health status

Module Classification

Core Modules

Cannot be disabled. Use for essential platform functionality.

core_module = ModuleDefinition(
    code="tenancy",
    name="Platform Tenancy",
    is_core=True,  # Cannot be disabled
    # ...
)

Optional Modules

Can be enabled/disabled per platform.

billing_module = ModuleDefinition(
    code="billing",
    name="Billing",
    is_core=False,  # Can be toggled
    is_internal=False,
    # ...
)

Internal Modules

Admin-only tools not visible to customers/vendors.

devtools_module = ModuleDefinition(
    code="dev-tools",
    name="Developer Tools",
    is_internal=True,  # Admin-only
    # ...
)

Module Dependencies

Declare dependencies using the requires field:

# orders module requires payments
orders_module = ModuleDefinition(
    code="orders",
    name="Orders",
    requires=["payments"],  # Must have payments enabled
    # ...
)

Dependency Rules:

  1. Core modules cannot depend on optional modules
  2. Enabling a module auto-enables its dependencies
  3. Disabling a module auto-disables modules that depend on it
  4. Circular dependencies are not allowed

Self-Contained Module Structure

For modules with their own services, models, and templates:

app/modules/cms/
├── __init__.py
├── definition.py        # ModuleDefinition
├── config.py            # Configuration schema (optional)
├── exceptions.py        # Module-specific exceptions
├── routes/
│   ├── __init__.py
│   ├── admin.py         # Admin API routes
│   └── vendor.py        # Vendor API routes
├── services/
│   ├── __init__.py
│   └── content_service.py
├── models/
│   ├── __init__.py
│   └── content_page.py
├── schemas/
│   ├── __init__.py
│   └── content.py
├── templates/
│   ├── admin/
│   └── vendor/
├── migrations/
│   └── versions/
│       ├── cms_001_create_content_pages.py
│       └── cms_002_add_seo_fields.py
└── locales/
    ├── en.json
    └── fr.json

Self-Contained Definition

# app/modules/cms/definition.py
from pydantic import BaseModel, Field
from app.modules.base import ModuleDefinition
from models.database.admin_menu_config import FrontendType

class CMSConfig(BaseModel):
    """CMS module configuration."""
    max_pages_per_vendor: int = Field(default=100, ge=1, le=1000)
    enable_seo: bool = True
    default_language: str = "en"

cms_module = ModuleDefinition(
    # Identity
    code="cms",
    name="Content Management",
    description="Content pages, media library, and themes",
    version="1.0.0",

    # Classification
    is_core=True,
    is_self_contained=True,

    # Features
    features=[
        "content_pages",
        "media_library",
        "vendor_themes",
    ],

    # Menu items
    menu_items={
        FrontendType.ADMIN: ["content-pages", "vendor-themes"],
        FrontendType.VENDOR: ["content-pages", "media"],
    },

    # Configuration
    config_schema=CMSConfig,
    default_config={
        "max_pages_per_vendor": 100,
        "enable_seo": True,
    },

    # Self-contained paths
    services_path="app.modules.cms.services",
    models_path="app.modules.cms.models",
    schemas_path="app.modules.cms.schemas",
    templates_path="templates",
    exceptions_path="app.modules.cms.exceptions",
    locales_path="locales",
    migrations_path="migrations",

    # Lifecycle
    health_check=lambda: {"status": "healthy"},
)

Creating Module Routes

Admin Routes

# app/modules/payments/routes/admin.py
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.api.deps import get_db, get_current_admin_user

admin_router = APIRouter(prefix="/api/admin/payments", tags=["Admin - Payments"])

@admin_router.get("/gateways")
async def list_gateways(
    db: Session = Depends(get_db),
    current_user = Depends(get_current_admin_user),
):
    """List configured payment gateways."""
    # Implementation
    pass

Vendor Routes

# app/modules/payments/routes/vendor.py
from fastapi import APIRouter, Depends
from app.api.deps import get_db, get_current_vendor_user

vendor_router = APIRouter(prefix="/api/vendor/payments", tags=["Vendor - Payments"])

@vendor_router.get("/methods")
async def list_payment_methods(
    db: Session = Depends(get_db),
    current_user = Depends(get_current_vendor_user),
):
    """List vendor's stored payment methods."""
    pass

Lazy Router Loading

To avoid circular imports, use lazy loading:

# app/modules/payments/definition.py

def _get_admin_router():
    """Lazy import to avoid circular imports."""
    from app.modules.payments.routes.admin import admin_router
    return admin_router

def _get_vendor_router():
    from app.modules.payments.routes.vendor import vendor_router
    return vendor_router

payments_module = ModuleDefinition(
    code="payments",
    # ...
)

def get_payments_module_with_routers():
    """Get module with routers attached."""
    payments_module.admin_router = _get_admin_router()
    payments_module.vendor_router = _get_vendor_router()
    return payments_module

Module Migrations

Naming Convention

{module_code}_{sequence}_{description}.py

Examples:

  • cms_001_create_content_pages.py
  • cms_002_add_seo_fields.py
  • billing_001_create_subscriptions.py

Migration Template

# app/modules/cms/migrations/versions/cms_001_create_content_pages.py
"""Create content pages table.

Revision ID: cms_001
Revises:
Create Date: 2026-01-27

"""
from alembic import op
import sqlalchemy as sa

revision = "cms_001"
down_revision = None  # Or previous module migration
branch_labels = ("cms",)
depends_on = None

def upgrade() -> None:
    op.create_table(
        "cms_content_pages",
        sa.Column("id", sa.Integer(), primary_key=True),
        sa.Column("vendor_id", sa.Integer(), sa.ForeignKey("vendors.id")),
        sa.Column("title", sa.String(200), nullable=False),
        sa.Column("slug", sa.String(200), nullable=False),
        sa.Column("content", sa.Text()),
        sa.Column("created_at", sa.DateTime(timezone=True)),
        sa.Column("updated_at", sa.DateTime(timezone=True)),
    )

def downgrade() -> None:
    op.drop_table("cms_content_pages")

Module Configuration

Defining Configuration Schema

from pydantic import BaseModel, Field

class BillingConfig(BaseModel):
    """Billing module configuration."""

    stripe_mode: Literal["test", "live"] = "test"
    trial_days: int = Field(default=14, ge=0, le=365)
    enable_invoices: bool = True
    invoice_prefix: str = Field(default="INV-", max_length=10)

Using Configuration

from app.modules.service import module_service

# Get module config for platform
config = module_service.get_module_config(db, platform_id, "billing")
# Returns: {"stripe_mode": "test", "trial_days": 14, ...}

# Set module config
module_service.set_module_config(
    db, platform_id, "billing",
    {"trial_days": 30}
)

Health Checks

Defining Health Check

def check_billing_health() -> dict:
    """Check billing service dependencies."""
    issues = []

    # Check Stripe
    try:
        stripe.Account.retrieve()
    except stripe.AuthenticationError:
        issues.append("Stripe authentication failed")

    if issues:
        return {
            "status": "unhealthy",
            "message": "; ".join(issues),
        }

    return {
        "status": "healthy",
        "details": {"stripe": "connected"},
    }

billing_module = ModuleDefinition(
    code="billing",
    health_check=check_billing_health,
    # ...
)

Registering the Module

Add to Registry

# app/modules/registry.py

from app.modules.analytics.definition import analytics_module

OPTIONAL_MODULES: dict[str, ModuleDefinition] = {
    # ... existing modules
    "analytics": analytics_module,
}

MODULES = {**CORE_MODULES, **OPTIONAL_MODULES, **INTERNAL_MODULES}

Module Events

Subscribe to module lifecycle events:

from app.modules.events import module_event_bus, ModuleEvent, ModuleEventData

@module_event_bus.subscribe(ModuleEvent.ENABLED)
def on_analytics_enabled(data: ModuleEventData):
    if data.module_code == "analytics":
        # Initialize analytics for this platform
        setup_analytics_dashboard(data.platform_id)

@module_event_bus.subscribe(ModuleEvent.DISABLED)
def on_analytics_disabled(data: ModuleEventData):
    if data.module_code == "analytics":
        # Cleanup
        cleanup_analytics_data(data.platform_id)

Checklist

New Module Checklist

  • Create module directory: app/modules/{code}/
  • Create definition.py with ModuleDefinition
  • Add module to appropriate dict in registry.py
  • Create routes if needed (admin.py, vendor.py)
  • Register menu items in menu registry
  • Create migrations if adding database tables
  • Add health check if module has dependencies
  • Document features and configuration options
  • Write tests for module functionality

Self-Contained Module Checklist

  • All items from basic checklist
  • Create services/ directory with business logic
  • Create models/ directory with SQLAlchemy models
  • Create schemas/ directory with Pydantic schemas
  • Create exceptions.py for module-specific errors
  • Create config.py with Pydantic config model
  • Set up migrations/versions/ directory
  • Create templates/ if module has UI
  • Create locales/ if module needs translations
  • Set is_self_contained=True and path attributes