feat: add multi-platform CMS architecture (Phase 1)
Implement the foundation for multi-platform support allowing independent business offerings (OMS, Loyalty, etc.) with their own CMS pages. Database Models: - Add Platform model for business offerings (domain, branding, config) - Add VendorPlatform junction table for many-to-many relationship - Update SubscriptionTier with platform_id and CMS limits - Update ContentPage with platform_id, is_platform_page for three-tier hierarchy - Add CMS feature codes (cms_basic, cms_custom_pages, cms_templates, etc.) Three-Tier Content Resolution: 1. Vendor override (platform_id + vendor_id + slug) 2. Vendor default (platform_id + vendor_id=NULL + is_platform_page=False) 3. Platform marketing pages (is_platform_page=True) New Components: - PlatformContextMiddleware for detecting platform from domain/path - ContentPageService updated with full three-tier resolution - Platform folder structure (app/platforms/oms/, app/platforms/loyalty/) - Alembic migration with backfill for existing data Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,18 +2,24 @@
|
||||
"""
|
||||
Content Page Service
|
||||
|
||||
Business logic for managing content pages with platform defaults
|
||||
and vendor-specific overrides.
|
||||
Business logic for managing content pages with three-tier hierarchy:
|
||||
|
||||
Lookup Strategy:
|
||||
1. Check for vendor-specific override (vendor_id + slug + published)
|
||||
2. If not found, check for platform default (slug + published)
|
||||
3. If neither exists, return None
|
||||
1. Platform Marketing Pages (is_platform_page=True, vendor_id=NULL)
|
||||
- Platform's own pages (homepage, pricing, about)
|
||||
- Describe the platform/business offering itself
|
||||
|
||||
This allows:
|
||||
- Platform admin to create default content for all vendors
|
||||
- Vendors to override specific pages with custom content
|
||||
- Fallback to platform defaults when vendor hasn't customized
|
||||
2. Vendor Default Pages (is_platform_page=False, vendor_id=NULL)
|
||||
- Fallback pages for vendors who haven't customized
|
||||
- About Us, Shipping Policy, Return Policy, etc.
|
||||
|
||||
3. Vendor Override/Custom Pages (is_platform_page=False, vendor_id=set)
|
||||
- Vendor-specific customizations
|
||||
- Either overrides a default or is a completely custom page
|
||||
|
||||
Lookup Strategy for Vendor Storefronts:
|
||||
1. Check for vendor override (platform_id + vendor_id + slug + published)
|
||||
2. If not found, check for vendor default (platform_id + vendor_id=NULL + is_platform_page=False + slug)
|
||||
3. If neither exists, return None/404
|
||||
"""
|
||||
|
||||
import logging
|
||||
@@ -32,67 +38,124 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ContentPageService:
|
||||
"""Service for content page operations."""
|
||||
"""Service for content page operations with multi-platform support."""
|
||||
|
||||
# =========================================================================
|
||||
# Three-Tier Resolution Methods (for vendor storefronts)
|
||||
# =========================================================================
|
||||
|
||||
@staticmethod
|
||||
def get_page_for_vendor(
|
||||
db: Session,
|
||||
platform_id: int,
|
||||
slug: str,
|
||||
vendor_id: int | None = None,
|
||||
include_unpublished: bool = False,
|
||||
) -> ContentPage | None:
|
||||
"""
|
||||
Get content page for a vendor with fallback to platform default.
|
||||
Get content page for a vendor with three-tier resolution.
|
||||
|
||||
Lookup order:
|
||||
1. Vendor-specific override (vendor_id + slug)
|
||||
2. Platform default (vendor_id=NULL + slug)
|
||||
Resolution order:
|
||||
1. Vendor override (platform_id + vendor_id + slug)
|
||||
2. Vendor default (platform_id + vendor_id=NULL + is_platform_page=False + slug)
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
platform_id: Platform ID (required for multi-platform support)
|
||||
slug: Page slug (about, faq, contact, etc.)
|
||||
vendor_id: Vendor ID (None for platform defaults only)
|
||||
vendor_id: Vendor ID (None for defaults only)
|
||||
include_unpublished: Include draft pages (for preview)
|
||||
|
||||
Returns:
|
||||
ContentPage or None
|
||||
"""
|
||||
filters = [ContentPage.slug == slug]
|
||||
base_filters = [
|
||||
ContentPage.platform_id == platform_id,
|
||||
ContentPage.slug == slug,
|
||||
]
|
||||
|
||||
if not include_unpublished:
|
||||
filters.append(ContentPage.is_published == True)
|
||||
base_filters.append(ContentPage.is_published == True)
|
||||
|
||||
# Try vendor-specific override first
|
||||
# Tier 1: Try vendor-specific override first
|
||||
if vendor_id:
|
||||
vendor_page = (
|
||||
db.query(ContentPage)
|
||||
.filter(and_(ContentPage.vendor_id == vendor_id, *filters))
|
||||
.filter(and_(ContentPage.vendor_id == vendor_id, *base_filters))
|
||||
.first()
|
||||
)
|
||||
|
||||
if vendor_page:
|
||||
logger.debug(
|
||||
f"Found vendor-specific page: {slug} for vendor_id={vendor_id}"
|
||||
f"[CMS] Found vendor override: {slug} for vendor_id={vendor_id}, platform_id={platform_id}"
|
||||
)
|
||||
return vendor_page
|
||||
|
||||
# Fallback to platform default
|
||||
platform_page = (
|
||||
# Tier 2: Fallback to vendor default (not platform page)
|
||||
vendor_default_page = (
|
||||
db.query(ContentPage)
|
||||
.filter(and_(ContentPage.vendor_id == None, *filters))
|
||||
.filter(
|
||||
and_(
|
||||
ContentPage.vendor_id == None,
|
||||
ContentPage.is_platform_page == False,
|
||||
*base_filters,
|
||||
)
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if platform_page:
|
||||
logger.debug(f"Using platform default page: {slug}")
|
||||
else:
|
||||
logger.debug(f"No page found for slug: {slug}")
|
||||
if vendor_default_page:
|
||||
logger.debug(f"[CMS] Using vendor default page: {slug} for platform_id={platform_id}")
|
||||
return vendor_default_page
|
||||
|
||||
return platform_page
|
||||
logger.debug(f"[CMS] No page found for slug: {slug}, platform_id={platform_id}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_platform_page(
|
||||
db: Session,
|
||||
platform_id: int,
|
||||
slug: str,
|
||||
include_unpublished: bool = False,
|
||||
) -> ContentPage | None:
|
||||
"""
|
||||
Get a platform marketing page.
|
||||
|
||||
Platform marketing pages are pages that describe the platform itself
|
||||
(homepage, pricing, about, features, etc.).
|
||||
|
||||
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.vendor_id == None,
|
||||
ContentPage.is_platform_page == True,
|
||||
]
|
||||
|
||||
if not include_unpublished:
|
||||
filters.append(ContentPage.is_published == True)
|
||||
|
||||
page = db.query(ContentPage).filter(and_(*filters)).first()
|
||||
|
||||
if page:
|
||||
logger.debug(f"[CMS] Found platform page: {slug} for platform_id={platform_id}")
|
||||
else:
|
||||
logger.debug(f"[CMS] No platform page found: {slug} for platform_id={platform_id}")
|
||||
|
||||
return page
|
||||
|
||||
@staticmethod
|
||||
def list_pages_for_vendor(
|
||||
db: Session,
|
||||
platform_id: int,
|
||||
vendor_id: int | None = None,
|
||||
include_unpublished: bool = False,
|
||||
footer_only: bool = False,
|
||||
@@ -100,13 +163,15 @@ class ContentPageService:
|
||||
legal_only: bool = False,
|
||||
) -> list[ContentPage]:
|
||||
"""
|
||||
List all available pages for a vendor (includes vendor overrides + platform defaults).
|
||||
List all available pages for a vendor storefront.
|
||||
|
||||
Merges vendor-specific overrides with platform defaults, prioritizing vendor overrides.
|
||||
Merges vendor overrides with vendor defaults, prioritizing overrides.
|
||||
Does NOT include platform marketing pages.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
vendor_id: Vendor ID (None for platform pages only)
|
||||
platform_id: Platform ID
|
||||
vendor_id: Vendor ID (None for vendor defaults only)
|
||||
include_unpublished: Include draft pages
|
||||
footer_only: Only pages marked for footer display
|
||||
header_only: Only pages marked for header display
|
||||
@@ -115,7 +180,81 @@ class ContentPageService:
|
||||
Returns:
|
||||
List of ContentPage objects
|
||||
"""
|
||||
filters = []
|
||||
base_filters = [ContentPage.platform_id == platform_id]
|
||||
|
||||
if not include_unpublished:
|
||||
base_filters.append(ContentPage.is_published == True)
|
||||
|
||||
if footer_only:
|
||||
base_filters.append(ContentPage.show_in_footer == True)
|
||||
|
||||
if header_only:
|
||||
base_filters.append(ContentPage.show_in_header == True)
|
||||
|
||||
if legal_only:
|
||||
base_filters.append(ContentPage.show_in_legal == True)
|
||||
|
||||
# Get vendor-specific pages
|
||||
vendor_pages = []
|
||||
if vendor_id:
|
||||
vendor_pages = (
|
||||
db.query(ContentPage)
|
||||
.filter(and_(ContentPage.vendor_id == vendor_id, *base_filters))
|
||||
.order_by(ContentPage.display_order, ContentPage.title)
|
||||
.all()
|
||||
)
|
||||
|
||||
# Get vendor defaults (not platform marketing pages)
|
||||
vendor_default_pages = (
|
||||
db.query(ContentPage)
|
||||
.filter(
|
||||
and_(
|
||||
ContentPage.vendor_id == None,
|
||||
ContentPage.is_platform_page == False,
|
||||
*base_filters,
|
||||
)
|
||||
)
|
||||
.order_by(ContentPage.display_order, ContentPage.title)
|
||||
.all()
|
||||
)
|
||||
|
||||
# Merge: vendor overrides take precedence
|
||||
vendor_slugs = {page.slug for page in vendor_pages}
|
||||
all_pages = vendor_pages + [
|
||||
page for page in vendor_default_pages if page.slug not in vendor_slugs
|
||||
]
|
||||
|
||||
# Sort by display_order
|
||||
all_pages.sort(key=lambda p: (p.display_order, p.title))
|
||||
|
||||
return all_pages
|
||||
|
||||
@staticmethod
|
||||
def list_platform_pages(
|
||||
db: Session,
|
||||
platform_id: int,
|
||||
include_unpublished: bool = False,
|
||||
footer_only: bool = False,
|
||||
header_only: bool = False,
|
||||
) -> list[ContentPage]:
|
||||
"""
|
||||
List platform marketing pages.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
platform_id: Platform ID
|
||||
include_unpublished: Include draft pages
|
||||
footer_only: Only pages marked for footer display
|
||||
header_only: Only pages marked for header display
|
||||
|
||||
Returns:
|
||||
List of platform marketing ContentPage objects
|
||||
"""
|
||||
filters = [
|
||||
ContentPage.platform_id == platform_id,
|
||||
ContentPage.vendor_id == None,
|
||||
ContentPage.is_platform_page == True,
|
||||
]
|
||||
|
||||
if not include_unpublished:
|
||||
filters.append(ContentPage.is_published == True)
|
||||
@@ -126,45 +265,59 @@ class ContentPageService:
|
||||
if header_only:
|
||||
filters.append(ContentPage.show_in_header == True)
|
||||
|
||||
if legal_only:
|
||||
filters.append(ContentPage.show_in_legal == True)
|
||||
|
||||
# Get vendor-specific pages
|
||||
vendor_pages = []
|
||||
if vendor_id:
|
||||
vendor_pages = (
|
||||
db.query(ContentPage)
|
||||
.filter(and_(ContentPage.vendor_id == vendor_id, *filters))
|
||||
.order_by(ContentPage.display_order, ContentPage.title)
|
||||
.all()
|
||||
)
|
||||
|
||||
# Get platform defaults
|
||||
platform_pages = (
|
||||
return (
|
||||
db.query(ContentPage)
|
||||
.filter(and_(ContentPage.vendor_id == None, *filters))
|
||||
.filter(and_(*filters))
|
||||
.order_by(ContentPage.display_order, ContentPage.title)
|
||||
.all()
|
||||
)
|
||||
|
||||
# Merge: vendor overrides take precedence
|
||||
vendor_slugs = {page.slug for page in vendor_pages}
|
||||
all_pages = vendor_pages + [
|
||||
page for page in platform_pages if page.slug not in vendor_slugs
|
||||
@staticmethod
|
||||
def list_vendor_defaults(
|
||||
db: Session,
|
||||
platform_id: int,
|
||||
include_unpublished: bool = False,
|
||||
) -> list[ContentPage]:
|
||||
"""
|
||||
List vendor default pages (fallbacks for vendors who haven't customized).
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
platform_id: Platform ID
|
||||
include_unpublished: Include draft pages
|
||||
|
||||
Returns:
|
||||
List of vendor default ContentPage objects
|
||||
"""
|
||||
filters = [
|
||||
ContentPage.platform_id == platform_id,
|
||||
ContentPage.vendor_id == None,
|
||||
ContentPage.is_platform_page == False,
|
||||
]
|
||||
|
||||
# Sort by display_order
|
||||
all_pages.sort(key=lambda p: (p.display_order, p.title))
|
||||
if not include_unpublished:
|
||||
filters.append(ContentPage.is_published == True)
|
||||
|
||||
return all_pages
|
||||
return (
|
||||
db.query(ContentPage)
|
||||
.filter(and_(*filters))
|
||||
.order_by(ContentPage.display_order, ContentPage.title)
|
||||
.all()
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# CRUD Methods
|
||||
# =========================================================================
|
||||
|
||||
@staticmethod
|
||||
def create_page(
|
||||
db: Session,
|
||||
platform_id: int,
|
||||
slug: str,
|
||||
title: str,
|
||||
content: str,
|
||||
vendor_id: int | None = None,
|
||||
is_platform_page: bool = False,
|
||||
content_format: str = "html",
|
||||
template: str = "default",
|
||||
meta_description: str | None = None,
|
||||
@@ -181,12 +334,14 @@ class ContentPageService:
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
platform_id: Platform ID (required)
|
||||
slug: URL-safe identifier
|
||||
title: Page title
|
||||
content: HTML or Markdown content
|
||||
vendor_id: Vendor ID (None for platform default)
|
||||
vendor_id: Vendor ID (None for platform/default pages)
|
||||
is_platform_page: True for platform marketing pages
|
||||
content_format: "html" or "markdown"
|
||||
template: Template name for homepage/landing pages (default, minimal, modern, etc.)
|
||||
template: Template name for landing pages
|
||||
meta_description: SEO description
|
||||
meta_keywords: SEO keywords
|
||||
is_published: Publish immediately
|
||||
@@ -200,7 +355,9 @@ class ContentPageService:
|
||||
Created ContentPage
|
||||
"""
|
||||
page = ContentPage(
|
||||
platform_id=platform_id,
|
||||
vendor_id=vendor_id,
|
||||
is_platform_page=is_platform_page,
|
||||
slug=slug,
|
||||
title=title,
|
||||
content=content,
|
||||
@@ -222,8 +379,9 @@ class ContentPageService:
|
||||
db.flush()
|
||||
db.refresh(page)
|
||||
|
||||
page_type = "platform" if is_platform_page else ("vendor" if vendor_id else "default")
|
||||
logger.info(
|
||||
f"Created content page: {slug} (vendor_id={vendor_id}, id={page.id})"
|
||||
f"[CMS] Created {page_type} page: {slug} (platform_id={platform_id}, vendor_id={vendor_id}, id={page.id})"
|
||||
)
|
||||
return page
|
||||
|
||||
@@ -250,18 +408,7 @@ class ContentPageService:
|
||||
Args:
|
||||
db: Database session
|
||||
page_id: Page ID
|
||||
title: New title
|
||||
content: New content
|
||||
content_format: New format
|
||||
template: New template name
|
||||
meta_description: New SEO description
|
||||
meta_keywords: New SEO keywords
|
||||
is_published: New publish status
|
||||
show_in_footer: New footer visibility
|
||||
show_in_header: New header visibility
|
||||
show_in_legal: New legal bar visibility
|
||||
display_order: New sort order
|
||||
updated_by: User ID who updated it
|
||||
... other fields
|
||||
|
||||
Returns:
|
||||
Updated ContentPage or None if not found
|
||||
@@ -269,7 +416,7 @@ class ContentPageService:
|
||||
page = db.query(ContentPage).filter(ContentPage.id == page_id).first()
|
||||
|
||||
if not page:
|
||||
logger.warning(f"Content page not found: id={page_id}")
|
||||
logger.warning(f"[CMS] Content page not found: id={page_id}")
|
||||
return None
|
||||
|
||||
# Update fields if provided
|
||||
@@ -303,7 +450,7 @@ class ContentPageService:
|
||||
db.flush()
|
||||
db.refresh(page)
|
||||
|
||||
logger.info(f"Updated content page: id={page_id}, slug={page.slug}")
|
||||
logger.info(f"[CMS] Updated content page: id={page_id}, slug={page.slug}")
|
||||
return page
|
||||
|
||||
@staticmethod
|
||||
@@ -321,12 +468,12 @@ class ContentPageService:
|
||||
page = db.query(ContentPage).filter(ContentPage.id == page_id).first()
|
||||
|
||||
if not page:
|
||||
logger.warning(f"Content page not found for deletion: id={page_id}")
|
||||
logger.warning(f"[CMS] Content page not found for deletion: id={page_id}")
|
||||
return False
|
||||
|
||||
db.delete(page)
|
||||
|
||||
logger.info(f"Deleted content page: id={page_id}, slug={page.slug}")
|
||||
logger.info(f"[CMS] Deleted content page: id={page_id}, slug={page.slug}")
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
@@ -338,16 +485,6 @@ class ContentPageService:
|
||||
def get_page_by_id_or_raise(db: Session, page_id: int) -> ContentPage:
|
||||
"""
|
||||
Get content page by ID or raise ContentPageNotFoundException.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
page_id: Page ID
|
||||
|
||||
Returns:
|
||||
ContentPage
|
||||
|
||||
Raises:
|
||||
ContentPageNotFoundException: If page not found
|
||||
"""
|
||||
page = db.query(ContentPage).filter(ContentPage.id == page_id).first()
|
||||
if not page:
|
||||
@@ -357,33 +494,269 @@ class ContentPageService:
|
||||
@staticmethod
|
||||
def get_page_for_vendor_or_raise(
|
||||
db: Session,
|
||||
platform_id: int,
|
||||
slug: str,
|
||||
vendor_id: int | None = None,
|
||||
include_unpublished: bool = False,
|
||||
) -> ContentPage:
|
||||
"""
|
||||
Get content page for a vendor with fallback to platform default.
|
||||
Get content page for a vendor with three-tier resolution.
|
||||
Raises ContentPageNotFoundException if not found.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
slug: Page slug
|
||||
vendor_id: Vendor ID
|
||||
include_unpublished: Include draft pages
|
||||
|
||||
Returns:
|
||||
ContentPage
|
||||
|
||||
Raises:
|
||||
ContentPageNotFoundException: If page not found
|
||||
"""
|
||||
page = ContentPageService.get_page_for_vendor(
|
||||
db, slug=slug, vendor_id=vendor_id, include_unpublished=include_unpublished
|
||||
db,
|
||||
platform_id=platform_id,
|
||||
slug=slug,
|
||||
vendor_id=vendor_id,
|
||||
include_unpublished=include_unpublished,
|
||||
)
|
||||
if not page:
|
||||
raise ContentPageNotFoundException(identifier=slug)
|
||||
return page
|
||||
|
||||
@staticmethod
|
||||
def get_platform_page_or_raise(
|
||||
db: Session,
|
||||
platform_id: int,
|
||||
slug: str,
|
||||
include_unpublished: bool = False,
|
||||
) -> ContentPage:
|
||||
"""
|
||||
Get platform marketing page or raise ContentPageNotFoundException.
|
||||
"""
|
||||
page = ContentPageService.get_platform_page(
|
||||
db,
|
||||
platform_id=platform_id,
|
||||
slug=slug,
|
||||
include_unpublished=include_unpublished,
|
||||
)
|
||||
if not page:
|
||||
raise ContentPageNotFoundException(identifier=slug)
|
||||
return page
|
||||
|
||||
# =========================================================================
|
||||
# Vendor Page Management (with ownership checks)
|
||||
# =========================================================================
|
||||
|
||||
@staticmethod
|
||||
def update_vendor_page(
|
||||
db: Session,
|
||||
page_id: int,
|
||||
vendor_id: int,
|
||||
title: str | None = None,
|
||||
content: str | None = None,
|
||||
content_format: str | None = None,
|
||||
meta_description: str | None = None,
|
||||
meta_keywords: str | None = None,
|
||||
is_published: bool | None = None,
|
||||
show_in_footer: bool | None = None,
|
||||
show_in_header: bool | None = None,
|
||||
show_in_legal: bool | None = None,
|
||||
display_order: int | None = None,
|
||||
updated_by: int | None = None,
|
||||
) -> ContentPage:
|
||||
"""
|
||||
Update a vendor-specific content page with ownership check.
|
||||
|
||||
Raises:
|
||||
ContentPageNotFoundException: If page not found
|
||||
UnauthorizedContentPageAccessException: If page doesn't belong to vendor
|
||||
"""
|
||||
page = ContentPageService.get_page_by_id_or_raise(db, page_id)
|
||||
|
||||
if page.vendor_id != vendor_id:
|
||||
raise UnauthorizedContentPageAccessException(action="edit")
|
||||
|
||||
return ContentPageService.update_page(
|
||||
db,
|
||||
page_id=page_id,
|
||||
title=title,
|
||||
content=content,
|
||||
content_format=content_format,
|
||||
meta_description=meta_description,
|
||||
meta_keywords=meta_keywords,
|
||||
is_published=is_published,
|
||||
show_in_footer=show_in_footer,
|
||||
show_in_header=show_in_header,
|
||||
show_in_legal=show_in_legal,
|
||||
display_order=display_order,
|
||||
updated_by=updated_by,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def delete_vendor_page(db: Session, page_id: int, vendor_id: int) -> None:
|
||||
"""
|
||||
Delete a vendor-specific content page with ownership check.
|
||||
|
||||
Raises:
|
||||
ContentPageNotFoundException: If page not found
|
||||
UnauthorizedContentPageAccessException: If page doesn't belong to vendor
|
||||
"""
|
||||
page = ContentPageService.get_page_by_id_or_raise(db, page_id)
|
||||
|
||||
if page.vendor_id != vendor_id:
|
||||
raise UnauthorizedContentPageAccessException(action="delete")
|
||||
|
||||
ContentPageService.delete_page(db, page_id)
|
||||
|
||||
@staticmethod
|
||||
def create_vendor_override(
|
||||
db: Session,
|
||||
platform_id: int,
|
||||
vendor_id: int,
|
||||
slug: str,
|
||||
title: str,
|
||||
content: str,
|
||||
content_format: str = "html",
|
||||
meta_description: str | None = None,
|
||||
meta_keywords: str | None = None,
|
||||
is_published: bool = False,
|
||||
show_in_footer: bool = True,
|
||||
show_in_header: bool = False,
|
||||
show_in_legal: bool = False,
|
||||
display_order: int = 0,
|
||||
created_by: int | None = None,
|
||||
) -> ContentPage:
|
||||
"""
|
||||
Create a vendor override page (vendor-specific customization of a default).
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
platform_id: Platform ID
|
||||
vendor_id: Vendor ID
|
||||
slug: Page slug (typically matches a default page)
|
||||
... other fields
|
||||
|
||||
Returns:
|
||||
Created ContentPage
|
||||
"""
|
||||
return ContentPageService.create_page(
|
||||
db,
|
||||
platform_id=platform_id,
|
||||
slug=slug,
|
||||
title=title,
|
||||
content=content,
|
||||
vendor_id=vendor_id,
|
||||
is_platform_page=False,
|
||||
content_format=content_format,
|
||||
meta_description=meta_description,
|
||||
meta_keywords=meta_keywords,
|
||||
is_published=is_published,
|
||||
show_in_footer=show_in_footer,
|
||||
show_in_header=show_in_header,
|
||||
show_in_legal=show_in_legal,
|
||||
display_order=display_order,
|
||||
created_by=created_by,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def revert_to_default(db: Session, page_id: int, vendor_id: int) -> None:
|
||||
"""
|
||||
Revert a vendor override to the default by deleting the override.
|
||||
|
||||
After deletion, the vendor storefront will use the vendor default page.
|
||||
|
||||
Raises:
|
||||
ContentPageNotFoundException: If page not found
|
||||
UnauthorizedContentPageAccessException: If page doesn't belong to vendor
|
||||
"""
|
||||
ContentPageService.delete_vendor_page(db, page_id, vendor_id)
|
||||
|
||||
# =========================================================================
|
||||
# Admin Methods (for listing all pages)
|
||||
# =========================================================================
|
||||
|
||||
@staticmethod
|
||||
def list_all_pages(
|
||||
db: Session,
|
||||
platform_id: int | None = None,
|
||||
vendor_id: int | None = None,
|
||||
include_unpublished: bool = False,
|
||||
page_tier: str | None = None,
|
||||
) -> list[ContentPage]:
|
||||
"""
|
||||
List all content pages for admin management.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
platform_id: Optional filter by platform ID
|
||||
vendor_id: Optional filter by vendor ID
|
||||
include_unpublished: Include draft pages
|
||||
page_tier: Optional filter by tier ("platform", "vendor_default", "vendor_override")
|
||||
|
||||
Returns:
|
||||
List of ContentPage objects
|
||||
"""
|
||||
filters = []
|
||||
|
||||
if platform_id:
|
||||
filters.append(ContentPage.platform_id == platform_id)
|
||||
|
||||
if vendor_id is not None:
|
||||
filters.append(ContentPage.vendor_id == vendor_id)
|
||||
|
||||
if not include_unpublished:
|
||||
filters.append(ContentPage.is_published == True)
|
||||
|
||||
if page_tier == "platform":
|
||||
filters.append(ContentPage.is_platform_page == True)
|
||||
filters.append(ContentPage.vendor_id == None)
|
||||
elif page_tier == "vendor_default":
|
||||
filters.append(ContentPage.is_platform_page == False)
|
||||
filters.append(ContentPage.vendor_id == None)
|
||||
elif page_tier == "vendor_override":
|
||||
filters.append(ContentPage.vendor_id != None)
|
||||
|
||||
return (
|
||||
db.query(ContentPage)
|
||||
.filter(and_(*filters) if filters else True)
|
||||
.order_by(
|
||||
ContentPage.platform_id,
|
||||
ContentPage.vendor_id,
|
||||
ContentPage.display_order,
|
||||
ContentPage.title,
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def list_all_vendor_pages(
|
||||
db: Session,
|
||||
vendor_id: int,
|
||||
platform_id: int | None = None,
|
||||
include_unpublished: bool = False,
|
||||
) -> list[ContentPage]:
|
||||
"""
|
||||
List only vendor-specific pages (overrides and custom pages).
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
vendor_id: Vendor ID
|
||||
platform_id: Optional filter by platform
|
||||
include_unpublished: Include draft pages
|
||||
|
||||
Returns:
|
||||
List of vendor-specific ContentPage objects
|
||||
"""
|
||||
filters = [ContentPage.vendor_id == vendor_id]
|
||||
|
||||
if platform_id:
|
||||
filters.append(ContentPage.platform_id == platform_id)
|
||||
|
||||
if not include_unpublished:
|
||||
filters.append(ContentPage.is_published == True)
|
||||
|
||||
return (
|
||||
db.query(ContentPage)
|
||||
.filter(and_(*filters))
|
||||
.order_by(ContentPage.display_order, ContentPage.title)
|
||||
.all()
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# Helper Methods for raising exceptions
|
||||
# =========================================================================
|
||||
|
||||
@staticmethod
|
||||
def update_page_or_raise(
|
||||
db: Session,
|
||||
@@ -439,168 +812,6 @@ class ContentPageService:
|
||||
if not success:
|
||||
raise ContentPageNotFoundException(identifier=page_id)
|
||||
|
||||
@staticmethod
|
||||
def update_vendor_page(
|
||||
db: Session,
|
||||
page_id: int,
|
||||
vendor_id: int,
|
||||
title: str | None = None,
|
||||
content: str | None = None,
|
||||
content_format: str | None = None,
|
||||
meta_description: str | None = None,
|
||||
meta_keywords: str | None = None,
|
||||
is_published: bool | None = None,
|
||||
show_in_footer: bool | None = None,
|
||||
show_in_header: bool | None = None,
|
||||
show_in_legal: bool | None = None,
|
||||
display_order: int | None = None,
|
||||
updated_by: int | None = None,
|
||||
) -> ContentPage:
|
||||
"""
|
||||
Update a vendor-specific content page with ownership check.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
page_id: Page ID
|
||||
vendor_id: Vendor ID (for ownership verification)
|
||||
... other fields
|
||||
|
||||
Returns:
|
||||
Updated ContentPage
|
||||
|
||||
Raises:
|
||||
ContentPageNotFoundException: If page not found
|
||||
UnauthorizedContentPageAccessException: If page doesn't belong to vendor
|
||||
"""
|
||||
page = ContentPageService.get_page_by_id_or_raise(db, page_id)
|
||||
|
||||
if page.vendor_id != vendor_id:
|
||||
raise UnauthorizedContentPageAccessException(action="edit")
|
||||
|
||||
return ContentPageService.update_page_or_raise(
|
||||
db,
|
||||
page_id=page_id,
|
||||
title=title,
|
||||
content=content,
|
||||
content_format=content_format,
|
||||
meta_description=meta_description,
|
||||
meta_keywords=meta_keywords,
|
||||
is_published=is_published,
|
||||
show_in_footer=show_in_footer,
|
||||
show_in_header=show_in_header,
|
||||
show_in_legal=show_in_legal,
|
||||
display_order=display_order,
|
||||
updated_by=updated_by,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def delete_vendor_page(db: Session, page_id: int, vendor_id: int) -> None:
|
||||
"""
|
||||
Delete a vendor-specific content page with ownership check.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
page_id: Page ID
|
||||
vendor_id: Vendor ID (for ownership verification)
|
||||
|
||||
Raises:
|
||||
ContentPageNotFoundException: If page not found
|
||||
UnauthorizedContentPageAccessException: If page doesn't belong to vendor
|
||||
"""
|
||||
page = ContentPageService.get_page_by_id_or_raise(db, page_id)
|
||||
|
||||
if page.vendor_id != vendor_id:
|
||||
raise UnauthorizedContentPageAccessException(action="delete")
|
||||
|
||||
ContentPageService.delete_page_or_raise(db, page_id)
|
||||
|
||||
@staticmethod
|
||||
def list_all_pages(
|
||||
db: Session,
|
||||
vendor_id: int | None = None,
|
||||
include_unpublished: bool = False,
|
||||
) -> list[ContentPage]:
|
||||
"""
|
||||
List all content pages (platform defaults and vendor overrides).
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
vendor_id: Optional filter by vendor ID
|
||||
include_unpublished: Include draft pages
|
||||
|
||||
Returns:
|
||||
List of ContentPage objects
|
||||
"""
|
||||
filters = []
|
||||
|
||||
if vendor_id:
|
||||
filters.append(ContentPage.vendor_id == vendor_id)
|
||||
|
||||
if not include_unpublished:
|
||||
filters.append(ContentPage.is_published == True)
|
||||
|
||||
return (
|
||||
db.query(ContentPage)
|
||||
.filter(and_(*filters) if filters else True)
|
||||
.order_by(
|
||||
ContentPage.vendor_id, ContentPage.display_order, ContentPage.title
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def list_all_vendor_pages(
|
||||
db: Session, vendor_id: int, include_unpublished: bool = False
|
||||
) -> list[ContentPage]:
|
||||
"""
|
||||
List only vendor-specific pages (no platform defaults).
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
vendor_id: Vendor ID
|
||||
include_unpublished: Include draft pages
|
||||
|
||||
Returns:
|
||||
List of vendor-specific ContentPage objects
|
||||
"""
|
||||
filters = [ContentPage.vendor_id == vendor_id]
|
||||
|
||||
if not include_unpublished:
|
||||
filters.append(ContentPage.is_published == True)
|
||||
|
||||
return (
|
||||
db.query(ContentPage)
|
||||
.filter(and_(*filters))
|
||||
.order_by(ContentPage.display_order, ContentPage.title)
|
||||
.all()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def list_all_platform_pages(
|
||||
db: Session, include_unpublished: bool = False
|
||||
) -> list[ContentPage]:
|
||||
"""
|
||||
List only platform default pages.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
include_unpublished: Include draft pages
|
||||
|
||||
Returns:
|
||||
List of platform default ContentPage objects
|
||||
"""
|
||||
filters = [ContentPage.vendor_id == None]
|
||||
|
||||
if not include_unpublished:
|
||||
filters.append(ContentPage.is_published == True)
|
||||
|
||||
return (
|
||||
db.query(ContentPage)
|
||||
.filter(and_(*filters))
|
||||
.order_by(ContentPage.display_order, ContentPage.title)
|
||||
.all()
|
||||
)
|
||||
|
||||
|
||||
# Singleton instance
|
||||
content_page_service = ContentPageService()
|
||||
|
||||
Reference in New Issue
Block a user