Skip to content

feat(billing): switch to Stripe Checkout for subscriptions - #320

Merged
steebchen merged 1 commit into
mainfrom
feat/checkout
Jun 9, 2025
Merged

steebchen merged 1 commit into
mainfrom
feat/checkout

Conversation

@steebchen

@steebchen steebchen commented Jun 8, 2025

Copy link
Copy Markdown
Member

Replaced the manual subscription and payment logic with Stripe Checkout. Simplified subscription handling in both API and UI, ensuring a seamless upgrade flow. Added success and cancellation handling in the billing UI.

Summary by CodeRabbit

  • New Features

    • Added automatic toast notifications on the billing settings page after returning from Stripe Checkout, indicating successful or canceled payments.
  • Enhancements

    • Updated the subscription upgrade process to use Stripe Checkout, redirecting users to a secure payment page rather than handling payments directly within the app.
    • Improved dashboard sidebar link to clear payment status query parameters when navigating to billing settings.
  • Bug Fixes

    • Simplified and improved error handling for subscription creation and payment processes.
  • Chores

    • Updated environment variable examples to include the UI URL.

@coderabbitai

coderabbitai Bot commented Jun 8, 2025

Copy link
Copy Markdown
Contributor
## Walkthrough

The subscription upgrade and billing flow was refactored to use Stripe Checkout sessions instead of direct Stripe API and client-side payment confirmation. Server-side logic now creates a Checkout session and returns its URL, while client-side code redirects users to Stripe for payment. Webhook handling was added for post-checkout subscription updates.

## Changes

| File(s)                                                                                      | Change Summary                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
|---------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| apps/api/.env.example                                                                       | Added `UI_URL` variable set to `https://llmgateway.io`.                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| apps/api/src/routes/subscriptions.ts                                                        | Refactored pro subscription creation to use Stripe Checkout sessions; removed direct Stripe subscription logic, updated response schema to return `checkoutUrl`.                                                                                                                                                                                                                                                                                                                                   |
| apps/api/src/stripe.ts                                                                      | Added handler for `checkout.session.completed` Stripe webhook event to update organization plan, record transaction, and track analytics.                                                                                                                                                                                                                                                                                                                                                          |
| apps/ui/src/components/billing/plan-management.tsx                                          | Removed Stripe SDK payment handling; now redirects user to Stripe Checkout via received `checkoutUrl` and updates UI text accordingly.                                                                                                                                                                                                                                                                                                                                                             |
| apps/ui/src/components/landing/pricing-plans.tsx                                            | Changed subscription creation to expect and use a `checkoutUrl` for redirecting to Stripe Checkout; removed client-side payment status handling.                                                                                                                                                                                                                                                                                                                                                   |
| apps/ui/src/lib/api/v1.d.ts                                                                 | Updated API response schema for `/subscriptions/create-pro-subscription` to return `{ checkoutUrl: string }` and changed response description.                                                                                                                                                                                                                                                                                                                                                     |
| apps/ui/src/routes/dashboard/_layout/settings/billing.tsx                                   | Added validation for `success` and `canceled` query parameters; displays toast notifications based on checkout outcome and clears query after showing toast.                                                                                                                                                                                                                                                                                                                                       |
| apps/ui/src/components/dashboard/dashboard-sidebar.tsx                                     | Modified "Upgrade" link to clear `success` and `canceled` query parameters when navigating to billing settings.                                                                                                                                                                                                                                                                                                                                                                                     |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant UI (Frontend)
    participant API (Backend)
    participant Stripe

    User->>UI (Frontend): Click "Upgrade to Pro"
    UI (Frontend)->>API (Backend): POST /subscriptions/create-pro-subscription
    API (Backend)->>Stripe: Create Checkout Session
    Stripe-->>API (Backend): Checkout Session URL
    API (Backend)-->>UI (Frontend): { checkoutUrl }
    UI (Frontend)->>User: Redirect to Stripe Checkout
    User->>Stripe: Complete payment
    Stripe-->>API (Backend): Webhook: checkout.session.completed
    API (Backend)->>DB: Update org plan, record transaction
    API (Backend)->>Analytics: Track subscription event
    API (Backend)-->>UI (Frontend): (via query param) success/canceled
    UI (Frontend)->>User: Show toast notification

