Files
orion/app/routes/platform_pages.py
Samir Boulahtit b9f08b853f refactor: clean up legacy models and migrate remaining schemas
Delete empty stub files from models/database/:
- audit.py, backup.py, configuration.py, monitoring.py
- notification.py, payment.py, search.py, task.py

Delete re-export files:
- models/database/subscription.py → app.modules.billing.models
- models/database/architecture_scan.py → app.modules.dev_tools.models
- models/database/test_run.py → app.modules.dev_tools.models
- models/schema/subscription.py → app.modules.billing.schemas
- models/schema/marketplace.py (empty)
- models/schema/monitoring.py (empty)

Migrate schemas to canonical module locations:
- billing.py → app/modules/billing/schemas/
- vendor_product.py → app/modules/catalog/schemas/
- homepage_sections.py → app/modules/cms/schemas/

Keep as CORE (framework-level, used everywhere):
- models/schema/: admin, auth, base, company, email, image, media, team, vendor*
- models/database/: admin*, base, company, email, feature, media, platform*, user, vendor*

Update 30+ files to use canonical import locations.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 18:45:46 +01:00

436 lines
15 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 app.templates_config import templates
from sqlalchemy.orm import Session
from app.core.config import settings
from app.core.database import get_db
from app.modules.cms.services 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 moved to app.templates_config
# templates imported from app.templates_config
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.storefront_pages import get_storefront_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_storefront_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
# Include subscription tiers for pricing section
from app.modules.billing.models 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
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 app.modules.billing.models 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 app.modules.billing.models 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 app.modules.billing.models 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,
)