mirror of
https://github.com/codeflash-ai/codeflash-internal.git
synced 2026-05-04 18:25:18 +00:00
## 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
364 lines
10 KiB
TypeScript
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)
|
|
}
|
|
}
|