import { expect, vi } from 'vitest' import sinon from 'sinon' import PaymentProviderEntities from '../../../../app/src/Features/Subscription/PaymentProviderEntities.mjs' import SubscriptionHelper from '../../../../app/src/Features/Subscription/SubscriptionHelper.mjs' import { AI_ADD_ON_CODE } from '../../../../app/src/Features/Subscription/AiHelper.mjs' const { PaymentProviderSubscription } = PaymentProviderEntities const MODULE_PATH = '../../../../app/src/Features/Subscription/SubscriptionHandler' const mockRecurlySubscriptions = { 'subscription-123-active': { uuid: 'subscription-123-active', plan: { name: 'Collaborator', plan_code: 'collaborator', }, current_period_ends_at: new Date(), state: 'active', unit_amount_in_cents: 999, account: { account_code: 'user-123', }, }, } const mockRecurlyClientSubscriptions = { 'subscription-123-active': new PaymentProviderSubscription({ id: 'subscription-123-active', userId: 'user-id', planCode: 'collaborator', planName: 'Collaborator', planPrice: 10, subtotal: 10, currency: 'USD', total: 10, }), } const mockSubscriptionChanges = { 'subscription-123-active': { id: 'subscription-change-id', subscriptionId: 'subscription-123-recurly-id', // not the UUID }, } describe('SubscriptionHandler', function () { beforeEach(async function (ctx) { ctx.Settings = { plans: [ { planCode: 'collaborator', name: 'Collaborator', price_in_cents: 1000, features: { collaborators: -1, versioning: true, }, }, { planCode: 'professional', price_in_cents: 1500, }, ], defaultPlanCode: { collaborators: 0, versioning: false, }, } ctx.activeRecurlySubscription = mockRecurlySubscriptions['subscription-123-active'] ctx.activeRecurlyClientSubscription = mockRecurlyClientSubscriptions['subscription-123-active'] ctx.activeRecurlySubscriptionChange = mockSubscriptionChanges['subscription-123-active'] ctx.User = {} ctx.user = { _id: (ctx.user_id = 'user_id_here_') } ctx.subscription = { recurlySubscription_id: ctx.activeRecurlySubscription.uuid, } ctx.RecurlyWrapper = { promises: { getSubscription: sinon.stub().resolves(ctx.activeRecurlySubscription), redeemCoupon: sinon.stub().resolves(), createSubscription: sinon .stub() .resolves(ctx.activeRecurlySubscription), getBillingInfo: sinon.stub().resolves(), getAccountPastDueInvoices: sinon.stub().resolves(), attemptInvoiceCollection: sinon.stub().resolves(), listAccountActiveSubscriptions: sinon.stub().resolves([]), }, } ctx.RecurlyClient = { promises: { reactivateSubscriptionByUuid: sinon .stub() .resolves(ctx.activeRecurlyClientSubscription), cancelSubscriptionByUuid: sinon.stub().resolves(), applySubscriptionChangeRequest: sinon .stub() .resolves(ctx.activeRecurlySubscriptionChange), getSubscription: sinon .stub() .resolves(ctx.activeRecurlyClientSubscription), pauseSubscriptionByUuid: sinon.stub().resolves(), resumeSubscriptionByUuid: sinon.stub().resolves(), }, } ctx.SubscriptionUpdater = { promises: { updateSubscriptionFromRecurly: sinon.stub().resolves(), syncSubscription: sinon.stub().resolves(), syncStripeSubscription: sinon.stub().resolves(), startFreeTrial: sinon.stub().resolves(), }, } ctx.LimitationsManager = { promises: { userHasSubscription: sinon.stub().resolves(), }, } ctx.SubscriptionLocator = { promises: { getUsersSubscription: sinon.stub().resolves(ctx.subscription), }, } ctx.EmailHandler = { sendEmail: sinon.stub(), sendDeferredEmail: sinon.stub(), } ctx.UserUpdater = { promises: { updateUser: sinon.stub().resolves(), }, } ctx.SplitTestHandler = { promises: { getAssignmentForUser: sinon.stub().resolves({ variant: 'default' }), }, } vi.doMock( '../../../../app/src/Features/Subscription/RecurlyWrapper', () => ({ default: ctx.RecurlyWrapper, }) ) vi.doMock( '../../../../app/src/Features/Subscription/RecurlyClient', () => ({ default: ctx.RecurlyClient, }) ) vi.doMock('@overleaf/settings', () => ({ default: ctx.Settings, })) vi.doMock('../../../../app/src/models/User', () => ({ User: ctx.User, })) vi.doMock( '../../../../app/src/Features/Subscription/SubscriptionHelper', () => ({ default: SubscriptionHelper, }) ) vi.doMock( '../../../../app/src/Features/Subscription/SubscriptionUpdater', () => ({ default: ctx.SubscriptionUpdater, }) ) vi.doMock( '../../../../app/src/Features/Subscription/SubscriptionLocator', () => ({ default: ctx.SubscriptionLocator, }) ) vi.doMock( '../../../../app/src/Features/Subscription/LimitationsManager', () => ({ default: ctx.LimitationsManager, }) ) vi.doMock('../../../../app/src/Features/Email/EmailHandler', () => ({ default: ctx.EmailHandler, })) vi.doMock( '../../../../app/src/Features/Analytics/AnalyticsManager', () => ({ default: ctx.AnalyticsManager, }) ) vi.doMock('../../../../app/src/Features/User/UserUpdater', () => ({ default: ctx.UserUpdater, })) vi.doMock( '../../../../app/src/Features/SplitTests/SplitTestHandler', () => ({ default: ctx.SplitTestHandler, }) ) vi.doMock('../../../../app/src/infrastructure/Modules', () => ({ default: (ctx.Modules = { promises: { hooks: { fire: sinon.stub(), }, }, }), })) ctx.SubscriptionHandler = (await import(MODULE_PATH)).default }) describe('createSubscription', function () { beforeEach(function (ctx) { ctx.subscriptionDetails = { cvv: '123', number: '12345', } ctx.recurlyTokenIds = { billing: '45555666' } }) describe('successfully', function () { beforeEach(async function (ctx) { await ctx.SubscriptionHandler.promises.createSubscription( ctx.user, ctx.subscriptionDetails, ctx.recurlyTokenIds ) }) it('should create the subscription with the wrapper', function (ctx) { ctx.RecurlyWrapper.promises.createSubscription .calledWith(ctx.user, ctx.subscriptionDetails, ctx.recurlyTokenIds) .should.equal(true) }) it('should sync the subscription to the user', function (ctx) { ctx.SubscriptionUpdater.promises.syncSubscription.calledOnce.should.equal( true ) ctx.SubscriptionUpdater.promises.syncSubscription.args[0][0].should.deep.equal( ctx.activeRecurlySubscription ) ctx.SubscriptionUpdater.promises.syncSubscription.args[0][1].should.deep.equal( ctx.user._id ) }) it('should not set last trial date if not a trial/the trial_started_at is not set', function (ctx) { ctx.UserUpdater.promises.updateUser.should.not.have.been.called }) }) describe('when the subscription is a trial and has a trial_started_at date', function () { beforeEach(async function (ctx) { ctx.activeRecurlySubscription.trial_started_at = '2024-01-01T09:58:35.531+00:00' await ctx.SubscriptionHandler.promises.createSubscription( ctx.user, ctx.subscriptionDetails, ctx.recurlyTokenIds ) }) it('should set the users lastTrial date', function (ctx) { ctx.UserUpdater.promises.updateUser.should.have.been.calledOnce expect(ctx.UserUpdater.promises.updateUser.args[0][0]).to.deep.equal({ _id: ctx.user_id, lastTrial: { $not: { $gt: new Date(ctx.activeRecurlySubscription.trial_started_at), }, }, }) expect(ctx.UserUpdater.promises.updateUser.args[0][1]).to.deep.equal({ $set: { lastTrial: new Date(ctx.activeRecurlySubscription.trial_started_at), }, }) }) }) describe('when there is already a subscription in Recurly', function () { beforeEach(function (ctx) { ctx.RecurlyWrapper.promises.listAccountActiveSubscriptions.resolves([ ctx.subscription, ]) }) it('should an error', function (ctx) { expect( ctx.SubscriptionHandler.promises.createSubscription( ctx.user, ctx.subscriptionDetails, ctx.recurlyTokenIds ) ).to.be.rejectedWith('user already has subscription in recurly') }) }) }) describe('updateSubscription', function () { beforeEach(function (ctx) { ctx.user.id = ctx.activeRecurlySubscription.account.account_code ctx.User.findById = (userId, projection) => ({ exec: () => { userId.should.equal(ctx.user.id) return Promise.resolve(ctx.user) }, }) }) it('should not fire updatePaidSubscription hook if user has no subscription', async function (ctx) { ctx.LimitationsManager.promises.userHasSubscription.resolves({ hasSubscription: false, subscription: null, }) await ctx.SubscriptionHandler.promises.updateSubscription( ctx.user, ctx.plan_code ) expect(ctx.Modules.promises.hooks.fire).to.not.have.been.calledWith( 'updatePaidSubscription', sinon.match.any, sinon.match.any, sinon.match.any ) }) it('should not fire updatePaidSubscription hook if user has custom subscription', async function (ctx) { ctx.LimitationsManager.promises.userHasSubscription.resolves({ hasSubscription: true, subscription: { customAccount: true }, }) await ctx.SubscriptionHandler.promises.updateSubscription( ctx.user, ctx.plan_code ) expect(ctx.Modules.promises.hooks.fire).to.not.have.been.calledWith( 'updatePaidSubscription', sinon.match.any, sinon.match.any, sinon.match.any ) }) it('should fire updatePaidSubscription to update a valid subscription', async function (ctx) { ctx.LimitationsManager.promises.userHasSubscription.resolves({ hasSubscription: true, subscription: ctx.subscription, }) await ctx.SubscriptionHandler.promises.updateSubscription( ctx.user, ctx.plan_code ) expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith( 'updatePaidSubscription', ctx.subscription, ctx.plan_code, ctx.user._id ) }) }) describe('cancelPendingSubscriptionChange', function () { beforeEach(function (ctx) { ctx.user.id = ctx.activeRecurlySubscription.account.account_code ctx.User.findById = (userId, projection) => ({ exec: () => { userId.should.equal(ctx.user.id) return Promise.resolve(ctx.user) }, }) }) it('should not fire hooks if user has no subscription', async function (ctx) { ctx.LimitationsManager.promises.userHasSubscription.resolves({ hasSubscription: false, subscription: null, }) await ctx.SubscriptionHandler.promises.cancelPendingSubscriptionChange( ctx.user, ctx.plan_code ) expect(ctx.Modules.promises.hooks.fire).to.not.have.been.called }) it('should get payment record and apply change request', async function (ctx) { const changeRequest = { subscription: { id: 'sub_123' } } const paymentProviderSubscription = { id: 'sub_123', service: 'stripe', getRequestForPlanChangeCancellation: sinon .stub() .returns(changeRequest), } const paymentRecord = { subscription: paymentProviderSubscription } ctx.LimitationsManager.promises.userHasSubscription.resolves({ hasSubscription: true, subscription: ctx.subscription, }) ctx.Modules.promises.hooks.fire .withArgs('getPaymentFromRecord', ctx.subscription) .resolves([paymentRecord]) ctx.Modules.promises.hooks.fire .withArgs( 'applySubscriptionChangeRequestAndSync', changeRequest, ctx.user._id.toString() ) .resolves([Promise.resolve()]) await ctx.SubscriptionHandler.promises.cancelPendingSubscriptionChange( ctx.user, ctx.plan_code ) expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith( 'getPaymentFromRecord', ctx.subscription ) expect(paymentProviderSubscription.getRequestForPlanChangeCancellation).to .have.been.called expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith( 'applySubscriptionChangeRequestAndSync', changeRequest, ctx.user._id.toString() ) }) it('should remove pending change when there are no add-on changes to preserve', async function (ctx) { const paymentProviderSubscription = { id: 'sub_123', service: 'stripe', pendingChange: { nextPlanCode: 'student' }, getRequestForPlanChangeCancellation: sinon.stub().returns(null), } const paymentRecord = { subscription: paymentProviderSubscription } ctx.LimitationsManager.promises.userHasSubscription.resolves({ hasSubscription: true, subscription: ctx.subscription, }) ctx.Modules.promises.hooks.fire .withArgs('getPaymentFromRecord', ctx.subscription) .resolves([paymentRecord]) ctx.Modules.promises.hooks.fire .withArgs('cancelPendingPaidSubscriptionChange', ctx.subscription) .resolves([Promise.resolve()]) await ctx.SubscriptionHandler.promises.cancelPendingSubscriptionChange( ctx.user, ctx.plan_code ) expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith( 'getPaymentFromRecord', ctx.subscription ) expect(paymentProviderSubscription.getRequestForPlanChangeCancellation).to .have.been.called expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith( 'cancelPendingPaidSubscriptionChange', ctx.subscription ) expect(ctx.Modules.promises.hooks.fire).to.not.have.been.calledWith( 'applySubscriptionChangeRequestAndSync' ) }) }) describe('removeAddon', function () { beforeEach(function (ctx) { ctx.addOnCode = AI_ADD_ON_CODE }) describe('when split test is disabled', function () { beforeEach(async function (ctx) { ctx.SplitTestHandler.promises.getAssignmentForUser.resolves({ variant: 'control', }) await ctx.SubscriptionHandler.promises.removeAddon( ctx.user, ctx.addOnCode ) }) it('should remove the addon', function (ctx) { expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith( 'removeAddOn', ctx.user._id, ctx.addOnCode ) }) it('should send the email after 1 hour', function (ctx) { const ONE_HOUR_IN_MS = 1000 * 60 * 60 expect(ctx.EmailHandler.sendDeferredEmail).to.have.been.calledWith( 'canceledSubscriptionOrAddOn', { to: ctx.user.email, first_name: ctx.user.first_name }, ONE_HOUR_IN_MS ) }) }) describe('when split test is enabled', function () { beforeEach(async function (ctx) { ctx.SplitTestHandler.promises.getAssignmentForUser.resolves({ variant: 'enabled', }) await ctx.SubscriptionHandler.promises.removeAddon( ctx.user, ctx.addOnCode ) }) it('should remove the addon', function (ctx) { expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith( 'removeAddOn', ctx.user._id, ctx.addOnCode ) }) it('should send the email after 1 hour', function (ctx) { const ONE_HOUR_IN_MS = 1000 * 60 * 60 expect(ctx.EmailHandler.sendDeferredEmail).to.have.been.calledWith( 'canceledSubscriptionOrAddOn', { to: ctx.user.email, first_name: ctx.user.first_name }, ONE_HOUR_IN_MS ) }) }) describe('when addon is not AI assistant', function () { beforeEach(async function (ctx) { ctx.addOnCode = 'other-addon' await ctx.SubscriptionHandler.promises.removeAddon( ctx.user, ctx.addOnCode ) }) it('should remove the addon', function (ctx) { expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith( 'removeAddOn', ctx.user._id, ctx.addOnCode ) }) it('should not send an email', function (ctx) { expect(ctx.EmailHandler.sendDeferredEmail).to.not.have.been.called }) }) }) describe('cancelSubscription', function () { describe('with a user without a subscription', function () { beforeEach(async function (ctx) { ctx.LimitationsManager.promises.userHasSubscription.resolves({ hasSubscription: false, subscription: ctx.subscription, }) await ctx.SubscriptionHandler.promises.cancelSubscription(ctx.user) }) it('should redirect to the subscription dashboard', function (ctx) { ctx.RecurlyClient.promises.cancelSubscriptionByUuid.called.should.equal( false ) }) }) describe('with a user with a subscription', function () { beforeEach(async function (ctx) { ctx.LimitationsManager.promises.userHasSubscription.resolves({ hasSubscription: true, subscription: ctx.subscription, }) }) describe('when split test is disabled', function () { beforeEach(async function (ctx) { ctx.SplitTestHandler.promises.getAssignmentForUser.resolves({ variant: 'control', }) await ctx.SubscriptionHandler.promises.cancelSubscription(ctx.user) }) it('should cancel the subscription', function (ctx) { expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith( 'cancelPaidSubscription', ctx.subscription ) }) it('should send the email after 1 hour', function (ctx) { const ONE_HOUR_IN_MS = 1000 * 60 * 60 expect(ctx.EmailHandler.sendDeferredEmail).to.have.been.calledWith( 'canceledSubscriptionOrAddOn', { to: ctx.user.email, first_name: ctx.user.first_name }, ONE_HOUR_IN_MS ) }) }) describe('when split test is enabled', function () { beforeEach(async function (ctx) { ctx.SplitTestHandler.promises.getAssignmentForUser.resolves({ variant: 'enabled', }) await ctx.SubscriptionHandler.promises.cancelSubscription(ctx.user) }) it('should cancel the subscription', function (ctx) { expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith( 'cancelPaidSubscription', ctx.subscription ) }) it('should send the email after 1 hour', function (ctx) { const ONE_HOUR_IN_MS = 1000 * 60 * 60 expect(ctx.EmailHandler.sendDeferredEmail).to.have.been.calledWith( 'canceledSubscriptionOrAddOn', { to: ctx.user.email, first_name: ctx.user.first_name }, ONE_HOUR_IN_MS ) }) }) }) }) describe('resumeSubscription', function () { describe('for a user without a subscription', function () { beforeEach(async function (ctx) { ctx.LimitationsManager.promises.userHasSubscription.resolves({ hasSubscription: false, subscription: ctx.subscription, }) }) it('should not make a resume call to recurly', async function (ctx) { expect( ctx.SubscriptionHandler.promises.resumeSubscription(ctx.user) ).to.be.rejectedWith('No active subscription to resume') ctx.RecurlyClient.promises.resumeSubscriptionByUuid.called.should.equal( false ) }) }) describe('for a user with a subscription', function () { beforeEach(async function (ctx) { ctx.LimitationsManager.promises.userHasSubscription.resolves({ hasSubscription: true, subscription: { recurlySubscription_id: ctx.activeRecurlySubscription.uuid, recurlyStatus: { state: 'non-trial' }, planCode: 'collaborator', }, }) }) it('should call resume hook', async function (ctx) { await ctx.SubscriptionHandler.promises.resumeSubscription(ctx.user) expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith( 'resumePaidSubscription', { recurlySubscription_id: ctx.activeRecurlySubscription.uuid, recurlyStatus: { state: 'non-trial' }, planCode: 'collaborator', } ) }) }) }) describe('pauseSubscription', function () { describe('for a user without a subscription', function () { beforeEach(async function (ctx) { ctx.LimitationsManager.promises.userHasSubscription.resolves({ hasSubscription: false, subscription: ctx.subscription, }) }) it('should not make a pause call to recurly', async function (ctx) { expect( ctx.SubscriptionHandler.promises.pauseSubscription(ctx.user, 3) ).to.be.rejectedWith('No active subscription to pause') ctx.RecurlyClient.promises.pauseSubscriptionByUuid.called.should.equal( false ) }) }) describe('for a user with an annual subscription', function () { beforeEach(async function (ctx) { ctx.LimitationsManager.promises.userHasSubscription.resolves({ hasSubscription: false, subscription: { recurlySubscription_id: ctx.activeRecurlySubscription.uuid, recurlyStatus: { state: 'non-trial' }, planCode: 'collaborator-annual', }, }) }) it('should not make a pause call to recurly', async function (ctx) { expect( ctx.SubscriptionHandler.promises.pauseSubscription(ctx.user, 3) ).to.be.rejectedWith('Can only pause monthly individual plans') ctx.RecurlyClient.promises.pauseSubscriptionByUuid.called.should.equal( false ) }) }) describe('for a user with a subscription', function () { beforeEach(async function (ctx) { ctx.LimitationsManager.promises.userHasSubscription.resolves({ hasSubscription: true, subscription: { recurlySubscription_id: ctx.activeRecurlySubscription.uuid, recurlyStatus: { state: 'non-trial' }, planCode: 'collaborator', addOns: [], }, }) }) it('should call pause hook', async function (ctx) { await ctx.SubscriptionHandler.promises.pauseSubscription(ctx.user, 3) expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith( 'pausePaidSubscription', { recurlySubscription_id: ctx.activeRecurlySubscription.uuid, recurlyStatus: { state: 'non-trial' }, planCode: 'collaborator', addOns: [], }, 3 ) }) }) describe('for a user in a trial', function () { beforeEach(async function (ctx) { ctx.LimitationsManager.promises.userHasSubscription.resolves({ hasSubscription: true, subscription: { recurlySubscription_id: ctx.activeRecurlySubscription.uuid, recurlyStatus: { state: 'trial', trialEndsAt: Date.now() + 1000000, }, planCode: 'collaborator', }, }) }) it('should not make a pause call to recurly', async function (ctx) { expect( ctx.SubscriptionHandler.promises.pauseSubscription(ctx.user, 3) ).to.be.rejectedWith('Cannot pause a subscription in a trial') ctx.RecurlyClient.promises.pauseSubscriptionByUuid.called.should.equal( false ) }) }) describe('for a user with addons', function () { beforeEach(async function (ctx) { ctx.LimitationsManager.promises.userHasSubscription.resolves({ hasSubscription: true, subscription: { recurlySubscription_id: ctx.activeRecurlySubscription.uuid, recurlyStatus: { state: 'non-trial' }, planCode: 'collaborator', addOns: ['mock-addon'], }, }) }) it('should not make a pause call to recurly', async function (ctx) { expect( ctx.SubscriptionHandler.promises.pauseSubscription(ctx.user, 3) ).to.be.rejectedWith('Cannot pause a subscription with addons') ctx.RecurlyClient.promises.pauseSubscriptionByUuid.called.should.equal( false ) }) }) }) describe('reactivateSubscription', function () { describe('with a user without a subscription', function () { beforeEach(async function (ctx) { ctx.LimitationsManager.promises.userHasSubscription.resolves({ hasSubscription: false, subscription: ctx.subscription, }) await ctx.SubscriptionHandler.promises.reactivateSubscription(ctx.user) }) it('should redirect to the subscription dashboard', function (ctx) { ctx.RecurlyClient.promises.reactivateSubscriptionByUuid.called.should.equal( false ) }) it('should not send a notification email', function (ctx) { sinon.assert.notCalled(ctx.EmailHandler.sendEmail) }) }) describe('with a user with a subscription', function () { beforeEach(async function (ctx) { ctx.LimitationsManager.promises.userHasSubscription.resolves({ hasSubscription: true, subscription: ctx.subscription, }) await ctx.SubscriptionHandler.promises.reactivateSubscription(ctx.user) }) it('should reactivate the subscription', function (ctx) { expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith( 'reactivatePaidSubscription', ctx.subscription ) }) it('should send a notification email', function (ctx) { sinon.assert.calledWith( ctx.EmailHandler.sendEmail, 'reactivatedSubscription' ) }) }) }) describe('syncSubscription', function () { describe('with an actionable request', function () { beforeEach(async function (ctx) { ctx.user.id = ctx.activeRecurlySubscription.account.account_code ctx.User.findById = (userId, projection) => ({ exec: () => { userId.should.equal(ctx.user.id) return Promise.resolve(ctx.user) }, }) await ctx.SubscriptionHandler.promises.syncSubscription( ctx.activeRecurlySubscription, {} ) }) it('should request the affected subscription from the API', function (ctx) { ctx.RecurlyWrapper.promises.getSubscription .calledWith(ctx.activeRecurlySubscription.uuid) .should.equal(true) }) it('should request the account details of the subscription', function (ctx) { const options = ctx.RecurlyWrapper.promises.getSubscription.args[0][1] options.includeAccount.should.equal(true) }) it('should sync the subscription to the user', function (ctx) { ctx.SubscriptionUpdater.promises.syncSubscription.calledOnce.should.equal( true ) ctx.SubscriptionUpdater.promises.syncSubscription.args[0][0].should.deep.equal( ctx.activeRecurlySubscription ) ctx.SubscriptionUpdater.promises.syncSubscription.args[0][1].should.deep.equal( ctx.user._id ) }) }) }) describe('attemptPaypalInvoiceCollection', function () { describe('for credit card users', function () { beforeEach(async function (ctx) { ctx.RecurlyWrapper.promises.getBillingInfo.resolves({ paypal_billing_agreement_id: null, }) await ctx.SubscriptionHandler.promises.attemptPaypalInvoiceCollection( ctx.activeRecurlySubscription.account.account_code ) }) it('gets billing infos', function (ctx) { sinon.assert.calledWith( ctx.RecurlyWrapper.promises.getBillingInfo, ctx.activeRecurlySubscription.account.account_code ) }) it('skips user', function (ctx) { sinon.assert.notCalled( ctx.RecurlyWrapper.promises.getAccountPastDueInvoices ) }) }) describe('for paypal users', function () { beforeEach(async function (ctx) { ctx.RecurlyWrapper.promises.getBillingInfo.resolves({ paypal_billing_agreement_id: 'mock-billing-agreement', }) ctx.RecurlyWrapper.promises.getAccountPastDueInvoices.resolves([ { invoice_number: 'mock-invoice-number' }, ]) await ctx.SubscriptionHandler.promises.attemptPaypalInvoiceCollection( ctx.activeRecurlySubscription.account.account_code ) }) it('gets past due invoices', function (ctx) { sinon.assert.calledWith( ctx.RecurlyWrapper.promises.getAccountPastDueInvoices, ctx.activeRecurlySubscription.account.account_code ) }) it('calls attemptInvoiceCollection', function (ctx) { sinon.assert.calledWith( ctx.RecurlyWrapper.promises.attemptInvoiceCollection, 'mock-invoice-number' ) }) }) }) describe('validateNoSubscriptionInRecurly', function () { describe('with a subscription in recurly', function () { beforeEach(async function (ctx) { ctx.RecurlyWrapper.promises.listAccountActiveSubscriptions.resolves([ ctx.subscription, ]) ctx.isValid = await ctx.SubscriptionHandler.promises.validateNoSubscriptionInRecurly( ctx.user_id ) }) it('should call RecurlyWrapper.promises.listAccountActiveSubscriptions with the user id', function (ctx) { ctx.RecurlyWrapper.promises.listAccountActiveSubscriptions .calledWith(ctx.user_id) .should.equal(true) }) it('should sync the subscription', function (ctx) { ctx.SubscriptionUpdater.promises.syncSubscription .calledWith(ctx.subscription, ctx.user_id) .should.equal(true) }) it('should return false', function (ctx) { expect(ctx.isValid).to.equal(false) }) }) describe('with no subscription in recurly', function () { beforeEach(async function (ctx) { ctx.isValid = await ctx.SubscriptionHandler.promises.validateNoSubscriptionInRecurly( ctx.user_id ) }) it('should be rejected and not sync the subscription', function (ctx) { ctx.SubscriptionUpdater.promises.syncSubscription.called.should.equal( false ) }) it('should return true', function (ctx) { expect(ctx.isValid).to.equal(true) }) }) }) })