# Creating Modules This guide explains how to create new **plug-and-play modules** for the Wizamart 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/vendor,static/vendor/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.VENDOR: ["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/vendor.py from fastapi import APIRouter, Depends from app.api.deps import get_current_vendor_api, get_db router = APIRouter() # MUST be named 'router' @router.get("") def get_mymodule_data(current_user=Depends(get_current_vendor_api)): return {"message": "Hello from mymodule"} ``` ```python # app/modules/mymodule/routes/pages/vendor.py from fastapi import APIRouter, Request, Path from fastapi.responses import HTMLResponse from app.api.deps import get_current_vendor_from_cookie_or_header, get_db, Depends from app.templates_config import templates router = APIRouter() # MUST be named 'router' @router.get("/{vendor_code}/mymodule", response_class=HTMLResponse) async def mymodule_page( request: Request, vendor_code: str = Path(...), current_user=Depends(get_current_vendor_from_cookie_or_header), db=Depends(get_db), ): return templates.TemplateResponse( "mymodule/vendor/index.html", {"request": request, "vendor_code": vendor_code}, ) ``` ### Step 4: Done! That's it! The framework automatically: - Discovers and registers the module - Mounts API routes at `/api/v1/vendor/mymodule` - Mounts page routes at `/vendor/{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 │ │ └── vendor.py # /api/v1/vendor/mymodule │ └── pages/ │ ├── __init__.py │ ├── admin.py # /admin/mymodule │ └── vendor.py # /vendor/{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 │ └── vendor/ │ └── index.html │ ├── static/ # Static files (auto-mounted) │ ├── admin/ │ │ └── js/ │ │ └── mymodule.js │ └── vendor/ │ └── 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 └── vendor/ └── index.html ``` Reference in code: ```python templates.TemplateResponse("mymodule/vendor/index.html", {...}) ``` ### Static File URLs Static files are mounted at `/static/modules/{module_code}/`: ```html ``` ### 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 vendors. ```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, vendor_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 WizamartException class MyModuleException(WizamartException): """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("vendor_id", sa.Integer(), sa.ForeignKey("vendors.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_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_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