Skip to content

Commit 5bc73fc

Browse files
committed
Improve dupe webhook event handling
1 parent 7f6d0e3 commit 5bc73fc

File tree

1 file changed

+35
-19
lines changed

1 file changed

+35
-19
lines changed

api/src/webhook-handling.ts

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,42 @@ export async function banUser(email: string) {
4646
await updateUserMetadata(user.user_id!, { banned: true });
4747
}
4848

49-
export async function updateProUserData(email: string, subscription: Partial<PayingUserMetadata>) {
50-
dropUndefinedValues(subscription);
49+
export async function updateProUserData(email: string, subscriptionUpdate: Partial<PayingUserMetadata>) {
50+
dropUndefinedValues(subscriptionUpdate);
5151

5252
const user = await getOrCreateUserData(email);
5353
const appData = user.app_metadata as AppMetadata;
5454

55+
// Does the user already have unrelated subscription data?
56+
if (
57+
appData &&
58+
'subscription_id' in appData &&
59+
subscriptionUpdate.subscription_id &&
60+
appData.subscription_id !== subscriptionUpdate.subscription_id
61+
) {
62+
// If the user has an existing subscription and we get an event for a new one, there's a few
63+
// possibilities. One (especially with PayPro) is that they're manually renewing an expiring
64+
// one, or they have briefly overlapping subscriptions and the old one has now lapsed.
65+
66+
// The possibilities here are quite complicated (e.g. new subs can start as 'cancelled' due to
67+
// manual renewal configuration in PayPro) but "latest expiry" tends to be the right answer.
68+
69+
if (subscriptionUpdate.subscription_expiry! < appData.subscription_expiry) {
70+
log.warn(`User ${email} received a outdated subscription event for an inactive subscription - ignoring`);
71+
return; // Ignore the update entirely in this case
72+
}
73+
74+
// If there's an update for a different sub, and the user's existing subscription is active and has
75+
// plenty of time left on it, this is probably a mistake (some users do accidentally complete the
76+
// checkout twice) which needs manual intervention.
77+
if (
78+
appData.subscription_status !== 'past_due' &&
79+
moment(appData.subscription_expiry).subtract(5, 'days').valueOf() > Date.now()
80+
) {
81+
reportError(`Mismatched subscription event for Pro user ${email} with existing subscription`);
82+
}
83+
}
84+
5585
// Is the user already a member of a team?
5686
if (appData && 'subscription_owner_id' in appData) {
5787
const owner = await getUserById(appData.subscription_owner_id!);
@@ -66,25 +96,11 @@ export async function updateProUserData(email: string, subscription: Partial<Pay
6696
// update the membership state on both sides:
6797
const updatedTeamMembers = ownerData.team_member_ids.filter(id => id !== user.user_id);
6898
await updateUserMetadata(appData.subscription_owner_id!, { team_member_ids: updatedTeamMembers });
69-
(subscription as Partial<TeamMemberMetadata>).subscription_owner_id = null as any; // Setting to null deletes the property
70-
}
71-
72-
// If the user has another subscription with time left on it, this is probably a mistake
73-
// (some users do accidentally complete the checkout twice) which needs manual intervention.
74-
if (
75-
// Existing subscription that hasn't expired yet (48+ hours left)
76-
appData &&
77-
'subscription_expiry' in appData &&
78-
moment(appData.subscription_expiry).subtract(48, 'hour').valueOf() > Date.now() &&
79-
// Not just another event for the same sub (payment vs creation etc)
80-
(appData as PayingUserMetadata).subscription_id !== subscription.subscription_id
81-
82-
) {
83-
reportError(`Signup for existing Pro user ${email} with active subscription`);
99+
(subscriptionUpdate as Partial<TeamMemberMetadata>).subscription_owner_id = null as any; // Setting to null deletes the property
84100
}
85101

86-
if (!_.isEmpty(subscription)) {
87-
await updateUserMetadata(user.user_id!, subscription);
102+
if (!_.isEmpty(subscriptionUpdate)) {
103+
await updateUserMetadata(user.user_id!, subscriptionUpdate);
88104
}
89105
}
90106

0 commit comments

Comments
 (0)