|
|
@@ -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',
|