Suggested reviewers

  • smakosh

<!-- walkthrough_end -->


---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 277966f1c46f14e0535c670a7d982e8ae10eaf2c and cc59352749889e0156a0c1e5d27fdba6dece7c4e.

</details>

<details>
<summary>📒 Files selected for processing (8)</summary>

* `apps/api/.env.example` (1 hunks)
* `apps/api/src/routes/subscriptions.ts` (4 hunks)
* `apps/api/src/stripe.ts` (2 hunks)
* `apps/ui/src/components/billing/plan-management.tsx` (2 hunks)
* `apps/ui/src/components/dashboard/dashboard-sidebar.tsx` (1 hunks)
* `apps/ui/src/components/landing/pricing-plans.tsx` (1 hunks)
* `apps/ui/src/lib/api/v1.d.ts` (1 hunks)
* `apps/ui/src/routes/dashboard/_layout/settings/billing.tsx` (2 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* apps/ui/src/components/dashboard/dashboard-sidebar.tsx

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (7)</summary>

* apps/api/.env.example
* apps/ui/src/routes/dashboard/_layout/settings/billing.tsx
* apps/ui/src/lib/api/v1.d.ts
* apps/api/src/stripe.ts
* apps/ui/src/components/landing/pricing-plans.tsx
* apps/api/src/routes/subscriptions.ts
* apps/ui/src/components/billing/plan-management.tsx

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: build / run
* GitHub Check: e2e / run

</details>

</details>
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=theopenco/llmgateway&utm_content=320):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🔭 Outside diff range comments (1)
apps/api/src/routes/subscriptions.ts (1)

166-168: ⚠️ Potential issue

Remove or justify the artificial delay in webhook processing.

The 5-second delay after updating the subscription seems arbitrary and could negatively impact user experience. Webhook processing should be handled asynchronously without blocking the API response.

Apply this diff to remove the delay:

-// let the webhook handler the rest to unify the logic
-await new Promise((resolve) => {
-  setTimeout(resolve, 5000);
-});
+// Webhook will handle the database updates asynchronously

The same issue appears in the resumeProSubscription function at lines 248-251.

🧹 Nitpick comments (3)
apps/api/.env.example (1)

8-8: Consider adding environment-specific guidance for UI_URL.

The hardcoded production URL might not be suitable for development or staging environments. Consider adding a comment indicating that this should be adjusted per environment.

+# Base URL for the frontend application (adjust per environment)
+# Development: http://localhost:3000
+# Production: https://llmgateway.io
 UI_URL=https://llmgateway.io
apps/ui/src/components/billing/plan-management.tsx (1)

256-264: Consider preserving the subscription dialog state during errors.

When an error occurs, the dialog remains open but the user might lose context after seeing the error toast. Since the code now redirects on success, the onSuccess callback is never called, which could leave the dialog in an inconsistent state if the user navigates back from Stripe.

Do you want me to help implement a more robust error handling approach that considers the dialog lifecycle and browser navigation?

apps/api/src/routes/subscriptions.ts (1)

90-95: Consider adding customer email to the checkout session.

The checkout session is created without specifying the customer email. While the customer ID is provided, including the email can improve the checkout experience by pre-filling the email field.

#!/bin/bash
# Check if customer email is available in the organization model
ast-grep --pattern 'interface $_ {
  $$$
  email$_
  $$$
}'
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between addb144 and 211955d.

