Files
orion/app/modules/tenancy/services/platform_service.py
Samir Boulahtit d7a0ff8818 refactor: complete module-driven architecture migration
This commit completes the migration to a fully module-driven architecture:

## Models Migration
- Moved all domain models from models/database/ to their respective modules:
  - tenancy: User, Admin, Vendor, Company, Platform, VendorDomain, etc.
  - cms: MediaFile, VendorTheme
  - messaging: Email, VendorEmailSettings, VendorEmailTemplate
  - core: AdminMenuConfig
- models/database/ now only contains Base and TimestampMixin (infrastructure)

## Schemas Migration
- Moved all domain schemas from models/schema/ to their respective modules:
  - tenancy: company, vendor, admin, team, vendor_domain
  - cms: media, image, vendor_theme
  - messaging: email
- models/schema/ now only contains base.py and auth.py (infrastructure)

## Routes Migration
- Moved admin routes from app/api/v1/admin/ to modules:
  - menu_config.py -> core module
  - modules.py -> tenancy module
  - module_config.py -> tenancy module
- app/api/v1/admin/ now only aggregates auto-discovered module routes

## Menu System
- Implemented module-driven menu system with MenuDiscoveryService
- Extended FrontendType enum: PLATFORM, ADMIN, VENDOR, STOREFRONT
- Added MenuItemDefinition and MenuSectionDefinition dataclasses
- Each module now defines its own menu items in definition.py
- MenuService integrates with MenuDiscoveryService for template rendering

## Documentation
- Updated docs/architecture/models-structure.md
- Updated docs/architecture/menu-management.md
- Updated architecture validation rules for new exceptions

## Architecture Validation
- Updated MOD-019 rule to allow base.py in models/schema/
- Created core module exceptions.py and schemas/ directory
- All validation errors resolved (only warnings remain)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 21:02:56 +01:00

311 lines
8.1 KiB
Python

