diff options
| author | Ben Sima <ben@bsima.me> | 2025-11-13 15:56:31 -0500 |
|---|---|---|
| committer | Ben Sima <ben@bsima.me> | 2025-11-13 15:56:31 -0500 |
| commit | 1ad2f8b234bcf227605f285450a41f40a0e0e3a7 (patch) | |
| tree | 56d43d101c257dfbf3867ccac3e4c5e7470179a7 | |
| parent | abc8230c04a787045a599327455445585edff46a (diff) | |
Add error handling for unconfigured Stripe billing portal
- Catch Stripe exceptions when portal not configured - Redirect to
account page with user-friendly error message - Display error alert
on account page when present - Change portal return URL to /account
instead of /
Fixes issue when Stripe billing portal settings haven't been configured
in test/production dashboard.
Amp-Thread-ID:
https://ampcode.com/threads/T-8edacbeb-b343-49ca-b524-1c999272acb6
Co-authored-by: Amp <amp@ampcode.com>
| -rw-r--r-- | .tasks/tasks.jsonl | 6 | ||||
| -rw-r--r-- | Biz/PodcastItLater/Billing.py | 25 | ||||
| -rw-r--r-- | Biz/PodcastItLater/Web.py | 34 |
3 files changed, 54 insertions, 11 deletions
diff --git a/.tasks/tasks.jsonl b/.tasks/tasks.jsonl index 3d0d504..9cf17f5 100644 --- a/.tasks/tasks.jsonl +++ b/.tasks/tasks.jsonl @@ -55,11 +55,11 @@ {"taskCreatedAt":"2025-11-13T19:38:33.491331064Z","taskDependencies":[],"taskId":"t-1fbABoD","taskNamespace":null,"taskParent":"t-1f9QP23","taskStatus":"Done","taskTitle":"Extract extract_og_metadata and send_magic_link to Core module for reusability","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T19:46:04.679290775Z"} {"taskCreatedAt":"2025-11-13T19:38:33.674140035Z","taskDependencies":[],"taskId":"t-1fbBmXa","taskNamespace":null,"taskParent":"t-1f9QP23","taskStatus":"Done","taskTitle":"Review and fix type: ignore comments - improve type safety","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T19:47:09.583640045Z"} {"taskCreatedAt":"2025-11-13T19:38:33.85804778Z","taskDependencies":[],"taskId":"t-1fbC8Nq","taskNamespace":null,"taskParent":"t-1f9QP23","taskStatus":"Done","taskTitle":"Remove PLR2004 magic number - use constant for month check","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T19:47:45.120428021Z"} -{"taskCreatedAt":"2025-11-13T19:38:34.035597081Z","taskDependencies":[],"taskId":"t-1fbCSZd","taskNamespace":null,"taskParent":"t-1f9RIzd","taskStatus":"InProgress","taskTitle":"Implement cancel subscription functionality","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T20:20:28.494715433Z"} +{"taskCreatedAt":"2025-11-13T19:38:34.035597081Z","taskDependencies":[],"taskId":"t-1fbCSZd","taskNamespace":null,"taskParent":"t-1f9RIzd","taskStatus":"Done","taskTitle":"Implement cancel subscription functionality","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T20:22:51.709672316Z"} {"taskCreatedAt":"2025-11-13T19:38:34.194926176Z","taskDependencies":[],"taskId":"t-1fbDyr2","taskNamespace":null,"taskParent":"t-1f9RIzd","taskStatus":"Open","taskTitle":"Implement delete account functionality","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T19:38:34.194926176Z"} {"taskCreatedAt":"2025-11-13T19:38:34.384489707Z","taskDependencies":[],"taskId":"t-1fbElKv","taskNamespace":null,"taskParent":"t-1f9RIzd","taskStatus":"Open","taskTitle":"Implement change email address functionality","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T19:38:34.384489707Z"} -{"taskCreatedAt":"2025-11-13T19:38:34.561871604Z","taskDependencies":[],"taskId":"t-1fbF5Tv","taskNamespace":null,"taskParent":"t-1f9RIzd","taskStatus":"InProgress","taskTitle":"Add logout button to account page","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T20:20:28.242735917Z"} -{"taskCreatedAt":"2025-11-13T19:38:34.777721397Z","taskDependencies":[],"taskId":"t-1fbG02X","taskNamespace":null,"taskParent":"t-1f9RIzd","taskStatus":"InProgress","taskTitle":"Replace Coming Soon placeholder with full account management UI","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T20:20:27.967202307Z"} +{"taskCreatedAt":"2025-11-13T19:38:34.561871604Z","taskDependencies":[],"taskId":"t-1fbF5Tv","taskNamespace":null,"taskParent":"t-1f9RIzd","taskStatus":"Done","taskTitle":"Add logout button to account page","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T20:22:51.65796855Z"} +{"taskCreatedAt":"2025-11-13T19:38:34.777721397Z","taskDependencies":[],"taskId":"t-1fbG02X","taskNamespace":null,"taskParent":"t-1f9RIzd","taskStatus":"Done","taskTitle":"Replace Coming Soon placeholder with full account management UI","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T20:22:51.606196024Z"} {"taskCreatedAt":"2025-11-13T19:38:34.962196629Z","taskDependencies":[],"taskId":"t-1fbGM2m","taskNamespace":null,"taskParent":"t-1f9SnU7","taskStatus":"Done","taskTitle":"Add remove button to queue status items","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T20:20:10.941908917Z"} {"taskCreatedAt":"2025-11-13T19:38:35.119686179Z","taskDependencies":[],"taskId":"t-1fbHr0w","taskNamespace":null,"taskParent":"t-1f9Td4U","taskStatus":"Done","taskTitle":"Remove button classes from navbar links (make them regular nav links)","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T20:18:17.185088389Z"} {"taskCreatedAt":"2025-11-13T19:38:35.311151364Z","taskDependencies":[],"taskId":"t-1fbIeOF","taskNamespace":null,"taskParent":"t-1f9Td4U","taskStatus":"Done","taskTitle":"Remove 'Logged in as' text from navbar","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T20:18:17.23552934Z"} diff --git a/Biz/PodcastItLater/Billing.py b/Biz/PodcastItLater/Billing.py index 0e2537a..bf907bf 100644 --- a/Biz/PodcastItLater/Billing.py +++ b/Biz/PodcastItLater/Billing.py @@ -201,7 +201,7 @@ def create_portal_session(user_id: int, base_url: str) -> str: Portal session URL to redirect user to Raises: - ValueError: If user has no Stripe customer ID + ValueError: If user has no Stripe customer ID or portal not configured """ user = Core.Database.get_user_by_id(user_id) if not user: @@ -212,12 +212,25 @@ def create_portal_session(user_id: int, base_url: str) -> str: msg = "User has no Stripe customer ID" raise ValueError(msg) - session = stripe.billing_portal.Session.create( - customer=user["stripe_customer_id"], - return_url=f"{base_url}/", - ) + try: + session = stripe.billing_portal.Session.create( + customer=user["stripe_customer_id"], + return_url=f"{base_url}/account", + ) + except Exception as e: + # Catch Stripe errors (portal not configured, etc.) + logger.exception("Stripe portal error") + msg = ( + "Billing portal not configured. " + "Please contact support or cancel via your account page." + ) + raise ValueError(msg) from e - logger.info("Created portal session for user %s: %s", user_id, session.id) + logger.info( + "Created portal session for user %s: %s", + user_id, + session.id, + ) return session.url diff --git a/Biz/PodcastItLater/Web.py b/Biz/PodcastItLater/Web.py index af603e2..c2f957a 100644 --- a/Biz/PodcastItLater/Web.py +++ b/Biz/PodcastItLater/Web.py @@ -1079,6 +1079,9 @@ def account_page(request: Request) -> html.html | RedirectResponse: subscription_status = user.get("subscription_status", "") cancel_at_period_end = user.get("cancel_at_period_end", 0) == 1 + # Get error message from query params + error_message = request.query_params.get("error") + return html.html( html.head( html.meta(charset="utf-8"), @@ -1128,6 +1131,28 @@ def account_page(request: Request) -> html.html | RedirectResponse: ], ), ), + # Error alert + html.div( + html.div( + html.i( + classes=[ + "bi", + "bi-exclamation-triangle-fill", + "me-2", + ], + ), + error_message or "", + classes=[ + "alert", + "alert-danger", + "d-flex", + "align-items-center", + ], + role="alert", # type: ignore[call-arg] + ), + ) + if error_message + else html.div(), # Account info section html.div( html.h4( @@ -1498,7 +1523,7 @@ def billing_checkout(request: Request, data: FormData) -> Response: @app.post("/billing/portal") -def billing_portal(request: Request) -> Response: +def billing_portal(request: Request) -> Response | RedirectResponse: """Create Stripe Billing Portal session.""" user_id = request.session.get("user_id") if not user_id: @@ -1509,7 +1534,12 @@ def billing_portal(request: Request) -> Response: return RedirectResponse(url=portal_url, status_code=303) except ValueError as e: logger.exception("Portal error") - return Response(f"Error: {e!s}", status_code=400) + # Redirect back to account page with error message + error_msg = str(e) + return RedirectResponse( + url=f"/account?error={urllib.parse.quote(error_msg)}", + status_code=303, + ) @app.post("/stripe/webhook") |
