Skip to content

Commit 9877105

Browse files
authored
Merge pull request #1690 from Shelf-nu/1689-fix-webhook-downgrading-with-multiple-subscriptions
fix: handle subscription updates based on tier:
2 parents 1059e20 + 2befc2e commit 9877105

File tree

1 file changed

+64
-35
lines changed

1 file changed

+64
-35
lines changed

app/routes/api+/stripe-webhook.ts

Lines changed: 64 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ import {
1515
stripe,
1616
} from "~/utils/stripe.server";
1717

18+
const subscriptionTiersPriority: Record<TierId, number> = {
19+
free: 0,
20+
tier_1: 1, // plus
21+
tier_2: 2, // team
22+
custom: 3, // Custom
23+
};
24+
1825
export async function action({ request }: ActionFunctionArgs) {
1926
try {
2027
const payload = await request.text();
@@ -29,6 +36,23 @@ export async function action({ request }: ActionFunctionArgs) {
2936
const customInstallUsers = (CUSTOM_INSTALL_CUSTOMERS ?? "").split(",");
3037
const eventData = event.data.object as { customer: string };
3138
const customerId = eventData.customer;
39+
const user = await db.user
40+
.findFirstOrThrow({
41+
where: { customerId },
42+
include: {
43+
tier: true,
44+
customTierLimit: true,
45+
},
46+
})
47+
.catch((cause) => {
48+
throw new ShelfError({
49+
cause,
50+
message: "No user found",
51+
additionalData: { customerId },
52+
label: "Stripe webhook",
53+
status: 500,
54+
});
55+
});
3256

3357
/** We don't have to do anything in case if the user is custom install. */
3458
if (customInstallUsers.includes(customerId)) {
@@ -160,11 +184,18 @@ export async function action({ request }: ActionFunctionArgs) {
160184
status: 500,
161185
});
162186
}
187+
/** Check whether the paused subscription is higher tier and the current one and only then cancel */
188+
const pausedSubscriptionIsHigherTier =
189+
subscriptionTiersPriority[tierId as TierId] >
190+
subscriptionTiersPriority[user.tierId];
163191

164192
/** When its a trial subscription, update the tier of the user
165193
* In that case we just set it back to free
166194
*/
167-
if (subscription.status === "paused") {
195+
if (
196+
subscription.status === "paused" &&
197+
pausedSubscriptionIsHigherTier
198+
) {
168199
await db.user
169200
.update({
170201
where: { customerId },
@@ -204,8 +235,15 @@ export async function action({ request }: ActionFunctionArgs) {
204235
*
205236
* We only update the tier if the subscription is not paused
206237
* We only do it if the subscription is active because this event gets triggered when cancelling or pausing for example
238+
* We only do it if the subscription is higher tier than the current subscription they have. The tier order is free -> plus -> team
207239
*/
208-
if (subscription.status === "active") {
240+
241+
/** Check whether the new subscription is higher tier and the current one and only then cancel */
242+
const newSubscriptionIsHigherTier =
243+
subscriptionTiersPriority[tierId as TierId] >
244+
subscriptionTiersPriority[user.tierId];
245+
246+
if (subscription.status === "active" && newSubscriptionIsHigherTier) {
209247
await db.user
210248
.update({
211249
where: { customerId },
@@ -229,33 +267,38 @@ export async function action({ request }: ActionFunctionArgs) {
229267

230268
case "customer.subscription.deleted": {
231269
// Occurs whenever a customer’s subscription ends.
232-
const subscription = event.data.object as Stripe.Subscription;
233-
const customerId = subscription.customer as string;
270+
const { customerId, tierId } = await getDataFromStripeEvent(event);
234271

235-
await db.user
236-
.update({
237-
where: { customerId },
238-
data: {
239-
tierId: TierId.free,
240-
},
241-
})
242-
.catch((cause) => {
243-
throw new ShelfError({
244-
cause,
245-
message: "Failed to delete user subscription",
246-
additionalData: { customerId, event },
247-
label: "Stripe webhook",
248-
status: 500,
272+
/** Check whether the deleted subscription is higher tier and the current one and only then cancel */
273+
const deletedSubscriptionIsHigherTier =
274+
subscriptionTiersPriority[tierId as TierId] >
275+
subscriptionTiersPriority[user.tierId];
276+
277+
if (deletedSubscriptionIsHigherTier) {
278+
await db.user
279+
.update({
280+
where: { customerId },
281+
data: {
282+
tierId: TierId.free,
283+
},
284+
})
285+
.catch((cause) => {
286+
throw new ShelfError({
287+
cause,
288+
message: "Failed to delete user subscription",
289+
additionalData: { customerId, event },
290+
label: "Stripe webhook",
291+
status: 500,
292+
});
249293
});
250-
});
294+
}
251295

252296
return new Response(null, { status: 200 });
253297
}
254298

255299
case "customer.subscription.trial_will_end": {
256300
// Occurs three days before the trial period of a subscription is scheduled to end.
257-
const { customerId, tierId, subscription } =
258-
await getDataFromStripeEvent(event);
301+
const { tierId, subscription } = await getDataFromStripeEvent(event);
259302

260303
if (!tierId) {
261304
throw new ShelfError({
@@ -271,20 +314,6 @@ export async function action({ request }: ActionFunctionArgs) {
271314
subscription.trial_end && subscription.trial_start;
272315

273316
if (isTrialSubscription) {
274-
const user = await db.user
275-
.findUniqueOrThrow({
276-
where: { customerId },
277-
})
278-
.catch((cause) => {
279-
throw new ShelfError({
280-
cause,
281-
message: "No user found",
282-
additionalData: { customerId },
283-
label: "Stripe webhook",
284-
status: 500,
285-
});
286-
});
287-
288317
sendEmail({
289318
to: user.email,
290319
subject: "Your shelf.nu free trial is ending soon",

0 commit comments

Comments
 (0)