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>
144 lines
4.4 KiB
Python
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,
|
|
)
|