Merge pull request #27670 from overleaf/rh-stripe-pause-addons

Prevent buying add-on while subscription is paused

GitOrigin-RevId: b8cfbbaa05a1031bedf37edf7b1ded2252eb6906
This commit is contained in:
roo hutton
2025-08-12 08:06:08 +00:00
committed by Copybot
parent ec0f719307
commit 6c185cd700
5 changed files with 467 additions and 0 deletions
@@ -159,6 +159,7 @@ describe('SubscriptionController', function () {
'./RecurlyWrapper': (this.RecurlyWrapper = {
promises: {
updateAccountEmailAddress: sinon.stub().resolves(),
getSubscription: sinon.stub().resolves({}),
},
}),
'./RecurlyEventHandler': {
@@ -744,4 +745,358 @@ describe('SubscriptionController', function () {
.called
})
})
describe('checkSubscriptionPauseStatus', function () {
beforeEach(function () {
this.user = {
_id: 'user-id-123',
email: 'test@example.com',
}
})
it('should return isPaused: false when user has no subscription', async function () {
this.LimitationsManager.promises.userHasSubscription.resolves({
subscription: null,
})
const result =
await this.SubscriptionController.checkSubscriptionPauseStatus(
this.user
)
expect(result).to.deep.equal({ isPaused: false })
})
it('should return isPaused: false when subscription has no paymentProvider', async function () {
const subscription = {
planCode: 'professional',
}
this.LimitationsManager.promises.userHasSubscription.resolves({
subscription,
})
const result =
await this.SubscriptionController.checkSubscriptionPauseStatus(
this.user
)
expect(result).to.deep.equal({ isPaused: false })
})
it('should return isPaused: false when subscription has no subscriptionId', async function () {
const subscription = {
paymentProvider: {
service: 'stripe',
subscriptionId: null,
},
}
this.LimitationsManager.promises.userHasSubscription.resolves({
subscription,
})
const result =
await this.SubscriptionController.checkSubscriptionPauseStatus(
this.user
)
expect(result).to.deep.equal({ isPaused: false })
})
it('should return isPaused: false when Stripe subscription has no remaining pause cycles', async function () {
const subscription = {
paymentProvider: {
service: 'stripe',
subscriptionId: 'sub-123',
},
}
this.LimitationsManager.promises.userHasSubscription.resolves({
subscription,
})
const paymentRecord = {
subscription: {
remainingPauseCycles: 0,
},
}
this.Modules.promises.hooks.fire
.withArgs('getPaymentFromRecord', subscription)
.resolves([paymentRecord])
const result =
await this.SubscriptionController.checkSubscriptionPauseStatus(
this.user
)
expect(result).to.deep.equal({ isPaused: false })
})
it('should return isPaused: false when Stripe subscription has no remainingPauseCycles property', async function () {
const subscription = {
paymentProvider: {
service: 'stripe',
subscriptionId: 'sub-123',
},
}
this.LimitationsManager.promises.userHasSubscription.resolves({
subscription,
})
const paymentRecord = {
subscription: {},
}
this.Modules.promises.hooks.fire
.withArgs('getPaymentFromRecord', subscription)
.resolves([paymentRecord])
const result =
await this.SubscriptionController.checkSubscriptionPauseStatus(
this.user
)
expect(result).to.deep.equal({ isPaused: false })
})
it('should return isPaused: true with redirect path when Stripe subscription has remaining pause cycles', async function () {
const subscription = {
paymentProvider: {
service: 'stripe',
subscriptionId: 'sub-123',
},
}
this.LimitationsManager.promises.userHasSubscription.resolves({
subscription,
})
const paymentRecord = {
subscription: {
remainingPauseCycles: 2,
},
}
this.Modules.promises.hooks.fire
.withArgs('getPaymentFromRecord', subscription)
.resolves([paymentRecord])
const result =
await this.SubscriptionController.checkSubscriptionPauseStatus(
this.user
)
expect(result).to.deep.equal({
isPaused: true,
redirectPath: '/user/subscription?redirect-reason=subscription-paused',
})
})
it('should return isPaused: true when remainingPauseCycles is exactly 1', async function () {
const subscription = {
paymentProvider: {
service: 'stripe',
subscriptionId: 'sub-123',
},
}
this.LimitationsManager.promises.userHasSubscription.resolves({
subscription,
})
const paymentRecord = {
subscription: {
remainingPauseCycles: 1,
},
}
this.Modules.promises.hooks.fire
.withArgs('getPaymentFromRecord', subscription)
.resolves([paymentRecord])
const result =
await this.SubscriptionController.checkSubscriptionPauseStatus(
this.user
)
expect(result).to.deep.equal({
isPaused: true,
redirectPath: '/user/subscription?redirect-reason=subscription-paused',
})
})
it('should return isPaused: false when userHasSubscription throws error', async function () {
const error = new Error('Something bad happened')
this.LimitationsManager.promises.userHasSubscription.rejects(error)
const result =
await this.SubscriptionController.checkSubscriptionPauseStatus(
this.user
)
expect(result).to.deep.equal({ isPaused: false })
})
it('should return isPaused: false when getPaymentFromRecord throws error', async function () {
const subscription = {
paymentProvider: {
service: 'stripe',
subscriptionId: 'sub-123',
},
}
this.LimitationsManager.promises.userHasSubscription.resolves({
subscription,
})
const error = new Error('Something bad happened')
this.Modules.promises.hooks.fire
.withArgs('getPaymentFromRecord', subscription)
.rejects(error)
const result =
await this.SubscriptionController.checkSubscriptionPauseStatus(
this.user
)
expect(result).to.deep.equal({ isPaused: false })
})
it('should return isPaused: false when Recurly subscription is not paused', async function () {
const subscription = {
recurlySubscription_id: 'uuid-123',
recurlyStatus: {
state: 'active',
},
}
this.LimitationsManager.promises.userHasSubscription.resolves({
subscription,
})
const result =
await this.SubscriptionController.checkSubscriptionPauseStatus(
this.user
)
expect(result).to.deep.equal({ isPaused: false })
})
it('should return isPaused: true when Recurly subscription is paused', async function () {
const subscription = {
recurlySubscription_id: 'uuid-123',
recurlyStatus: {
state: 'paused',
},
}
this.LimitationsManager.promises.userHasSubscription.resolves({
subscription,
})
const result =
await this.SubscriptionController.checkSubscriptionPauseStatus(
this.user
)
expect(result).to.deep.equal({
isPaused: true,
redirectPath: '/user/subscription?redirect-reason=subscription-paused',
})
})
it('should return isPaused: true when Recurly subscription has pending pause cycles', async function () {
const subscription = {
recurlySubscription_id: 'uuid-123',
recurlyStatus: {
state: 'active',
},
}
this.LimitationsManager.promises.userHasSubscription.resolves({
subscription,
})
const recurlySubscriptionData = {
remaining_pause_cycles: 2,
}
this.RecurlyWrapper.promises.getSubscription.resolves(
recurlySubscriptionData
)
const result =
await this.SubscriptionController.checkSubscriptionPauseStatus(
this.user
)
expect(result).to.deep.equal({
isPaused: true,
redirectPath: '/user/subscription?redirect-reason=subscription-paused',
})
expect(
this.RecurlyWrapper.promises.getSubscription
).to.have.been.calledWith('uuid-123')
})
it('should return isPaused: false when Recurly subscription has no remaining pause cycles', async function () {
const subscription = {
recurlySubscription_id: 'uuid-123',
recurlyStatus: {
state: 'active',
},
}
this.LimitationsManager.promises.userHasSubscription.resolves({
subscription,
})
const recurlySubscriptionData = {
remaining_pause_cycles: 0,
}
this.RecurlyWrapper.promises.getSubscription.resolves(
recurlySubscriptionData
)
const result =
await this.SubscriptionController.checkSubscriptionPauseStatus(
this.user
)
expect(result).to.deep.equal({ isPaused: false })
})
it('should return isPaused: false when Recurly subscription has no remaining_pause_cycles property', async function () {
const subscription = {
recurlySubscription_id: 'uuid-123',
recurlyStatus: {
state: 'active',
},
}
this.LimitationsManager.promises.userHasSubscription.resolves({
subscription,
})
const recurlySubscriptionData = {}
this.RecurlyWrapper.promises.getSubscription.resolves(
recurlySubscriptionData
)
const result =
await this.SubscriptionController.checkSubscriptionPauseStatus(
this.user
)
expect(result).to.deep.equal({ isPaused: false })
})
it('should return isPaused: false when Recurly API call fails', async function () {
const subscription = {
recurlySubscription_id: 'uuid-123',
recurlyStatus: {
state: 'active',
},
}
this.LimitationsManager.promises.userHasSubscription.resolves({
subscription,
})
const error = new Error('Recurly API failed')
this.RecurlyWrapper.promises.getSubscription.rejects(error)
const result =
await this.SubscriptionController.checkSubscriptionPauseStatus(
this.user
)
expect(result).to.deep.equal({ isPaused: false })
})
})
})