Merge pull request #25318 from overleaf/ii-flexible-licensing-manually-collected-3

[web] Add seats feature for manually collected subscriptions improvements

GitOrigin-RevId: 4fbd93097590d97ad6464d1988471a78bf7cb9e2
This commit is contained in:
ilkin-overleaf
2025-05-09 08:05:07 +00:00
committed by Copybot
parent d39d92cce8
commit 2ccdb74d20
21 changed files with 293 additions and 83 deletions
@@ -412,6 +412,7 @@ describe('PaymentProviderEntities', function () {
periodStart: new Date(),
periodEnd: new Date(),
collectionMethod: 'automatic',
netTerms: 0,
poNumber: '012345',
termsAndConditions: 'T&C copy',
})
@@ -58,6 +58,7 @@ describe('RecurlyClient', function () {
periodStart: new Date(),
periodEnd: new Date(),
collectionMethod: 'automatic',
netTerms: 0,
poNumber: '',
termsAndConditions: '',
})
@@ -90,6 +91,7 @@ describe('RecurlyClient', function () {
currentPeriodStartedAt: this.subscription.periodStart,
currentPeriodEndsAt: this.subscription.periodEnd,
collectionMethod: this.subscription.collectionMethod,
netTerms: this.subscription.netTerms,
poNumber: this.subscription.poNumber,
termsAndConditions: this.subscription.termsAndConditions,
}
@@ -690,29 +692,4 @@ describe('RecurlyClient', function () {
).to.be.rejectedWith(Error)
})
})
describe('getCountryCode', function () {
it('should return the country code from the account info', async function () {
this.client.getAccount = sinon.stub().resolves({
address: {
country: 'GB',
},
})
const countryCode = await this.RecurlyClient.promises.getCountryCode(
this.user._id
)
expect(countryCode).to.equal('GB')
})
it('should throw if country code doesnt exist', async function () {
this.client.getAccount = sinon.stub().resolves({
address: {
country: '',
},
})
await expect(
this.RecurlyClient.promises.getCountryCode(this.user._id)
).to.be.rejectedWith(Error, 'Country code not found')
})
})
})
@@ -67,6 +67,7 @@ describe('SubscriptionGroupController', function () {
ensureSubscriptionIsActive: sinon.stub().resolves(),
ensureSubscriptionCollectionMethodIsNotManual: sinon.stub().resolves(),
ensureSubscriptionHasNoPendingChanges: sinon.stub().resolves(),
ensureSubscriptionHasNoPastDueInvoice: sinon.stub().resolves(),
getGroupPlanUpgradePreview: sinon
.stub()
.resolves(this.previewSubscriptionChangeData),
@@ -138,6 +139,7 @@ describe('SubscriptionGroupController', function () {
PendingChangeError: class extends Error {},
InactiveError: class extends Error {},
SubtotalLimitExceededError: class extends Error {},
HasPastDueInvoiceError: class extends Error {},
}
this.Controller = await esmock.strict(modulePath, {
@@ -370,6 +372,9 @@ describe('SubscriptionGroupController', function () {
this.SubscriptionGroupHandler.promises.ensureSubscriptionIsActive
.calledWith(this.subscription)
.should.equal(true)
this.SubscriptionGroupHandler.promises.ensureSubscriptionHasNoPastDueInvoice
.calledWith(this.subscription)
.should.equal(true)
this.SubscriptionGroupHandler.promises.checkBillingInfoExistence
.calledWith(this.recurlySubscription, this.adminUserId)
.should.equal(true)
@@ -459,6 +464,20 @@ describe('SubscriptionGroupController', function () {
this.Controller.addSeatsToGroupSubscription(this.req, res)
})
it('should redirect to subscription page when subscription has pending invoice', function (done) {
this.SubscriptionGroupHandler.promises.ensureSubscriptionHasNoPastDueInvoice =
sinon.stub().rejects()
const res = {
redirect: url => {
url.should.equal('/user/subscription')
done()
},
}
this.Controller.addSeatsToGroupSubscription(this.req, res)
})
})
describe('previewAddSeatsSubscriptionChange', function () {
@@ -146,7 +146,6 @@ describe('SubscriptionGroupHandler', function () {
applySubscriptionChangeRequest: sinon
.stub()
.resolves(this.applySubscriptionChange),
getCountryCode: sinon.stub().resolves('BG'),
updateSubscriptionDetails: sinon.stub().resolves(),
},
}
@@ -198,6 +197,11 @@ describe('SubscriptionGroupHandler', function () {
if (hookName === 'generateTermsAndConditions') {
return Promise.resolve(['T&Cs'])
}
if (hookName === 'getPaymentFromRecord') {
return Promise.resolve([
{ account: { hasPastDueInvoice: false } },
])
}
return Promise.resolve()
}),
},
@@ -504,9 +508,6 @@ describe('SubscriptionGroupHandler', function () {
describe('updateSubscriptionPaymentTerms', function () {
describe('accounts with PO number', function () {
it('should update the subscription PO number and T&C', async function () {
this.RecurlyClient.promises.getCountryCode = sinon
.stub()
.resolves('GB')
await this.Handler.promises.updateSubscriptionPaymentTerms(
this.adminUser_id,
this.recurlySubscription,
@@ -526,9 +527,6 @@ describe('SubscriptionGroupHandler', function () {
describe('accounts with no PO number', function () {
it('should update the subscription T&C only', async function () {
this.RecurlyClient.promises.getCountryCode = sinon
.stub()
.resolves('GB')
await this.Handler.promises.updateSubscriptionPaymentTerms(
this.adminUser_id,
this.recurlySubscription
@@ -758,6 +756,27 @@ describe('SubscriptionGroupHandler', function () {
})
})
describe('ensureSubscriptionHasNoPastDueInvoice', function () {
it('should throw if the subscription has past due invoice', async function () {
this.Modules.promises.hooks.fire
.withArgs('getPaymentFromRecord')
.resolves([{ account: { hasPastDueInvoice: true } }])
await expect(
this.Handler.promises.ensureSubscriptionHasNoPastDueInvoice(
this.subscription
)
).to.be.rejectedWith('This subscription has a past due invoice')
})
it('should not throw if the subscription has no past due invoice', async function () {
await expect(
this.Handler.promises.ensureSubscriptionHasNoPastDueInvoice(
this.subscription
)
).to.not.be.rejected
})
})
describe('upgradeGroupPlan', function () {
it('should upgrade the subscription for flexible licensing group plans', async function () {
this.SubscriptionLocator.promises.getUsersSubscription = sinon