feat: complete modular platform architecture (Phases 1-5)

Phase 1 - Vendor Router Integration:
- Wire up vendor module routers in app/api/v1/vendor/__init__.py
- Use lazy imports via __getattr__ to avoid circular dependencies

Phase 2 - Extract Remaining Modules:
- Create 6 new module directories: customers, cms, analytics, messaging,
  dev_tools, monitoring
- Each module has definition.py and route wrappers
- Update registry to import from extracted modules

Phase 3 - Database Table Migration:
- Add PlatformModule junction table for auditable module tracking
- Add migration zc2m3n4o5p6q7_add_platform_modules_table.py
- Add modules relationship to Platform model
- Update ModuleService with JSON-to-junction-table migration

Phase 4 - Module-Specific Configuration UI:
- Add /api/v1/admin/module-config/* endpoints
- Add module-config.html template and JS

Phase 5 - Integration Tests:
- Add tests/fixtures/module_fixtures.py
- Add tests/integration/api/v1/admin/test_modules.py
- Add tests/integration/api/v1/modules/test_module_access.py

Architecture fixes:
- Fix JS-003 errors: use ...data() directly in Alpine components
- Fix JS-005 warnings: add init() guards to prevent duplicate init
- Fix API-001 errors: add MenuActionResponse Pydantic model
- Add FE-008 noqa for dynamic number input in template

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-26 18:19:00 +01:00
parent f29f1113cd
commit c419090531
55 changed files with 4059 additions and 206 deletions

View File

@@ -61,6 +61,7 @@ from . import (
media,
menu_config,
messages,
module_config,
modules,
monitoring,
notifications,
@@ -80,8 +81,9 @@ from . import (
)
# Import extracted module routers
from app.modules.billing.routes import admin_router as billing_admin_router
from app.modules.inventory.routes import admin_router as inventory_admin_router
# NOTE: Import directly from admin.py files to avoid circular imports through __init__.py
from app.modules.billing.routes.admin import admin_router as billing_admin_router
from app.modules.inventory.routes.admin import admin_router as inventory_admin_router
from app.modules.orders.routes.admin import admin_router as orders_admin_router
from app.modules.orders.routes.admin import admin_exceptions_router as orders_exceptions_router
from app.modules.marketplace.routes.admin import admin_router as marketplace_admin_router
@@ -129,6 +131,9 @@ router.include_router(menu_config.router, tags=["admin-menu-config"])
# Include module management endpoints (super admin only)
router.include_router(modules.router, tags=["admin-modules"])
# Include module configuration endpoints (super admin only)
router.include_router(module_config.router, tags=["admin-module-config"])
# ============================================================================
# User Management

View File

@@ -0,0 +1,463 @@
# app/api/v1/admin/menu_config.py
"""
Admin API endpoints for Platform Menu Configuration.
Provides menu visibility configuration for admin and vendor frontends:
- GET /menu-config/platforms/{platform_id} - Get menu config for a platform
- PUT /menu-config/platforms/{platform_id} - Update menu visibility for a platform
- POST /menu-config/platforms/{platform_id}/reset - Reset to defaults
- GET /menu-config/user - Get current user's menu config (super admins)
- PUT /menu-config/user - Update current user's menu config (super admins)
- GET /menu/admin - Get rendered admin menu for current user
- GET /menu/vendor - Get rendered vendor menu for current platform
All configuration endpoints require super admin access.
Menu rendering endpoints require authenticated admin/vendor access.
"""
import logging
from typing import Any
from fastapi import APIRouter, Depends, Path, Query
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from app.api.deps import (
get_current_admin_from_cookie_or_header,
get_current_super_admin,
get_db,
)
from app.services.menu_service import MenuItemConfig, menu_service
from app.services.platform_service import platform_service
from models.database.admin_menu_config import FrontendType
from models.database.user import User
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/menu-config")
# =============================================================================
# Pydantic Schemas
# =============================================================================
class MenuItemResponse(BaseModel):
"""Menu item configuration response."""
id: str
label: str
icon: str
url: str
section_id: str
section_label: str | None = None
is_visible: bool = True
is_mandatory: bool = False
is_super_admin_only: bool = False
class MenuConfigResponse(BaseModel):
"""Menu configuration response for a platform or user."""
frontend_type: str
platform_id: int | None = None
user_id: int | None = None
items: list[MenuItemResponse]
total_items: int
visible_items: int
hidden_items: int
class MenuVisibilityUpdateRequest(BaseModel):
"""Request to update menu item visibility."""
menu_item_id: str = Field(..., description="Menu item ID to update")
is_visible: bool = Field(..., description="Whether the item should be visible")
class BulkMenuVisibilityUpdateRequest(BaseModel):
"""Request to update multiple menu items at once."""
visibility: dict[str, bool] = Field(
...,
description="Map of menu_item_id to is_visible",
examples=[{"inventory": False, "orders": True}],
)
class MenuSectionResponse(BaseModel):
"""Menu section for rendering."""
id: str
label: str | None = None
items: list[dict[str, Any]]
class RenderedMenuResponse(BaseModel):
"""Rendered menu for frontend."""
frontend_type: str
sections: list[MenuSectionResponse]
class MenuActionResponse(BaseModel):
"""Response for menu action operations (reset, show-all, etc.)."""
success: bool
message: str
# =============================================================================
# Helper Functions
# =============================================================================
def _build_menu_item_response(item: MenuItemConfig) -> MenuItemResponse:
"""Convert MenuItemConfig to API response."""
return MenuItemResponse(
id=item.id,
label=item.label,
icon=item.icon,
url=item.url,
section_id=item.section_id,
section_label=item.section_label,
is_visible=item.is_visible,
is_mandatory=item.is_mandatory,
is_super_admin_only=item.is_super_admin_only,
)
def _build_menu_config_response(
items: list[MenuItemConfig],
frontend_type: FrontendType,
platform_id: int | None = None,
user_id: int | None = None,
) -> MenuConfigResponse:
"""Build menu configuration response."""
item_responses = [_build_menu_item_response(item) for item in items]
visible_count = sum(1 for item in items if item.is_visible)
return MenuConfigResponse(
frontend_type=frontend_type.value,
platform_id=platform_id,
user_id=user_id,
items=item_responses,
total_items=len(items),
visible_items=visible_count,
hidden_items=len(items) - visible_count,
)
# =============================================================================
# Platform Menu Configuration (Super Admin Only)
# =============================================================================
@router.get("/platforms/{platform_id}", response_model=MenuConfigResponse)
async def get_platform_menu_config(
platform_id: int = Path(..., description="Platform ID"),
frontend_type: FrontendType = Query(
FrontendType.ADMIN, description="Frontend type (admin or vendor)"
),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_super_admin),
):
"""
Get menu configuration for a platform.
Returns all menu items with their visibility status for the specified
platform and frontend type. Super admin only.
"""
# Verify platform exists
platform = platform_service.get_platform_by_id(db, platform_id)
items = menu_service.get_platform_menu_config(db, frontend_type, platform_id)
logger.info(
f"[MENU_CONFIG] Super admin {current_user.email} fetched menu config "
f"for platform {platform.code} ({frontend_type.value})"
)
return _build_menu_config_response(items, frontend_type, platform_id=platform_id)
@router.put("/platforms/{platform_id}")
async def update_platform_menu_visibility(
update_data: MenuVisibilityUpdateRequest,
platform_id: int = Path(..., description="Platform ID"),
frontend_type: FrontendType = Query(
FrontendType.ADMIN, description="Frontend type (admin or vendor)"
),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_super_admin),
):
"""
Update visibility for a single menu item for a platform.
Super admin only. Cannot hide mandatory items.
"""
# Verify platform exists
platform = platform_service.get_platform_by_id(db, platform_id)
menu_service.update_menu_visibility(
db=db,
frontend_type=frontend_type,
menu_item_id=update_data.menu_item_id,
is_visible=update_data.is_visible,
platform_id=platform_id,
)
db.commit()
logger.info(
f"[MENU_CONFIG] Super admin {current_user.email} updated menu visibility: "
f"{update_data.menu_item_id}={update_data.is_visible} "
f"for platform {platform.code} ({frontend_type.value})"
)
return {"success": True, "message": "Menu visibility updated"}
@router.put("/platforms/{platform_id}/bulk")
async def bulk_update_platform_menu_visibility(
update_data: BulkMenuVisibilityUpdateRequest,
platform_id: int = Path(..., description="Platform ID"),
frontend_type: FrontendType = Query(
FrontendType.ADMIN, description="Frontend type (admin or vendor)"
),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_super_admin),
):
"""
Update visibility for multiple menu items at once.
Super admin only. Skips mandatory items silently.
"""
# Verify platform exists
platform = platform_service.get_platform_by_id(db, platform_id)
menu_service.bulk_update_menu_visibility(
db=db,
frontend_type=frontend_type,
visibility_map=update_data.visibility,
platform_id=platform_id,
)
db.commit()
logger.info(
f"[MENU_CONFIG] Super admin {current_user.email} bulk updated menu visibility: "
f"{len(update_data.visibility)} items for platform {platform.code} ({frontend_type.value})"
)
return {"success": True, "message": f"Updated {len(update_data.visibility)} menu items"}
@router.post("/platforms/{platform_id}/reset")
async def reset_platform_menu_config(
platform_id: int = Path(..., description="Platform ID"),
frontend_type: FrontendType = Query(
FrontendType.ADMIN, description="Frontend type (admin or vendor)"
),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_super_admin),
):
"""
Reset menu configuration for a platform to defaults.
Removes all visibility overrides, making all items visible.
Super admin only.
"""
# Verify platform exists
platform = platform_service.get_platform_by_id(db, platform_id)
menu_service.reset_platform_menu_config(db, frontend_type, platform_id)
db.commit()
logger.info(
f"[MENU_CONFIG] Super admin {current_user.email} reset menu config "
f"for platform {platform.code} ({frontend_type.value})"
)
return {"success": True, "message": "Menu configuration reset to defaults"}
# =============================================================================
# User Menu Configuration (Super Admin Only)
# =============================================================================
@router.get("/user", response_model=MenuConfigResponse)
async def get_user_menu_config(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_super_admin),
):
"""
Get the current super admin's personal menu configuration.
Only super admins can configure their own admin menu.
"""
items = menu_service.get_user_menu_config(db, current_user.id)
logger.info(
f"[MENU_CONFIG] Super admin {current_user.email} fetched their personal menu config"
)
return _build_menu_config_response(
items, FrontendType.ADMIN, user_id=current_user.id
)
@router.put("/user")
async def update_user_menu_visibility(
update_data: MenuVisibilityUpdateRequest,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_super_admin),
):
"""
Update visibility for a single menu item for the current super admin.
Super admin only. Cannot hide mandatory items.
"""
menu_service.update_menu_visibility(
db=db,
frontend_type=FrontendType.ADMIN,
menu_item_id=update_data.menu_item_id,
is_visible=update_data.is_visible,
user_id=current_user.id,
)
db.commit()
logger.info(
f"[MENU_CONFIG] Super admin {current_user.email} updated personal menu: "
f"{update_data.menu_item_id}={update_data.is_visible}"
)
return {"success": True, "message": "Menu visibility updated"}
@router.post("/user/reset", response_model=MenuActionResponse)
async def reset_user_menu_config(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_super_admin),
):
"""
Reset the current super admin's menu configuration (hide all except mandatory).
Super admin only.
"""
menu_service.reset_user_menu_config(db, current_user.id)
db.commit()
logger.info(
f"[MENU_CONFIG] Super admin {current_user.email} reset their personal menu config (hide all)"
)
return MenuActionResponse(success=True, message="Menu configuration reset - all items hidden")
@router.post("/user/show-all", response_model=MenuActionResponse)
async def show_all_user_menu_config(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_super_admin),
):
"""
Show all menu items for the current super admin.
Super admin only.
"""
menu_service.show_all_user_menu_config(db, current_user.id)
db.commit()
logger.info(
f"[MENU_CONFIG] Super admin {current_user.email} enabled all menu items"
)
return MenuActionResponse(success=True, message="All menu items are now visible")
@router.post("/platforms/{platform_id}/show-all")
async def show_all_platform_menu_config(
platform_id: int = Path(..., description="Platform ID"),
frontend_type: FrontendType = Query(
FrontendType.ADMIN, description="Frontend type (admin or vendor)"
),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_super_admin),
):
"""
Show all menu items for a platform.
Super admin only.
"""
# Verify platform exists
platform = platform_service.get_platform_by_id(db, platform_id)
menu_service.show_all_platform_menu_config(db, frontend_type, platform_id)
db.commit()
logger.info(
f"[MENU_CONFIG] Super admin {current_user.email} enabled all menu items "
f"for platform {platform.code} ({frontend_type.value})"
)
return {"success": True, "message": "All menu items are now visible"}
# =============================================================================
# Menu Rendering (For Sidebar)
# =============================================================================
@router.get("/render/admin", response_model=RenderedMenuResponse)
async def get_rendered_admin_menu(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_admin_from_cookie_or_header),
):
"""
Get the rendered admin menu for the current user.
Returns the filtered menu structure based on:
- Super admins: user-level config
- Platform admins: platform-level config
Used by the frontend to render the sidebar.
"""
if current_user.is_super_admin:
# Super admin: use user-level config
menu = menu_service.get_menu_for_rendering(
db=db,
frontend_type=FrontendType.ADMIN,
user_id=current_user.id,
is_super_admin=True,
)
else:
# Platform admin: use platform-level config
# Get the selected platform from the JWT token
platform_id = getattr(current_user, "token_platform_id", None)
# Fallback to first platform if no platform in token (shouldn't happen)
if platform_id is None and current_user.admin_platforms:
platform_id = current_user.admin_platforms[0].id
logger.warning(
f"[MENU_CONFIG] No platform_id in token for {current_user.email}, "
f"falling back to first platform: {platform_id}"
)
menu = menu_service.get_menu_for_rendering(
db=db,
frontend_type=FrontendType.ADMIN,
platform_id=platform_id,
is_super_admin=False,
)
sections = [
MenuSectionResponse(
id=section["id"],
label=section.get("label"),
items=section["items"],
)
for section in menu.get("sections", [])
]
return RenderedMenuResponse(
frontend_type=FrontendType.ADMIN.value,
sections=sections,
)

View File

@@ -0,0 +1,425 @@
# app/api/v1/admin/module_config.py
"""
Admin API endpoints for Module Configuration Management.
Provides per-module configuration for platforms:
- GET /module-config/platforms/{platform_id}/modules/{module_code}/config - Get module config
- PUT /module-config/platforms/{platform_id}/modules/{module_code}/config - Update module config
- GET /module-config/defaults/{module_code} - Get config defaults for a module
All endpoints require super admin access.
"""
import logging
from typing import Any
from fastapi import APIRouter, Depends, Path
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from app.api.deps import get_current_super_admin, get_db
from app.modules.registry import MODULES
from app.modules.service import module_service
from app.services.platform_service import platform_service
from models.database.user import User
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/module-config")
# =============================================================================
# Config Defaults per Module
# =============================================================================
# Default configuration options per module
MODULE_CONFIG_DEFAULTS: dict[str, dict[str, Any]] = {
"billing": {
"stripe_mode": "test",
"default_trial_days": 14,
"allow_free_tier": True,
},
"inventory": {
"low_stock_threshold": 10,
"enable_locations": False,
},
"orders": {
"require_payment": True,
"auto_archive_days": 90,
},
"marketplace": {
"sync_frequency_hours": 24,
"auto_import_products": False,
},
"customers": {
"enable_segmentation": True,
"marketing_consent_default": False,
},
"cms": {
"max_pages": 50,
"enable_seo": True,
},
"analytics": {
"data_retention_days": 365,
"enable_export": True,
},
"messaging": {
"enable_attachments": True,
"max_attachment_size_mb": 10,
},
"monitoring": {
"log_retention_days": 30,
"alert_email": "",
},
}
# Config option metadata for UI rendering
MODULE_CONFIG_SCHEMA: dict[str, list[dict[str, Any]]] = {
"billing": [
{
"key": "stripe_mode",
"label": "Stripe Mode",
"type": "select",
"options": ["test", "live"],
"description": "Use test or live Stripe API keys",
},
{
"key": "default_trial_days",
"label": "Default Trial Days",
"type": "number",
"min": 0,
"max": 90,
"description": "Number of trial days for new subscriptions",
},
{
"key": "allow_free_tier",
"label": "Allow Free Tier",
"type": "boolean",
"description": "Allow vendors to use free tier indefinitely",
},
],
"inventory": [
{
"key": "low_stock_threshold",
"label": "Low Stock Threshold",
"type": "number",
"min": 0,
"max": 1000,
"description": "Stock level below which low stock alerts trigger",
},
{
"key": "enable_locations",
"label": "Enable Locations",
"type": "boolean",
"description": "Enable multiple inventory locations",
},
],
"orders": [
{
"key": "require_payment",
"label": "Require Payment",
"type": "boolean",
"description": "Require payment before order confirmation",
},
{
"key": "auto_archive_days",
"label": "Auto Archive Days",
"type": "number",
"min": 30,
"max": 365,
"description": "Days after which completed orders are archived",
},
],
"marketplace": [
{
"key": "sync_frequency_hours",
"label": "Sync Frequency (hours)",
"type": "number",
"min": 1,
"max": 168,
"description": "How often to sync with external marketplaces",
},
{
"key": "auto_import_products",
"label": "Auto Import Products",
"type": "boolean",
"description": "Automatically import new products from marketplace",
},
],
"customers": [
{
"key": "enable_segmentation",
"label": "Enable Segmentation",
"type": "boolean",
"description": "Enable customer segmentation and tagging",
},
{
"key": "marketing_consent_default",
"label": "Marketing Consent Default",
"type": "boolean",
"description": "Default value for marketing consent checkbox",
},
],
"cms": [
{
"key": "max_pages",
"label": "Max Pages",
"type": "number",
"min": 1,
"max": 500,
"description": "Maximum number of content pages allowed",
},
{
"key": "enable_seo",
"label": "Enable SEO",
"type": "boolean",
"description": "Enable SEO fields for content pages",
},
],
"analytics": [
{
"key": "data_retention_days",
"label": "Data Retention (days)",
"type": "number",
"min": 30,
"max": 730,
"description": "How long to keep analytics data",
},
{
"key": "enable_export",
"label": "Enable Export",
"type": "boolean",
"description": "Allow exporting analytics data",
},
],
"messaging": [
{
"key": "enable_attachments",
"label": "Enable Attachments",
"type": "boolean",
"description": "Allow file attachments in messages",
},
{
"key": "max_attachment_size_mb",
"label": "Max Attachment Size (MB)",
"type": "number",
"min": 1,
"max": 50,
"description": "Maximum attachment file size in megabytes",
},
],
"monitoring": [
{
"key": "log_retention_days",
"label": "Log Retention (days)",
"type": "number",
"min": 7,
"max": 365,
"description": "How long to keep log files",
},
{
"key": "alert_email",
"label": "Alert Email",
"type": "string",
"description": "Email address for system alerts (blank to disable)",
},
],
}
# =============================================================================
# Pydantic Schemas
# =============================================================================
class ModuleConfigResponse(BaseModel):
"""Module configuration response."""
module_code: str
module_name: str
config: dict[str, Any]
schema_info: list[dict[str, Any]] = Field(default_factory=list)
defaults: dict[str, Any] = Field(default_factory=dict)
class UpdateConfigRequest(BaseModel):
"""Request to update module configuration."""
config: dict[str, Any] = Field(..., description="Configuration key-value pairs")
class ConfigDefaultsResponse(BaseModel):
"""Response for module config defaults."""
module_code: str
module_name: str
defaults: dict[str, Any]
schema_info: list[dict[str, Any]]
# =============================================================================
# API Endpoints
# =============================================================================
@router.get("/platforms/{platform_id}/modules/{module_code}/config", response_model=ModuleConfigResponse)
async def get_module_config(
platform_id: int = Path(..., description="Platform ID"),
module_code: str = Path(..., description="Module code"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_super_admin),
):
"""
Get configuration for a specific module on a platform.
Returns current config values merged with defaults.
Super admin only.
"""
from app.exceptions import BadRequestException
# Verify platform exists
platform = platform_service.get_platform_by_id(db, platform_id)
# Validate module code
if module_code not in MODULES:
raise BadRequestException(f"Unknown module: {module_code}")
module = MODULES[module_code]
# Get current config
current_config = module_service.get_module_config(db, platform_id, module_code)
# Merge with defaults
defaults = MODULE_CONFIG_DEFAULTS.get(module_code, {})
merged_config = {**defaults, **current_config}
logger.info(
f"[MODULE_CONFIG] Super admin {current_user.email} fetched config "
f"for module '{module_code}' on platform {platform.code}"
)
return ModuleConfigResponse(
module_code=module_code,
module_name=module.name,
config=merged_config,
schema_info=MODULE_CONFIG_SCHEMA.get(module_code, []),
defaults=defaults,
)
@router.put("/platforms/{platform_id}/modules/{module_code}/config", response_model=ModuleConfigResponse)
async def update_module_config(
update_data: UpdateConfigRequest,
platform_id: int = Path(..., description="Platform ID"),
module_code: str = Path(..., description="Module code"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_super_admin),
):
"""
Update configuration for a specific module on a platform.
Super admin only.
"""
from app.exceptions import BadRequestException
# Verify platform exists
platform = platform_service.get_platform_by_id(db, platform_id)
# Validate module code
if module_code not in MODULES:
raise BadRequestException(f"Unknown module: {module_code}")
module = MODULES[module_code]
# Update config
success = module_service.set_module_config(db, platform_id, module_code, update_data.config)
if success:
db.commit()
# Get updated config
current_config = module_service.get_module_config(db, platform_id, module_code)
defaults = MODULE_CONFIG_DEFAULTS.get(module_code, {})
merged_config = {**defaults, **current_config}
logger.info(
f"[MODULE_CONFIG] Super admin {current_user.email} updated config "
f"for module '{module_code}' on platform {platform.code}: {update_data.config}"
)
return ModuleConfigResponse(
module_code=module_code,
module_name=module.name,
config=merged_config,
schema_info=MODULE_CONFIG_SCHEMA.get(module_code, []),
defaults=defaults,
)
@router.get("/defaults/{module_code}", response_model=ConfigDefaultsResponse)
async def get_config_defaults(
module_code: str = Path(..., description="Module code"),
current_user: User = Depends(get_current_super_admin),
):
"""
Get default configuration for a module.
Returns the default config values and schema for a module.
Super admin only.
"""
from app.exceptions import BadRequestException
# Validate module code
if module_code not in MODULES:
raise BadRequestException(f"Unknown module: {module_code}")
module = MODULES[module_code]
logger.info(
f"[MODULE_CONFIG] Super admin {current_user.email} fetched defaults "
f"for module '{module_code}'"
)
return ConfigDefaultsResponse(
module_code=module_code,
module_name=module.name,
defaults=MODULE_CONFIG_DEFAULTS.get(module_code, {}),
schema_info=MODULE_CONFIG_SCHEMA.get(module_code, []),
)
@router.post("/platforms/{platform_id}/modules/{module_code}/reset")
async def reset_module_config(
platform_id: int = Path(..., description="Platform ID"),
module_code: str = Path(..., description="Module code"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_super_admin),
):
"""
Reset module configuration to defaults.
Super admin only.
"""
from app.exceptions import BadRequestException
# Verify platform exists
platform = platform_service.get_platform_by_id(db, platform_id)
# Validate module code
if module_code not in MODULES:
raise BadRequestException(f"Unknown module: {module_code}")
# Reset to defaults
defaults = MODULE_CONFIG_DEFAULTS.get(module_code, {})
success = module_service.set_module_config(db, platform_id, module_code, defaults)
if success:
db.commit()
logger.info(
f"[MODULE_CONFIG] Super admin {current_user.email} reset config "
f"for module '{module_code}' on platform {platform.code} to defaults"
)
return {
"success": success,
"message": f"Module '{module_code}' config reset to defaults",
"config": defaults,
}

View File

@@ -8,6 +8,23 @@ IMPORTANT:
- This router is for JSON API endpoints only
- HTML page routes are mounted separately in main.py at /vendor/*
- Do NOT include pages.router here - it causes route conflicts
MODULE SYSTEM:
Routes can be module-gated using require_module_access() dependency.
For multi-tenant apps, module enablement is checked at request time
based on platform context (not at route registration time).
Extracted modules (app/modules/{module}/routes/):
- billing: Subscription tiers, vendor billing, invoices
- inventory: Stock management, inventory tracking
- orders: Order management, fulfillment, exceptions
- marketplace: Letzshop integration, product sync
Module extraction pattern:
1. Create app/modules/{module}/ directory
2. Create routes/vendor.py with require_module_access("{module}") dependency
3. Import module router here and include it
4. Comment out legacy router include
"""
from fastapi import APIRouter
@@ -42,6 +59,15 @@ from . import (
usage,
)
# Import extracted module routers
# NOTE: Import directly from vendor.py files to avoid circular imports through __init__.py
from app.modules.billing.routes.vendor import vendor_router as billing_vendor_router
from app.modules.inventory.routes.vendor import vendor_router as inventory_vendor_router
from app.modules.orders.routes.vendor import vendor_router as orders_vendor_router
from app.modules.orders.routes.vendor import vendor_exceptions_router as orders_exceptions_router
from app.modules.marketplace.routes.vendor import vendor_router as marketplace_vendor_router
from app.modules.marketplace.routes.vendor import vendor_letzshop_router as letzshop_vendor_router
# Create vendor router
router = APIRouter()
@@ -67,14 +93,26 @@ router.include_router(onboarding.router, tags=["vendor-onboarding"])
# Business operations (with prefixes: /products/*, /orders/*, etc.)
router.include_router(products.router, tags=["vendor-products"])
router.include_router(orders.router, tags=["vendor-orders"])
router.include_router(order_item_exceptions.router, tags=["vendor-order-exceptions"])
# Include orders module router (with module access control)
router.include_router(orders_vendor_router, tags=["vendor-orders"])
router.include_router(orders_exceptions_router, tags=["vendor-order-exceptions"])
# Legacy: router.include_router(orders.router, tags=["vendor-orders"])
# Legacy: router.include_router(order_item_exceptions.router, tags=["vendor-order-exceptions"])
router.include_router(invoices.router, tags=["vendor-invoices"])
router.include_router(customers.router, tags=["vendor-customers"])
router.include_router(team.router, tags=["vendor-team"])
router.include_router(inventory.router, tags=["vendor-inventory"])
router.include_router(marketplace.router, tags=["vendor-marketplace"])
router.include_router(letzshop.router, tags=["vendor-letzshop"])
# Include inventory module router (with module access control)
router.include_router(inventory_vendor_router, tags=["vendor-inventory"])
# Legacy: router.include_router(inventory.router, tags=["vendor-inventory"])
# Include marketplace module router (with module access control)
router.include_router(marketplace_vendor_router, tags=["vendor-marketplace"])
router.include_router(letzshop_vendor_router, tags=["vendor-letzshop"])
# Legacy: router.include_router(marketplace.router, tags=["vendor-marketplace"])
# Legacy: router.include_router(letzshop.router, tags=["vendor-letzshop"])
# Services (with prefixes: /payments/*, /media/*, etc.)
router.include_router(payments.router, tags=["vendor-payments"])
@@ -82,7 +120,11 @@ router.include_router(media.router, tags=["vendor-media"])
router.include_router(notifications.router, tags=["vendor-notifications"])
router.include_router(messages.router, tags=["vendor-messages"])
router.include_router(analytics.router, tags=["vendor-analytics"])
router.include_router(billing.router, tags=["vendor-billing"])
# Include billing module router (with module access control)
router.include_router(billing_vendor_router, tags=["vendor-billing"])
# Legacy: router.include_router(billing.router, tags=["vendor-billing"])
router.include_router(features.router, tags=["vendor-features"])
router.include_router(usage.router, tags=["vendor-usage"])