feat(billing): end-to-end Stripe subscription signup with platform enforcement
Move core signup service from marketplace to billing module, add automatic Stripe product/price sync for tiers, create loyalty-specific signup wizard, and enforce that platform is always explicitly known (no silent defaulting to primary/hardcoded ID). Key changes: - New billing SignupService with separated account/store creation steps - Stripe auto-sync on tier create/update (new prices, archive old) - Loyalty signup template (Plan → Account → Store → Payment) - platform_code is now required throughout the signup flow - Pricing/signup pages return 404 if platform not detected - OMS-specific logic (Letzshop claiming) stays in marketplace module - Bootstrap script: scripts/seed/sync_stripe_products.py Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,7 @@ Platform (unauthenticated) pages for pricing and signup:
|
||||
- Signup success
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -16,22 +16,30 @@ 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) -> list[dict]:
|
||||
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
|
||||
|
||||
tiers_db = (
|
||||
db.query(SubscriptionTier)
|
||||
.filter(
|
||||
SubscriptionTier.is_active == True,
|
||||
SubscriptionTier.is_public == True,
|
||||
)
|
||||
.order_by(SubscriptionTier.display_order)
|
||||
.all()
|
||||
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:
|
||||
@@ -63,9 +71,12 @@ async def pricing_page(
|
||||
):
|
||||
"""
|
||||
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)
|
||||
context["tiers"] = _get_tiers_data(db, platform_id=platform.id)
|
||||
context["page_title"] = "Pricing"
|
||||
|
||||
return templates.TemplateResponse(
|
||||
@@ -89,18 +100,28 @@ async def signup_page(
|
||||
"""
|
||||
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)
|
||||
context["tiers"] = _get_tiers_data(db, platform_id=platform.id)
|
||||
|
||||
# Route to platform-specific signup template
|
||||
if platform.code == "loyalty":
|
||||
template_name = "billing/platform/signup-loyalty.html"
|
||||
else:
|
||||
template_name = "billing/platform/signup.html"
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"billing/platform/signup.html",
|
||||
template_name,
|
||||
context,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user