# app/api/v1/vendor/content_pages.py """ Vendor Content Pages API Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern). The get_current_vendor_api dependency guarantees token_vendor_id is present. Vendors can: - View their content pages (includes platform defaults) - Create/edit/delete their own content page overrides - Preview pages before publishing """ import logging from fastapi import APIRouter, Depends, Query from pydantic import BaseModel, Field 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__) # ============================================================================ # REQUEST/RESPONSE SCHEMAS # ============================================================================ class VendorContentPageCreate(BaseModel): """Schema for creating a vendor content page.""" slug: str = Field( ..., max_length=100, description="URL-safe identifier (about, faq, contact, etc.)", ) title: str = Field(..., max_length=200, description="Page title") content: str = Field(..., description="HTML or Markdown content") content_format: str = Field( default="html", description="Content format: html or markdown" ) meta_description: str | None = Field( None, max_length=300, description="SEO meta description" ) meta_keywords: str | None = Field(None, max_length=300, description="SEO keywords") is_published: bool = Field(default=False, description="Publish immediately") show_in_footer: bool = Field(default=True, description="Show in footer navigation") show_in_header: bool = Field(default=False, description="Show in header navigation") show_in_legal: bool = Field( default=False, description="Show in legal/bottom bar (next to copyright)" ) display_order: int = Field(default=0, description="Display order (lower = first)") class VendorContentPageUpdate(BaseModel): """Schema for updating a vendor content page.""" title: str | None = Field(None, max_length=200) content: str | None = None content_format: str | None = None meta_description: str | None = Field(None, max_length=300) meta_keywords: str | None = Field(None, max_length=300) 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 class ContentPageResponse(BaseModel): """Schema for content page response.""" id: int vendor_id: int | None vendor_name: str | None slug: str title: str content: str content_format: str meta_description: str | None meta_keywords: str | None is_published: bool published_at: str | None display_order: int show_in_footer: bool show_in_header: bool show_in_legal: bool is_platform_default: bool is_vendor_override: bool created_at: str updated_at: str created_by: int | None updated_by: int | None class CMSUsageResponse(BaseModel): """Schema for CMS usage statistics.""" total_pages: int custom_pages: int override_pages: int pages_limit: int | None custom_pages_limit: int | None can_create_page: bool can_create_custom: bool usage_percent: float custom_usage_percent: float # ============================================================================ # VENDOR CONTENT PAGES # ============================================================================ @router.get("/", response_model=list[ContentPageResponse]) def list_vendor_pages( include_unpublished: bool = Query(False, description="Include draft pages"), current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), ): """ List all content pages available for this vendor. Returns vendor-specific overrides + platform defaults (vendor overrides take precedence). """ pages = content_page_service.list_pages_for_vendor( db, vendor_id=current_user.token_vendor_id, include_unpublished=include_unpublished ) return [page.to_dict() for page in pages] @router.get("/overrides", response_model=list[ContentPageResponse]) def list_vendor_overrides( include_unpublished: bool = Query(False, description="Include draft pages"), current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), ): """ List only vendor-specific content pages (no platform defaults). Shows what the vendor has customized. """ pages = content_page_service.list_all_vendor_pages( db, vendor_id=current_user.token_vendor_id, include_unpublished=include_unpublished ) return [page.to_dict() for page in pages] @router.get("/{slug}", response_model=ContentPageResponse) def get_page( slug: str, include_unpublished: bool = Query(False, description="Include draft pages"), current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), ): """ Get a specific content page by slug. Returns vendor override if exists, otherwise platform default. """ page = content_page_service.get_page_for_vendor_or_raise( db, slug=slug, vendor_id=current_user.token_vendor_id, include_unpublished=include_unpublished, ) return page.to_dict() @router.post("/", response_model=ContentPageResponse, status_code=201) def create_vendor_page( page_data: VendorContentPageCreate, current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), ): """ Create a vendor-specific content page override. This will be shown instead of the platform default for this vendor. """ page = content_page_service.create_page( db, slug=page_data.slug, title=page_data.title, content=page_data.content, vendor_id=current_user.token_vendor_id, content_format=page_data.content_format, meta_description=page_data.meta_description, meta_keywords=page_data.meta_keywords, is_published=page_data.is_published, show_in_footer=page_data.show_in_footer, show_in_header=page_data.show_in_header, show_in_legal=page_data.show_in_legal, display_order=page_data.display_order, created_by=current_user.id, ) db.commit() return page.to_dict() @router.put("/{page_id}", response_model=ContentPageResponse) def update_vendor_page( page_id: int, page_data: VendorContentPageUpdate, current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), ): """ Update a vendor-specific content page. Can only update pages owned by this vendor. """ # Update with ownership check in service layer page = content_page_service.update_vendor_page( db, page_id=page_id, vendor_id=current_user.token_vendor_id, title=page_data.title, content=page_data.content, content_format=page_data.content_format, meta_description=page_data.meta_description, meta_keywords=page_data.meta_keywords, is_published=page_data.is_published, show_in_footer=page_data.show_in_footer, show_in_header=page_data.show_in_header, show_in_legal=page_data.show_in_legal, display_order=page_data.display_order, updated_by=current_user.id, ) db.commit() return page.to_dict() @router.delete("/{page_id}", status_code=204) def delete_vendor_page( page_id: int, current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), ): """ Delete a vendor-specific content page. Can only delete pages owned by this vendor. After deletion, platform default will be shown (if exists). """ # Delete with ownership check in service layer content_page_service.delete_vendor_page(db, page_id, current_user.token_vendor_id) db.commit() # ============================================================================ # CMS USAGE & LIMITS # ============================================================================ @router.get("/usage", response_model=CMSUsageResponse) def get_cms_usage( current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), ): """ Get CMS usage statistics for the vendor. Returns page counts and limits based on subscription tier. """ vendor = vendor_service.get_vendor_by_id_optional(db, current_user.token_vendor_id) if not vendor: return CMSUsageResponse( total_pages=0, custom_pages=0, override_pages=0, pages_limit=3, custom_pages_limit=0, can_create_page=False, can_create_custom=False, usage_percent=0, custom_usage_percent=0, ) # Get vendor's pages vendor_pages = content_page_service.list_all_vendor_pages( db, vendor_id=current_user.token_vendor_id, include_unpublished=True ) total_pages = len(vendor_pages) override_pages = sum(1 for p in vendor_pages if p.is_vendor_override) custom_pages = total_pages - override_pages # Get limits from subscription tier pages_limit = None custom_pages_limit = None if vendor.subscription and vendor.subscription.tier: pages_limit = vendor.subscription.tier.cms_pages_limit custom_pages_limit = vendor.subscription.tier.cms_custom_pages_limit # Calculate can_create flags can_create_page = pages_limit is None or total_pages < pages_limit can_create_custom = custom_pages_limit is None or custom_pages < custom_pages_limit # Calculate usage percentages usage_percent = 0 if pages_limit is None else min(100, (total_pages / pages_limit) * 100) if pages_limit > 0 else 100 custom_usage_percent = 0 if custom_pages_limit is None else min(100, (custom_pages / custom_pages_limit) * 100) if custom_pages_limit > 0 else 100 return CMSUsageResponse( total_pages=total_pages, custom_pages=custom_pages, override_pages=override_pages, pages_limit=pages_limit, custom_pages_limit=custom_pages_limit, can_create_page=can_create_page, can_create_custom=can_create_custom, usage_percent=usage_percent, custom_usage_percent=custom_usage_percent, ) @router.get("/platform-default/{slug}", response_model=ContentPageResponse) def get_platform_default( slug: str, current_user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db), ): """ Get the platform default content for a slug. Useful for vendors to view the original before/after overriding. """ # Get vendor's platform 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: platform_id = vendor.platforms[0].id # Get platform default (vendor_id=None) page = content_page_service.get_vendor_default_page( db, platform_id=platform_id, slug=slug, include_unpublished=True ) if not page: from app.exceptions import NotFoundException raise NotFoundException(f"No platform default found for slug: {slug}") return page.to_dict()