📒 Files selected for processing (7)
  • apps/api/.env.example (1 hunks)
  • apps/api/src/routes/subscriptions.ts (4 hunks)
  • apps/api/src/stripe.ts (2 hunks)
  • apps/ui/src/components/billing/plan-management.tsx (2 hunks)
  • apps/ui/src/components/landing/pricing-plans.tsx (1 hunks)
  • apps/ui/src/lib/api/v1.d.ts (1 hunks)
  • apps/ui/src/routes/dashboard/_layout/settings/billing.tsx (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
apps/ui/src/components/billing/plan-management.tsx (2)
apps/ui/src/lib/components/use-toast.ts (1)
  • toast (192-192)
apps/ui/src/lib/components/dialog.tsx (1)
  • DialogFooter (127-127)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: e2e / run
🔇 Additional comments (3)
apps/ui/src/lib/api/v1.d.ts (1)

2076-2083: LGTM! API type definition correctly reflects the new Stripe Checkout flow.

The response schema change from {clientSecret, subscriptionId} to {checkoutUrl} aligns perfectly with the backend migration to Stripe Checkout sessions.

apps/ui/src/routes/dashboard/_layout/settings/billing.tsx (1)

22-25: LGTM! Robust boolean parsing for URL parameters.

The validation logic correctly handles both string and boolean values for the search parameters.

apps/api/src/stripe.ts (1)

189-191: LGTM! Proper webhook event handling for checkout sessions.

The addition correctly handles the new Stripe Checkout completion event.

Comment thread apps/ui/src/routes/dashboard/_layout/settings/billing.tsx
Comment thread apps/api/src/stripe.ts
Comment on lines +86 to +89
const { checkoutUrl } = await response.json();

if (clientSecret) {
toast({
title: "Payment confirmation required",
description:
"Please confirm your payment to complete the subscription.",
});
} else {
toast({
title: "Subscription created successfully!",
description: "Welcome to Pro! Your subscription is now active.",
});
await fetchSubscriptionStatus();
navigate({ to: "/dashboard" });
}
// Redirect to Stripe Checkout
window.location.href = checkoutUrl;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ensure the checkout URL is valid before redirecting.

The code directly redirects to checkoutUrl without validating that it's a non-empty string. Since the API returns an empty string when session.url is null (line 99 in subscriptions.ts), this could lead to redirecting users to the current page.

Apply this diff to add validation:

 const { checkoutUrl } = await response.json();

+if (!checkoutUrl) {
+  throw new Error("Invalid checkout URL received from server");
+}
+
 // Redirect to Stripe Checkout
 window.location.href = checkoutUrl;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { checkoutUrl } = await response.json();
if (clientSecret) {
toast({
title: "Payment confirmation required",
description:
"Please confirm your payment to complete the subscription.",
});
} else {
toast({
title: "Subscription created successfully!",
description: "Welcome to Pro! Your subscription is now active.",
});
await fetchSubscriptionStatus();
navigate({ to: "/dashboard" });
}
// Redirect to Stripe Checkout
window.location.href = checkoutUrl;
const { checkoutUrl } = await response.json();
if (!checkoutUrl) {
throw new Error("Invalid checkout URL received from server");
}
// Redirect to Stripe Checkout
window.location.href = checkoutUrl;
🤖 Prompt for AI Agents
In apps/ui/src/components/landing/pricing-plans.tsx around lines 86 to 89, the
code redirects to checkoutUrl without checking if it is a valid non-empty
string. To fix this, add a validation step before the redirect to ensure
checkoutUrl is not empty or null. Only set window.location.href to checkoutUrl
if it passes this validation; otherwise, handle the error or fallback
appropriately to avoid redirecting users to the current page.

Comment on lines +252 to +255
const { checkoutUrl } = await createSubscriptionMutation.mutateAsync({});

if (status === "succeeded") {
await queryClient.invalidateQueries({
queryKey: $api.queryOptions("get", "/subscriptions/status").queryKey,
});
toast({
title: "Upgrade Successful",
description:
"Welcome to Pro! You may need to wait for a minute and/or refresh the page.",
});
onSuccess();
} else if (status === "requires_action") {
toast({
title: "Additional Authentication Required",
description: "Please complete the additional authentication steps.",
});
} else if (status === "processing") {
toast({
title: "Payment Processing",
description:
"Your payment is being processed. Please check later for completion.",
});
} else if (status === "requires_capture") {
toast({
title: "Payment Authorized",
description:
"Your payment has been authorized and will be captured soon.",
});
} else {
toast({
title: `Payment Status: ${status}`,
description:
"Unable to determine payment status. Please contact support.",
variant: "destructive",
className: "text-white",
});
}
} catch (error: any) {
// Redirect to Stripe Checkout
window.location.href = checkoutUrl;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add checkout URL validation to prevent invalid redirects.

Similar to the pricing plans component, the checkout URL should be validated before redirecting to avoid potential issues with empty URLs.

Apply this diff to add validation:

 const { checkoutUrl } = await createSubscriptionMutation.mutateAsync({});

+if (!checkoutUrl) {
+  throw new Error("Invalid checkout URL received from server");
+}
+
 // Redirect to Stripe Checkout
 window.location.href = checkoutUrl;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { checkoutUrl } = await createSubscriptionMutation.mutateAsync({});
if (status === "succeeded") {
await queryClient.invalidateQueries({
queryKey: $api.queryOptions("get", "/subscriptions/status").queryKey,
});
toast({
title: "Upgrade Successful",
description:
"Welcome to Pro! You may need to wait for a minute and/or refresh the page.",
});
onSuccess();
} else if (status === "requires_action") {
toast({
title: "Additional Authentication Required",
description: "Please complete the additional authentication steps.",
});
} else if (status === "processing") {
toast({
title: "Payment Processing",
description:
"Your payment is being processed. Please check later for completion.",
});
} else if (status === "requires_capture") {
toast({
title: "Payment Authorized",
description:
"Your payment has been authorized and will be captured soon.",
});
} else {
toast({
title: `Payment Status: ${status}`,
description:
"Unable to determine payment status. Please contact support.",
variant: "destructive",
className: "text-white",
});
}
} catch (error: any) {
// Redirect to Stripe Checkout
window.location.href = checkoutUrl;
const { checkoutUrl } = await createSubscriptionMutation.mutateAsync({});
if (!checkoutUrl) {
throw new Error("Invalid checkout URL received from server");
}
// Redirect to Stripe Checkout
window.location.href = checkoutUrl;
🤖 Prompt for AI Agents
In apps/ui/src/components/billing/plan-management.tsx around lines 252 to 255,
the code redirects to the checkoutUrl without validating it, which may cause
invalid redirects if the URL is empty or undefined. Add a check to ensure
checkoutUrl is a non-empty string before assigning it to window.location.href.
If the URL is invalid, avoid redirecting or handle the error appropriately.

Comment thread apps/api/src/routes/subscriptions.ts
Comment thread apps/api/src/routes/subscriptions.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
apps/api/src/stripe.ts (1)

288-300: Fix PostHog distinctId to use organization ID.

The static distinctId: "organization" prevents proper event attribution to individual organizations, which was flagged in previous reviews.

 posthog.capture({
-	distinctId: "organization",
+	distinctId: organizationId,
 	event: "subscription_created",
 	groups: {
 		organization: organizationId,
 	},
 	properties: {
 		plan: "pro",
 		organization: organizationId,
-		subscriptionId: subscriptionId,
+		subscriptionId: typeof subscription === "string" ? subscription : subscription?.id,
 		source: "stripe_checkout",
 	},
 });
🧹 Nitpick comments (1)
apps/api/src/stripe.ts (1)

250-262: Fix redundant subscription ID extraction.

The subscription ID is extracted twice - once on line 218 and again on lines 251-252.

-	const subscriptionId =
-		typeof subscription === "string" ? subscription : subscription?.id;
-
 	const result = await db
 		.update(tables.organization)
 		.set({
 			plan: "pro",
-			stripeSubscriptionId: subscriptionId,
+			stripeSubscriptionId: typeof subscription === "string" ? subscription : subscription?.id,
 			subscriptionCancelled: false,
 		})
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 211955d and 277966f.

📒 Files selected for processing (8)
  • apps/api/.env.example (1 hunks)
  • apps/api/src/routes/subscriptions.ts (4 hunks)
  • apps/api/src/stripe.ts (2 hunks)
  • apps/ui/src/components/billing/plan-management.tsx (2 hunks)
  • apps/ui/src/components/dashboard/dashboard-sidebar.tsx (1 hunks)
  • apps/ui/src/components/landing/pricing-plans.tsx (1 hunks)
  • apps/ui/src/lib/api/v1.d.ts (1 hunks)
  • apps/ui/src/routes/dashboard/_layout/settings/billing.tsx (2 hunks)
✅ Files skipped from review due to trivial changes (1)
  • apps/ui/src/components/dashboard/dashboard-sidebar.tsx
🚧 Files skipped from review as they are similar to previous changes (6)
  • apps/api/.env.example
  • apps/ui/src/routes/dashboard/_layout/settings/billing.tsx
  • apps/api/src/routes/subscriptions.ts
  • apps/ui/src/lib/api/v1.d.ts
  • apps/ui/src/components/billing/plan-management.tsx
  • apps/ui/src/components/landing/pricing-plans.tsx
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: e2e / run
🔇 Additional comments (3)
apps/api/src/stripe.ts (3)

189-191: LGTM! Proper event routing for checkout session completion.

The switch case addition correctly routes checkout session completion events to the new handler.


214-227: LGTM! Solid validation and early return pattern.

The function properly validates the subscription presence and exits early for non-subscription checkout sessions.


229-241: LGTM! Good use of the organization resolution helper.

The function correctly leverages the existing resolveOrganizationFromStripeEvent helper with proper error handling for cases where the organization cannot be found.

Comment thread apps/api/src/stripe.ts
Comment on lines +270 to +278
await db.insert(tables.transaction).values({
organizationId,
type: "subscription_start",
amount: ((session.amount_total || 0) / 100).toString(),
currency: (session.currency || "USD").toUpperCase(),
status: "completed",
stripeInvoiceId: session.invoice as string,
description: "Pro subscription started via Stripe Checkout",
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add null safety for session amount and invoice.

The code assumes session.amount_total and session.invoice are non-null, but they could be undefined according to Stripe's API.

 	await db.insert(tables.transaction).values({
 		organizationId,
 		type: "subscription_start",
-		amount: ((session.amount_total || 0) / 100).toString(),
+		amount: session.amount_total ? (session.amount_total / 100).toString() : "0",
 		currency: (session.currency || "USD").toUpperCase(),
 		status: "completed",
-		stripeInvoiceId: session.invoice as string,
+		stripeInvoiceId: session.invoice ? String(session.invoice) : null,
 		description: "Pro subscription started via Stripe Checkout",
 	});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await db.insert(tables.transaction).values({
organizationId,
type: "subscription_start",
amount: ((session.amount_total || 0) / 100).toString(),
currency: (session.currency || "USD").toUpperCase(),
status: "completed",
stripeInvoiceId: session.invoice as string,
description: "Pro subscription started via Stripe Checkout",
});
await db.insert(tables.transaction).values({
organizationId,
type: "subscription_start",
- amount: ((session.amount_total || 0) / 100).toString(),
+ amount: session.amount_total ? (session.amount_total / 100).toString() : "0",
currency: (session.currency || "USD").toUpperCase(),
status: "completed",
- stripeInvoiceId: session.invoice as string,
+ stripeInvoiceId: session.invoice ? String(session.invoice) : null,
description: "Pro subscription started via Stripe Checkout",
});
🤖 Prompt for AI Agents
In apps/api/src/stripe.ts around lines 270 to 278, the code assumes
session.amount_total and session.invoice are always defined, but Stripe's API
allows them to be undefined. To fix this, add null checks or default values
before using these properties. For amount_total, ensure you handle undefined by
defaulting to 0 or skipping the insert if appropriate. For invoice, verify it is
a non-null string before casting or provide a fallback value to avoid runtime
errors.

Replaced the manual subscription and payment logic with Stripe Checkout. Simplified subscription handling in both API and UI, ensuring a seamless upgrade flow. Added success and cancellation handling in the billing UI.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant