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>
This commit is contained in:
2026-01-27 22:41:19 +01:00
parent e3cab29c1a
commit 7dbdbd4c7e
6 changed files with 1583 additions and 8 deletions

View File

@@ -0,0 +1,483 @@
# 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:
```python
# 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.
```python
core_module = ModuleDefinition(
code="tenancy",
name="Platform Tenancy",
is_core=True, # Cannot be disabled
# ...
)
```
### Optional Modules
Can be enabled/disabled per platform.
```python
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.
```python
devtools_module = ModuleDefinition(
code="dev-tools",
name="Developer Tools",
is_internal=True, # Admin-only
# ...
)
```
## Module Dependencies
Declare dependencies using the `requires` field:
```python
# 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
```python
# 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
```python
# 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
```python
# 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:
```python
# 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
```python
# 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
```python
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
```python
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
```python
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
```python
# 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:
```python
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
## Related Documentation
- [Module System](../architecture/module-system.md) - Architecture overview
- [Menu Management](../architecture/menu-management.md) - Menu item integration
- [Database Migrations](migration/database-migrations.md) - Migration guide