mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 17:19:37 +02:00
Merge pull request #28110 from overleaf/ls-handle-manual-subscription-on-add-on-purchase-page
Handle manual subscription on AddOn purchase page GitOrigin-RevId: 54281d3471d7c2b60d333e6264904b3744156138
This commit is contained in:
@@ -21,6 +21,7 @@ const {
|
||||
AddOnNotPresentError,
|
||||
PaymentActionRequiredError,
|
||||
PaymentFailedError,
|
||||
MissingBillingInfoError,
|
||||
} = require('./Errors')
|
||||
const SplitTestHandler = require('../SplitTests/SplitTestHandler')
|
||||
const AuthorizationManager = require('../Authorization/AuthorizationManager')
|
||||
@@ -40,6 +41,7 @@ const {
|
||||
sanitizeSessionUserForFrontEnd,
|
||||
} = require('../../infrastructure/FrontEndUser')
|
||||
const { IndeterminateInvoiceError } = require('../Errors/Errors')
|
||||
const SubscriptionLocator = require('./SubscriptionLocator')
|
||||
|
||||
const SUBSCRIPTION_PAUSED_REDIRECT_PATH =
|
||||
'/user/subscription?redirect-reason=subscription-paused'
|
||||
@@ -93,6 +95,23 @@ async function _checkRecurlySubscriptionPauseStatus(subscription) {
|
||||
)
|
||||
}
|
||||
|
||||
/** Check if a user's subscription is manual or custom
|
||||
* @param {Object} user - The user object
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async function _isManualOrCustomSubscription(user) {
|
||||
const subscription = await SubscriptionLocator.promises.getUsersSubscription(
|
||||
user._id
|
||||
)
|
||||
if (!subscription) {
|
||||
return false
|
||||
}
|
||||
|
||||
return (
|
||||
subscription.customAccount || subscription.collectionMethod === 'manual'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user's subscription is currently paused
|
||||
* @param {Object} user - The user object
|
||||
@@ -460,16 +479,39 @@ async function previewAddonPurchase(req, res) {
|
||||
)
|
||||
}
|
||||
|
||||
const isManualOrCustom = await _isManualOrCustomSubscription(user)
|
||||
if (isManualOrCustom) {
|
||||
return res.redirect(
|
||||
'/user/subscription?redirect-reason=ai-assist-unavailable'
|
||||
)
|
||||
}
|
||||
|
||||
const { isPaused, redirectPath } = await checkSubscriptionPauseStatus(user)
|
||||
if (isPaused) {
|
||||
return res.redirect(redirectPath)
|
||||
}
|
||||
|
||||
/** @type {PaymentMethod[]} */
|
||||
const paymentMethod = await Modules.promises.hooks.fire(
|
||||
'getPaymentMethod',
|
||||
userId
|
||||
)
|
||||
let paymentMethod
|
||||
try {
|
||||
/** @type {PaymentMethod[]} */
|
||||
paymentMethod = await Modules.promises.hooks.fire(
|
||||
'getPaymentMethod',
|
||||
userId
|
||||
)
|
||||
} catch (err) {
|
||||
if (err instanceof MissingBillingInfoError) {
|
||||
// We will get MissingBillingInfoError if a manual subscription doesn't have billing info
|
||||
// but doesn't marked as manual on the Overleaf side
|
||||
logger.error(
|
||||
{ err },
|
||||
'User has no billing info, cannot preview add-on purchase'
|
||||
)
|
||||
return res.redirect(
|
||||
'/user/subscription?redirect-reason=ai-assist-unavailable'
|
||||
)
|
||||
}
|
||||
throw err
|
||||
}
|
||||
|
||||
let subscriptionChange
|
||||
try {
|
||||
|
||||
@@ -63,6 +63,22 @@ describe('SubscriptionController', function () {
|
||||
attemptPaypalInvoiceCollection: sinon.stub().resolves(),
|
||||
startFreeTrial: sinon.stub().resolves(),
|
||||
purchaseAddon: sinon.stub().resolves(),
|
||||
previewAddonPurchase: sinon.stub().resolves({
|
||||
subscription: {
|
||||
currency: 'USD',
|
||||
netTerms: 0,
|
||||
periodEnd: new Date(),
|
||||
taxRate: 0,
|
||||
},
|
||||
immediateCharge: { amount: 0 },
|
||||
nextPlanCode: 'professional',
|
||||
nextPlanName: 'Professional',
|
||||
nextPlanPrice: 2000,
|
||||
nextAddOns: [],
|
||||
subtotal: 2000,
|
||||
tax: 0,
|
||||
total: 2000,
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -201,6 +217,29 @@ describe('SubscriptionController', function () {
|
||||
findById: sinon.stub().resolves(this.user),
|
||||
},
|
||||
},
|
||||
'./SubscriptionLocator': (this.SubscriptionLocator = {
|
||||
promises: {
|
||||
getUsersSubscription: sinon.stub().resolves(null),
|
||||
},
|
||||
}),
|
||||
'../Authorization/PermissionsManager': (this.PermissionsManager = {
|
||||
promises: {
|
||||
checkUserPermissions: sinon.stub().resolves(true),
|
||||
},
|
||||
}),
|
||||
'./RecurlyClient': (this.RecurlyClient = {
|
||||
promises: {
|
||||
getAddOn: sinon.stub().resolves({
|
||||
code: 'ai-assistant',
|
||||
name: 'AI Assistant',
|
||||
}),
|
||||
},
|
||||
}),
|
||||
'./PlansLocator': (this.PlansLocator = {
|
||||
findLocalPlanInSettings: sinon.stub().returns({
|
||||
annual: false,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1099,4 +1138,138 @@ describe('SubscriptionController', function () {
|
||||
expect(result).to.deep.equal({ isPaused: false })
|
||||
})
|
||||
})
|
||||
|
||||
describe('previewAddonPurchase', function () {
|
||||
beforeEach(function () {
|
||||
this.req = new MockRequest()
|
||||
this.req.params = { addOnCode: 'assistant' }
|
||||
this.req.query = { purchaseReferrer: 'fake-referrer' }
|
||||
this.res = new MockResponse()
|
||||
|
||||
this.Modules.promises.hooks.fire
|
||||
.withArgs('getPaymentMethod')
|
||||
.resolves(['fake-method'])
|
||||
this.SubscriptionLocator.promises.getUsersSubscription.resolves(null)
|
||||
})
|
||||
|
||||
describe('when user has manual or custom subscription', function () {
|
||||
it('should redirect with ai-assist-unavailable when subscription has customAccount = true', async function () {
|
||||
const customSubscription = {
|
||||
_id: 'sub-123',
|
||||
customAccount: true,
|
||||
collectionMethod: 'automatic',
|
||||
}
|
||||
this.SubscriptionLocator.promises.getUsersSubscription.resolves(
|
||||
customSubscription
|
||||
)
|
||||
|
||||
this.res.redirect = sinon.stub()
|
||||
|
||||
await this.SubscriptionController.previewAddonPurchase(
|
||||
this.req,
|
||||
this.res
|
||||
)
|
||||
|
||||
expect(this.res.redirect).to.have.been.calledWith(
|
||||
'/user/subscription?redirect-reason=ai-assist-unavailable'
|
||||
)
|
||||
})
|
||||
|
||||
it('should redirect with ai-assist-unavailable when subscription has collectionMethod = manual', async function () {
|
||||
const manualSubscription = {
|
||||
_id: 'sub-123',
|
||||
customAccount: false,
|
||||
collectionMethod: 'manual',
|
||||
}
|
||||
this.SubscriptionLocator.promises.getUsersSubscription.resolves(
|
||||
manualSubscription
|
||||
)
|
||||
|
||||
this.res.redirect = sinon.stub()
|
||||
|
||||
await this.SubscriptionController.previewAddonPurchase(
|
||||
this.req,
|
||||
this.res
|
||||
)
|
||||
|
||||
expect(this.res.redirect).to.have.been.calledWith(
|
||||
'/user/subscription?redirect-reason=ai-assist-unavailable'
|
||||
)
|
||||
})
|
||||
|
||||
it('should redirect with ai-assist-unavailable when subscription has both customAccount and manual collection', async function () {
|
||||
const customManualSubscription = {
|
||||
_id: 'sub-123',
|
||||
customAccount: true,
|
||||
collectionMethod: 'manual',
|
||||
}
|
||||
this.SubscriptionLocator.promises.getUsersSubscription.resolves(
|
||||
customManualSubscription
|
||||
)
|
||||
|
||||
this.res.redirect = sinon.stub()
|
||||
|
||||
await this.SubscriptionController.previewAddonPurchase(
|
||||
this.req,
|
||||
this.res
|
||||
)
|
||||
|
||||
expect(this.res.redirect).to.have.been.calledWith(
|
||||
'/user/subscription?redirect-reason=ai-assist-unavailable'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when user has normal subscription', function () {
|
||||
it('should proceed with preview when subscription is not manual or custom', async function () {
|
||||
const normalSubscription = {
|
||||
_id: 'sub-123',
|
||||
customAccount: false,
|
||||
collectionMethod: 'automatic',
|
||||
}
|
||||
this.SubscriptionLocator.promises.getUsersSubscription.resolves(
|
||||
normalSubscription
|
||||
)
|
||||
|
||||
this.res.render = sinon.stub()
|
||||
|
||||
await this.SubscriptionController.previewAddonPurchase(
|
||||
this.req,
|
||||
this.res
|
||||
)
|
||||
|
||||
expect(this.res.render).to.have.been.calledWith(
|
||||
'subscriptions/preview-change'
|
||||
)
|
||||
expect(
|
||||
this.SubscriptionHandler.promises.previewAddonPurchase
|
||||
).to.have.been.calledWith(this.user._id, 'assistant')
|
||||
})
|
||||
|
||||
it('should proceed with preview when customAccount is undefined and collectionMethod is automatic', async function () {
|
||||
const normalSubscription = {
|
||||
_id: 'sub-123',
|
||||
// customAccount: undefined (not set)
|
||||
collectionMethod: 'automatic',
|
||||
}
|
||||
this.SubscriptionLocator.promises.getUsersSubscription.resolves(
|
||||
normalSubscription
|
||||
)
|
||||
|
||||
this.res.render = sinon.stub()
|
||||
|
||||
await this.SubscriptionController.previewAddonPurchase(
|
||||
this.req,
|
||||
this.res
|
||||
)
|
||||
|
||||
expect(this.res.render).to.have.been.calledWith(
|
||||
'subscriptions/preview-change'
|
||||
)
|
||||
expect(
|
||||
this.SubscriptionHandler.promises.previewAddonPurchase
|
||||
).to.have.been.calledWith(this.user._id, 'assistant')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user