fix: Stripe billing portal auto-config programmatique, separation changer-forfait vs gerer-facturation
Some checks failed
Deploy to Production / Build and Deploy (push) Has been cancelled

This commit is contained in:
2026-05-31 22:45:45 +02:00
parent 00c54997bf
commit ab296ea259
2 changed files with 88 additions and 11 deletions

View File

@@ -278,13 +278,22 @@ export default function ProfilePage() {
</div>
{!isFreePlan && (
<button
onClick={handleBillingPortal}
disabled={loadingPortal}
className="relative z-10 px-10 py-4 bg-white text-brand-dark rounded-xl font-black text-[10px] uppercase tracking-widest shadow-xl hover:scale-105 transition-all shrink-0 disabled:opacity-50"
>
{loadingPortal ? <RefreshCw className="w-4 h-4 animate-spin" /> : t('profile.subscription.changePlan')}
</button>
<div className="relative z-10 flex flex-col sm:flex-row gap-3">
{/* Change plan → pricing page */}
<Link href="/pricing">
<button className="px-10 py-4 bg-white text-brand-dark rounded-xl font-black text-[10px] uppercase tracking-widest shadow-xl hover:scale-105 transition-all shrink-0">
{t('profile.subscription.changePlan')}
</button>
</Link>
{/* Billing portal → invoices / cancel / payment method */}
<button
onClick={handleBillingPortal}
disabled={loadingPortal}
className="px-10 py-4 bg-white/10 border border-white/20 text-white rounded-xl font-black text-[10px] uppercase tracking-widest hover:bg-white/20 transition-all shrink-0 disabled:opacity-50"
>
{loadingPortal ? <RefreshCw className="w-4 h-4 animate-spin inline" /> : t('profile.subscription.manageBilling')}
</button>
</div>
)}
{isFreePlan && (
<Link href="/pricing">

View File

@@ -434,6 +434,65 @@ async def cancel_subscription(user_id: str) -> Dict[str, Any]:
return {"error": str(e)}
# ─── cached portal config id (created once per process) ───────────────────────
_PORTAL_CONFIG_ID: Optional[str] = None
def _get_or_create_portal_config() -> Optional[str]:
"""
Return a Stripe Customer Portal configuration ID.
Creates one programmatically if none exists yet, so no Stripe Dashboard
manual setup is required.
"""
global _PORTAL_CONFIG_ID
if _PORTAL_CONFIG_ID:
return _PORTAL_CONFIG_ID
try:
# Re-use the first existing configuration if there is one
existing = stripe.billing_portal.Configuration.list(active=True, limit=1)
if existing.data:
_PORTAL_CONFIG_ID = existing.data[0].id
return _PORTAL_CONFIG_ID
# Create a minimal configuration programmatically
config = stripe.billing_portal.Configuration.create(
business_profile={
"headline": "Wordly.art — Gérer mon abonnement",
},
features={
"invoice_history": {"enabled": True},
"payment_method_update": {"enabled": True},
"subscription_cancel": {
"enabled": True,
"mode": "at_period_end", # keeps access until end of paid period
"proration_behavior": "none",
"cancellation_reason": {
"enabled": True,
"options": [
"too_expensive",
"missing_features",
"switched_service",
"unused",
"other",
],
},
},
"customer_update": {
"enabled": True,
"allowed_updates": ["email", "name", "address"],
},
},
)
_PORTAL_CONFIG_ID = config.id
logger.info("Stripe portal configuration created: %s", _PORTAL_CONFIG_ID)
return _PORTAL_CONFIG_ID
except Exception as e:
logger.error("Failed to get/create Stripe portal config: %s", e)
return None
async def get_billing_portal_url(user_id: str) -> Optional[str]:
"""Get Stripe billing portal URL for customer"""
if not is_stripe_configured():
@@ -444,11 +503,20 @@ async def get_billing_portal_url(user_id: str) -> Optional[str]:
return None
try:
session = stripe.billing_portal.Session.create(
customer=user.stripe_customer_id,
return_url=f"{os.getenv('FRONTEND_URL', 'http://localhost:3000')}/dashboard/profile?tab=subscription"
)
config_id = _get_or_create_portal_config()
kwargs: Dict[str, Any] = {
"customer": user.stripe_customer_id,
"return_url": (
f"{os.getenv('FRONTEND_URL', 'http://localhost:3000')}"
"/dashboard/profile?tab=subscription"
),
}
if config_id:
kwargs["configuration"] = config_id
session = stripe.billing_portal.Session.create(**kwargs)
return session.url
except Exception as e:
logger.error("get_billing_portal_url error: %s", e)
return None