Skip to content

Commit d7f70b5

Browse files
improve plan update (#1906)
* stripe downgrade * improve plan update * ci: apply automated fixes * improve plan update * ci: apply automated fixes * ci: apply automated fixes * pr review --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 6fad0b6 commit d7f70b5

File tree

2 files changed

+90
-44
lines changed

2 files changed

+90
-44
lines changed

apps/web/src/app/api/webhook/stripe/route.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ export async function POST(req: NextRequest) {
3030
await caller.stripeRouter.webhooks.sessionCompleted({ event });
3131
break;
3232
case "customer.subscription.updated":
33-
console.log(event);
3433
await caller.stripeRouter.webhooks.customerSubscriptionUpdated({
3534
event,
3635
});

packages/api/src/router/stripe/webhook.ts

Lines changed: 90 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ export const webhookRouter = createTRPCRouter({
4242
customerSubscriptionUpdated: webhookProcedure.mutation(async (opts) => {
4343
const subscription = opts.input.event.data.object as Stripe.Subscription;
4444

45+
if (subscription.status !== "active") {
46+
return;
47+
}
48+
4549
const customerId =
4650
typeof subscription.customer === "string"
4751
? subscription.customer
@@ -59,50 +63,77 @@ export const webhookRouter = createTRPCRouter({
5963
});
6064
}
6165

62-
// for (const item of subscription.items.data) {
63-
// const feature = getFeatureFromPriceId(item.price.id);
64-
// if (!feature) {
65-
// continue;
66-
// }
67-
// const _ws = await opts.ctx.db
68-
// .select()
69-
// .from(workspace)
70-
// .where(eq(workspace.stripeId, customerId))
71-
// .get();
72-
73-
// const ws = selectWorkspaceSchema.parse(_ws);
74-
75-
// const currentValue = ws.limits[feature.feature];
76-
// const newValue =
77-
// typeof currentValue === "boolean"
78-
// ? true
79-
// : typeof currentValue === "number"
80-
// ? currentValue + 1
81-
// : currentValue;
82-
83-
// const newLimits = updateAddonInLimits(
84-
// ws.limits,
85-
// feature.feature,
86-
// newValue,
87-
// );
88-
89-
// await opts.ctx.db
90-
// .update(workspace)
91-
// .set({
92-
// limits: JSON.stringify(newLimits),
93-
// })
94-
// .where(eq(workspace.id, result.id))
95-
// .run();
96-
// }
66+
const ws = selectWorkspaceSchema.parse(result);
67+
const oldPlan = ws.plan;
9768

98-
const customer = await stripe.customers.retrieve(customerId);
99-
if (!customer.deleted && customer.email) {
100-
const userResult = await opts.ctx.db
101-
.select()
102-
.from(user)
103-
.where(eq(user.email, customer.email))
104-
.get();
105-
if (!userResult) return;
69+
let detectedPlan: ReturnType<typeof getPlanFromPriceId> = undefined;
70+
71+
for (const item of subscription.items.data) {
72+
const plan = getPlanFromPriceId(item.price.id);
73+
if (plan) {
74+
detectedPlan = plan;
75+
break;
76+
}
77+
}
78+
79+
if (!detectedPlan) {
80+
return;
81+
}
82+
83+
await opts.ctx.db
84+
.update(workspace)
85+
.set({
86+
plan: detectedPlan.plan,
87+
subscriptionId: subscription.id,
88+
endsAt: new Date(subscription.current_period_end * 1000),
89+
paidUntil: new Date(subscription.current_period_end * 1000),
90+
limits: JSON.stringify(getLimits(detectedPlan.plan)),
91+
})
92+
.where(eq(workspace.id, result.id))
93+
.run();
94+
95+
const allActive = await stripe.subscriptions.list({
96+
customer: customerId,
97+
status: "active",
98+
});
99+
100+
for (const sub of allActive.data) {
101+
if (sub.id === subscription.id) continue;
102+
try {
103+
await stripe.subscriptions.cancel(sub.id);
104+
} catch (e) {
105+
console.error(`Failed to cancel duplicate subscription ${sub.id}:`, e);
106+
}
107+
}
108+
109+
const newPlan = detectedPlan?.plan ?? oldPlan;
110+
if (detectedPlan && newPlan !== oldPlan) {
111+
const customer = await stripe.customers.retrieve(customerId);
112+
if (!customer.deleted && customer.email) {
113+
const userResult = await opts.ctx.db
114+
.select()
115+
.from(user)
116+
.where(eq(user.email, customer.email))
117+
.get();
118+
if (!userResult) return;
119+
120+
const planOrder = ["free", "starter", "team"] as const;
121+
const oldIndex = planOrder.indexOf(oldPlan ?? "free");
122+
const newIndex = planOrder.indexOf(newPlan ?? "free");
123+
124+
const event =
125+
newIndex > oldIndex
126+
? Events.UpgradeWorkspace
127+
: Events.DowngradeWorkspace;
128+
129+
const analytics = await setupAnalytics({
130+
userId: `usr_${userResult.id}`,
131+
email: userResult.email || undefined,
132+
workspaceId: String(result.id),
133+
plan: newPlan,
134+
});
135+
await analytics.track(event);
136+
}
106137
}
107138
}),
108139
sessionCompleted: webhookProcedure.mutation(async (opts) => {
@@ -212,6 +243,15 @@ export const webhookRouter = createTRPCRouter({
212243
? subscription.customer
213244
: subscription.customer.id;
214245

246+
const activeSubscriptions = await stripe.subscriptions.list({
247+
customer: customerId,
248+
status: "active",
249+
});
250+
251+
if (activeSubscriptions.data.length > 0) {
252+
return;
253+
}
254+
215255
const _workspace = await opts.ctx.db.transaction(async (tx) => {
216256
const _workspace = await tx
217257
.update(workspace)
@@ -312,6 +352,13 @@ export const webhookRouter = createTRPCRouter({
312352
return _workspace;
313353
});
314354

355+
if (!_workspace[0]) {
356+
throw new TRPCError({
357+
code: "BAD_REQUEST",
358+
message: "Workspace not found",
359+
});
360+
}
361+
315362
const workspaceId = _workspace[0].id;
316363
const customer = await stripe.customers.retrieve(customerId);
317364

0 commit comments

Comments
 (0)