Merge pull request #26829 from overleaf/ii-flexible-licensing-manually-billed-users-add-seats

[web] FL manually billed subscriptions with no upsell

GitOrigin-RevId: b5f2083c7eabd0a1a5d024d5699d2c5e5556671a
This commit is contained in:
ilkin-overleaf
2025-07-08 15:22:57 +03:00
committed by Copybot
parent 933df2beb5
commit d898582b2f
8 changed files with 108 additions and 6 deletions

View File

@@ -26,6 +26,8 @@ class SubtotalLimitExceededError extends OError {}
class HasPastDueInvoiceError extends OError {}
class HasNoAdditionalLicenseWhenManuallyCollectedError extends OError {}
class PaymentActionRequiredError extends OError {
constructor(info) {
super('Payment action required', info)
@@ -43,4 +45,5 @@ module.exports = {
InactiveError,
SubtotalLimitExceededError,
HasPastDueInvoiceError,
HasNoAdditionalLicenseWhenManuallyCollectedError,
}

View File

@@ -19,6 +19,7 @@ import {
InactiveError,
SubtotalLimitExceededError,
HasPastDueInvoiceError,
HasNoAdditionalLicenseWhenManuallyCollectedError,
} from './Errors.js'
/**
@@ -164,6 +165,9 @@ async function addSeatsToGroupSubscription(req, res) {
paymentProviderSubscription,
userId
)
await SubscriptionGroupHandler.promises.ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual(
paymentProviderSubscription
)
} else {
await SubscriptionGroupHandler.promises.ensureSubscriptionCollectionMethodIsNotManual(
paymentProviderSubscription
@@ -187,7 +191,10 @@ async function addSeatsToGroupSubscription(req, res) {
)
}
if (error instanceof ManuallyCollectedError) {
if (
error instanceof ManuallyCollectedError ||
error instanceof HasNoAdditionalLicenseWhenManuallyCollectedError
) {
return res.redirect(
'/user/subscription/group/manually-collected-subscription'
)
@@ -231,7 +238,8 @@ async function previewAddSeatsSubscriptionChange(req, res) {
error instanceof ManuallyCollectedError ||
error instanceof PendingChangeError ||
error instanceof InactiveError ||
error instanceof HasPastDueInvoiceError
error instanceof HasPastDueInvoiceError ||
error instanceof HasNoAdditionalLicenseWhenManuallyCollectedError
) {
return res.status(422).end()
}
@@ -274,7 +282,8 @@ async function createAddSeatsSubscriptionChange(req, res) {
error instanceof ManuallyCollectedError ||
error instanceof PendingChangeError ||
error instanceof InactiveError ||
error instanceof HasPastDueInvoiceError
error instanceof HasPastDueInvoiceError ||
error instanceof HasNoAdditionalLicenseWhenManuallyCollectedError
) {
return res.status(422).end()
}

View File

@@ -18,6 +18,7 @@ const {
PendingChangeError,
InactiveError,
HasPastDueInvoiceError,
HasNoAdditionalLicenseWhenManuallyCollectedError,
} = require('./Errors')
const EmailHelper = require('../Helpers/EmailHelper')
const { InvalidEmailError } = require('../Errors/Errors')
@@ -123,6 +124,22 @@ async function ensureSubscriptionHasNoPastDueInvoice(subscription) {
}
}
async function ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual(
paymentProviderSubscription
) {
if (
paymentProviderSubscription.isCollectionMethodManual &&
!paymentProviderSubscription.hasAddOn(MEMBERS_LIMIT_ADD_ON_CODE)
) {
throw new HasNoAdditionalLicenseWhenManuallyCollectedError(
'This subscription is being collected manually has no "additional-license" add-on',
{
subscription_id: paymentProviderSubscription.id,
}
)
}
}
async function getUsersGroupSubscriptionDetails(userId) {
const subscription =
await SubscriptionLocator.promises.getUsersSubscription(userId)
@@ -470,6 +487,10 @@ module.exports = {
ensureSubscriptionHasNoPastDueInvoice: callbackify(
ensureSubscriptionHasNoPastDueInvoice
),
ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual:
callbackify(
ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual
),
getTotalConfirmedUsersInGroup: callbackify(getTotalConfirmedUsersInGroup),
isUserPartOfGroup: callbackify(isUserPartOfGroup),
getGroupPlanUpgradePreview: callbackify(getGroupPlanUpgradePreview),
@@ -484,6 +505,7 @@ module.exports = {
ensureSubscriptionCollectionMethodIsNotManual,
ensureSubscriptionHasNoPendingChanges,
ensureSubscriptionHasNoPastDueInvoice,
ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual,
getTotalConfirmedUsersInGroup,
isUserPartOfGroup,
getUsersGroupSubscriptionDetails,

View File

@@ -873,7 +873,7 @@
"issued_on": "",
"it_looks_like_that_didnt_work_you_can_try_again_or_get_in_touch": "",
"it_looks_like_your_account_is_billed_manually": "",
"it_looks_like_your_account_is_billed_manually_upgrading_subscription": "",
"it_looks_like_your_account_is_billed_manually_purchasing_additional_license_or_upgrading_subscription": "",
"it_looks_like_your_payment_details_are_missing_please_update_your_billing_information": "",
"italics": "",
"join_beta_program": "",

View File

@@ -24,7 +24,7 @@ function ManuallyCollectedSubscription() {
content={
isFlexibleGroupLicensingForManuallyBilledSubscriptions ? (
<Trans
i18nKey="it_looks_like_your_account_is_billed_manually_upgrading_subscription"
i18nKey="it_looks_like_your_account_is_billed_manually_purchasing_additional_license_or_upgrading_subscription"
components={[
// eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key
<a href="/contact" rel="noreferrer noopener" />,

View File

@@ -1131,7 +1131,7 @@
"it": "Italian",
"it_looks_like_that_didnt_work_you_can_try_again_or_get_in_touch": "It looks like that didnt work. You can try again or <0>get in touch</0> with our Support team for more help.",
"it_looks_like_your_account_is_billed_manually": "It looks like your account is being billed manually - adding seats or upgrading your subscription can only be done by the Support team. Please <0>get in touch</0> for help.",
"it_looks_like_your_account_is_billed_manually_upgrading_subscription": "It looks like your account is being billed manually - upgrading your subscription can only be done by the Support team. Please <0>get in touch</0> for help.",
"it_looks_like_your_account_is_billed_manually_purchasing_additional_license_or_upgrading_subscription": "It looks like your account is being billed manually - purchasing additional licenses or upgrading your subscription can only be done by the Support team. Please <0>get in touch</0> for help.",
"it_looks_like_your_payment_details_are_missing_please_update_your_billing_information": "It looks like your payment details are missing. Please <0>update your billing information</0>, or <1>get in touch</1> with our Support team for more help.",
"italics": "Italics",
"ja": "Japanese",

View File

@@ -73,6 +73,8 @@ describe('SubscriptionGroupController', function () {
.resolves(ctx.previewSubscriptionChangeData),
checkBillingInfoExistence: sinon.stub().resolves(ctx.paymentMethod),
updateSubscriptionPaymentTerms: sinon.stub().resolves(),
ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual:
sinon.stub().resolves(),
},
}
@@ -140,6 +142,7 @@ describe('SubscriptionGroupController', function () {
InactiveError: class extends Error {},
SubtotalLimitExceededError: class extends Error {},
HasPastDueInvoiceError: class extends Error {},
HasNoAdditionalLicenseWhenManuallyCollectedError: class extends Error {},
}
vi.doMock(
@@ -521,6 +524,28 @@ describe('SubscriptionGroupController', function () {
})
})
it('should redirect to manually collected subscription error page when collection method is manual and has no additional license add-on', async function (ctx) {
await new Promise(resolve => {
ctx.SubscriptionGroupHandler.promises.ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual =
sinon
.stub()
.throws(
new ctx.Errors.HasNoAdditionalLicenseWhenManuallyCollectedError()
)
const res = {
redirect: url => {
url.should.equal(
'/user/subscription/group/manually-collected-subscription'
)
resolve()
},
}
ctx.Controller.addSeatsToGroupSubscription(ctx.req, res)
})
})
it('should redirect to subscription page when there is a pending change', async function (ctx) {
await new Promise(resolve => {
ctx.SubscriptionGroupHandler.promises.ensureSubscriptionHasNoPendingChanges =

View File

@@ -806,6 +806,49 @@ describe('SubscriptionGroupHandler', function () {
})
})
describe('ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual', function () {
it('should throw if the subscription is manually collected and has no additional license add-on', async function () {
await expect(
this.Handler.promises.ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual(
{
isCollectionMethodManual: true,
hasAddOn: sinon
.stub()
.withArgs('additional-license')
.returns(false),
}
)
).to.be.rejectedWith(
'This subscription is being collected manually has no "additional-license" add-on'
)
})
it('should not throw if the subscription is not manually collected and has no additional license add-on and ', async function () {
await expect(
this.Handler.promises.ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual(
{
isCollectionMethodManual: false,
hasAddOn: sinon
.stub()
.withArgs('additional-license')
.returns(false),
}
)
).to.not.be.rejected
})
it('should not throw if the subscription is not manually collected and has additional license add-on', async function () {
await expect(
this.Handler.promises.ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual(
{
isCollectionMethodManual: true,
hasAddOn: sinon.stub().withArgs('additional-license').returns(true),
}
)
).to.not.be.rejected
})
})
describe('getGroupPlanUpgradePreview', function () {
it('should generate preview for subscription upgrade', async function () {
const result = await this.Handler.promises.getGroupPlanUpgradePreview(