diff --git a/apps/web/src/app/api/webhook/stripe/route.ts b/apps/web/src/app/api/webhook/stripe/route.ts index c2aaa0d90b..9d16223c22 100644 --- a/apps/web/src/app/api/webhook/stripe/route.ts +++ b/apps/web/src/app/api/webhook/stripe/route.ts @@ -30,7 +30,6 @@ export async function POST(req: NextRequest) { await caller.stripeRouter.webhooks.sessionCompleted({ event }); break; case "customer.subscription.updated": - console.log(event); await caller.stripeRouter.webhooks.customerSubscriptionUpdated({ event, }); diff --git a/packages/api/src/router/stripe/webhook.ts b/packages/api/src/router/stripe/webhook.ts index 5003848df9..84ed2fca1e 100644 --- a/packages/api/src/router/stripe/webhook.ts +++ b/packages/api/src/router/stripe/webhook.ts @@ -42,6 +42,10 @@ export const webhookRouter = createTRPCRouter({ customerSubscriptionUpdated: webhookProcedure.mutation(async (opts) => { const subscription = opts.input.event.data.object as Stripe.Subscription; + if (subscription.status !== "active") { + return; + } + const customerId = typeof subscription.customer === "string" ? subscription.customer @@ -59,50 +63,77 @@ export const webhookRouter = createTRPCRouter({ }); } - // for (const item of subscription.items.data) { - // const feature = getFeatureFromPriceId(item.price.id); - // if (!feature) { - // continue; - // } - // const _ws = await opts.ctx.db - // .select() - // .from(workspace) - // .where(eq(workspace.stripeId, customerId)) - // .get(); - - // const ws = selectWorkspaceSchema.parse(_ws); - - // const currentValue = ws.limits[feature.feature]; - // const newValue = - // typeof currentValue === "boolean" - // ? true - // : typeof currentValue === "number" - // ? currentValue + 1 - // : currentValue; - - // const newLimits = updateAddonInLimits( - // ws.limits, - // feature.feature, - // newValue, - // ); - - // await opts.ctx.db - // .update(workspace) - // .set({ - // limits: JSON.stringify(newLimits), - // }) - // .where(eq(workspace.id, result.id)) - // .run(); - // } + const ws = selectWorkspaceSchema.parse(result); + const oldPlan = ws.plan; - const customer = await stripe.customers.retrieve(customerId); - if (!customer.deleted && customer.email) { - const userResult = await opts.ctx.db - .select() - .from(user) - .where(eq(user.email, customer.email)) - .get(); - if (!userResult) return; + let detectedPlan: ReturnType = undefined; + + for (const item of subscription.items.data) { + const plan = getPlanFromPriceId(item.price.id); + if (plan) { + detectedPlan = plan; + break; + } + } + + if (!detectedPlan) { + return; + } + + await opts.ctx.db + .update(workspace) + .set({ + plan: detectedPlan.plan, + subscriptionId: subscription.id, + endsAt: new Date(subscription.current_period_end * 1000), + paidUntil: new Date(subscription.current_period_end * 1000), + limits: JSON.stringify(getLimits(detectedPlan.plan)), + }) + .where(eq(workspace.id, result.id)) + .run(); + + const allActive = await stripe.subscriptions.list({ + customer: customerId, + status: "active", + }); + + for (const sub of allActive.data) { + if (sub.id === subscription.id) continue; + try { + await stripe.subscriptions.cancel(sub.id); + } catch (e) { + console.error(`Failed to cancel duplicate subscription ${sub.id}:`, e); + } + } + + const newPlan = detectedPlan?.plan ?? oldPlan; + if (detectedPlan && newPlan !== oldPlan) { + const customer = await stripe.customers.retrieve(customerId); + if (!customer.deleted && customer.email) { + const userResult = await opts.ctx.db + .select() + .from(user) + .where(eq(user.email, customer.email)) + .get(); + if (!userResult) return; + + const planOrder = ["free", "starter", "team"] as const; + const oldIndex = planOrder.indexOf(oldPlan ?? "free"); + const newIndex = planOrder.indexOf(newPlan ?? "free"); + + const event = + newIndex > oldIndex + ? Events.UpgradeWorkspace + : Events.DowngradeWorkspace; + + const analytics = await setupAnalytics({ + userId: `usr_${userResult.id}`, + email: userResult.email || undefined, + workspaceId: String(result.id), + plan: newPlan, + }); + await analytics.track(event); + } } }), sessionCompleted: webhookProcedure.mutation(async (opts) => { @@ -212,6 +243,15 @@ export const webhookRouter = createTRPCRouter({ ? subscription.customer : subscription.customer.id; + const activeSubscriptions = await stripe.subscriptions.list({ + customer: customerId, + status: "active", + }); + + if (activeSubscriptions.data.length > 0) { + return; + } + const _workspace = await opts.ctx.db.transaction(async (tx) => { const _workspace = await tx .update(workspace) @@ -312,6 +352,13 @@ export const webhookRouter = createTRPCRouter({ return _workspace; }); + if (!_workspace[0]) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Workspace not found", + }); + } + const workspaceId = _workspace[0].id; const customer = await stripe.customers.retrieve(customerId);