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>
426 lines
13 KiB
Python
426 lines
13 KiB
Python
# 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,
|
|
}
|