mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-12 21:23:45 +01:00
[web] ensure Stripe & Recurly webhooks can handle a migrating subscription (#30787)
* return early from various event handlers if the subscription isn't controlled by the payment provider sending the event * update `Subscription` type for webhook events GitOrigin-RevId: f3fc345c37bbe134b7696ccde9d6d8c7608f8f12
This commit is contained in:
@@ -3,6 +3,7 @@ import AnalyticsManager from '../Analytics/AnalyticsManager.mjs'
|
||||
import SubscriptionEmailHandler from './SubscriptionEmailHandler.mjs'
|
||||
import { AI_ADD_ON_CODE } from './AiHelper.mjs'
|
||||
import mongodb from 'mongodb-legacy'
|
||||
import SubscriptionLocator from './SubscriptionLocator.mjs'
|
||||
|
||||
const { ObjectId } = mongodb
|
||||
|
||||
@@ -14,6 +15,17 @@ async function sendRecurlyAnalyticsEvent(event, eventData) {
|
||||
return
|
||||
}
|
||||
|
||||
const subscription =
|
||||
await SubscriptionLocator.promises.getUsersSubscription(userId)
|
||||
|
||||
if (
|
||||
subscription?.paymentProvider?.service &&
|
||||
subscription.paymentProvider.service.includes('stripe')
|
||||
) {
|
||||
// do not send recurly events for subscriptions managed by stripe
|
||||
return
|
||||
}
|
||||
|
||||
const customerIoEnabled =
|
||||
await SplitTestHandler.promises.hasUserBeenAssignedToVariant(
|
||||
{},
|
||||
|
||||
@@ -333,6 +333,17 @@ async function updateSubscriptionFromRecurly(
|
||||
subscription,
|
||||
requesterData
|
||||
) {
|
||||
if (
|
||||
subscription?.paymentProvider?.service &&
|
||||
subscription.paymentProvider.service.includes('stripe')
|
||||
) {
|
||||
logger.warn(
|
||||
{ subscriptionId: subscription._id },
|
||||
'attempted to update non-recurly subscription from Recurly data'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (recurlySubscription.state === 'expired') {
|
||||
await handleExpiredSubscription(subscription, requesterData)
|
||||
return
|
||||
|
||||
@@ -66,9 +66,59 @@ describe('RecurlyEventHandler', function () {
|
||||
})
|
||||
)
|
||||
|
||||
vi.doMock(
|
||||
'../../../../app/src/Features/Subscription/SubscriptionLocator',
|
||||
() => ({
|
||||
default: (ctx.SubscriptionLocator = {
|
||||
promises: {
|
||||
getUsersSubscription: sinon.stub().resolves(null),
|
||||
},
|
||||
}),
|
||||
})
|
||||
)
|
||||
|
||||
ctx.RecurlyEventHandler = (await import(modulePath)).default
|
||||
})
|
||||
|
||||
it('should not send events for subscriptions managed by stripe', async function (ctx) {
|
||||
ctx.SubscriptionLocator.promises.getUsersSubscription.resolves({
|
||||
_id: 'sub123',
|
||||
paymentProvider: {
|
||||
service: 'stripe-uk',
|
||||
},
|
||||
})
|
||||
|
||||
await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
|
||||
'new_subscription_notification',
|
||||
ctx.eventData
|
||||
)
|
||||
|
||||
sinon.assert.notCalled(ctx.AnalyticsManager.recordEventForUserInBackground)
|
||||
sinon.assert.notCalled(
|
||||
ctx.AnalyticsManager.setUserPropertyForUserInBackground
|
||||
)
|
||||
sinon.assert.notCalled(
|
||||
ctx.SubscriptionEmailHandler.sendTrialOnboardingEmail
|
||||
)
|
||||
})
|
||||
|
||||
it('should send events for subscriptions without stripe payment provider', async function (ctx) {
|
||||
ctx.SubscriptionLocator.promises.getUsersSubscription.resolves({
|
||||
_id: 'sub123',
|
||||
paymentProvider: {
|
||||
service: 'recurly',
|
||||
},
|
||||
})
|
||||
|
||||
await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
|
||||
'new_subscription_notification',
|
||||
ctx.eventData
|
||||
)
|
||||
|
||||
sinon.assert.called(ctx.AnalyticsManager.recordEventForUserInBackground)
|
||||
sinon.assert.called(ctx.AnalyticsManager.setUserPropertyForUserInBackground)
|
||||
})
|
||||
|
||||
it('with new_subscription_notification - free trial', async function (ctx) {
|
||||
await ctx.RecurlyEventHandler.sendRecurlyAnalyticsEvent(
|
||||
'new_subscription_notification',
|
||||
|
||||
@@ -410,6 +410,7 @@ describe('SubscriptionUpdater', function () {
|
||||
describe('updateSubscriptionFromRecurly', function () {
|
||||
afterEach(function (ctx) {
|
||||
ctx.subscription.member_ids = []
|
||||
delete ctx.subscription.paymentProvider
|
||||
})
|
||||
|
||||
it('should update the subscription with token etc when not expired', async function (ctx) {
|
||||
@@ -465,6 +466,20 @@ describe('SubscriptionUpdater', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('should not update subscription when paymentProvider service contains stripe', async function (ctx) {
|
||||
ctx.subscription.paymentProvider = {
|
||||
service: 'stripe-uk',
|
||||
}
|
||||
await ctx.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
|
||||
ctx.recurlySubscription,
|
||||
ctx.subscription,
|
||||
{}
|
||||
)
|
||||
ctx.subscription.save.called.should.equal(false)
|
||||
expect(ctx.FeaturesUpdater.promises.scheduleRefreshFeatures).to.not.have
|
||||
.been.called
|
||||
})
|
||||
|
||||
it('should remove the subscription when expired', async function (ctx) {
|
||||
ctx.recurlySubscription.state = 'expired'
|
||||
await ctx.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import Stripe from 'stripe'
|
||||
|
||||
type StripeSubscription = Stripe.Subscription & {
|
||||
metadata: {
|
||||
billing_migration_id?: string
|
||||
recurly_to_stripe_migration_status?: 'in_progress' | 'completed'
|
||||
}
|
||||
customer: string
|
||||
}
|
||||
|
||||
export interface CustomerSubscriptionUpdatedWebhookEvent
|
||||
extends Stripe.EventBase {
|
||||
type: 'customer.subscription.updated'
|
||||
data: {
|
||||
object: Stripe.Subscription & {
|
||||
metadata: {
|
||||
adminUserId?: string
|
||||
}
|
||||
customer: string
|
||||
}
|
||||
object: StripeSubscription
|
||||
// https://docs.stripe.com/api/events/object?api-version=2025-04-30.basil#event_object-data-previous_attributes
|
||||
previous_attributes: {
|
||||
cancel_at_period_end?: boolean // will only be present if the subscription was cancelled or reactivated
|
||||
@@ -34,12 +37,7 @@ export interface CustomerSubscriptionCreatedWebhookEvent
|
||||
extends Stripe.EventBase {
|
||||
type: 'customer.subscription.created'
|
||||
data: {
|
||||
object: Stripe.Subscription & {
|
||||
metadata: {
|
||||
adminUserId?: string
|
||||
}
|
||||
customer: string
|
||||
}
|
||||
object: StripeSubscription
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,12 +45,7 @@ export interface CustomerSubscriptionsDeletedWebhookEvent
|
||||
extends Stripe.EventBase {
|
||||
type: 'customer.subscription.deleted'
|
||||
data: {
|
||||
object: Stripe.Subscription & {
|
||||
metadata: {
|
||||
adminUserId?: string
|
||||
}
|
||||
customer: string
|
||||
}
|
||||
object: StripeSubscription
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +56,8 @@ export interface InvoicePaidWebhookEvent extends Stripe.EventBase {
|
||||
parent: Stripe.Invoice.Parent & {
|
||||
subscription_details: Stripe.Invoice.Parent.SubscriptionDetails & {
|
||||
metadata: {
|
||||
adminUserId?: string
|
||||
billing_migration_id?: string
|
||||
recurly_to_stripe_migration_status?: 'in_progress' | 'completed'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user