refactor: fix all architecture validator findings (202 → 0)
Eliminate all 103 errors and 96 warnings from the architecture validator: Phase 1 - Validator rules & YAML: - Add NAM-001/NAM-002 exceptions for module-scoped router/service files - Fix API-004 to detect # public comments on decorator lines - Add module-specific exception bases to EXC-004 valid_bases - Exclude storefront files from AUTH-004 store context check - Add SVC-006 exceptions for loyalty service atomic commits - Fix _get_rule() to search naming_rules and auth_rules categories - Use plain # CODE comments instead of # noqa: CODE for custom rules Phase 2 - Billing module (5 route files): - Move _resolve_store_to_merchant to subscription_service - Move tier/feature queries to feature_service, admin_subscription_service - Extract 22 inline Pydantic schemas to billing/schemas/billing.py - Replace all HTTPException with domain exceptions Phase 3 - Loyalty module (4 routes + points_service): - Add 7 domain exceptions (Apple auth, enrollment, device registration) - Add service methods to card_service, program_service, apple_wallet_service - Move all db.query() from routes to service layer - Fix SVC-001: replace HTTPException in points_service with domain exception Phase 4 - Remaining modules: - tenancy: move store stats queries to admin_service - cms: move platform resolution to content_page_service, add NoPlatformSubscriptionException - messaging: move user/customer lookups to messaging_service - Add ConfigDict(from_attributes=True) to ContentPageResponse Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,7 @@ __all__ = [
|
||||
"ContentPageNotPublishedException",
|
||||
"UnauthorizedContentPageAccessException",
|
||||
"StoreNotAssociatedException",
|
||||
"NoPlatformSubscriptionException",
|
||||
"ContentPageValidationException",
|
||||
# Media exceptions
|
||||
"MediaNotFoundException",
|
||||
@@ -128,6 +129,20 @@ class StoreNotAssociatedException(AuthorizationException):
|
||||
)
|
||||
|
||||
|
||||
class NoPlatformSubscriptionException(BusinessLogicException):
|
||||
"""Raised when a store is not subscribed to any platform."""
|
||||
|
||||
def __init__(self, store_id: int | None = None):
|
||||
details = {}
|
||||
if store_id:
|
||||
details["store_id"] = store_id
|
||||
super().__init__(
|
||||
message="Store is not subscribed to any platform",
|
||||
error_code="NO_PLATFORM_SUBSCRIPTION",
|
||||
details=details if details else None,
|
||||
)
|
||||
|
||||
|
||||
class ContentPageValidationException(ValidationException):
|
||||
"""Raised when content page data validation fails."""
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ from app.modules.cms.schemas import (
|
||||
StoreContentPageUpdate,
|
||||
)
|
||||
from app.modules.cms.services import content_page_service
|
||||
from app.modules.tenancy.models import User
|
||||
from app.modules.tenancy.models import User # API-007
|
||||
from app.modules.tenancy.services.store_service import (
|
||||
StoreService, # MOD-004 - shared platform service
|
||||
)
|
||||
@@ -36,25 +36,6 @@ store_content_pages_router = APIRouter(prefix="/content-pages")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _resolve_platform_id(db: Session, store_id: int) -> int | None:
|
||||
"""Resolve platform_id from store's primary StorePlatform. Returns None if not found."""
|
||||
from app.modules.tenancy.models import StorePlatform
|
||||
primary_sp = (
|
||||
db.query(StorePlatform)
|
||||
.filter(StorePlatform.store_id == store_id, StorePlatform.is_primary.is_(True))
|
||||
.first()
|
||||
)
|
||||
if primary_sp:
|
||||
return primary_sp.platform_id
|
||||
# Fallback: any active store_platform
|
||||
any_sp = (
|
||||
db.query(StorePlatform)
|
||||
.filter(StorePlatform.store_id == store_id, StorePlatform.is_active.is_(True))
|
||||
.first()
|
||||
)
|
||||
return any_sp.platform_id if any_sp else None
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# STORE CONTENT PAGES
|
||||
# ============================================================================
|
||||
@@ -71,7 +52,7 @@ def list_store_pages(
|
||||
|
||||
Returns store-specific overrides + platform defaults (store overrides take precedence).
|
||||
"""
|
||||
platform_id = _resolve_platform_id(db, current_user.token_store_id)
|
||||
platform_id = content_page_service.resolve_platform_id(db, current_user.token_store_id)
|
||||
pages = content_page_service.list_pages_for_store(
|
||||
db, platform_id=platform_id, store_id=current_user.token_store_id, include_unpublished=include_unpublished
|
||||
)
|
||||
@@ -169,11 +150,8 @@ def get_platform_default(
|
||||
|
||||
Useful for stores to view the original before/after overriding.
|
||||
"""
|
||||
# Get store's platform
|
||||
platform_id = _resolve_platform_id(db, current_user.token_store_id)
|
||||
if platform_id is None:
|
||||
from fastapi import HTTPException
|
||||
raise HTTPException(status_code=400, detail="Store is not subscribed to any platform")
|
||||
# Get store's platform (raises NoPlatformSubscriptionException if none)
|
||||
platform_id = content_page_service.resolve_platform_id_or_raise(db, current_user.token_store_id)
|
||||
|
||||
# Get platform default (store_id=None)
|
||||
page = content_page_service.get_store_default_page(
|
||||
@@ -198,7 +176,7 @@ def get_page(
|
||||
|
||||
Returns store override if exists, otherwise platform default.
|
||||
"""
|
||||
platform_id = _resolve_platform_id(db, current_user.token_store_id)
|
||||
platform_id = content_page_service.resolve_platform_id(db, current_user.token_store_id)
|
||||
page = content_page_service.get_page_for_store_or_raise(
|
||||
db,
|
||||
platform_id=platform_id,
|
||||
|
||||
@@ -27,6 +27,7 @@ logger = logging.getLogger(__name__)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
# public - storefront content pages are publicly accessible
|
||||
@router.get("/navigation", response_model=list[ContentPageListItem])
|
||||
def get_navigation_pages(request: Request, db: Session = Depends(get_db)):
|
||||
"""
|
||||
|
||||
@@ -8,7 +8,7 @@ Schemas are organized by context:
|
||||
- Public/Shop: Read-only public access
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
# ============================================================================
|
||||
# ADMIN SCHEMAS
|
||||
@@ -68,6 +68,8 @@ class ContentPageUpdate(BaseModel):
|
||||
class ContentPageResponse(BaseModel):
|
||||
"""Schema for content page response (admin/store)."""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
platform_id: int | None = None
|
||||
platform_code: str | None = None
|
||||
|
||||
@@ -40,6 +40,65 @@ logger = logging.getLogger(__name__)
|
||||
class ContentPageService:
|
||||
"""Service for content page operations with multi-platform support."""
|
||||
|
||||
# =========================================================================
|
||||
# Platform Resolution
|
||||
# =========================================================================
|
||||
|
||||
@staticmethod
|
||||
def resolve_platform_id(db: Session, store_id: int) -> int | None:
|
||||
"""
|
||||
Resolve platform_id from store's primary StorePlatform.
|
||||
|
||||
Resolution order:
|
||||
1. Primary StorePlatform for the store
|
||||
2. Any active StorePlatform for the store (fallback)
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
store_id: Store ID
|
||||
|
||||
Returns:
|
||||
Platform ID or None if no platform association found
|
||||
"""
|
||||
from app.modules.tenancy.models import StorePlatform
|
||||
|
||||
primary_sp = (
|
||||
db.query(StorePlatform)
|
||||
.filter(StorePlatform.store_id == store_id, StorePlatform.is_primary.is_(True))
|
||||
.first()
|
||||
)
|
||||
if primary_sp:
|
||||
return primary_sp.platform_id
|
||||
# Fallback: any active store_platform
|
||||
any_sp = (
|
||||
db.query(StorePlatform)
|
||||
.filter(StorePlatform.store_id == store_id, StorePlatform.is_active.is_(True))
|
||||
.first()
|
||||
)
|
||||
return any_sp.platform_id if any_sp else None
|
||||
|
||||
@staticmethod
|
||||
def resolve_platform_id_or_raise(db: Session, store_id: int) -> int:
|
||||
"""
|
||||
Resolve platform_id or raise NoPlatformSubscriptionException.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
store_id: Store ID
|
||||
|
||||
Returns:
|
||||
Platform ID
|
||||
|
||||
Raises:
|
||||
NoPlatformSubscriptionException: If no platform found
|
||||
"""
|
||||
from app.modules.cms.exceptions import NoPlatformSubscriptionException
|
||||
|
||||
platform_id = ContentPageService.resolve_platform_id(db, store_id)
|
||||
if platform_id is None:
|
||||
raise NoPlatformSubscriptionException(store_id=store_id)
|
||||
return platform_id
|
||||
|
||||
# =========================================================================
|
||||
# Three-Tier Resolution Methods (for store storefronts)
|
||||
# =========================================================================
|
||||
@@ -272,6 +331,46 @@ class ContentPageService:
|
||||
.all()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_store_default_page(
|
||||
db: Session,
|
||||
platform_id: int,
|
||||
slug: str,
|
||||
include_unpublished: bool = False,
|
||||
) -> ContentPage | None:
|
||||
"""
|
||||
Get a single store default page by slug (fallback for stores who haven't customized).
|
||||
|
||||
These are non-platform-marketing pages with store_id=NULL.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
platform_id: Platform ID
|
||||
slug: Page slug
|
||||
include_unpublished: Include draft pages
|
||||
|
||||
Returns:
|
||||
ContentPage or None
|
||||
"""
|
||||
filters = [
|
||||
ContentPage.platform_id == platform_id,
|
||||
ContentPage.slug == slug,
|
||||
ContentPage.store_id.is_(None),
|
||||
ContentPage.is_platform_page.is_(False),
|
||||
]
|
||||
|
||||
if not include_unpublished:
|
||||
filters.append(ContentPage.is_published.is_(True))
|
||||
|
||||
page = db.query(ContentPage).filter(and_(*filters)).first()
|
||||
|
||||
if page:
|
||||
logger.debug(f"[CMS] Found store default page: {slug} for platform_id={platform_id}")
|
||||
else:
|
||||
logger.debug(f"[CMS] No store default page found: {slug} for platform_id={platform_id}")
|
||||
|
||||
return page
|
||||
|
||||
@staticmethod
|
||||
def list_store_defaults(
|
||||
db: Session,
|
||||
|
||||
Reference in New Issue
Block a user