codeflash-internal/js/cf-api/endpoints/stripe-webhook.ts
Sarthak Agarwal b0eccb722b
pricing dedupe and flow improve (#1511)
### **PR Type**
- Enhancement



___

### **Description**
- Update subscription checkout and cancellation logic

- Add subscription limits and usage reset utilities

- Enhance login, onboarding, and billing redirects

- Upgrade dependency versions and lint configurations


___



### **Changes walkthrough** 📝
<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Enhancement</strong></td><td><details><summary>20
files</summary><table>
<tr>
<td><strong>actions.ts</strong><dd><code>Refactor checkout session and
cancellation functions</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></td>
<td><a
href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-483bd280b775abd99ae9bbbbcbee9cdcd7407a5c8f09e97591143ea7a460a349">+23/-76</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>subscription-functions.ts</strong><dd><code>Add subscription
limits and monthly reset utilities</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-9fc439c25400b7ac049770509373f3e17ec84f0ba471fda2814bd37e4155b01b">+95/-6</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>subscription-management.ts</strong><dd><code>Integrate
common subscription functions in endpoints</code>&nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-a8e2aca1f0313219f56d75c5d57f36d869fada2292457444f50843b304c26c99">+15/-24</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>billing-view.tsx</strong><dd><code>Improve billing view UI
and period handling</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-4b7dc7632b66abfc90fcb72aaca8222a6f72b2cd65386f5cdf3d6affec187565">+22/-8</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>page.tsx</strong><dd><code>Add error handling and fallback
subscription data</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></td>
<td><a
href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-86ac324c6a48fe59d742a236c9abad3998b7c571534ba8b4a757a89f5ad3ef83">+34/-18</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>page.tsx</strong><dd><code>Enhance login redirect with safe
return URL check</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></td>
<td><a
href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-51fffa5e02e56eafab1bd3288c83046bb460d6f72cd89aee2f2fb106f19250e1">+29/-1</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>page.tsx</strong><dd><code>Improve checkout flow with
logging and Sentry tracking</code>&nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-ab1265c18dc76b25db6970b8b1943cdbd6d9a36092a6091cdf705a3e9fc0b42a">+22/-7</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>SubmitFirstOnboardingPage.tsx</strong><dd><code>Implement
cookie-based redirect after onboarding</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-f7408066207ec9b618596437140aa986df7b733d84a04897b9fc1b48e9691772">+19/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>prisma-client.ts</strong><dd><code>Introduce singleton
Prisma client initialization</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-2961161048a21c0e00e92d799ccbe21870c5638af2aab0670c963c3d5dd929b0">+14/-16</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>stripe-client.ts</strong><dd><code>Add browser check for
Stripe client initialization</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-688ec9d856f21b99370442e1c056bcdca697f39f0773d0ed551ad3402d385f2e">+12/-5</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>is-github-app-installed.ts</strong><dd><code>Update import
paths with explicit file extensions</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-4b97bd2b461108930999b4af7e47d4763ad1b9490f7352130109c39f5abf4561">+3/-3</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Modify build script and
update dependency versions</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-4edec169b0f8d3312edaf35b5cc8521fe1edfa163ce174f60eff51906896601f">+5/-6</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Upgrade dependency and dev
tool versions</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></td>
<td><a
href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-b0d32af9c2caaba1377ec3e924eb553105cdc86e244018ffc6a866c530523599">+7/-5</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>package.json</strong><dd><code>Refine lint commands and
dependency configurations</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-53ddfb1f8a02f1231d3d15a2e694ffe1407d2cc01d3e685de5653b67fec571c7">+2/-3</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>.eslintrc.mjs</strong><dd><code>Simplify ESLint settings for
cf-api</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-64f33493724c98b62fa6fc15a3e6150006f9e276eec35982f0121c93d4615858">+14/-28</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>.eslintrc.mjs</strong><dd><code>Revise ESLint configuration
for webapp</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></td>
<td><a
href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-3e792fa9bf87a9f417a08617cdcd8f5465c92cca62190490bbeb4a76ed3775fd">+26/-15</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>.eslintrc.json</strong><dd><code>Adjust ESLint rules and
ignore patterns</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></td>
<td><a
href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-b44cde4a55654414f4e45c08668a213ddf35250ea82ffef282107956af25efcb">+24/-2</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>eslint.config.mjs</strong><dd><code>Add new ESLint
configuration for common package</code>&nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-24b160f5da434c3e13caaa30e6a33be7a7950af40ec0fcac07cdfd724036dccd">+29/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>lefthook.yml</strong><dd><code>Comment out js-lint hook in
lefthook configuration</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></td>
<td><a
href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-ad6a01e589b8b1b214ca310dbb8d2e4314f6c612b921050c73c97455de43884d">+5/-5</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>tsconfig.json</strong><dd><code>Include express types and
refine TS settings</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-3532a852c82c88daeed6b57a35cd52c4a2589c909edc756613d67e280ab9b23e">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></details></td></tr><tr><td><strong>Additional
files</strong></td><td><details><summary>8 files</summary><table>
<tr>
  <td><strong>.eslintignore</strong></td>
<td><a
href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-285326f37b4ef7a8e43dc3fd17135ad3a3a51a85a9d2ddc5bdd234ffbf3ffada">+9/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>package-lock.json</strong></td>
<td><a
href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-75446c74353509ca0232d6a1350aef075ced8f72bd568e9bafa09cf255683142">+231/-213</a></td>

</tr>

<tr>
  <td><strong>subscription-utils.ts</strong></td>
<td><a
href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-846af69dc1c830de7e6d0c1f73a01e75d60d3e3788b0b60d907d8a161140c679">+0/-122</a>&nbsp;
</td>

</tr>

<tr>
  <td><strong>.eslintignore</strong></td>
<td><a
href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-9c369a46ac4f2c69b2e6ee4bc3d38a9837f0ec0d0ab51002f5b8220c4644fd4d">+12/-1</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>next.config.mjs</strong></td>
<td><a
href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-457975a67fc5d4317fca780879ee23a0e6bae3b3ac41a8d69b8e3dad006d0bb6">+1/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
  <td><strong>package-lock.json</strong></td>
<td><a
href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-0214c85d1717ad8b736e0296bb8cbf50db2aed068f31316d3c39904824a14f8e">+797/-1850</a></td>

</tr>

<tr>
  <td><strong>.eslintignore</strong></td>
<td><a
href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-fdc1690e7d0eea11a33263e0c96b5f1c506946809567531f9ed7ce764b37e802">+11/-0</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
  <td><strong>package-lock.json</strong></td>
<td><a
href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-54c17cef859f033fc84a59da2e977235ebc494943710c25d132e310ec500c5ef">+2361/-108</a></td>

</tr>
</table></details></td></tr></tr></tbody></table>

___

> <details> <summary> Need help?</summary><li>Type <code>/help how to
...</code> in the comments thread for any questions about PR-Agent
usage.</li><li>Check out the <a
href="https://qodo-merge-docs.qodo.ai/usage-guide/">documentation</a>
for more information.</li></details>
2025-03-11 23:29:20 +00:00

229 lines
7.3 KiB
TypeScript

import { Request, Response } from "express"
import { stripe } from "@codeflash-ai/common"
import { prisma } from "@codeflash-ai/common"
import * as Sentry from "@sentry/node"
// Define types for better type safety
type SubscriptionStatus = "active" | "past_due" | "canceled" | "incomplete"
type PlanType = "free" | "pro" | "enterprise"
interface SubscriptionData {
user_id: string
stripe_customer_id: string
stripe_subscription_id: string
plan_type: PlanType
optimizations_limit: number
subscription_status: SubscriptionStatus
current_period_start: Date
current_period_end: Date
}
export async function stripeWebhookHandler(req: Request, res: Response) {
const sig = req.headers["stripe-signature"]
try {
const event = stripe.webhooks.constructEvent(req.body, sig!, process.env.STRIPE_WEBHOOK_SECRET!)
console.log(`Processing Stripe webhook: ${event.type}`)
// Extract useful data for debugging
const eventObject = event.data.object
console.log(`Event ID: ${event.id}`)
// Handle different event types
switch (event.type) {
case "customer.subscription.created":
case "customer.subscription.updated":
await handleSubscriptionUpdate(eventObject)
break
case "customer.subscription.deleted":
await handleSubscriptionCancellation(eventObject)
break
case "checkout.session.completed":
await handleCheckoutCompleted(eventObject)
break
case "invoice.payment_failed":
await handleFailedPayment(eventObject)
break
// We can acknowledge other events without processing them
default:
console.log(`No handler implemented for event type: ${event.type}`)
}
// Always return 200 to acknowledge receipt
res.json({ received: true })
} catch (err: any) {
console.error("Webhook Error:", err.message)
Sentry.captureException(err)
return res.status(400).send(`Webhook Error: ${err.message}`)
}
}
async function handleCheckoutCompleted(session: any) {
// Extract userId from the session metadata
const userId = session.metadata?.userId
if (!userId) {
console.error("No userId in checkout session metadata", session.id)
return
}
console.log(`Processing checkout completion for user ${userId}`)
// If this is a subscription checkout, fetch the subscription
if (session.mode === "subscription" && session.subscription) {
// Update the subscription with userId metadata if missing
try {
await stripe.subscriptions.update(session.subscription, {
metadata: { userId },
})
// Also update customer metadata
if (session.customer) {
await stripe.customers.update(session.customer, {
metadata: { userId },
})
}
// Now fetch and process the subscription
const subscription = await stripe.subscriptions.retrieve(session.subscription)
await handleSubscriptionUpdate(subscription)
} catch (error) {
console.error("Error processing checkout session:", error)
Sentry.captureException(error)
}
}
}
async function handleSubscriptionUpdate(subscription: any) {
// First check subscription metadata
let userId = subscription.metadata?.userId
// If not in subscription metadata, check customer metadata
if (!userId && subscription.customer) {
try {
const customer = await stripe.customers.retrieve(subscription.customer as string)
if ("metadata" in customer) {
userId = customer.metadata?.userId
}
} catch (error) {
console.error("Error retrieving customer:", error)
}
}
if (!userId) {
console.error(
"No userId found in subscription or customer metadata",
JSON.stringify({
subscription_id: subscription.id,
customer_id: subscription.customer,
}),
)
return
}
try {
const priceId = subscription.items.data[0].price.id
const price = await stripe.prices.retrieve(priceId)
console.log(`Updating subscription for user ${userId}`)
// Prepare update data
const updateData: any = {
stripe_subscription_id: subscription.id,
plan_type: price.metadata?.tier || "pro",
optimizations_limit: parseInt(price.metadata?.optimizations || "500"),
subscription_status: subscription.status,
current_period_start: new Date(subscription.current_period_start * 1000),
current_period_end: new Date(subscription.current_period_end * 1000),
updated_at: new Date(),
}
// Handle cancel_at_period_end if present in the subscription
if (subscription.cancel_at_period_end !== undefined) {
updateData.cancel_at_period_end = subscription.cancel_at_period_end
// If subscription was just cancelled, record the cancellation date
if (subscription.cancel_at_period_end === true && !subscription.cancellation_request_date) {
updateData.cancellation_request_date = new Date()
}
// If subscription was reactivated, clear the cancellation date
else if (subscription.cancel_at_period_end === false) {
updateData.cancellation_request_date = null
}
}
await prisma.subscriptions.upsert({
where: { user_id: userId },
create: {
user_id: userId,
stripe_customer_id: subscription.customer as string,
stripe_subscription_id: subscription.id,
plan_type: price.metadata?.tier || "pro",
optimizations_limit: parseInt(price.metadata?.optimizations || "500"),
optimizations_used: 0,
subscription_status: subscription.status,
current_period_start: new Date(subscription.current_period_start * 1000),
current_period_end: new Date(subscription.current_period_end * 1000),
created_at: new Date(),
updated_at: new Date(),
cancel_at_period_end: subscription.cancel_at_period_end || false,
cancellation_request_date: subscription.cancel_at_period_end ? new Date() : null,
},
update: updateData,
})
console.log(`Successfully updated subscription for user ${userId}`)
} catch (error) {
console.error("Error updating subscription:", error)
Sentry.captureException(error)
}
}
async function handleSubscriptionCancellation(subscription: any) {
const userId = subscription.metadata.userId
if (!userId) return
try {
await prisma.subscriptions.update({
where: { user_id: userId },
data: {
subscription_status: "canceled",
plan_type: "free",
optimizations_limit: 100,
stripe_subscription_id: null,
cancel_at_period_end: false, // The subscription is fully canceled, not pending cancellation
cancellation_request_date: null,
},
})
console.log(`Cancelled subscription for user ${userId}`)
} catch (error) {
console.error("Error cancelling subscription:", error)
Sentry.captureException(error)
}
}
async function handleFailedPayment(invoice: any) {
if (!invoice.subscription) return
try {
const subscription = await stripe.subscriptions.retrieve(invoice.subscription)
const userId = subscription.metadata.userId
if (!userId) return
await prisma.subscriptions.update({
where: { user_id: userId },
data: {
subscription_status: "past_due",
},
})
console.log(`Updated subscription status to past_due for user ${userId}`)
} catch (error) {
console.error("Error handling failed payment:", error)
Sentry.captureException(error)
}
}