fix: resolve architecture validation errors

- Create platform_service.py to move DB queries from platforms.py API
- Create platform.py exceptions for PlatformNotFoundException
- Update platforms.py API to use platform_service
- Update vendor/content_pages.py to use vendor_service
- Add get_vendor_by_id_optional method to VendorService
- Fix platforms.js: add centralized logger and init guard
- Fix content-page-edit.html: use modal macro instead of inline modal

All 21 architecture validation errors resolved.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-19 18:42:30 +01:00
parent 4f55fe31c8
commit d70a9f38d4
7 changed files with 432 additions and 276 deletions

View File

@@ -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,
)

View File

@@ -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: