codeflash-internal/js/cf-api/endpoints/stripe-webhook.ts
Kevin Turcios d7a8b8f227
perf: fix CI build + lazy-load heavy libs + parallelize DB queries (#2601)
## Summary
- **Fix CI build failure**: Auth0Client crashes during Next.js
prerendering when env vars aren't set. Returns a no-op stub (`getSession
→ null`) when domain is missing — semantically correct for static
generation
- **Lazy-load markdown libs (~260kb)**: ReactMarkdown, remarkGfm, and
react-syntax-highlighter were eagerly imported in monaco-diff-viewer but
only rendered when user expands "Generated Tests". Extracted into a
dynamic component
- **Parallelize repo detail query**: `getRepositoryById` ran the
activity count sequentially after the repo lookup. Since `repoId` is
already available, all three queries now run in parallel

## Test plan
- [ ] CI `build` check passes (was failing since #2598)
- [ ] Trace page still renders generated tests correctly when expanded
- [ ] Repository detail page loads correctly with activity status
2026-04-13 11:03:05 -05:00

364 lines
10 KiB
TypeScript

import { Request, Response } from "express"
import { addMonthsSafe, stripe, SUBSCRIPTION_PLANS, prisma } from "@codeflash-ai/common"
import * as Sentry from "@sentry/node"
import { logger } from "../utils/logger.js"
import { badRequest } from "../exceptions/index.js"
// 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
}
// Dependencies interface for easier testing
export interface StripeWebhookDependencies {
stripe: typeof stripe
prisma: typeof prisma
Sentry: typeof Sentry
getWebhookSecret: () => string | undefined
}
// Default dependencies
let dependencies: StripeWebhookDependencies = {
stripe,
prisma,
Sentry,
getWebhookSecret: () => process.env.STRIPE_WEBHOOK_SECRET,
}
// For testing - allow dependency injection
export function setStripeWebhookDependencies(deps: Partial<StripeWebhookDependencies>) {
dependencies = { ...dependencies, ...deps }
}
export function resetStripeWebhookDependencies() {
dependencies = {
stripe,
prisma,
Sentry,
getWebhookSecret: () => process.env.STRIPE_WEBHOOK_SECRET,
}
}
export async function stripeWebhookHandler(req: Request, res: Response) {
const sig = req.headers["stripe-signature"]
try {
const webhookSecret = dependencies.getWebhookSecret()
if (!webhookSecret) {
throw new Error("STRIPE_WEBHOOK_SECRET is not configured")
}
const event = dependencies.stripe.webhooks.constructEvent(req.body, sig, webhookSecret)
logger.info("Processing Stripe webhook", req, {
eventType: event.type,
eventId: event.id,
})
// Extract useful data for debugging
const eventObject = event.data.object
// 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:
logger.info("No handler implemented for event type", req, {
eventType: event.type,
})
}
// Always return 200 to acknowledge receipt
res.json({ received: true })
} catch (err: any) {
logger.errorWithSentry("Webhook Error", req, { errorMessage: err.message }, err)
dependencies.Sentry.captureException(err)
throw badRequest(`Webhook Error: ${err.message}`)
}
}
export async function handleCheckoutCompleted(session: any) {
// Extract userId from the session metadata
const userId = session.metadata?.userId
if (!userId) {
logger.error(
"No userId in checkout session metadata",
{
operation: "checkout_completed",
},
{
sessionId: session.id,
},
)
return
}
logger.info("Processing checkout completion", {
operation: "checkout_completed",
userId,
sessionId: session.id,
mode: session.mode,
})
// 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 dependencies.stripe.subscriptions.update(session.subscription, {
metadata: { userId },
})
// Also update customer metadata
if (session.customer) {
await dependencies.stripe.customers.update(session.customer, {
metadata: { userId },
})
}
// Now fetch and process the subscription
const subscription = await dependencies.stripe.subscriptions.retrieve(session.subscription)
await handleSubscriptionUpdate(subscription)
} catch (error) {
logger.errorWithSentry(
"Error processing checkout session:",
{
operation: "checkout_completed",
userId,
sessionId: session.id,
},
{},
error as Error,
)
dependencies.Sentry.captureException(error)
}
}
}
export 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 dependencies.stripe.customers.retrieve(subscription.customer as string)
if ("metadata" in customer) {
userId = customer.metadata?.userId
}
} catch (error) {
logger.error(
"Error retrieving customer:",
{
operation: "subscription_update",
subscriptionId: subscription.id,
customerId: subscription.customer,
},
{},
error as Error,
)
}
}
if (!userId) {
logger.error(
"No userId found in subscription or customer metadata",
{
operation: "subscription_update",
},
{
subscriptionId: subscription.id,
customerId: subscription.customer,
},
)
return
}
try {
const priceId = subscription.items.data[0].price.id
const price = await dependencies.stripe.prices.retrieve(priceId)
logger.info("Updating subscription", {
operation: "subscription_update",
userId,
subscriptionId: subscription.id,
priceId,
planType: price.metadata?.tier || "pro",
})
const currentPeriodStart = new Date()
let currentPeriodEnd
// Adjust currentPeriodEnd based on interval and interval_count if needed
if (price.recurring) {
const interval = price.recurring.interval
const interval_count = price.recurring.interval_count || 1
if (interval === "month") {
currentPeriodEnd = addMonthsSafe(currentPeriodStart, interval_count)
} else if (interval === "year") {
currentPeriodEnd = addMonthsSafe(currentPeriodStart, interval_count * 12)
}
}
// Prepare update data
const updateData: any = {
stripe_subscription_id: subscription.id,
plan_type: price.metadata?.tier || "pro",
optimizations_limit: SUBSCRIPTION_PLANS.PRO.optimizations,
subscription_status: subscription.status,
current_period_start: currentPeriodStart,
current_period_end: currentPeriodEnd,
optimizations_used: 0,
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 dependencies.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: currentPeriodStart,
current_period_end: currentPeriodEnd,
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,
})
logger.info("Successfully updated subscription", {
operation: "subscription_update",
userId,
subscriptionId: subscription.id,
planType: price.metadata?.tier || "pro",
status: subscription.status,
})
} catch (error) {
logger.errorWithSentry(
"Error updating subscription:",
{
operation: "subscription_update",
userId,
subscriptionId: subscription.id,
},
{},
error as Error,
)
dependencies.Sentry.captureException(error)
}
}
export async function handleSubscriptionCancellation(subscription: any) {
const userId = subscription.metadata.userId
if (!userId) return
try {
await dependencies.prisma.subscriptions.update({
where: { user_id: userId },
data: {
subscription_status: "canceled",
plan_type: "free",
optimizations_limit: SUBSCRIPTION_PLANS.FREE.optimizations,
stripe_subscription_id: null,
cancel_at_period_end: false,
cancellation_request_date: null,
},
})
logger.info("Cancelled subscription", {
operation: "subscription_cancellation",
userId,
subscriptionId: subscription.id,
})
} catch (error) {
logger.errorWithSentry(
"Error cancelling subscription:",
{
operation: "subscription_cancellation",
userId,
subscriptionId: subscription.id,
},
{},
error as Error,
)
dependencies.Sentry.captureException(error)
}
}
export async function handleFailedPayment(invoice: any) {
if (!invoice.subscription) return
try {
const subscription = await dependencies.stripe.subscriptions.retrieve(invoice.subscription)
const userId = subscription.metadata.userId
if (!userId) return
await dependencies.prisma.subscriptions.update({
where: { user_id: userId },
data: {
subscription_status: "past_due",
},
})
logger.info("Updated subscription status to past_due", {
operation: "failed_payment",
userId,
subscriptionId: invoice.subscription,
})
} catch (error) {
logger.errorWithSentry(
"Error handling failed payment:",
{
operation: "failed_payment",
subscriptionId: invoice.subscription,
},
{},
error as Error,
)
dependencies.Sentry.captureException(error)
}
}