| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962 |
- import { vi, expect } from 'vitest'
- import Errors from '../../../../app/src/Features/Subscription/Errors.mjs'
- import PaymentProviderEntities from '../../../../app/src/Features/Subscription/PaymentProviderEntities.mjs'
- import { AI_ADD_ON_CODE } from '../../../../app/src/Features/Subscription/AiHelper.mjs'
- import SubscriptionHelper from '../../../../app/src/Features/Subscription/SubscriptionHelper.mjs'
- const {
- PaymentProviderSubscriptionChangeRequest,
- PaymentProviderSubscriptionUpdateRequest,
- PaymentProviderSubscriptionChange,
- PaymentProviderSubscription,
- PaymentProviderSubscriptionAddOnUpdate,
- } = PaymentProviderEntities
- const MODULE_PATH =
- '../../../../app/src/Features/Subscription/PaymentProviderEntities'
- describe('PaymentProviderEntities', function () {
- describe('PaymentProviderSubscription', function () {
- beforeEach(async function (ctx) {
- ctx.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: [],
- }
- vi.doMock('@overleaf/settings', () => ({
- default: ctx.Settings,
- }))
- vi.doMock(
- '../../../../app/src/Features/Subscription/Errors',
- () => Errors
- )
- vi.doMock(
- '../../../../app/src/Features/Subscription/SubscriptionHelper',
- () => ({
- default: SubscriptionHelper,
- })
- )
- ctx.PaymentProviderEntities = (await import(MODULE_PATH)).default
- })
- describe('with add-ons', function () {
- beforeEach(function (ctx) {
- const {
- PaymentProviderSubscription,
- PaymentProviderSubscriptionAddOn,
- } = ctx.PaymentProviderEntities
- ctx.addOn = new PaymentProviderSubscriptionAddOn({
- code: 'add-on-code',
- name: 'My Add-On',
- quantity: 1,
- unitPrice: 2,
- })
- ctx.subscription = new PaymentProviderSubscription({
- id: 'subscription-id',
- userId: 'user-id',
- planCode: 'regular-plan',
- planName: 'My Plan',
- planPrice: 10,
- addOns: [ctx.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 (ctx) {
- expect(ctx.subscription.hasAddOn(ctx.addOn.code)).to.be.true
- })
- it("returns false if the subscription doesn't have the given add-on", function (ctx) {
- expect(ctx.subscription.hasAddOn('another-add-on')).to.be.false
- })
- })
- describe('getRequestForPlanChange()', function () {
- it('returns a change request for upgrades', function (ctx) {
- const { PaymentProviderSubscriptionChangeRequest } =
- ctx.PaymentProviderEntities
- const changeRequest = ctx.subscription.getRequestForPlanChange(
- 'premium-plan',
- 1,
- ctx.subscription.shouldPlanChangeAtTermEnd('premium-plan')
- )
- expect(changeRequest).to.deep.equal(
- new PaymentProviderSubscriptionChangeRequest({
- subscription: ctx.subscription,
- timeframe: 'now',
- planCode: 'premium-plan',
- addOnUpdates: [
- new PaymentProviderSubscriptionAddOnUpdate({
- code: ctx.addOn.code,
- quantity: ctx.addOn.quantity,
- unitPrice: ctx.addOn.unitPrice,
- }),
- ],
- })
- )
- })
- it('returns a change request for downgrades', function (ctx) {
- const { PaymentProviderSubscriptionChangeRequest } =
- ctx.PaymentProviderEntities
- const changeRequest = ctx.subscription.getRequestForPlanChange(
- 'cheap-plan',
- 1,
- ctx.subscription.shouldPlanChangeAtTermEnd('cheap-plan')
- )
- expect(changeRequest).to.deep.equal(
- new PaymentProviderSubscriptionChangeRequest({
- subscription: ctx.subscription,
- timeframe: 'term_end',
- planCode: 'cheap-plan',
- addOnUpdates: [
- new PaymentProviderSubscriptionAddOnUpdate({
- code: ctx.addOn.code,
- quantity: ctx.addOn.quantity,
- unitPrice: ctx.addOn.unitPrice,
- }),
- ],
- })
- )
- })
- it('returns a change request for downgrades while on trial', function (ctx) {
- const fiveDaysFromNow = new Date()
- fiveDaysFromNow.setDate(fiveDaysFromNow.getDate() + 5)
- ctx.subscription.trialPeriodEnd = fiveDaysFromNow
- const { PaymentProviderSubscriptionChangeRequest } =
- ctx.PaymentProviderEntities
- const changeRequest = ctx.subscription.getRequestForPlanChange(
- 'cheap-plan',
- 1,
- ctx.subscription.shouldPlanChangeAtTermEnd('cheap-plan')
- )
- expect(changeRequest).to.deep.equal(
- new PaymentProviderSubscriptionChangeRequest({
- subscription: ctx.subscription,
- timeframe: 'now',
- planCode: 'cheap-plan',
- addOnUpdates: [
- new PaymentProviderSubscriptionAddOnUpdate({
- code: ctx.addOn.code,
- quantity: ctx.addOn.quantity,
- unitPrice: ctx.addOn.unitPrice,
- }),
- ],
- })
- )
- })
- it('preserves the AI add-on on upgrades', function (ctx) {
- const { PaymentProviderSubscriptionChangeRequest } =
- ctx.PaymentProviderEntities
- ctx.addOn.code = AI_ADD_ON_CODE
- const changeRequest = ctx.subscription.getRequestForPlanChange(
- 'premium-plan',
- 1,
- ctx.subscription.shouldPlanChangeAtTermEnd('premium-plan')
- )
- expect(changeRequest).to.deep.equal(
- new PaymentProviderSubscriptionChangeRequest({
- subscription: ctx.subscription,
- timeframe: 'now',
- planCode: 'premium-plan',
- addOnUpdates: [
- new PaymentProviderSubscriptionAddOnUpdate({
- code: AI_ADD_ON_CODE,
- quantity: 1,
- unitPrice: ctx.addOn.unitPrice,
- }),
- ],
- })
- )
- })
- it('preserves the AI add-on on downgrades', function (ctx) {
- const { PaymentProviderSubscriptionChangeRequest } =
- ctx.PaymentProviderEntities
- ctx.addOn.code = AI_ADD_ON_CODE
- const changeRequest = ctx.subscription.getRequestForPlanChange(
- 'cheap-plan',
- 1,
- ctx.subscription.shouldPlanChangeAtTermEnd('cheap-plan')
- )
- expect(changeRequest).to.deep.equal(
- new PaymentProviderSubscriptionChangeRequest({
- subscription: ctx.subscription,
- timeframe: 'term_end',
- planCode: 'cheap-plan',
- addOnUpdates: [
- new PaymentProviderSubscriptionAddOnUpdate({
- code: AI_ADD_ON_CODE,
- quantity: 1,
- unitPrice: ctx.addOn.unitPrice,
- }),
- ],
- })
- )
- })
- it('preserves the AI add-on on upgrades from the standalone AI plan', function (ctx) {
- const { PaymentProviderSubscriptionChangeRequest } =
- ctx.PaymentProviderEntities
- ctx.subscription.planCode = 'assistant-annual'
- ctx.subscription.addOns = []
- const changeRequest = ctx.subscription.getRequestForPlanChange(
- 'cheap-plan',
- 1,
- ctx.subscription.shouldPlanChangeAtTermEnd('cheap-plan')
- )
- expect(changeRequest).to.deep.equal(
- new PaymentProviderSubscriptionChangeRequest({
- subscription: ctx.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 (ctx) {
- ctx.subscription.service = 'stripe-uk'
- const { PaymentProviderSubscriptionChangeRequest } =
- ctx.PaymentProviderEntities
- const changeRequest = ctx.subscription.getRequestForPlanChange(
- 'group_collaborator',
- 10,
- ctx.subscription.shouldPlanChangeAtTermEnd(
- 'group_collaborator_10_enterprise'
- )
- )
- expect(changeRequest).to.deep.equal(
- new PaymentProviderSubscriptionChangeRequest({
- subscription: ctx.subscription,
- timeframe: 'now',
- planCode: 'group_collaborator',
- addOnUpdates: [
- new PaymentProviderSubscriptionAddOnUpdate({
- code: ctx.addOn.code,
- quantity: ctx.addOn.quantity,
- unitPrice: ctx.addOn.unitPrice,
- }),
- new PaymentProviderSubscriptionAddOnUpdate({
- code: 'additional-license',
- quantity: 10,
- }),
- ],
- })
- )
- })
- it('upgrade from individual to group plan and preserves the AI add-on for Stripe subscription', function (ctx) {
- ctx.subscription.service = 'stripe-uk'
- const { PaymentProviderSubscriptionChangeRequest } =
- ctx.PaymentProviderEntities
- ctx.addOn.code = AI_ADD_ON_CODE
- const changeRequest = ctx.subscription.getRequestForPlanChange(
- 'group_collaborator',
- 10,
- ctx.subscription.shouldPlanChangeAtTermEnd(
- 'group_collaborator_10_enterprise'
- )
- )
- expect(changeRequest).to.deep.equal(
- new PaymentProviderSubscriptionChangeRequest({
- subscription: ctx.subscription,
- timeframe: 'now',
- planCode: 'group_collaborator',
- addOnUpdates: [
- new PaymentProviderSubscriptionAddOnUpdate({
- code: AI_ADD_ON_CODE,
- quantity: 1,
- unitPrice: ctx.addOn.unitPrice,
- }),
- new PaymentProviderSubscriptionAddOnUpdate({
- code: 'additional-license',
- quantity: 10,
- }),
- ],
- })
- )
- })
- describe('with pending add-on removal', function () {
- beforeEach(function (ctx) {
- // Reset to a subscription with an add-on and pending removal
- const {
- PaymentProviderSubscription,
- PaymentProviderSubscriptionAddOn,
- } = ctx.PaymentProviderEntities
- ctx.addOn = new PaymentProviderSubscriptionAddOn({
- code: 'add-on-code',
- name: 'My Add-On',
- quantity: 1,
- unitPrice: 2,
- })
- ctx.subscription = new PaymentProviderSubscription({
- id: 'subscription-id',
- userId: 'user-id',
- planCode: 'regular-plan',
- planName: 'My Plan',
- planPrice: 10,
- addOns: [ctx.addOn],
- subtotal: 10.99,
- taxRate: 0.2,
- taxAmount: 2.4,
- total: 14.4,
- currency: 'USD',
- })
- // Set up a pending change that removes the add-on at term_end
- ctx.subscription.pendingChange =
- new PaymentProviderSubscriptionChange({
- subscription: ctx.subscription,
- nextPlanCode: ctx.subscription.planCode,
- nextPlanName: ctx.subscription.planName,
- nextPlanPrice: ctx.subscription.planPrice,
- nextAddOns: [], // Add-on is scheduled to be removed
- })
- })
- it('preserves current add-ons for immediate upgrade', function (ctx) {
- const changeRequest = ctx.subscription.getRequestForPlanChange(
- 'premium-plan',
- 1,
- false // immediate change
- )
- expect(changeRequest.timeframe).to.equal('now')
- expect(changeRequest.addOnUpdates).to.deep.equal([
- new PaymentProviderSubscriptionAddOnUpdate({
- code: ctx.addOn.code,
- quantity: ctx.addOn.quantity,
- unitPrice: ctx.addOn.unitPrice,
- }),
- ])
- })
- it('stacks with pending add-on removal for term_end downgrade', function (ctx) {
- const changeRequest = ctx.subscription.getRequestForPlanChange(
- 'cheap-plan',
- 1,
- true // term_end change
- )
- expect(changeRequest.timeframe).to.equal('term_end')
- // Should have NO add-ons because the pending change already removed them
- expect(changeRequest.addOnUpdates).to.deep.equal([])
- })
- })
- })
- describe('getRequestForAddOnPurchase()', function () {
- it('returns a change request', function (ctx) {
- const {
- PaymentProviderSubscriptionChangeRequest,
- PaymentProviderSubscriptionAddOnUpdate,
- } = ctx.PaymentProviderEntities
- const changeRequest =
- ctx.subscription.getRequestForAddOnPurchase('another-add-on')
- expect(changeRequest).to.deep.equal(
- new PaymentProviderSubscriptionChangeRequest({
- subscription: ctx.subscription,
- timeframe: 'now',
- addOnUpdates: [
- new PaymentProviderSubscriptionAddOnUpdate({
- code: ctx.addOn.code,
- quantity: ctx.addOn.quantity,
- unitPrice: ctx.addOn.unitPrice,
- }),
- new PaymentProviderSubscriptionAddOnUpdate({
- code: 'another-add-on',
- quantity: 1,
- }),
- ],
- })
- )
- })
- it('returns a change request with quantity and unit price specified', function (ctx) {
- const {
- PaymentProviderSubscriptionChangeRequest,
- PaymentProviderSubscriptionAddOnUpdate,
- } = ctx.PaymentProviderEntities
- const quantity = 5
- const unitPrice = 10
- const changeRequest = ctx.subscription.getRequestForAddOnPurchase(
- 'another-add-on',
- quantity,
- unitPrice
- )
- expect(changeRequest).to.deep.equal(
- new PaymentProviderSubscriptionChangeRequest({
- subscription: ctx.subscription,
- timeframe: 'now',
- addOnUpdates: [
- new PaymentProviderSubscriptionAddOnUpdate({
- code: ctx.addOn.code,
- quantity: ctx.addOn.quantity,
- unitPrice: ctx.addOn.unitPrice,
- }),
- new PaymentProviderSubscriptionAddOnUpdate({
- code: 'another-add-on',
- quantity,
- unitPrice,
- }),
- ],
- })
- )
- })
- it('throws a DuplicateAddOnError if the subscription already has the add-on', function (ctx) {
- expect(() =>
- ctx.subscription.getRequestForAddOnPurchase(ctx.addOn.code)
- ).to.throw(Errors.DuplicateAddOnError)
- })
- describe('with pending plan downgrade', function () {
- beforeEach(function (ctx) {
- // Scenario: downgrade is scheduled, and then they want to buy an add-on immediately
- const {
- PaymentProviderSubscription,
- PaymentProviderSubscriptionChange,
- } = ctx.PaymentProviderEntities
- ctx.subscription = new PaymentProviderSubscription({
- id: 'subscription-id',
- userId: 'user-id',
- planCode: 'premium-plan',
- planName: 'Premium Plan',
- planPrice: 20,
- addOns: [],
- subtotal: 20,
- taxRate: 0.2,
- taxAmount: 4,
- total: 24,
- currency: 'USD',
- })
- // Pending downgrade to cheaper plan at term_end
- ctx.subscription.pendingChange =
- new PaymentProviderSubscriptionChange({
- subscription: ctx.subscription,
- nextPlanCode: 'cheap-plan',
- nextPlanName: 'Cheap Plan',
- nextPlanPrice: 5,
- nextAddOns: [],
- })
- })
- it('uses current add-ons for immediate add-on purchase', function (ctx) {
- const changeRequest =
- ctx.subscription.getRequestForAddOnPurchase('assistant')
- expect(changeRequest.timeframe).to.equal('now')
- // Should only have the new add-on, based on current state, client will reapply the pending change
- expect(changeRequest.addOnUpdates).to.deep.equal([
- new PaymentProviderSubscriptionAddOnUpdate({
- code: 'assistant',
- quantity: 1,
- }),
- ])
- })
- })
- })
- describe('getRequestForAddOnUpdate()', function () {
- it('returns a change request', function (ctx) {
- const {
- PaymentProviderSubscriptionChangeRequest,
- PaymentProviderSubscriptionAddOnUpdate,
- } = ctx.PaymentProviderEntities
- const newQuantity = 2
- const changeRequest = ctx.subscription.getRequestForAddOnUpdate(
- 'add-on-code',
- newQuantity
- )
- expect(changeRequest).to.deep.equal(
- new PaymentProviderSubscriptionChangeRequest({
- subscription: ctx.subscription,
- timeframe: 'now',
- addOnUpdates: [
- new PaymentProviderSubscriptionAddOnUpdate({
- code: ctx.addOn.code,
- quantity: newQuantity,
- unitPrice: ctx.addOn.unitPrice,
- }),
- ],
- })
- )
- })
- it("throws a AddOnNotPresentError if the subscription doesn't have the add-on", function (ctx) {
- expect(() =>
- ctx.subscription.getRequestForAddOnUpdate('another-add-on', 2)
- ).to.throw(Errors.AddOnNotPresentError)
- })
- })
- describe('getRequestForAddOnRemoval()', function () {
- it('returns a change request', function (ctx) {
- const changeRequest = ctx.subscription.getRequestForAddOnRemoval(
- ctx.addOn.code
- )
- expect(changeRequest).to.deep.equal(
- new PaymentProviderSubscriptionChangeRequest({
- subscription: ctx.subscription,
- timeframe: 'term_end',
- addOnUpdates: [],
- })
- )
- })
- it('returns a change request when in trial', function (ctx) {
- const fiveDaysFromNow = new Date()
- fiveDaysFromNow.setDate(fiveDaysFromNow.getDate() + 5)
- ctx.subscription.trialPeriodEnd = fiveDaysFromNow
- const changeRequest = ctx.subscription.getRequestForAddOnRemoval(
- ctx.addOn.code
- )
- expect(changeRequest).to.deep.equal(
- new PaymentProviderSubscriptionChangeRequest({
- subscription: ctx.subscription,
- timeframe: 'now',
- addOnUpdates: [],
- })
- )
- })
- it("throws an AddOnNotPresentError if the subscription doesn't have the add-on", function (ctx) {
- expect(() =>
- ctx.subscription.getRequestForAddOnRemoval('another-add-on')
- ).to.throw(Errors.AddOnNotPresentError)
- })
- describe('with pending changes', function () {
- beforeEach(function (ctx) {
- // Scenario: a subscription has two add-ons, one already scheduled for removal
- const {
- PaymentProviderSubscription,
- PaymentProviderSubscriptionAddOn,
- } = ctx.PaymentProviderEntities
- ctx.addOn1 = new PaymentProviderSubscriptionAddOn({
- code: 'addon-1',
- name: 'Add-On 1',
- quantity: 1,
- unitPrice: 2,
- })
- ctx.addOn2 = new PaymentProviderSubscriptionAddOn({
- code: 'addon-2',
- name: 'Add-On 2',
- quantity: 1,
- unitPrice: 3,
- })
- ctx.subscription = new PaymentProviderSubscription({
- id: 'subscription-id',
- userId: 'user-id',
- planCode: 'regular-plan',
- planName: 'My Plan',
- planPrice: 10,
- addOns: [ctx.addOn1, ctx.addOn2],
- subtotal: 10.99,
- taxRate: 0.2,
- taxAmount: 2.4,
- total: 14.4,
- currency: 'USD',
- })
- // Set up a pending change that removes addon-1 at term_end
- ctx.subscription.pendingChange =
- new PaymentProviderSubscriptionChange({
- subscription: ctx.subscription,
- nextPlanCode: ctx.subscription.planCode,
- nextPlanName: ctx.subscription.planName,
- nextPlanPrice: ctx.subscription.planPrice,
- nextAddOns: [ctx.addOn2], // Only addon-2 remains
- })
- })
- it('stacks multiple add-on removals at term_end', function (ctx) {
- const changeRequest =
- ctx.subscription.getRequestForAddOnRemoval('addon-2')
- expect(changeRequest.timeframe).to.equal('term_end')
- expect(changeRequest.addOnUpdates).to.deep.equal([]) // empty because both are scheduled for removal
- })
- })
- })
- describe('getRequestForAddOnReactivation()', function () {
- it('throws an AddOnNotPresentError', function (ctx) {
- expect(() =>
- ctx.subscription.getRequestForAddOnReactivation(ctx.addOn.code)
- ).to.throw(Errors.AddOnNotPresentError)
- })
- })
- describe('getRequestForGroupPlanUpgrade()', function () {
- it('returns a correct change request', function (ctx) {
- const changeRequest =
- ctx.subscription.getRequestForGroupPlanUpgrade('test_plan_code')
- const addOns = [
- new PaymentProviderSubscriptionAddOnUpdate({
- code: 'add-on-code',
- quantity: 1,
- }),
- ]
- expect(changeRequest).to.deep.equal(
- new PaymentProviderSubscriptionChangeRequest({
- subscription: ctx.subscription,
- timeframe: 'now',
- addOnUpdates: addOns,
- planCode: 'test_plan_code',
- })
- )
- })
- })
- describe('getRequestForPoNumberAndTermsAndConditionsUpdate()', function () {
- it('returns a correct update request', function (ctx) {
- const updateRequest =
- ctx.subscription.getRequestForPoNumberAndTermsAndConditionsUpdate(
- 'O12345',
- 'T&C copy'
- )
- expect(updateRequest).to.deep.equal(
- new PaymentProviderSubscriptionUpdateRequest({
- subscription: ctx.subscription,
- poNumber: 'O12345',
- termsAndConditions: 'T&C copy',
- })
- )
- })
- })
- describe('getRequestForTermsAndConditionsUpdate()', function () {
- it('returns a correct update request', function (ctx) {
- const updateRequest =
- ctx.subscription.getRequestForTermsAndConditionsUpdate('T&C copy')
- expect(updateRequest).to.deep.equal(
- new PaymentProviderSubscriptionUpdateRequest({
- subscription: ctx.subscription,
- termsAndConditions: 'T&C copy',
- })
- )
- })
- })
- describe('getRequestForPlanChangeCancellation()', function () {
- it('returns null when there is no pending change', function (ctx) {
- ctx.subscription.pendingChange = null
- const result = ctx.subscription.getRequestForPlanChangeCancellation()
- expect(result).to.be.null
- })
- it('returns null when pending change has no add-on changes', function (ctx) {
- // Same add-ons in pending change as current subscription
- ctx.subscription.pendingChange =
- new PaymentProviderSubscriptionChange({
- subscription: ctx.subscription,
- nextPlanCode: 'cheap-plan', // only plan change, no add-on change
- nextPlanName: 'Cheap Plan',
- nextPlanPrice: 5,
- nextAddOns: [ctx.addOn], // same add-on as current subscription
- })
- const result = ctx.subscription.getRequestForPlanChangeCancellation()
- expect(result).to.be.null
- })
- it('returns a change request preserving add-on removal when canceling plan change', function (ctx) {
- // Pending change has both plan downgrade AND add-on removal
- ctx.subscription.pendingChange =
- new PaymentProviderSubscriptionChange({
- subscription: ctx.subscription,
- nextPlanCode: 'cheap-plan',
- nextPlanName: 'Cheap Plan',
- nextPlanPrice: 5,
- nextAddOns: [], // add-on is being removed
- })
- const result = ctx.subscription.getRequestForPlanChangeCancellation()
- expect(result).to.not.be.null
- expect(result.timeframe).to.equal('term_end')
- expect(result.planCode).to.equal(ctx.subscription.planCode) // keep current plan
- expect(result.addOnUpdates).to.deep.equal([]) // preserve add-on removal
- })
- it('returns a change request preserving add-on addition when canceling plan change', function (ctx) {
- const {
- PaymentProviderSubscriptionAddOn,
- PaymentProviderSubscriptionAddOnUpdate,
- } = ctx.PaymentProviderEntities
- // Current subscription has one add-on
- // Pending change has plan downgrade AND a new add-on
- const newAddOn = new PaymentProviderSubscriptionAddOn({
- code: 'new-addon',
- name: 'New Add-On',
- quantity: 1,
- unitPrice: 3,
- })
- ctx.subscription.pendingChange =
- new PaymentProviderSubscriptionChange({
- subscription: ctx.subscription,
- nextPlanCode: 'cheap-plan',
- nextPlanName: 'Cheap Plan',
- nextPlanPrice: 5,
- nextAddOns: [ctx.addOn, newAddOn], // current add-on plus new one
- })
- const result = ctx.subscription.getRequestForPlanChangeCancellation()
- expect(result).to.not.be.null
- expect(result.timeframe).to.equal('term_end')
- expect(result.planCode).to.equal(ctx.subscription.planCode) // keep current plan
- expect(result.addOnUpdates).to.deep.equal([
- new PaymentProviderSubscriptionAddOnUpdate({
- code: ctx.addOn.code,
- quantity: ctx.addOn.quantity,
- unitPrice: ctx.addOn.unitPrice,
- }),
- new PaymentProviderSubscriptionAddOnUpdate({
- code: newAddOn.code,
- quantity: newAddOn.quantity,
- unitPrice: newAddOn.unitPrice,
- }),
- ])
- })
- it('removes additional-license add-on when canceling pending group plan upgrade', function (ctx) {
- const { MEMBERS_LIMIT_ADD_ON_CODE } = ctx.PaymentProviderEntities
- // Current: professional-annual (no add-ons, not a group plan)
- ctx.subscription.planCode = 'professional-annual'
- ctx.subscription.planName = 'Professional Annual'
- ctx.subscription.addOns = []
- // Pending: group_professional_10_educational + 5 additional-license
- const additionalLicenseAddOn =
- new ctx.PaymentProviderEntities.PaymentProviderSubscriptionAddOn({
- code: MEMBERS_LIMIT_ADD_ON_CODE,
- name: 'Additional License',
- quantity: 5,
- unitPrice: 10,
- })
- ctx.subscription.pendingChange =
- new PaymentProviderSubscriptionChange({
- subscription: ctx.subscription,
- nextPlanCode: 'group_professional_10_educational',
- nextPlanName: 'Group Professional Educational',
- nextPlanPrice: 100,
- nextAddOns: [additionalLicenseAddOn],
- })
- const result = ctx.subscription.getRequestForPlanChangeCancellation()
- // Should return a change request that keeps the current plan
- // but removes the additional-license add-on
- expect(result).to.not.be.null
- expect(result.timeframe).to.equal('term_end')
- expect(result.planCode).to.equal('professional-annual')
- expect(result.addOnUpdates).to.deep.equal([])
- })
- it('preserves non-members-limit add-ons when canceling pending group plan upgrade', function (ctx) {
- const { MEMBERS_LIMIT_ADD_ON_CODE } = ctx.PaymentProviderEntities
- // Current: professional-annual with AI add-on (not a group plan)
- ctx.subscription.planCode = 'professional-annual'
- ctx.subscription.planName = 'Professional Annual'
- const aiAddOn =
- new ctx.PaymentProviderEntities.PaymentProviderSubscriptionAddOn({
- code: AI_ADD_ON_CODE,
- name: 'AI Add-On',
- quantity: 1,
- unitPrice: 20,
- })
- ctx.subscription.addOns = [aiAddOn]
- // Pending: group_professional_10_educational + AI add-on + 5 additional-license
- const additionalLicenseAddOn =
- new ctx.PaymentProviderEntities.PaymentProviderSubscriptionAddOn({
- code: MEMBERS_LIMIT_ADD_ON_CODE,
- name: 'Additional License',
- quantity: 5,
- unitPrice: 10,
- })
- ctx.subscription.pendingChange =
- new PaymentProviderSubscriptionChange({
- subscription: ctx.subscription,
- nextPlanCode: 'group_professional_10_educational',
- nextPlanName: 'Group Professional Educational',
- nextPlanPrice: 100,
- nextAddOns: [aiAddOn, additionalLicenseAddOn],
- })
- const result = ctx.subscription.getRequestForPlanChangeCancellation()
- // Should return a change request that keeps the current plan and AI add-on
- // but removes the additional-license add-on
- expect(result).to.not.be.null
- expect(result.timeframe).to.equal('term_end')
- expect(result.planCode).to.equal('professional-annual')
- expect(result.addOnUpdates).to.deep.equal([
- new PaymentProviderSubscriptionAddOnUpdate({
- code: AI_ADD_ON_CODE,
- quantity: 1,
- unitPrice: 20,
- }),
- ])
- })
- })
- describe('with an add-on pending cancellation', function () {
- beforeEach(function (ctx) {
- ctx.subscription.pendingChange =
- new PaymentProviderSubscriptionChange({
- subscription: ctx.subscription,
- nextPlanCode: ctx.subscription.planCode,
- nextPlanName: ctx.subscription.planName,
- nextPlanPrice: ctx.subscription.planPrice,
- nextAddOns: [],
- })
- })
- describe('getRequestForAddOnReactivation()', function () {
- it('returns a change request', function (ctx) {
- const changeRequest =
- ctx.subscription.getRequestForAddOnReactivation(ctx.addOn.code)
- expect(changeRequest).to.deep.equal(
- new PaymentProviderSubscriptionChangeRequest({
- subscription: ctx.subscription,
- timeframe: 'term_end',
- planCode: ctx.subscription.pendingChange.nextPlanCode,
- addOnUpdates: [ctx.addOn.toAddOnUpdate()],
- })
- )
- })
- it('throws an AddOnNotPresentError if given the wrong add-on', function (ctx) {
- expect(() =>
- ctx.subscription.getRequestForAddOnReactivation('some-add-on')
- ).to.throw(Errors.AddOnNotPresentError)
- })
- })
- })
- })
- describe('without add-ons', function () {
- beforeEach(function (ctx) {
- const { PaymentProviderSubscription } = ctx.PaymentProviderEntities
- ctx.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 (ctx) {
- expect(ctx.subscription.hasAddOn('some-add-on')).to.be.false
- })
- })
- describe('getRequestForAddOnPurchase()', function () {
- it('returns a change request', function (ctx) {
- const {
- PaymentProviderSubscriptionChangeRequest,
- PaymentProviderSubscriptionAddOnUpdate,
- } = ctx.PaymentProviderEntities
- const changeRequest =
- ctx.subscription.getRequestForAddOnPurchase('some-add-on')
- expect(changeRequest).to.deep.equal(
- new PaymentProviderSubscriptionChangeRequest({
- subscription: ctx.subscription,
- timeframe: 'now',
- addOnUpdates: [
- new PaymentProviderSubscriptionAddOnUpdate({
- code: 'some-add-on',
- quantity: 1,
- }),
- ],
- })
- )
- })
- })
- describe('getRequestForAddOnRemoval()', function () {
- it('throws an AddOnNotPresentError', function (ctx) {
- expect(() =>
- ctx.subscription.getRequestForAddOnRemoval('some-add-on')
- ).to.throw(Errors.AddOnNotPresentError)
- })
- })
- describe('getRequestForAddOnReactivation()', function () {
- it('throws an AddOnNotPresentError', function (ctx) {
- expect(() =>
- ctx.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)
- })
- })
- })
- })
|