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:
75
scripts/seed/sync_stripe_products.py
Normal file
75
scripts/seed/sync_stripe_products.py
Normal 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()
|
||||
Reference in New Issue
Block a user