mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 17:19:37 +02:00
Merge pull request #33679 from overleaf/oa-plan-names
[web] Get plan names from the settings GitOrigin-RevId: 1e61975c3306c025f33e05686f9d2b57964b4f65
This commit is contained in:
@@ -22,7 +22,6 @@ import AuthorizationManager from '../Authorization/AuthorizationManager.mjs'
|
||||
import Modules from '../../infrastructure/Modules.mjs'
|
||||
import async from 'async'
|
||||
import HttpErrorHandler from '../Errors/HttpErrorHandler.mjs'
|
||||
import RecurlyClient from './RecurlyClient.mjs'
|
||||
import {
|
||||
AI_ADD_ON_CODE,
|
||||
subscriptionChangeIsAiAssistUpgrade,
|
||||
@@ -628,18 +627,17 @@ async function previewAddonPurchase(req, res) {
|
||||
throw err
|
||||
}
|
||||
|
||||
const subscription = subscriptionChange.subscription
|
||||
const addOn = await RecurlyClient.promises.getAddOn(
|
||||
subscription.planCode,
|
||||
addOnCode
|
||||
)
|
||||
const addOn = PlansLocator.findLocalPlanInSettings(addOnCode)
|
||||
if (!addOn) {
|
||||
return HttpErrorHandler.notFound(req, res, `Unknown add-on: ${addOnCode}`)
|
||||
}
|
||||
|
||||
/** @type {SubscriptionChangePreview} */
|
||||
const changePreview = makeChangePreview(
|
||||
{
|
||||
type: 'add-on-purchase',
|
||||
addOn: {
|
||||
code: addOn.code,
|
||||
code: addOn.planCode,
|
||||
name: addOn.name,
|
||||
},
|
||||
},
|
||||
@@ -868,8 +866,10 @@ async function previewSubscription(req, res, next) {
|
||||
if (!planCode) {
|
||||
return HttpErrorHandler.notFound(req, res, 'Missing plan code')
|
||||
}
|
||||
// TODO: use PaymentService to fetch plan information
|
||||
const plan = await RecurlyClient.promises.getPlan(planCode)
|
||||
const plan = PlansLocator.findLocalPlanInSettings(planCode)
|
||||
if (!plan) {
|
||||
return HttpErrorHandler.notFound(req, res, `Unknown plan: ${planCode}`)
|
||||
}
|
||||
const user = SessionManager.getSessionUser(req.session)
|
||||
const userId = user?._id
|
||||
|
||||
@@ -896,7 +896,7 @@ async function previewSubscription(req, res, next) {
|
||||
const changePreview = makeChangePreview(
|
||||
{
|
||||
type: 'premium-subscription',
|
||||
plan: { code: plan.code, name: plan.name },
|
||||
plan: { code: plan.planCode, name: plan.name },
|
||||
},
|
||||
subscriptionChange,
|
||||
paymentMethod[0]
|
||||
@@ -1277,7 +1277,7 @@ function makeChangePreview(
|
||||
date: subscription.periodEnd.toISOString(),
|
||||
plan: {
|
||||
name: getPlanNameForDisplay(
|
||||
futureInvoiceChange.nextPlanName,
|
||||
nextPlan?.name ?? futureInvoiceChange.nextPlanName,
|
||||
futureInvoiceChange.nextPlanCode
|
||||
),
|
||||
amount: futureInvoiceChange.nextPlanPrice,
|
||||
|
||||
@@ -287,6 +287,10 @@ describe('SubscriptionController', function () {
|
||||
res.status(403)
|
||||
res.json({ message })
|
||||
}),
|
||||
notFound: sinon.stub().callsFake((req, res, message) => {
|
||||
res.status(404)
|
||||
res.json({ message })
|
||||
}),
|
||||
}),
|
||||
}))
|
||||
|
||||
@@ -350,20 +354,6 @@ describe('SubscriptionController', function () {
|
||||
})
|
||||
)
|
||||
|
||||
vi.doMock(
|
||||
'../../../../app/src/Features/Subscription/RecurlyClient',
|
||||
() => ({
|
||||
default: (ctx.RecurlyClient = {
|
||||
promises: {
|
||||
getAddOn: sinon.stub().resolves({
|
||||
code: 'ai-assistant',
|
||||
name: 'AI Assistant',
|
||||
}),
|
||||
},
|
||||
}),
|
||||
})
|
||||
)
|
||||
|
||||
vi.doMock('../../../../app/src/Features/Subscription/PlansLocator', () => ({
|
||||
default: (ctx.PlansLocator = {
|
||||
findLocalPlanInSettings: sinon.stub().returns({
|
||||
@@ -1538,6 +1528,130 @@ describe('SubscriptionController', function () {
|
||||
expect(preview.nextInvoice.plan.name).to.equal('Professional')
|
||||
expect(preview.nextInvoice.plan.amount).to.equal(2000)
|
||||
})
|
||||
|
||||
it('prefers the local plan name over the legacy payment-provider name for the future invoice', function (ctx) {
|
||||
baseSubscription.pendingChange = undefined
|
||||
ctx.PlansLocator.findLocalPlanInSettings
|
||||
.withArgs('professional')
|
||||
.returns({
|
||||
planCode: 'professional',
|
||||
name: 'Pro monthly',
|
||||
annual: false,
|
||||
})
|
||||
const preview = ctx.SubscriptionController.makeChangePreview(
|
||||
{
|
||||
type: 'premium-subscription',
|
||||
plan: { code: 'professional', name: 'Pro monthly' },
|
||||
},
|
||||
subscriptionChange
|
||||
)
|
||||
expect(preview.nextInvoice.plan.name).to.equal('Pro monthly')
|
||||
})
|
||||
})
|
||||
|
||||
describe('previewSubscription', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.req = new MockRequest(vi)
|
||||
ctx.req.query = { planCode: 'collaborator' }
|
||||
ctx.res = new MockResponse(vi)
|
||||
ctx.res.render = sinon.stub()
|
||||
|
||||
ctx.PlansLocator.findLocalPlanInSettings.returns({
|
||||
planCode: 'collaborator',
|
||||
name: 'Standard monthly',
|
||||
annual: false,
|
||||
})
|
||||
|
||||
ctx.SubscriptionHandler.promises.previewSubscriptionChange = sinon
|
||||
.stub()
|
||||
.resolves({
|
||||
subscription: {
|
||||
currency: 'USD',
|
||||
netTerms: 0,
|
||||
periodEnd: new Date('2027-04-29'),
|
||||
taxRate: 0,
|
||||
},
|
||||
nextPlanCode: 'collaborator',
|
||||
nextPlanName: 'Standard monthly',
|
||||
nextPlanPrice: 2300,
|
||||
nextAddOns: [],
|
||||
immediateCharge: { subtotal: 0, tax: 0, total: 0, discount: 0 },
|
||||
subtotal: 2300,
|
||||
tax: 0,
|
||||
total: 2300,
|
||||
})
|
||||
|
||||
ctx.Modules.promises.hooks.fire
|
||||
.withArgs('getPaymentMethod')
|
||||
.resolves(['fake-method'])
|
||||
})
|
||||
|
||||
it('renders the renamed local plan name in changePreview.change.plan', async function (ctx) {
|
||||
await ctx.SubscriptionController.previewSubscription(ctx.req, ctx.res)
|
||||
|
||||
expect(ctx.res.render).to.have.been.calledWith(
|
||||
'subscriptions/preview-change',
|
||||
sinon.match({
|
||||
changePreview: sinon.match({
|
||||
change: {
|
||||
type: 'premium-subscription',
|
||||
plan: { code: 'collaborator', name: 'Standard monthly' },
|
||||
},
|
||||
}),
|
||||
})
|
||||
)
|
||||
expect(ctx.PlansLocator.findLocalPlanInSettings).to.have.been.calledWith(
|
||||
'collaborator'
|
||||
)
|
||||
})
|
||||
|
||||
it('returns 404 when planCode is missing', async function (ctx) {
|
||||
ctx.req.query = {}
|
||||
|
||||
await ctx.SubscriptionController.previewSubscription(ctx.req, ctx.res)
|
||||
|
||||
expect(ctx.HttpErrorHandler.notFound).to.have.been.calledWith(
|
||||
ctx.req,
|
||||
ctx.res,
|
||||
'Missing plan code'
|
||||
)
|
||||
expect(ctx.res.render).not.to.have.been.called
|
||||
})
|
||||
|
||||
it('returns 404 when planCode is unknown to the local plan registry', async function (ctx) {
|
||||
ctx.req.query = { planCode: 'does-not-exist' }
|
||||
ctx.PlansLocator.findLocalPlanInSettings.returns(null)
|
||||
|
||||
await ctx.SubscriptionController.previewSubscription(ctx.req, ctx.res)
|
||||
|
||||
expect(ctx.HttpErrorHandler.notFound).to.have.been.calledWith(
|
||||
ctx.req,
|
||||
ctx.res,
|
||||
'Unknown plan: does-not-exist'
|
||||
)
|
||||
expect(ctx.res.render).not.to.have.been.called
|
||||
})
|
||||
|
||||
it('passes trialDisabledReason to the view when the user is ineligible for a free trial', async function (ctx) {
|
||||
ctx.req.query = { planCode: 'collaborator_free_trial_7_days' }
|
||||
ctx.PlansLocator.findLocalPlanInSettings.returns({
|
||||
planCode: 'collaborator_free_trial_7_days',
|
||||
name: 'Standard monthly',
|
||||
annual: false,
|
||||
})
|
||||
ctx.Modules.promises.hooks.fire
|
||||
.withArgs('userCanStartTrial', ctx.user)
|
||||
.resolves([{ canStartTrial: false, disabledReason: 'already-used' }])
|
||||
|
||||
await ctx.SubscriptionController.previewSubscription(ctx.req, ctx.res)
|
||||
|
||||
expect(ctx.res.render).to.have.been.calledWith(
|
||||
'subscriptions/preview-change',
|
||||
sinon.match({
|
||||
trialDisabledReason: 'already-used',
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('previewAddonPurchase', function () {
|
||||
@@ -1635,6 +1749,11 @@ describe('SubscriptionController', function () {
|
||||
ctx.SubscriptionLocator.promises.getUsersSubscription.resolves(
|
||||
normalSubscription
|
||||
)
|
||||
ctx.PlansLocator.findLocalPlanInSettings.withArgs('assistant').returns({
|
||||
planCode: 'assistant',
|
||||
name: 'AI Assist',
|
||||
annual: false,
|
||||
})
|
||||
|
||||
ctx.res.render = sinon.stub()
|
||||
|
||||
@@ -1643,7 +1762,12 @@ describe('SubscriptionController', function () {
|
||||
expect(ctx.res.render).to.have.been.calledWith(
|
||||
'subscriptions/preview-change',
|
||||
sinon.match({
|
||||
changePreview: sinon.match.object,
|
||||
changePreview: sinon.match({
|
||||
change: {
|
||||
type: 'add-on-purchase',
|
||||
addOn: { code: 'assistant', name: 'AI Assist' },
|
||||
},
|
||||
}),
|
||||
purchaseReferrer: 'fake-referrer',
|
||||
redirectedPaymentErrorCode: undefined,
|
||||
})
|
||||
@@ -1651,6 +1775,9 @@ describe('SubscriptionController', function () {
|
||||
expect(
|
||||
ctx.SubscriptionHandler.promises.previewAddonPurchase
|
||||
).to.have.been.calledWith(ctx.user._id, 'assistant')
|
||||
expect(
|
||||
ctx.PlansLocator.findLocalPlanInSettings
|
||||
).to.have.been.calledWith('assistant')
|
||||
})
|
||||
|
||||
it('should pass redirectedPaymentErrorCode to the view when errorCode query param is present', async function (ctx) {
|
||||
@@ -1678,6 +1805,31 @@ describe('SubscriptionController', function () {
|
||||
)
|
||||
})
|
||||
|
||||
it('returns 404 when the add-on code is not in the local plan registry', async function (ctx) {
|
||||
const normalSubscription = {
|
||||
_id: 'sub-123',
|
||||
customAccount: false,
|
||||
collectionMethod: 'automatic',
|
||||
}
|
||||
ctx.SubscriptionLocator.promises.getUsersSubscription.resolves(
|
||||
normalSubscription
|
||||
)
|
||||
ctx.PlansLocator.findLocalPlanInSettings
|
||||
.withArgs('assistant')
|
||||
.returns(null)
|
||||
|
||||
ctx.res.render = sinon.stub()
|
||||
|
||||
await ctx.SubscriptionController.previewAddonPurchase(ctx.req, ctx.res)
|
||||
|
||||
expect(ctx.HttpErrorHandler.notFound).to.have.been.calledWith(
|
||||
ctx.req,
|
||||
ctx.res,
|
||||
'Unknown add-on: assistant'
|
||||
)
|
||||
expect(ctx.res.render).not.to.have.been.called
|
||||
})
|
||||
|
||||
it('should proceed with preview when customAccount is undefined and collectionMethod is automatic', async function (ctx) {
|
||||
const normalSubscription = {
|
||||
_id: 'sub-123',
|
||||
|
||||
Reference in New Issue
Block a user