# app/modules/tenancy/services/platform_service.py
"""
Platform Service
Business logic for platform management in the Multi-Platform CMS.
Platforms represent different business offerings (OMS, Loyalty, Site Builder, Main Marketing).
Each platform has its own:
- Marketing pages (homepage, pricing, features)
- Vendor defaults (about, terms, privacy)
- Configuration and branding
"""
import logging
from dataclasses import dataclass
from sqlalchemy import func
from sqlalchemy.orm import Session
from app.modules.tenancy.exceptions import (
PlatformNotFoundException,
)
from app.modules.cms.models import ContentPage
from app.modules.tenancy.models import Platform
from app.modules.tenancy.models import VendorPlatform
logger = logging.getLogger(__name__)
@dataclass
class PlatformStats:
"""Platform statistics."""
platform_id: int
platform_code: str
platform_name: str
vendor_count: int
platform_pages_count: int
vendor_defaults_count: int
vendor_overrides_count: int = 0
published_pages_count: int = 0
draft_pages_count: int = 0
class PlatformService:
"""Service for platform operations."""
@staticmethod
def get_platform_by_code(db: Session, code: str) -> Platform:
"""
Get platform by code.
Args:
db: Database session
code: Platform code (oms, loyalty, main, etc.)
Returns:
Platform object
Raises:
PlatformNotFoundException: If platform not found
"""
platform = db.query(Platform).filter(Platform.code == code).first()
if not platform:
raise PlatformNotFoundException(code)
return platform
@staticmethod
def get_platform_by_code_optional(db: Session, code: str) -> Platform | None:
"""
Get platform by code, returns None if not found.
Args:
db: Database session
code: Platform code
Returns:
Platform object or None
"""
return db.query(Platform).filter(Platform.code == code).first()
@staticmethod
def get_platform_by_id(db: Session, platform_id: int) -> Platform:
"""
Get platform by ID.
Args:
db: Database session
platform_id: Platform ID
Returns:
Platform object
Raises:
PlatformNotFoundException: If platform not found
"""
platform = db.query(Platform).filter(Platform.id == platform_id).first()
if not platform:
raise PlatformNotFoundException(str(platform_id))
return platform
@staticmethod
def list_platforms(
db: Session, include_inactive: bool = False
) -> list[Platform]:
"""
List all platforms.
Args:
db: Database session
include_inactive: Include inactive platforms
Returns:
List of Platform objects
"""
query = db.query(Platform)
if not include_inactive:
query = query.filter(Platform.is_active == True)
return query.order_by(Platform.id).all()
@staticmethod
def get_vendor_count(db: Session, platform_id: int) -> int:
"""
Get count of vendors on a platform.
Args:
db: Database session
platform_id: Platform ID
Returns:
Vendor count
"""
return (
db.query(func.count(VendorPlatform.vendor_id))
.filter(VendorPlatform.platform_id == platform_id)
.scalar()
or 0
)
@staticmethod
def get_platform_pages_count(db: Session, platform_id: int) -> int:
"""
Get count of platform marketing pages.
Args:
db: Database session
platform_id: Platform ID
Returns:
Platform pages count
"""
return (
db.query(func.count(ContentPage.id))
.filter(
ContentPage.platform_id == platform_id,
ContentPage.vendor_id == None,
ContentPage.is_platform_page == True,
)
.scalar()
or 0
)
@staticmethod
def get_vendor_defaults_count(db: Session, platform_id: int) -> int:
"""
Get count of vendor default pages.
Args:
db: Database session
platform_id: Platform ID
Returns:
Vendor defaults count
"""
return (
db.query(func.count(ContentPage.id))
.filter(
ContentPage.platform_id == platform_id,
ContentPage.vendor_id == None,
ContentPage.is_platform_page == False,
)
.scalar()
or 0
)
@staticmethod
def get_vendor_overrides_count(db: Session, platform_id: int) -> int:
"""
Get count of vendor override pages.
Args:
db: Database session
platform_id: Platform ID
Returns:
Vendor overrides count
"""
return (
db.query(func.count(ContentPage.id))
.filter(
ContentPage.platform_id == platform_id,
ContentPage.vendor_id != None,
)
.scalar()
or 0
)
@staticmethod
def get_published_pages_count(db: Session, platform_id: int) -> int:
"""
Get count of published pages on a platform.
Args:
db: Database session
platform_id: Platform ID
Returns:
Published pages count
"""
return (
db.query(func.count(ContentPage.id))
.filter(
ContentPage.platform_id == platform_id,
ContentPage.is_published == True,
)
.scalar()
or 0
)
@staticmethod
def get_draft_pages_count(db: Session, platform_id: int) -> int:
"""
Get count of draft pages on a platform.
Args:
db: Database session
platform_id: Platform ID
Returns:
Draft pages count
"""
return (
db.query(func.count(ContentPage.id))
.filter(
ContentPage.platform_id == platform_id,
ContentPage.is_published == False,
)
.scalar()
or 0
)
@classmethod
def get_platform_stats(cls, db: Session, platform: Platform) -> PlatformStats:
"""
Get comprehensive statistics for a platform.
Args:
db: Database session
platform: Platform object
Returns:
PlatformStats dataclass
"""
return PlatformStats(
platform_id=platform.id,
platform_code=platform.code,
platform_name=platform.name,
vendor_count=cls.get_vendor_count(db, platform.id),
platform_pages_count=cls.get_platform_pages_count(db, platform.id),
vendor_defaults_count=cls.get_vendor_defaults_count(db, platform.id),
vendor_overrides_count=cls.get_vendor_overrides_count(db, platform.id),
published_pages_count=cls.get_published_pages_count(db, platform.id),
draft_pages_count=cls.get_draft_pages_count(db, platform.id),
)
@staticmethod
def update_platform(
db: Session, platform: Platform, update_data: dict
) -> Platform:
"""
Update platform fields.
Note: This method does NOT commit the transaction.
The caller (API endpoint) is responsible for committing.
Args:
db: Database session
platform: Platform to update
update_data: Dictionary of fields to update
Returns:
Updated Platform object (with pending changes)
"""
for field, value in update_data.items():
if hasattr(platform, field):
setattr(platform, field, value)
logger.info(f"[PLATFORMS] Updated platform: {platform.code}")
return platform
# Singleton instance for convenience
platform_service = PlatformService()