diff --git a/app/api/v1/admin/platforms.py b/app/api/v1/admin/platforms.py index b0491265..bae6e1e5 100644 --- a/app/api/v1/admin/platforms.py +++ b/app/api/v1/admin/platforms.py @@ -17,16 +17,13 @@ Platforms are business offerings (OMS, Loyalty, Site Builder) with their own: import logging from typing import Any -from fastapi import APIRouter, Depends, HTTPException, Path, Query +from fastapi import APIRouter, Depends, Path, Query from pydantic import BaseModel, Field -from sqlalchemy import func from sqlalchemy.orm import Session from app.api.deps import get_current_admin_from_cookie_or_header, get_db -from models.database.content_page import ContentPage -from models.database.platform import Platform +from app.services.platform_service import platform_service from models.database.user import User -from models.database.vendor_platform import VendorPlatform logger = logging.getLogger(__name__) router = APIRouter(prefix="/platforms") @@ -107,140 +104,12 @@ class PlatformStatsResponse(BaseModel): # ============================================================================= -# API Endpoints +# Helper Functions # ============================================================================= -@router.get("", response_model=PlatformListResponse) -async def list_platforms( - db: Session = Depends(get_db), - current_user: User = Depends(get_current_admin_from_cookie_or_header), - include_inactive: bool = Query(False, description="Include inactive platforms"), -): - """ - List all platforms with their statistics. - - Returns all platforms (OMS, Loyalty, etc.) with vendor counts and page counts. - """ - query = db.query(Platform) - - if not include_inactive: - query = query.filter(Platform.is_active == True) - - platforms = query.order_by(Platform.id).all() - - # Build response with computed fields - result = [] - for platform in platforms: - # Count vendors on this platform - vendor_count = ( - db.query(func.count(VendorPlatform.vendor_id)) - .filter(VendorPlatform.platform_id == platform.id) - .scalar() - or 0 - ) - - # Count platform marketing pages - platform_pages_count = ( - db.query(func.count(ContentPage.id)) - .filter( - ContentPage.platform_id == platform.id, - ContentPage.vendor_id == None, - ContentPage.is_platform_page == True, - ) - .scalar() - or 0 - ) - - # Count vendor default pages - vendor_defaults_count = ( - db.query(func.count(ContentPage.id)) - .filter( - ContentPage.platform_id == platform.id, - ContentPage.vendor_id == None, - ContentPage.is_platform_page == False, - ) - .scalar() - or 0 - ) - - platform_data = PlatformResponse( - id=platform.id, - code=platform.code, - name=platform.name, - description=platform.description, - domain=platform.domain, - path_prefix=platform.path_prefix, - logo=platform.logo, - logo_dark=platform.logo_dark, - favicon=platform.favicon, - theme_config=platform.theme_config or {}, - default_language=platform.default_language, - supported_languages=platform.supported_languages or ["fr", "de", "en"], - is_active=platform.is_active, - is_public=platform.is_public, - settings=platform.settings or {}, - created_at=platform.created_at.isoformat(), - updated_at=platform.updated_at.isoformat(), - vendor_count=vendor_count, - platform_pages_count=platform_pages_count, - vendor_defaults_count=vendor_defaults_count, - ) - result.append(platform_data) - - logger.info(f"[PLATFORMS] Listed {len(result)} platforms") - - return PlatformListResponse(platforms=result, total=len(result)) - - -@router.get("/{code}", response_model=PlatformResponse) -async def get_platform( - code: str = Path(..., description="Platform code (oms, loyalty, etc.)"), - db: Session = Depends(get_db), - current_user: User = Depends(get_current_admin_from_cookie_or_header), -): - """ - Get platform details by code. - - Returns full platform configuration including statistics. - """ - platform = db.query(Platform).filter(Platform.code == code).first() - - if not platform: - raise HTTPException(status_code=404, detail=f"Platform not found: {code}") - - # Count vendors on this platform - vendor_count = ( - db.query(func.count(VendorPlatform.vendor_id)) - .filter(VendorPlatform.platform_id == platform.id) - .scalar() - or 0 - ) - - # Count platform marketing pages - platform_pages_count = ( - db.query(func.count(ContentPage.id)) - .filter( - ContentPage.platform_id == platform.id, - ContentPage.vendor_id == None, - ContentPage.is_platform_page == True, - ) - .scalar() - or 0 - ) - - # Count vendor default pages - vendor_defaults_count = ( - db.query(func.count(ContentPage.id)) - .filter( - ContentPage.platform_id == platform.id, - ContentPage.vendor_id == None, - ContentPage.is_platform_page == False, - ) - .scalar() - or 0 - ) - +def _build_platform_response(db: Session, platform) -> PlatformResponse: + """Build PlatformResponse from Platform model with computed fields.""" return PlatformResponse( id=platform.id, code=platform.code, @@ -259,12 +128,52 @@ async def get_platform( settings=platform.settings or {}, created_at=platform.created_at.isoformat(), updated_at=platform.updated_at.isoformat(), - vendor_count=vendor_count, - platform_pages_count=platform_pages_count, - vendor_defaults_count=vendor_defaults_count, + vendor_count=platform_service.get_vendor_count(db, platform.id), + platform_pages_count=platform_service.get_platform_pages_count(db, platform.id), + vendor_defaults_count=platform_service.get_vendor_defaults_count(db, platform.id), ) +# ============================================================================= +# API Endpoints +# ============================================================================= + + +@router.get("", response_model=PlatformListResponse) +async def list_platforms( + db: Session = Depends(get_db), + current_user: User = Depends(get_current_admin_from_cookie_or_header), + include_inactive: bool = Query(False, description="Include inactive platforms"), +): + """ + List all platforms with their statistics. + + Returns all platforms (OMS, Loyalty, etc.) with vendor counts and page counts. + """ + platforms = platform_service.list_platforms(db, include_inactive=include_inactive) + + result = [_build_platform_response(db, platform) for platform in platforms] + + logger.info(f"[PLATFORMS] Listed {len(result)} platforms") + + return PlatformListResponse(platforms=result, total=len(result)) + + +@router.get("/{code}", response_model=PlatformResponse) +async def get_platform( + code: str = Path(..., description="Platform code (oms, loyalty, etc.)"), + db: Session = Depends(get_db), + current_user: User = Depends(get_current_admin_from_cookie_or_header), +): + """ + Get platform details by code. + + Returns full platform configuration including statistics. + """ + platform = platform_service.get_platform_by_code(db, code) + return _build_platform_response(db, platform) + + @router.put("/{code}", response_model=PlatformResponse) async def update_platform( update_data: PlatformUpdateRequest, @@ -277,24 +186,15 @@ async def update_platform( Allows updating name, description, branding, and configuration. """ - platform = db.query(Platform).filter(Platform.code == code).first() + platform = platform_service.get_platform_by_code(db, code) - if not platform: - raise HTTPException(status_code=404, detail=f"Platform not found: {code}") - - # Update fields if provided update_dict = update_data.model_dump(exclude_unset=True) - for field, value in update_dict.items(): - if hasattr(platform, field): - setattr(platform, field, value) + platform = platform_service.update_platform(db, platform, update_dict) db.commit() db.refresh(platform) - logger.info(f"[PLATFORMS] Updated platform: {code}") - - # Return updated platform with stats - return await get_platform(code=code, db=db, current_user=current_user) + return _build_platform_response(db, platform) @router.get("/{code}/stats", response_model=PlatformStatsResponse) @@ -308,84 +208,17 @@ async def get_platform_stats( Returns counts for vendors, pages, and content breakdown. """ - platform = db.query(Platform).filter(Platform.code == code).first() - - if not platform: - raise HTTPException(status_code=404, detail=f"Platform not found: {code}") - - # Count vendors - vendor_count = ( - db.query(func.count(VendorPlatform.vendor_id)) - .filter(VendorPlatform.platform_id == platform.id) - .scalar() - or 0 - ) - - # Count platform marketing pages - platform_pages_count = ( - db.query(func.count(ContentPage.id)) - .filter( - ContentPage.platform_id == platform.id, - ContentPage.vendor_id == None, - ContentPage.is_platform_page == True, - ) - .scalar() - or 0 - ) - - # Count vendor default pages - vendor_defaults_count = ( - db.query(func.count(ContentPage.id)) - .filter( - ContentPage.platform_id == platform.id, - ContentPage.vendor_id == None, - ContentPage.is_platform_page == False, - ) - .scalar() - or 0 - ) - - # Count vendor override pages - vendor_overrides_count = ( - db.query(func.count(ContentPage.id)) - .filter( - ContentPage.platform_id == platform.id, - ContentPage.vendor_id != None, - ) - .scalar() - or 0 - ) - - # Count published pages - published_pages_count = ( - db.query(func.count(ContentPage.id)) - .filter( - ContentPage.platform_id == platform.id, - ContentPage.is_published == True, - ) - .scalar() - or 0 - ) - - # Count draft pages - draft_pages_count = ( - db.query(func.count(ContentPage.id)) - .filter( - ContentPage.platform_id == platform.id, - ContentPage.is_published == False, - ) - .scalar() - or 0 - ) + platform = platform_service.get_platform_by_code(db, code) + stats = platform_service.get_platform_stats(db, platform) return PlatformStatsResponse( - platform_id=platform.id, - platform_code=platform.code, - platform_name=platform.name, - vendor_count=vendor_count, - platform_pages_count=platform_pages_count, - vendor_defaults_count=vendor_defaults_count, - vendor_overrides_count=vendor_overrides_count, - published_pages_count=published_pages_count, - draft_pages_count=draft_pages_count, + platform_id=stats.platform_id, + platform_code=stats.platform_code, + platform_name=stats.platform_name, + vendor_count=stats.vendor_count, + platform_pages_count=stats.platform_pages_count, + vendor_defaults_count=stats.vendor_defaults_count, + vendor_overrides_count=stats.vendor_overrides_count, + published_pages_count=stats.published_pages_count, + draft_pages_count=stats.draft_pages_count, ) diff --git a/app/api/v1/vendor/content_pages.py b/app/api/v1/vendor/content_pages.py index 1d22ce49..a833c7ca 100644 --- a/app/api/v1/vendor/content_pages.py +++ b/app/api/v1/vendor/content_pages.py @@ -19,8 +19,11 @@ from sqlalchemy.orm import Session from app.api.deps import get_current_vendor_api, get_db from app.services.content_page_service import content_page_service +from app.services.vendor_service import VendorService from models.database.user import User +vendor_service = VendorService() + router = APIRouter(prefix="/content-pages") logger = logging.getLogger(__name__) @@ -272,9 +275,7 @@ def get_cms_usage( Returns page counts and limits based on subscription tier. """ - from models.database.vendor import Vendor - - vendor = db.query(Vendor).filter(Vendor.id == current_user.token_vendor_id).first() + vendor = vendor_service.get_vendor_by_id_optional(db, current_user.token_vendor_id) if not vendor: return CMSUsageResponse( total_pages=0, @@ -336,10 +337,8 @@ def get_platform_default( Useful for vendors to view the original before/after overriding. """ - from models.database.vendor import Vendor - # Get vendor's platform - vendor = db.query(Vendor).filter(Vendor.id == current_user.token_vendor_id).first() + vendor = vendor_service.get_vendor_by_id_optional(db, current_user.token_vendor_id) platform_id = 1 # Default to OMS if vendor and vendor.platforms: diff --git a/app/exceptions/platform.py b/app/exceptions/platform.py new file mode 100644 index 00000000..9ed9d7a2 --- /dev/null +++ b/app/exceptions/platform.py @@ -0,0 +1,44 @@ +# app/exceptions/platform.py +""" +Platform-related exceptions. + +Custom exceptions for platform management operations. +""" + +from app.exceptions.base import WizamartException + + +class PlatformNotFoundException(WizamartException): + """Raised when a platform is not found.""" + + def __init__(self, code: str): + super().__init__( + message=f"Platform not found: {code}", + error_code="PLATFORM_NOT_FOUND", + status_code=404, + details={"platform_code": code}, + ) + + +class PlatformInactiveException(WizamartException): + """Raised when trying to access an inactive platform.""" + + def __init__(self, code: str): + super().__init__( + message=f"Platform is inactive: {code}", + error_code="PLATFORM_INACTIVE", + status_code=403, + details={"platform_code": code}, + ) + + +class PlatformUpdateException(WizamartException): + """Raised when platform update fails.""" + + def __init__(self, code: str, reason: str): + super().__init__( + message=f"Failed to update platform {code}: {reason}", + error_code="PLATFORM_UPDATE_FAILED", + status_code=400, + details={"platform_code": code, "reason": reason}, + ) diff --git a/app/services/platform_service.py b/app/services/platform_service.py new file mode 100644 index 00000000..248b12d9 --- /dev/null +++ b/app/services/platform_service.py @@ -0,0 +1,288 @@ +# app/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.exceptions.platform import ( + PlatformNotFoundException, +) +from models.database.content_page import ContentPage +from models.database.platform import Platform +from models.database.vendor_platform 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 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() diff --git a/app/services/vendor_service.py b/app/services/vendor_service.py index 754647f1..46c10dbf 100644 --- a/app/services/vendor_service.py +++ b/app/services/vendor_service.py @@ -257,6 +257,19 @@ class VendorService: return vendor + def get_vendor_by_id_optional(self, db: Session, vendor_id: int) -> Vendor | None: + """ + Get vendor by ID, returns None if not found. + + Args: + db: Database session + vendor_id: Vendor ID to find + + Returns: + Vendor object or None if not found + """ + return db.query(Vendor).filter(Vendor.id == vendor_id).first() + def get_active_vendor_by_code(self, db: Session, vendor_code: str) -> Vendor: """ Get active vendor by vendor_code for public access (no auth required). diff --git a/app/templates/vendor/content-page-edit.html b/app/templates/vendor/content-page-edit.html index 7a56e547..06bf5e5f 100644 --- a/app/templates/vendor/content-page-edit.html +++ b/app/templates/vendor/content-page-edit.html @@ -3,6 +3,7 @@ {% from 'shared/macros/alerts.html' import loading_state, error_state, alert_dynamic %} {% from 'shared/macros/headers.html' import back_button %} {% from 'shared/macros/inputs.html' import number_stepper %} +{% from 'shared/macros/modals.html' import modal %} {% block title %}{% if page_id %}Edit{% else %}Create{% endif %} Content Page{% endblock %} @@ -71,46 +72,18 @@ -
Loading default content...
-Loading default content...