Merge pull request #27141 from overleaf/rh-stripe-schedule-pause

Support subscription pausing in Stripe

GitOrigin-RevId: 5550b2af2db99fd456d591c9bb4ba64d34dc7615
This commit is contained in:
roo hutton
2025-07-23 12:16:19 +01:00
committed by Copybot
parent 7129fe4455
commit 8cb07fdb08
4 changed files with 92 additions and 13 deletions

View File

@@ -319,8 +319,9 @@ async function pauseSubscription(user, pauseCycles) {
throw new Error('Cannot pause a subscription with addons')
}
await RecurlyClient.promises.pauseSubscriptionByUuid(
subscription.recurlySubscription_id,
await Modules.promises.hooks.fire(
'pausePaidSubscription',
subscription,
pauseCycles
)
}

View File

@@ -17,6 +17,7 @@ const { V1ConnectionError } = require('../Errors/Errors')
const FeaturesHelper = require('./FeaturesHelper')
const { formatCurrency } = require('../../util/currency')
const Modules = require('../../infrastructure/Modules')
const SplitTestHandler = require('../SplitTests/SplitTestHandler')
/**
* @import { Subscription } from "../../../../types/project/dashboard/subscription"
@@ -254,6 +255,32 @@ async function buildUsersSubscriptionViewModel(user, locale = 'en') {
const isInTrial =
paymentRecord.subscription.trialPeriodEnd &&
paymentRecord.subscription.trialPeriodEnd.getTime() > Date.now()
let isEligibleForPause = false
const commonPauseConditions =
!personalSubscription.pendingPlan &&
!personalSubscription.groupPlan &&
!isInTrial &&
!paymentRecord.subscription.planCode.includes('ann') &&
!paymentRecord.subscription.addOns?.length
if (
paymentRecord.subscription.service === 'recurly' &&
commonPauseConditions
) {
isEligibleForPause = true
} else if (
paymentRecord.subscription.service.includes('stripe') &&
commonPauseConditions
) {
const stripePauseAssignment =
await SplitTestHandler.promises.getAssignmentForUser(
user._id,
'stripe-pause'
)
isEligibleForPause = stripePauseAssignment.variant === 'enabled'
}
personalSubscription.payment = {
taxRate,
billingDetailsLink:
@@ -281,13 +308,7 @@ async function buildUsersSubscriptionViewModel(user, locale = 'en') {
hasPastDueInvoice: paymentRecord.account.hasPastDueInvoice,
pausedAt: paymentRecord.subscription.pausePeriodStart,
remainingPauseCycles: paymentRecord.subscription.remainingPauseCycles,
isEligibleForPause:
paymentRecord.subscription.service === 'recurly' &&
!personalSubscription.pendingPlan &&
!personalSubscription.groupPlan &&
!isInTrial &&
!paymentRecord.subscription.planCode.includes('ann') &&
!paymentRecord.subscription.addOns?.length > 0,
isEligibleForPause,
isEligibleForGroupPlan: await isEligibleForGroupPlan(
paymentRecord.subscription.service,
isInTrial

View File

@@ -497,11 +497,18 @@ describe('SubscriptionHandler', function () {
},
})
})
it('should make a pause call to recurly', async function () {
it('should call pause hook', async function () {
await this.SubscriptionHandler.promises.pauseSubscription(this.user, 3)
this.RecurlyClient.promises.pauseSubscriptionByUuid.called.should.equal(
true
expect(this.Modules.promises.hooks.fire).to.have.been.calledWith(
'pausePaidSubscription',
{
recurlySubscription_id: this.activeRecurlySubscription.uuid,
recurlyStatus: { state: 'non-trial' },
planCode: 'collaborator',
addOns: [],
},
3
)
})
})

View File

@@ -704,8 +704,58 @@ describe('SubscriptionViewModelBuilder', function () {
})
describe('isEligibleForPause', function () {
it('is false for Stripe subscriptions', async function () {
beforeEach(function () {
this.paymentRecord.service = 'recurly'
this.paymentRecord.addOns = []
this.paymentRecord.planCode = 'plan-code'
this.paymentRecord.trialPeriodEnd = null
this.individualSubscription.pendingPlan = undefined
this.individualSubscription.groupPlan = undefined
})
it('is false for Stripe subscriptions when feature flag is disabled', async function () {
this.paymentRecord.service = 'stripe-us'
this.SplitTestHandler.promises.getAssignmentForUser
.withArgs(this.user._id, 'stripe-pause')
.resolves({ variant: 'default' })
const result =
await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
this.user
)
assert.isFalse(result.personalSubscription.payment.isEligibleForPause)
})
it('is true for Stripe subscriptions when feature flag is enabled', async function () {
this.paymentRecord.service = 'stripe-us'
this.SplitTestHandler.promises.getAssignmentForUser
.withArgs(this.user._id, 'stripe-pause')
.resolves({ variant: 'enabled' })
const result =
await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
this.user
)
assert.isTrue(result.personalSubscription.payment.isEligibleForPause)
})
it('is false for Stripe subscriptions with pending plan even when feature flag is enabled', async function () {
this.paymentRecord.service = 'stripe-us'
this.individualSubscription.pendingPlan = {} // anything
this.SplitTestHandler.promises.getAssignmentForUser
.withArgs(this.user._id, 'stripe-pause')
.resolves({ variant: 'enabled' })
const result =
await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
this.user
)
assert.isFalse(result.personalSubscription.payment.isEligibleForPause)
})
it('is false for Stripe subscriptions with annual plan even when feature flag is enabled', async function () {
this.paymentRecord.service = 'stripe-us'
this.paymentRecord.planCode = 'collaborator-annual'
this.SplitTestHandler.promises.getAssignmentForUser
.withArgs(this.user._id, 'stripe-pause')
.resolves({ variant: 'enabled' })
const result =
await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
this.user