feat: platform-aware storefront routing and billing improvements

Overhaul storefront URL routing to be platform-aware:
- Dev: /platforms/{code}/storefront/{store_code}/
- Prod: subdomain.platform.lu/ (internally rewritten to /storefront/)
- Add subdomain detection in PlatformContextMiddleware
- Add /storefront/ path rewrite for prod mode (subdomain/custom domain)
- Remove all silent platform fallbacks (platform_id=1)
- Add require_platform dependency for clean endpoint validation
- Update route registration, templates, module definitions, base_url calc
- Update StoreContextMiddleware for /storefront/ path detection
- Remove /stores/ from FrontendDetector STOREFRONT_PATH_PREFIXES

Billing service improvements:
- Add store_platform_sync_service to keep store_platforms in sync
- Make tier lookups platform-aware across billing services
- Add tiers for all platforms in seed data
- Add demo subscriptions to seed

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-23 23:42:41 +01:00
parent d36783a7f1
commit 32acc76b49
56 changed files with 951 additions and 306 deletions

57
main.py
View File

@@ -22,7 +22,7 @@ from datetime import UTC, datetime
from pathlib import Path
import sentry_sdk
from fastapi import Depends, FastAPI, HTTPException, Request, Response
from fastapi import Depends, FastAPI, Response
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
@@ -435,13 +435,13 @@ for route_info in store_page_routes:
# STOREFRONT PAGES (Customer Shop)
# =============================================================================
# Customer shop pages - Register at TWO prefixes:
# 1. /storefront/* (for subdomain/custom domain modes)
# 2. /stores/{code}/storefront/* (for path-based development mode)
# 1. /storefront/* (for prod: subdomain/custom domain, after path rewrite by middleware)
# 2. /storefront/{store_code}/* (for dev: path-based, after /platforms/{code}/ strip)
logger.info("Auto-discovering storefront page routes...")
storefront_page_routes = get_storefront_page_routes()
logger.info(f" Found {len(storefront_page_routes)} storefront page route modules")
# Register at /storefront/* (direct access)
# Register at /storefront/* (prod mode — middleware rewrites /products → /storefront/products)
logger.info(" Registering storefront routes at /storefront/*")
for route_info in storefront_page_routes:
prefix = f"/storefront{route_info.custom_prefix}" if route_info.custom_prefix else "/storefront"
@@ -453,10 +453,10 @@ for route_info in storefront_page_routes:
include_in_schema=False,
)
# Register at /stores/{code}/storefront/* (path-based development mode)
logger.info(" Registering storefront routes at /stores/{code}/storefront/*")
# Register at /storefront/{store_code}/* (dev mode — /platforms/oms/storefront/WIZATECH/...)
logger.info(" Registering storefront routes at /storefront/{store_code}/*")
for route_info in storefront_page_routes:
prefix = f"/stores/{{store_code}}/storefront{route_info.custom_prefix}" if route_info.custom_prefix else "/stores/{store_code}/storefront"
prefix = f"/storefront/{{store_code}}{route_info.custom_prefix}" if route_info.custom_prefix else "/storefront/{store_code}"
app.include_router(
route_info.router,
prefix=prefix,
@@ -465,49 +465,6 @@ for route_info in storefront_page_routes:
)
# Add handler for /stores/{store_code}/ root path
@app.get(
"/stores/{store_code}/", response_class=HTMLResponse, include_in_schema=False
)
async def store_root_path(
store_code: str, request: Request, db: Session = Depends(get_db)
):
"""Handle store root path (e.g., /stores/orion/)"""
# Store should already be in request.state from middleware
store = getattr(request.state, "store", None)
platform = getattr(request.state, "platform", None)
if not store:
raise HTTPException(status_code=404, detail=f"Store '{store_code}' not found")
from app.modules.cms.services import content_page_service
from app.modules.core.utils.page_context import get_storefront_context
# Get platform_id (use platform from context or default to 1 for OMS)
platform_id = platform.id if platform else 1
# Try to find landing page (with three-tier resolution)
landing_page = content_page_service.get_page_for_store(
db, platform_id=platform_id, slug="landing", store_id=store.id, include_unpublished=False
)
if not landing_page:
landing_page = content_page_service.get_page_for_store(
db, platform_id=platform_id, slug="home", store_id=store.id, include_unpublished=False
)
if landing_page:
# Render landing page with selected template
template_name = landing_page.template or "default"
template_path = f"store/landing-{template_name}.html"
return templates.TemplateResponse(
template_path, get_storefront_context(request, db=db, page=landing_page)
)
# No landing page - redirect to shop
return RedirectResponse(url=f"/stores/{store_code}/storefront/", status_code=302)
# ============================================================================
# PLATFORM ROUTING (via PlatformContextMiddleware)
#