### **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> </dd></td> <td><a href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-483bd280b775abd99ae9bbbbcbee9cdcd7407a5c8f09e97591143ea7a460a349">+23/-76</a> </td> </tr> <tr> <td><strong>subscription-functions.ts</strong><dd><code>Add subscription limits and monthly reset utilities</code> </dd></td> <td><a href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-9fc439c25400b7ac049770509373f3e17ec84f0ba471fda2814bd37e4155b01b">+95/-6</a> </td> </tr> <tr> <td><strong>subscription-management.ts</strong><dd><code>Integrate common subscription functions in endpoints</code> </dd></td> <td><a href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-a8e2aca1f0313219f56d75c5d57f36d869fada2292457444f50843b304c26c99">+15/-24</a> </td> </tr> <tr> <td><strong>billing-view.tsx</strong><dd><code>Improve billing view UI and period handling</code> </dd></td> <td><a href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-4b7dc7632b66abfc90fcb72aaca8222a6f72b2cd65386f5cdf3d6affec187565">+22/-8</a> </td> </tr> <tr> <td><strong>page.tsx</strong><dd><code>Add error handling and fallback subscription data</code> </dd></td> <td><a href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-86ac324c6a48fe59d742a236c9abad3998b7c571534ba8b4a757a89f5ad3ef83">+34/-18</a> </td> </tr> <tr> <td><strong>page.tsx</strong><dd><code>Enhance login redirect with safe return URL check</code> </dd></td> <td><a href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-51fffa5e02e56eafab1bd3288c83046bb460d6f72cd89aee2f2fb106f19250e1">+29/-1</a> </td> </tr> <tr> <td><strong>page.tsx</strong><dd><code>Improve checkout flow with logging and Sentry tracking</code> </dd></td> <td><a href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-ab1265c18dc76b25db6970b8b1943cdbd6d9a36092a6091cdf705a3e9fc0b42a">+22/-7</a> </td> </tr> <tr> <td><strong>SubmitFirstOnboardingPage.tsx</strong><dd><code>Implement cookie-based redirect after onboarding</code> </dd></td> <td><a href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-f7408066207ec9b618596437140aa986df7b733d84a04897b9fc1b48e9691772">+19/-0</a> </td> </tr> <tr> <td><strong>prisma-client.ts</strong><dd><code>Introduce singleton Prisma client initialization</code> </dd></td> <td><a href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-2961161048a21c0e00e92d799ccbe21870c5638af2aab0670c963c3d5dd929b0">+14/-16</a> </td> </tr> <tr> <td><strong>stripe-client.ts</strong><dd><code>Add browser check for Stripe client initialization</code> </dd></td> <td><a href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-688ec9d856f21b99370442e1c056bcdca697f39f0773d0ed551ad3402d385f2e">+12/-5</a> </td> </tr> <tr> <td><strong>is-github-app-installed.ts</strong><dd><code>Update import paths with explicit file extensions</code> </dd></td> <td><a href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-4b97bd2b461108930999b4af7e47d4763ad1b9490f7352130109c39f5abf4561">+3/-3</a> </td> </tr> <tr> <td><strong>package.json</strong><dd><code>Modify build script and update dependency versions</code> </dd></td> <td><a href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-4edec169b0f8d3312edaf35b5cc8521fe1edfa163ce174f60eff51906896601f">+5/-6</a> </td> </tr> <tr> <td><strong>package.json</strong><dd><code>Upgrade dependency and dev tool versions</code> </dd></td> <td><a href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-b0d32af9c2caaba1377ec3e924eb553105cdc86e244018ffc6a866c530523599">+7/-5</a> </td> </tr> <tr> <td><strong>package.json</strong><dd><code>Refine lint commands and dependency configurations</code> </dd></td> <td><a href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-53ddfb1f8a02f1231d3d15a2e694ffe1407d2cc01d3e685de5653b67fec571c7">+2/-3</a> </td> </tr> <tr> <td><strong>.eslintrc.mjs</strong><dd><code>Simplify ESLint settings for cf-api</code> </dd></td> <td><a href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-64f33493724c98b62fa6fc15a3e6150006f9e276eec35982f0121c93d4615858">+14/-28</a> </td> </tr> <tr> <td><strong>.eslintrc.mjs</strong><dd><code>Revise ESLint configuration for webapp</code> </dd></td> <td><a href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-3e792fa9bf87a9f417a08617cdcd8f5465c92cca62190490bbeb4a76ed3775fd">+26/-15</a> </td> </tr> <tr> <td><strong>.eslintrc.json</strong><dd><code>Adjust ESLint rules and ignore patterns</code> </dd></td> <td><a href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-b44cde4a55654414f4e45c08668a213ddf35250ea82ffef282107956af25efcb">+24/-2</a> </td> </tr> <tr> <td><strong>eslint.config.mjs</strong><dd><code>Add new ESLint configuration for common package</code> </dd></td> <td><a href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-24b160f5da434c3e13caaa30e6a33be7a7950af40ec0fcac07cdfd724036dccd">+29/-0</a> </td> </tr> <tr> <td><strong>lefthook.yml</strong><dd><code>Comment out js-lint hook in lefthook configuration</code> </dd></td> <td><a href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-ad6a01e589b8b1b214ca310dbb8d2e4314f6c612b921050c73c97455de43884d">+5/-5</a> </td> </tr> <tr> <td><strong>tsconfig.json</strong><dd><code>Include express types and refine TS settings</code> </dd></td> <td><a href="https://github.com/codeflash-ai/codeflash-internal/pull/1511/files#diff-3532a852c82c88daeed6b57a35cd52c4a2589c909edc756613d67e280ab9b23e">+1/-1</a> </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> </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> </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> </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> </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> </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>
229 lines
7.3 KiB
TypeScript
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)
|
|
}
|
|
}
|