Files
orion/app/modules/billing/routes/pages/platform.py
Samir Boulahtit ef9ea29643 feat: module-driven onboarding system + simplified 3-step signup
Add OnboardingProviderProtocol so modules declare their own post-signup
onboarding steps. The core OnboardingAggregator discovers enabled
providers and exposes a dashboard API (GET /dashboard/onboarding).
A session-scoped banner on the store dashboard shows a checklist that
guides merchants through setup without blocking signup.

Signup is simplified from 4 steps to 3 (Plan → Account → Payment):
store creation is merged into account creation, store language is
captured from the user's browsing language, and platform-specific
template branching is removed.

Includes 47 unit and integration tests covering all new providers,
the aggregator, the API endpoint, and the signup service changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 23:39:42 +01:00

144 lines
4.4 KiB
Python

# app/modules/billing/routes/pages/platform.py
"""
Billing Platform Page Routes (HTML rendering).
Platform (unauthenticated) pages for pricing and signup:
- Pricing page
- Signup wizard
- Signup success
"""
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import HTMLResponse
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.modules.core.utils.page_context import get_platform_context
from app.templates_config import templates
def _require_platform(request: Request):
"""Get the current platform or raise 404. Platform must always be known."""
platform = getattr(request.state, "platform", None)
if not platform:
raise HTTPException(
status_code=404,
detail="Platform not detected. Pricing and signup require a known platform.",
)
return platform
router = APIRouter()
def _get_tiers_data(db: Session, platform_id: int) -> list[dict]:
"""Build tier data for display in templates from database."""
from app.modules.billing.models import SubscriptionTier, TierCode
query = db.query(SubscriptionTier).filter(
SubscriptionTier.is_active == True,
SubscriptionTier.is_public == True,
SubscriptionTier.platform_id == platform_id,
)
tiers_db = query.order_by(SubscriptionTier.display_order).all()
tiers = []
for tier in tiers_db:
feature_codes = sorted(tier.get_feature_codes())
tiers.append({
"code": tier.code,
"name": tier.name,
"price_monthly": tier.price_monthly_cents / 100,
"price_annual": (tier.price_annual_cents / 100) if tier.price_annual_cents else None,
"feature_codes": feature_codes,
"products_limit": tier.get_limit_for_feature("products_limit"),
"orders_per_month": tier.get_limit_for_feature("orders_per_month"),
"team_members": tier.get_limit_for_feature("team_members"),
"is_popular": tier.code == TierCode.PROFESSIONAL.value,
"is_enterprise": tier.code == TierCode.ENTERPRISE.value,
})
return tiers
# ============================================================================
# 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.
Tiers are filtered by the current platform (detected from domain/path).
"""
platform = _require_platform(request)
context = get_platform_context(request, db)
context["tiers"] = _get_tiers_data(db, platform_id=platform.id)
context["page_title"] = "Pricing"
return templates.TemplateResponse(
"billing/platform/pricing.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.
Routes to platform-specific signup templates. Each platform defines
its own signup flow (different steps, different UI).
Query params:
- tier: Pre-selected tier code
- annual: Pre-select annual billing
"""
platform = _require_platform(request)
context = get_platform_context(request, db)
context["page_title"] = "Start Your Free Trial"
context["selected_tier"] = tier
context["is_annual"] = annual
context["tiers"] = _get_tiers_data(db, platform_id=platform.id)
return templates.TemplateResponse(
"billing/platform/signup.html",
context,
)
@router.get(
"/signup/success", response_class=HTMLResponse, name="platform_signup_success"
)
async def signup_success_page(
request: Request,
store_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 Orion!"
context["store_code"] = store_code
return templates.TemplateResponse(
"billing/platform/signup-success.html",
context,
)