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
+