Files
orion/app/routes/platform_pages.py
Samir Boulahtit 3d3b8cae22 feat: add platform detail/edit admin UI and service enhancements
- Add platform detail and edit admin pages with templates and JS
- Add ContentPageService methods: list_all_platform_pages, list_all_vendor_defaults
- Deprecate /admin/platform-homepage route (redirects to /admin/platforms)
- Add migration to fix content_page nullable columns
- Refine platform and vendor context middleware
- Add platform context middleware unit tests
- Update platforms.js with improved functionality
- Add section-based homepage plan documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 14:08:02 +01:00

417 lines
14 KiB
Python

# app/routes/platform_pages.py
"""
Platform public page routes.
These routes serve the marketing homepage, pricing page,
Letzshop vendor finder, and signup wizard.
"""
import logging
from pathlib import Path
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session
from app.core.config import settings
from app.core.database import get_db
from app.services.content_page_service import content_page_service
from app.utils.i18n import get_jinja2_globals
router = APIRouter()
logger = logging.getLogger(__name__)
# Get the templates directory
BASE_DIR = Path(__file__).resolve().parent.parent.parent
TEMPLATES_DIR = BASE_DIR / "app" / "templates"
templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
def get_platform_context(request: Request, db: Session) -> dict:
"""Build context for platform pages."""
# Get language from request state (set by middleware)
language = getattr(request.state, "language", "fr")
# Get platform from middleware (default to OMS platform_id=1)
platform = getattr(request.state, "platform", None)
platform_id = platform.id if platform else 1
# Get translation function
i18n_globals = get_jinja2_globals(language)
context = {
"request": request,
"platform_name": "Wizamart",
"platform_domain": settings.platform_domain,
"stripe_publishable_key": settings.stripe_publishable_key,
"trial_days": settings.stripe_trial_days,
}
# Add i18n globals (_, t, current_language, SUPPORTED_LANGUAGES, etc.)
context.update(i18n_globals)
# Load CMS pages for header, footer, and legal navigation
header_pages = []
footer_pages = []
legal_pages = []
try:
# Platform marketing pages (is_platform_page=True)
header_pages = content_page_service.list_platform_pages(
db, platform_id=platform_id, header_only=True, include_unpublished=False
)
footer_pages = content_page_service.list_platform_pages(
db, platform_id=platform_id, footer_only=True, include_unpublished=False
)
# For legal pages, we need to add footer support or use a different approach
# For now, legal pages come from footer pages with show_in_legal flag
legal_pages = [] # Will be handled separately if needed
logger.debug(
f"Loaded CMS pages: {len(header_pages)} header, {len(footer_pages)} footer, {len(legal_pages)} legal"
)
except Exception as e:
logger.error(f"Failed to load CMS navigation pages: {e}")
context["header_pages"] = header_pages
context["footer_pages"] = footer_pages
context["legal_pages"] = legal_pages
return context
# =============================================================================
# Homepage
# =============================================================================
@router.get("/", response_class=HTMLResponse, name="platform_homepage")
async def homepage(
request: Request,
db: Session = Depends(get_db),
):
"""
Homepage handler.
Handles two scenarios:
1. Vendor on custom domain (vendor.com) → Show vendor landing page or redirect to shop
2. Platform marketing site → Show platform homepage from CMS or default template
URL routing:
- localhost:9999/ → Main marketing site ('main' platform)
- localhost:9999/platforms/oms/ → OMS platform (middleware rewrites to /)
- oms.lu/ → OMS platform (domain-based)
- shop.mycompany.com/ → Vendor landing page (custom domain)
"""
from fastapi.responses import RedirectResponse
# Get platform and vendor from middleware
platform = getattr(request.state, "platform", None)
vendor = getattr(request.state, "vendor", None)
# Scenario 1: Vendor detected (custom domain like vendor.com)
if vendor:
logger.debug(f"[HOMEPAGE] Vendor detected: {vendor.subdomain}")
# Get platform_id (use platform from context or default to 1 for OMS)
platform_id = platform.id if platform else 1
# Try to find vendor landing page (slug='landing' or 'home')
landing_page = content_page_service.get_page_for_vendor(
db, platform_id=platform_id, slug="landing", vendor_id=vendor.id, include_unpublished=False
)
if not landing_page:
landing_page = content_page_service.get_page_for_vendor(
db, platform_id=platform_id, slug="home", vendor_id=vendor.id, include_unpublished=False
)
if landing_page:
# Render landing page with selected template
from app.routes.shop_pages import get_shop_context
template_name = landing_page.template or "default"
template_path = f"vendor/landing-{template_name}.html"
logger.info(f"[HOMEPAGE] Rendering vendor landing page: {template_path}")
return templates.TemplateResponse(
template_path, get_shop_context(request, db=db, page=landing_page)
)
# No landing page - redirect to shop
vendor_context = getattr(request.state, "vendor_context", None)
access_method = (
vendor_context.get("detection_method", "unknown")
if vendor_context
else "unknown"
)
if access_method == "path":
full_prefix = (
vendor_context.get("full_prefix", "/vendor/")
if vendor_context
else "/vendor/"
)
return RedirectResponse(
url=f"{full_prefix}{vendor.subdomain}/shop/", status_code=302
)
# Domain/subdomain - redirect to /shop/
return RedirectResponse(url="/shop/", status_code=302)
# Scenario 2: Platform marketing site (no vendor)
# Load platform homepage from CMS (slug='home')
platform_id = platform.id if platform else 1
cms_homepage = content_page_service.get_platform_page(
db, platform_id=platform_id, slug="home", include_unpublished=False
)
if cms_homepage:
# Use CMS-based homepage with template selection
context = get_platform_context(request, db)
context["page"] = cms_homepage
context["platform"] = platform
template_name = cms_homepage.template or "default"
template_path = f"platform/homepage-{template_name}.html"
logger.info(f"[HOMEPAGE] Rendering CMS homepage with template: {template_path}")
return templates.TemplateResponse(template_path, context)
# Fallback: Default wizamart homepage (no CMS content)
logger.info("[HOMEPAGE] No CMS homepage found, using default wizamart template")
context = get_platform_context(request, db)
context["platform"] = platform
# Fetch tiers for display (use API service internally)
from models.database.subscription import TIER_LIMITS, TierCode
tiers = []
for tier_code, limits in TIER_LIMITS.items():
tiers.append({
"code": tier_code.value,
"name": limits["name"],
"price_monthly": limits["price_monthly_cents"] / 100,
"price_annual": (limits["price_annual_cents"] / 100) if limits.get("price_annual_cents") else None,
"orders_per_month": limits.get("orders_per_month"),
"products_limit": limits.get("products_limit"),
"team_members": limits.get("team_members"),
"features": limits.get("features", []),
"is_popular": tier_code == TierCode.PROFESSIONAL,
"is_enterprise": tier_code == TierCode.ENTERPRISE,
})
context["tiers"] = tiers
# Add-ons (hardcoded for now, will come from DB)
context["addons"] = [
{
"code": "domain",
"name": "Custom Domain",
"description": "Use your own domain (mydomain.com)",
"price": 15,
"billing_period": "year",
"icon": "globe",
},
{
"code": "ssl_premium",
"name": "Premium SSL",
"description": "EV certificate for trust badges",
"price": 49,
"billing_period": "year",
"icon": "shield-check",
},
{
"code": "email",
"name": "Email Package",
"description": "Professional email addresses",
"price": 5,
"billing_period": "month",
"icon": "mail",
"options": [
{"quantity": 5, "price": 5},
{"quantity": 10, "price": 9},
{"quantity": 25, "price": 19},
],
},
]
return templates.TemplateResponse(
"platform/homepage-wizamart.html",
context,
)
# =============================================================================
# Pricing Page
# =============================================================================
@router.get("/pricing", response_class=HTMLResponse, name="platform_pricing")
async def pricing_page(
request: Request,
db: Session = Depends(get_db),
):
"""
Standalone pricing page with detailed tier comparison.
"""
context = get_platform_context(request, db)
# Reuse tier data from homepage
from models.database.subscription import TIER_LIMITS, TierCode
tiers = []
for tier_code, limits in TIER_LIMITS.items():
tiers.append({
"code": tier_code.value,
"name": limits["name"],
"price_monthly": limits["price_monthly_cents"] / 100,
"price_annual": (limits["price_annual_cents"] / 100) if limits.get("price_annual_cents") else None,
"orders_per_month": limits.get("orders_per_month"),
"products_limit": limits.get("products_limit"),
"team_members": limits.get("team_members"),
"order_history_months": limits.get("order_history_months"),
"features": limits.get("features", []),
"is_popular": tier_code == TierCode.PROFESSIONAL,
"is_enterprise": tier_code == TierCode.ENTERPRISE,
})
context["tiers"] = tiers
context["page_title"] = "Pricing"
return templates.TemplateResponse(
"platform/pricing.html",
context,
)
# =============================================================================
# Find Your Shop (Letzshop Vendor Browser)
# =============================================================================
@router.get("/find-shop", response_class=HTMLResponse, name="platform_find_shop")
async def find_shop_page(
request: Request,
db: Session = Depends(get_db),
):
"""
Letzshop vendor browser page.
Allows vendors to search for and claim their Letzshop shop.
"""
context = get_platform_context(request, db)
context["page_title"] = "Find Your Letzshop Shop"
return templates.TemplateResponse(
"platform/find-shop.html",
context,
)
# =============================================================================
# Signup Wizard
# =============================================================================
@router.get("/signup", response_class=HTMLResponse, name="platform_signup")
async def signup_page(
request: Request,
tier: str | None = None,
annual: bool = False,
db: Session = Depends(get_db),
):
"""
Multi-step signup wizard.
Query params:
- tier: Pre-selected tier code
- annual: Pre-select annual billing
"""
context = get_platform_context(request, db)
context["page_title"] = "Start Your Free Trial"
context["selected_tier"] = tier
context["is_annual"] = annual
# Get tiers for tier selection step
from models.database.subscription import TIER_LIMITS, TierCode
tiers = []
for tier_code, limits in TIER_LIMITS.items():
tiers.append({
"code": tier_code.value,
"name": limits["name"],
"price_monthly": limits["price_monthly_cents"] / 100,
"price_annual": (limits["price_annual_cents"] / 100) if limits.get("price_annual_cents") else None,
"orders_per_month": limits.get("orders_per_month"),
"team_members": limits.get("team_members"),
"is_enterprise": tier_code == TierCode.ENTERPRISE,
})
context["tiers"] = tiers
return templates.TemplateResponse(
"platform/signup.html",
context,
)
@router.get("/signup/success", response_class=HTMLResponse, name="platform_signup_success")
async def signup_success_page(
request: Request,
vendor_code: str | None = None,
db: Session = Depends(get_db),
):
"""
Signup success page.
Shown after successful account creation.
"""
context = get_platform_context(request, db)
context["page_title"] = "Welcome to Wizamart!"
context["vendor_code"] = vendor_code
return templates.TemplateResponse(
"platform/signup-success.html",
context,
)
# =============================================================================
# Generic Content Pages (CMS)
# =============================================================================
# IMPORTANT: This route must be LAST as it catches all /{slug} URLs
@router.get("/{slug}", response_class=HTMLResponse, name="platform_content_page")
async def content_page(
request: Request,
slug: str,
db: Session = Depends(get_db),
):
"""
Serve CMS content pages (about, contact, faq, privacy, terms, etc.).
This is a catch-all route for dynamic content pages managed via the admin CMS.
Platform pages have vendor_id=None and is_platform_page=True.
"""
# Get platform from middleware (default to OMS platform_id=1)
platform = getattr(request.state, "platform", None)
platform_id = platform.id if platform else 1
# Load platform marketing page from database
page = content_page_service.get_platform_page(
db, platform_id=platform_id, slug=slug, include_unpublished=False
)
if not page:
raise HTTPException(status_code=404, detail=f"Page not found: {slug}")
context = get_platform_context(request, db)
context["page"] = page
context["page_title"] = page.title
return templates.TemplateResponse(
"platform/content-page.html",
context,
)