| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665 |
- // @ts-check
- const SandboxedModule = require('sandboxed-module')
- const { expect } = require('chai')
- const Errors = require('../../../../app/src/Features/Subscription/Errors')
- const {
- PaymentProviderSubscriptionChangeRequest,
- PaymentProviderSubscriptionUpdateRequest,
- PaymentProviderSubscriptionChange,
- PaymentProviderSubscription,
- PaymentProviderSubscriptionAddOnUpdate,
- } = require('../../../../app/src/Features/Subscription/PaymentProviderEntities')
- const {
- AI_ADD_ON_CODE,
- } = require('../../../../app/src/Features/Subscription/AiHelper')
- const SubscriptionHelper = require('../../../../app/src/Features/Subscription/SubscriptionHelper')
- const MODULE_PATH =
- '../../../../app/src/Features/Subscription/PaymentProviderEntities'
- describe('PaymentProviderEntities', function () {
- describe('PaymentProviderSubscription', function () {
- beforeEach(function () {
- this.Settings = {
- plans: [
- { planCode: 'assistant-annual', price_in_cents: 5900 },
- { planCode: 'cheap-plan', price_in_cents: 500 },
- { planCode: 'regular-plan', price_in_cents: 1000 },
- { planCode: 'premium-plan', price_in_cents: 2000 },
- {
- planCode: 'group_collaborator_10_enterprise',
- price_in_cents: 10000,
- },
- ],
- features: [],
- }
- this.PaymentProviderEntities = SandboxedModule.require(MODULE_PATH, {
- requires: {
- '@overleaf/settings': this.Settings,
- './Errors': Errors,
- './SubscriptionHelper': SubscriptionHelper,
- },
- })
- })
- describe('with add-ons', function () {
- beforeEach(function () {
- const {
- PaymentProviderSubscription,
- PaymentProviderSubscriptionAddOn,
- } = this.PaymentProviderEntities
- this.addOn = new PaymentProviderSubscriptionAddOn({
- code: 'add-on-code',
- name: 'My Add-On',
- quantity: 1,
- unitPrice: 2,
- })
- this.subscription = new PaymentProviderSubscription({
- id: 'subscription-id',
- userId: 'user-id',
- planCode: 'regular-plan',
- planName: 'My Plan',
- planPrice: 10,
- addOns: [this.addOn],
- subtotal: 10.99,
- taxRate: 0.2,
- taxAmount: 2.4,
- total: 14.4,
- currency: 'USD',
- })
- })
- describe('hasAddOn()', function () {
- it('returns true if the subscription has the given add-on', function () {
- expect(this.subscription.hasAddOn(this.addOn.code)).to.be.true
- })
- it("returns false if the subscription doesn't have the given add-on", function () {
- expect(this.subscription.hasAddOn('another-add-on')).to.be.false
- })
- })
- describe('getRequestForPlanChange()', function () {
- it('returns a change request for upgrades', function () {
- const { PaymentProviderSubscriptionChangeRequest } =
- this.PaymentProviderEntities
- const changeRequest = this.subscription.getRequestForPlanChange(
- 'premium-plan',
- 1,
- this.subscription.shouldPlanChangeAtTermEnd('premium-plan')
- )
- expect(changeRequest).to.deep.equal(
- new PaymentProviderSubscriptionChangeRequest({
- subscription: this.subscription,
- timeframe: 'now',
- planCode: 'premium-plan',
- })
- )
- })
- it('returns a change request for downgrades', function () {
- const { PaymentProviderSubscriptionChangeRequest } =
- this.PaymentProviderEntities
- const changeRequest = this.subscription.getRequestForPlanChange(
- 'cheap-plan',
- 1,
- this.subscription.shouldPlanChangeAtTermEnd('cheap-plan')
- )
- expect(changeRequest).to.deep.equal(
- new PaymentProviderSubscriptionChangeRequest({
- subscription: this.subscription,
- timeframe: 'term_end',
- planCode: 'cheap-plan',
- })
- )
- })
- it('returns a change request for downgrades while on trial', function () {
- const fiveDaysFromNow = new Date()
- fiveDaysFromNow.setDate(fiveDaysFromNow.getDate() + 5)
- this.subscription.trialPeriodEnd = fiveDaysFromNow
- const { PaymentProviderSubscriptionChangeRequest } =
- this.PaymentProviderEntities
- const changeRequest = this.subscription.getRequestForPlanChange(
- 'cheap-plan',
- 1,
- this.subscription.shouldPlanChangeAtTermEnd('cheap-plan')
- )
- expect(changeRequest).to.deep.equal(
- new PaymentProviderSubscriptionChangeRequest({
- subscription: this.subscription,
- timeframe: 'now',
- planCode: 'cheap-plan',
- })
- )
- })
- it('preserves the AI add-on on upgrades', function () {
- const { PaymentProviderSubscriptionChangeRequest } =
- this.PaymentProviderEntities
- this.addOn.code = AI_ADD_ON_CODE
- const changeRequest = this.subscription.getRequestForPlanChange(
- 'premium-plan',
- 1,
- this.subscription.shouldPlanChangeAtTermEnd('premium-plan')
- )
- expect(changeRequest).to.deep.equal(
- new PaymentProviderSubscriptionChangeRequest({
- subscription: this.subscription,
- timeframe: 'now',
- planCode: 'premium-plan',
- addOnUpdates: [
- new PaymentProviderSubscriptionAddOnUpdate({
- code: AI_ADD_ON_CODE,
- quantity: 1,
- }),
- ],
- })
- )
- })
- it('preserves the AI add-on on downgrades', function () {
- const { PaymentProviderSubscriptionChangeRequest } =
- this.PaymentProviderEntities
- this.addOn.code = AI_ADD_ON_CODE
- const changeRequest = this.subscription.getRequestForPlanChange(
- 'cheap-plan',
- 1,
- this.subscription.shouldPlanChangeAtTermEnd('cheap-plan')
- )
- expect(changeRequest).to.deep.equal(
- new PaymentProviderSubscriptionChangeRequest({
- subscription: this.subscription,
- timeframe: 'term_end',
- planCode: 'cheap-plan',
- addOnUpdates: [
- new PaymentProviderSubscriptionAddOnUpdate({
- code: AI_ADD_ON_CODE,
- quantity: 1,
- }),
- ],
- })
- )
- })
- it('preserves the AI add-on on upgrades from the standalone AI plan', function () {
- const { PaymentProviderSubscriptionChangeRequest } =
- this.PaymentProviderEntities
- this.subscription.planCode = 'assistant-annual'
- this.subscription.addOns = []
- const changeRequest = this.subscription.getRequestForPlanChange(
- 'cheap-plan',
- 1,
- this.subscription.shouldPlanChangeAtTermEnd('cheap-plan')
- )
- expect(changeRequest).to.deep.equal(
- new PaymentProviderSubscriptionChangeRequest({
- subscription: this.subscription,
- timeframe: 'now',
- planCode: 'cheap-plan',
- addOnUpdates: [
- new PaymentProviderSubscriptionAddOnUpdate({
- code: AI_ADD_ON_CODE,
- quantity: 1,
- }),
- ],
- })
- )
- })
- it('upgrade from individual to group plan for Stripe subscription', function () {
- this.subscription.service = 'stripe-uk'
- const { PaymentProviderSubscriptionChangeRequest } =
- this.PaymentProviderEntities
- const changeRequest = this.subscription.getRequestForPlanChange(
- 'group_collaborator',
- 10,
- this.subscription.shouldPlanChangeAtTermEnd(
- 'group_collaborator_10_enterprise'
- )
- )
- expect(changeRequest).to.deep.equal(
- new PaymentProviderSubscriptionChangeRequest({
- subscription: this.subscription,
- timeframe: 'now',
- planCode: 'group_collaborator',
- addOnUpdates: [
- new PaymentProviderSubscriptionAddOnUpdate({
- code: 'additional-license',
- quantity: 10,
- }),
- ],
- })
- )
- })
- it('upgrade from individual to group plan and preserves the AI add-on for Stripe subscription', function () {
- this.subscription.service = 'stripe-uk'
- const { PaymentProviderSubscriptionChangeRequest } =
- this.PaymentProviderEntities
- this.addOn.code = AI_ADD_ON_CODE
- const changeRequest = this.subscription.getRequestForPlanChange(
- 'group_collaborator',
- 10,
- this.subscription.shouldPlanChangeAtTermEnd(
- 'group_collaborator_10_enterprise'
- )
- )
- expect(changeRequest).to.deep.equal(
- new PaymentProviderSubscriptionChangeRequest({
- subscription: this.subscription,
- timeframe: 'now',
- planCode: 'group_collaborator',
- addOnUpdates: [
- new PaymentProviderSubscriptionAddOnUpdate({
- code: 'additional-license',
- quantity: 10,
- }),
- new PaymentProviderSubscriptionAddOnUpdate({
- code: AI_ADD_ON_CODE,
- quantity: 1,
- }),
- ],
- })
- )
- })
- })
- describe('getRequestForAddOnPurchase()', function () {
- it('returns a change request', function () {
- const {
- PaymentProviderSubscriptionChangeRequest,
- PaymentProviderSubscriptionAddOnUpdate,
- } = this.PaymentProviderEntities
- const changeRequest =
- this.subscription.getRequestForAddOnPurchase('another-add-on')
- expect(changeRequest).to.deep.equal(
- new PaymentProviderSubscriptionChangeRequest({
- subscription: this.subscription,
- timeframe: 'now',
- addOnUpdates: [
- new PaymentProviderSubscriptionAddOnUpdate({
- code: this.addOn.code,
- quantity: this.addOn.quantity,
- unitPrice: this.addOn.unitPrice,
- }),
- new PaymentProviderSubscriptionAddOnUpdate({
- code: 'another-add-on',
- quantity: 1,
- }),
- ],
- })
- )
- })
- it('returns a change request with quantity and unit price specified', function () {
- const {
- PaymentProviderSubscriptionChangeRequest,
- PaymentProviderSubscriptionAddOnUpdate,
- } = this.PaymentProviderEntities
- const quantity = 5
- const unitPrice = 10
- const changeRequest = this.subscription.getRequestForAddOnPurchase(
- 'another-add-on',
- quantity,
- unitPrice
- )
- expect(changeRequest).to.deep.equal(
- new PaymentProviderSubscriptionChangeRequest({
- subscription: this.subscription,
- timeframe: 'now',
- addOnUpdates: [
- new PaymentProviderSubscriptionAddOnUpdate({
- code: this.addOn.code,
- quantity: this.addOn.quantity,
- unitPrice: this.addOn.unitPrice,
- }),
- new PaymentProviderSubscriptionAddOnUpdate({
- code: 'another-add-on',
- quantity,
- unitPrice,
- }),
- ],
- })
- )
- })
- it('throws a DuplicateAddOnError if the subscription already has the add-on', function () {
- expect(() =>
- this.subscription.getRequestForAddOnPurchase(this.addOn.code)
- ).to.throw(Errors.DuplicateAddOnError)
- })
- })
- describe('getRequestForAddOnUpdate()', function () {
- it('returns a change request', function () {
- const {
- PaymentProviderSubscriptionChangeRequest,
- PaymentProviderSubscriptionAddOnUpdate,
- } = this.PaymentProviderEntities
- const newQuantity = 2
- const changeRequest = this.subscription.getRequestForAddOnUpdate(
- 'add-on-code',
- newQuantity
- )
- expect(changeRequest).to.deep.equal(
- new PaymentProviderSubscriptionChangeRequest({
- subscription: this.subscription,
- timeframe: 'now',
- addOnUpdates: [
- new PaymentProviderSubscriptionAddOnUpdate({
- code: this.addOn.code,
- quantity: newQuantity,
- unitPrice: this.addOn.unitPrice,
- }),
- ],
- })
- )
- })
- it("throws a AddOnNotPresentError if the subscription doesn't have the add-on", function () {
- expect(() =>
- this.subscription.getRequestForAddOnUpdate('another-add-on', 2)
- ).to.throw(Errors.AddOnNotPresentError)
- })
- })
- describe('getRequestForAddOnRemoval()', function () {
- it('returns a change request', function () {
- const changeRequest = this.subscription.getRequestForAddOnRemoval(
- this.addOn.code
- )
- expect(changeRequest).to.deep.equal(
- new PaymentProviderSubscriptionChangeRequest({
- subscription: this.subscription,
- timeframe: 'term_end',
- addOnUpdates: [],
- })
- )
- })
- it('returns a change request when in trial', function () {
- const fiveDaysFromNow = new Date()
- fiveDaysFromNow.setDate(fiveDaysFromNow.getDate() + 5)
- this.subscription.trialPeriodEnd = fiveDaysFromNow
- const changeRequest = this.subscription.getRequestForAddOnRemoval(
- this.addOn.code
- )
- expect(changeRequest).to.deep.equal(
- new PaymentProviderSubscriptionChangeRequest({
- subscription: this.subscription,
- timeframe: 'now',
- addOnUpdates: [],
- })
- )
- })
- it("throws an AddOnNotPresentError if the subscription doesn't have the add-on", function () {
- expect(() =>
- this.subscription.getRequestForAddOnRemoval('another-add-on')
- ).to.throw(Errors.AddOnNotPresentError)
- })
- })
- describe('getRequestForAddOnReactivation()', function () {
- it('throws an AddOnNotPresentError', function () {
- expect(() =>
- this.subscription.getRequestForAddOnReactivation(this.addOn.code)
- ).to.throw(Errors.AddOnNotPresentError)
- })
- })
- describe('getRequestForGroupPlanUpgrade()', function () {
- it('returns a correct change request', function () {
- const changeRequest =
- this.subscription.getRequestForGroupPlanUpgrade('test_plan_code')
- const addOns = [
- new PaymentProviderSubscriptionAddOnUpdate({
- code: 'add-on-code',
- quantity: 1,
- }),
- ]
- expect(changeRequest).to.deep.equal(
- new PaymentProviderSubscriptionChangeRequest({
- subscription: this.subscription,
- timeframe: 'now',
- addOnUpdates: addOns,
- planCode: 'test_plan_code',
- })
- )
- })
- })
- describe('getRequestForPoNumberAndTermsAndConditionsUpdate()', function () {
- it('returns a correct update request', function () {
- const updateRequest =
- this.subscription.getRequestForPoNumberAndTermsAndConditionsUpdate(
- 'O12345',
- 'T&C copy'
- )
- expect(updateRequest).to.deep.equal(
- new PaymentProviderSubscriptionUpdateRequest({
- subscription: this.subscription,
- poNumber: 'O12345',
- termsAndConditions: 'T&C copy',
- })
- )
- })
- })
- describe('getRequestForTermsAndConditionsUpdate()', function () {
- it('returns a correct update request', function () {
- const updateRequest =
- this.subscription.getRequestForTermsAndConditionsUpdate('T&C copy')
- expect(updateRequest).to.deep.equal(
- new PaymentProviderSubscriptionUpdateRequest({
- subscription: this.subscription,
- termsAndConditions: 'T&C copy',
- })
- )
- })
- })
- describe('with an add-on pending cancellation', function () {
- beforeEach(function () {
- this.subscription.pendingChange =
- new PaymentProviderSubscriptionChange({
- subscription: this.subscription,
- nextPlanCode: this.subscription.planCode,
- nextPlanName: this.subscription.planName,
- nextPlanPrice: this.subscription.planPrice,
- nextAddOns: [],
- })
- })
- describe('getRequestForAddOnReactivation()', function () {
- it('returns a change request', function () {
- const changeRequest =
- this.subscription.getRequestForAddOnReactivation(this.addOn.code)
- expect(changeRequest).to.deep.equal(
- new PaymentProviderSubscriptionChangeRequest({
- subscription: this.subscription,
- timeframe: 'term_end',
- addOnUpdates: [this.addOn.toAddOnUpdate()],
- })
- )
- })
- it('throws an AddOnNotPresentError if given the wrong add-on', function () {
- expect(() =>
- this.subscription.getRequestForAddOnReactivation('some-add-on')
- ).to.throw(Errors.AddOnNotPresentError)
- })
- })
- describe('getRequestForPlanRevert()', function () {
- beforeEach(function () {
- const { PaymentProviderSubscription } = this.PaymentProviderEntities
- this.subscription = new PaymentProviderSubscription({
- id: 'subscription-id',
- userId: 'user-id',
- planCode: 'regular-plan',
- planName: 'My Plan',
- planPrice: 10,
- addOns: [
- {
- addOnCode: 'addon-1',
- quantity: 2,
- unitAmountInCents: 500,
- },
- {
- addOnCode: 'addon-2',
- quantity: 1,
- unitAmountInCents: 600,
- },
- ],
- subtotal: 10.99,
- taxRate: 0.2,
- taxAmount: 2.4,
- total: 14.4,
- currency: 'USD',
- })
- })
- it('throws if the plan to revert to doesnt exist', function () {
- const invalidPlanCode = 'non-existent-plan'
- expect(() =>
- this.subscription.getRequestForPlanRevert(invalidPlanCode, null)
- ).to.throw('Unable to find plan in settings')
- })
- it('creates a change request with the restore point', function () {
- const previousPlanCode = 'cheap-plan'
- const previousAddOns = [
- { addOnCode: 'addon-1', quantity: 1, unitAmountInCents: 500 },
- ]
- const changeRequest = this.subscription.getRequestForPlanRevert(
- previousPlanCode,
- previousAddOns
- )
- expect(changeRequest).to.be.an.instanceOf(
- this.PaymentProviderEntities
- .PaymentProviderSubscriptionChangeRequest
- )
- expect(changeRequest.planCode).to.equal(previousPlanCode)
- expect(changeRequest.addOnUpdates).to.deep.equal([
- {
- code: 'addon-1',
- quantity: 1,
- unitPrice: 5,
- },
- ])
- })
- it('defaults to addons to an empty array to clear the addon state', function () {
- const previousPlanCode = 'cheap-plan'
- const changeRequest = this.subscription.getRequestForPlanRevert(
- previousPlanCode,
- null
- )
- expect(changeRequest.addOnUpdates).to.deep.equal([])
- })
- })
- })
- })
- describe('without add-ons', function () {
- beforeEach(function () {
- const { PaymentProviderSubscription } = this.PaymentProviderEntities
- this.subscription = new PaymentProviderSubscription({
- id: 'subscription-id',
- userId: 'user-id',
- planCode: 'regular-plan',
- planName: 'My Plan',
- planPrice: 10,
- subtotal: 10.99,
- taxRate: 0.2,
- taxAmount: 2.4,
- total: 14.4,
- currency: 'USD',
- })
- })
- describe('hasAddOn()', function () {
- it('returns false for any add-on', function () {
- expect(this.subscription.hasAddOn('some-add-on')).to.be.false
- })
- })
- describe('getRequestForAddOnPurchase()', function () {
- it('returns a change request', function () {
- const {
- PaymentProviderSubscriptionChangeRequest,
- PaymentProviderSubscriptionAddOnUpdate,
- } = this.PaymentProviderEntities
- const changeRequest =
- this.subscription.getRequestForAddOnPurchase('some-add-on')
- expect(changeRequest).to.deep.equal(
- new PaymentProviderSubscriptionChangeRequest({
- subscription: this.subscription,
- timeframe: 'now',
- addOnUpdates: [
- new PaymentProviderSubscriptionAddOnUpdate({
- code: 'some-add-on',
- quantity: 1,
- }),
- ],
- })
- )
- })
- })
- describe('getRequestForAddOnRemoval()', function () {
- it('throws an AddOnNotPresentError', function () {
- expect(() =>
- this.subscription.getRequestForAddOnRemoval('some-add-on')
- ).to.throw(Errors.AddOnNotPresentError)
- })
- })
- describe('getRequestForAddOnReactivation()', function () {
- it('throws an AddOnNotPresentError', function () {
- expect(() =>
- this.subscription.getRequestForAddOnReactivation('some-add-on')
- ).to.throw(Errors.AddOnNotPresentError)
- })
- })
- })
- })
- describe('PaymentProviderSubscriptionChange', function () {
- describe('constructor', function () {
- it('rounds the amounts when calculating the taxes', function () {
- const subscription = new PaymentProviderSubscription({
- id: 'subscription-id',
- userId: 'user-id',
- planCode: 'premium-plan',
- planName: 'Premium plan',
- planPrice: 10,
- subtotal: 10,
- taxRate: 0.15,
- taxAmount: 1.5,
- currency: 'USD',
- total: 11.5,
- periodStart: new Date(),
- periodEnd: new Date(),
- collectionMethod: 'automatic',
- netTerms: 0,
- poNumber: '012345',
- termsAndConditions: 'T&C copy',
- })
- const change = new PaymentProviderSubscriptionChange({
- subscription,
- nextPlanCode: 'promotional-plan',
- nextPlanName: 'Promotial plan',
- nextPlanPrice: 8.99,
- nextAddOns: [],
- })
- expect(change.tax).to.equal(1.35)
- expect(change.total).to.equal(10.34)
- })
- })
- })
- })
|