diff --git a/frontend/src/app/dashboard/profile/page.tsx b/frontend/src/app/dashboard/profile/page.tsx index c25b2cd..fef0f32 100644 --- a/frontend/src/app/dashboard/profile/page.tsx +++ b/frontend/src/app/dashboard/profile/page.tsx @@ -278,13 +278,22 @@ export default function ProfilePage() { {!isFreePlan && ( - +
+ {/* Change plan → pricing page */} + + + + {/* Billing portal → invoices / cancel / payment method */} + +
)} {isFreePlan && ( diff --git a/services/payment_service.py b/services/payment_service.py index c622495..1fa699a 100644 --- a/services/payment_service.py +++ b/services/payment_service.py @@ -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 +