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:
2026-02-28 19:16:14 +01:00
parent 2078ce35b2
commit 9684747d08
12 changed files with 1723 additions and 689 deletions

View File

@@ -0,0 +1,75 @@
#!/usr/bin/env python3
"""
Sync Subscription Tiers to Stripe.
Creates Stripe Products and Prices for all subscription tiers that don't
have them yet. Safe to run multiple times (idempotent).
Usage:
python scripts/seed/sync_stripe_products.py
"""
import sys
from pathlib import Path
# Add project root to path
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
from app.core.config import settings
from app.core.database import SessionLocal
from app.modules.billing.models.subscription import SubscriptionTier
from app.modules.billing.services.admin_subscription_service import (
AdminSubscriptionService,
)
def main():
if not settings.stripe_secret_key:
print("ERROR: STRIPE_SECRET_KEY not configured. Set it in .env")
sys.exit(1)
db = SessionLocal()
try:
tiers = db.query(SubscriptionTier).order_by(
SubscriptionTier.platform_id,
SubscriptionTier.display_order,
).all()
if not tiers:
print("No subscription tiers found in database.")
return
print(f"Found {len(tiers)} tiers to sync.\n")
synced = 0
skipped = 0
for tier in tiers:
already_synced = (
tier.stripe_product_id
and tier.stripe_price_monthly_id
and (tier.stripe_price_annual_id or not tier.price_annual_cents)
)
print(f" [{tier.code}] {tier.name} (platform_id={tier.platform_id})")
if already_synced:
print(f" -> Already synced (product={tier.stripe_product_id})")
skipped += 1
continue
AdminSubscriptionService._sync_tier_to_stripe(db, tier)
db.commit()
synced += 1
print(f" -> Product: {tier.stripe_product_id}")
print(f" -> Monthly: {tier.stripe_price_monthly_id}")
if tier.stripe_price_annual_id:
print(f" -> Annual: {tier.stripe_price_annual_id}")
print(f"\nDone. Synced: {synced}, Already up-to-date: {skipped}")
finally:
db.close()
if __name__ == "__main__":
main()