diff --git a/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js b/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js index 129463dcf0..180a78f294 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js +++ b/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js @@ -282,6 +282,18 @@ async function buildUsersSubscriptionViewModel(user, locale = 'en') { isEligibleForGroupPlan: paymentRecord.subscription.service === 'recurly' && !isInTrial, } + + const isMonthlyCollaboratorPlan = + personalSubscription.planCode.includes('collaborator') && + !personalSubscription.planCode.includes('ann') && + !personalSubscription.plan.groupPlan + personalSubscription.payment.isEligibleForDowngradeUpsell = + !personalSubscription.payment.pausedAt && + !personalSubscription.payment.remainingPauseCycles && + isMonthlyCollaboratorPlan && + !isInTrial && + paymentRecord.subscription.service === 'recurly' + if (paymentRecord.subscription.pendingChange) { const pendingPlanCode = paymentRecord.subscription.pendingChange.nextPlanCode diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-plan/cancel-subscription.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-plan/cancel-subscription.tsx index dc34d38693..1b04e6385f 100644 --- a/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-plan/cancel-subscription.tsx +++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-plan/cancel-subscription.tsx @@ -8,7 +8,6 @@ import { cancelSubscriptionUrl, redirectAfterCancelSubscriptionUrl, } from '../../../../../data/subscription-url' -import showDowngradeOption from '../../../../../util/show-downgrade-option' import GenericErrorAlert from '../../../generic-error-alert' import DowngradePlanButton from './downgrade-plan-button' import ExtendTrialButton from './extend-trial-button' @@ -157,13 +156,8 @@ export function CancelSubscription() { if (!personalSubscription || !('payment' in personalSubscription)) return null - const showDowngrade = showDowngradeOption( - personalSubscription.plan.planCode, - personalSubscription.plan.groupPlan, - personalSubscription.payment.trialEndsAt, - personalSubscription.payment.pausedAt, - personalSubscription.payment.remainingPauseCycles - ) + const showDowngrade = + personalSubscription.payment.isEligibleForDowngradeUpsell const planToDowngradeTo = plans.find( plan => plan.planCode === planCodeToDowngradeTo ) diff --git a/services/web/frontend/js/features/subscription/util/is-monthly-collaborator-plan.ts b/services/web/frontend/js/features/subscription/util/is-monthly-collaborator-plan.ts deleted file mode 100644 index 550c5ec2a5..0000000000 --- a/services/web/frontend/js/features/subscription/util/is-monthly-collaborator-plan.ts +++ /dev/null @@ -1,10 +0,0 @@ -export default function isMonthlyCollaboratorPlan( - planCode: string, - isGroupPlan?: boolean -) { - return ( - planCode.indexOf('collaborator') !== -1 && - planCode.indexOf('ann') === -1 && - !isGroupPlan - ) -} diff --git a/services/web/frontend/js/features/subscription/util/show-downgrade-option.ts b/services/web/frontend/js/features/subscription/util/show-downgrade-option.ts deleted file mode 100644 index 283823f481..0000000000 --- a/services/web/frontend/js/features/subscription/util/show-downgrade-option.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Nullable } from '../../../../../types/utils' -import isInFreeTrial from './is-in-free-trial' -import isMonthlyCollaboratorPlan from './is-monthly-collaborator-plan' - -export default function showDowngradeOption( - planCode: string, - isGroupPlan?: boolean, - trialEndsAt?: string | null, - pausedAt?: Nullable, - remainingPauseCycles?: Nullable -) { - return ( - !pausedAt && - !remainingPauseCycles && - isMonthlyCollaboratorPlan(planCode, isGroupPlan) && - !isInFreeTrial(trialEndsAt) - ) -} diff --git a/services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx index 15dd65b6ba..4102760cd1 100644 --- a/services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx +++ b/services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx @@ -470,10 +470,10 @@ describe('', function () { }) }) - it('does not show option to downgrade when not a collaborator plan', function () { - const trialPlan = cloneDeep(monthlyActiveCollaborator) - trialPlan.plan.planCode = 'anotherplan' - renderActiveSubscription(trialPlan) + it('does not show option to downgrade when plan is not eligible for downgrades', function () { + const ineligiblePlan = cloneDeep(monthlyActiveCollaborator) + ineligiblePlan.payment.isEligibleForDowngradeUpsell = false + renderActiveSubscription(ineligiblePlan) showConfirmCancelUI() expect( screen.queryByRole('button', { diff --git a/services/web/test/frontend/features/subscription/fixtures/subscriptions.ts b/services/web/test/frontend/features/subscription/fixtures/subscriptions.ts index ebd741b240..08690742d3 100644 --- a/services/web/test/frontend/features/subscription/fixtures/subscriptions.ts +++ b/services/web/test/frontend/features/subscription/fixtures/subscriptions.ts @@ -54,6 +54,7 @@ export const annualActiveSubscription: PaidSubscription = { addOnDisplayPricesWithoutAdditionalLicense: {}, isEligibleForGroupPlan: true, isEligibleForPause: false, + isEligibleForDowngradeUpsell: false, }, } @@ -96,6 +97,7 @@ export const annualActiveSubscriptionEuro: PaidSubscription = { addOnDisplayPricesWithoutAdditionalLicense: {}, isEligibleForGroupPlan: true, isEligibleForPause: true, + isEligibleForDowngradeUpsell: false, }, } @@ -137,6 +139,7 @@ export const annualActiveSubscriptionPro: PaidSubscription = { addOnDisplayPricesWithoutAdditionalLicense: {}, isEligibleForGroupPlan: true, isEligibleForPause: true, + isEligibleForDowngradeUpsell: false, }, } @@ -179,6 +182,7 @@ export const pastDueExpiredSubscription: PaidSubscription = { addOnDisplayPricesWithoutAdditionalLicense: {}, isEligibleForGroupPlan: true, isEligibleForPause: true, + isEligibleForDowngradeUpsell: false, }, } @@ -221,6 +225,7 @@ export const canceledSubscription: PaidSubscription = { addOnDisplayPricesWithoutAdditionalLicense: {}, isEligibleForGroupPlan: true, isEligibleForPause: true, + isEligibleForDowngradeUpsell: false, }, } @@ -263,6 +268,7 @@ export const pendingSubscriptionChange: PaidSubscription = { addOnDisplayPricesWithoutAdditionalLicense: {}, isEligibleForGroupPlan: true, isEligibleForPause: false, + isEligibleForDowngradeUpsell: false, }, pendingPlan: { planCode: 'professional-annual', @@ -316,6 +322,7 @@ export const groupActiveSubscription: GroupSubscription = { addOnDisplayPricesWithoutAdditionalLicense: {}, isEligibleForGroupPlan: true, isEligibleForPause: false, + isEligibleForDowngradeUpsell: false, }, } @@ -365,6 +372,7 @@ export const groupActiveSubscriptionWithPendingLicenseChange: GroupSubscription addOnDisplayPricesWithoutAdditionalLicense: {}, isEligibleForGroupPlan: true, isEligibleForPause: false, + isEligibleForDowngradeUpsell: false, }, pendingPlan: { planCode: 'group_collaborator_10_enterprise', @@ -417,6 +425,7 @@ export const trialSubscription: PaidSubscription = { addOnDisplayPricesWithoutAdditionalLicense: {}, isEligibleForGroupPlan: true, isEligibleForPause: false, + isEligibleForDowngradeUpsell: false, }, } @@ -480,6 +489,7 @@ export const trialCollaboratorSubscription: PaidSubscription = { addOnDisplayPricesWithoutAdditionalLicense: {}, isEligibleForGroupPlan: true, isEligibleForPause: true, + isEligibleForDowngradeUpsell: false, }, } @@ -521,5 +531,6 @@ export const monthlyActiveCollaborator: PaidSubscription = { addOnDisplayPricesWithoutAdditionalLicense: {}, isEligibleForGroupPlan: true, isEligibleForPause: true, + isEligibleForDowngradeUpsell: true, }, } diff --git a/services/web/test/frontend/features/subscription/util/is-monthly-collaborator-plan.test.ts b/services/web/test/frontend/features/subscription/util/is-monthly-collaborator-plan.test.ts deleted file mode 100644 index dd9c2a64b1..0000000000 --- a/services/web/test/frontend/features/subscription/util/is-monthly-collaborator-plan.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { expect } from 'chai' -import isMonthlyCollaboratorPlan from '../../../../../frontend/js/features/subscription/util/is-monthly-collaborator-plan' - -describe('isMonthlyCollaboratorPlan', function () { - it('returns false when a plan code without "collaborator" ', function () { - expect(isMonthlyCollaboratorPlan('test', false)).to.be.false - }) - it('returns false when on a plan with "collaborator" and "ann"', function () { - expect(isMonthlyCollaboratorPlan('collaborator-annual', false)).to.be.false - }) - it('returns false when on a plan with "collaborator" and without "ann" but is a group plan', function () { - expect(isMonthlyCollaboratorPlan('collaborator', true)).to.be.false - }) - it('returns true when on a plan with non-group "collaborator" monthly plan', function () { - expect(isMonthlyCollaboratorPlan('collaborator', false)).to.be.true - }) -}) diff --git a/services/web/test/frontend/features/subscription/util/show-downgrade-option.test.ts b/services/web/test/frontend/features/subscription/util/show-downgrade-option.test.ts deleted file mode 100644 index 8a464ac796..0000000000 --- a/services/web/test/frontend/features/subscription/util/show-downgrade-option.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { expect } from 'chai' -import showDowngradeOption from '../../../../../frontend/js/features/subscription/util/show-downgrade-option' -import dateformat from 'dateformat' - -describe('showDowngradeOption', function () { - const today = new Date() - const sevenDaysFromToday = new Date().setDate(today.getDate() + 7) - const sevenDaysFromTodayFormatted = dateformat( - sevenDaysFromToday, - 'dS mmmm yyyy' - ) - - it('returns false when no trial end date', function () { - expect(showDowngradeOption('collab')).to.be.false - }) - it('returns false when a plan code without "collaborator" ', function () { - expect(showDowngradeOption('test', false, sevenDaysFromTodayFormatted)).to - .be.false - }) - it('returns false when on a plan with trial date in future but has "collaborator" and "ann" in plan code', function () { - expect( - showDowngradeOption( - 'collaborator-annual', - false, - sevenDaysFromTodayFormatted - ) - ).to.be.false - }) - it('returns false when on a plan with trial date in future and plan code has "collaborator" and no "ann" but is a group plan', function () { - expect( - showDowngradeOption('collaborator', true, sevenDaysFromTodayFormatted) - ).to.be.false - }) - it('returns false when on a plan with "collaborator" and without "ann" and trial date in future', function () { - expect( - showDowngradeOption('collaborator', false, sevenDaysFromTodayFormatted) - ).to.be.false - }) - it('returns true when on a plan with "collaborator" and without "ann" and no trial date', function () { - expect(showDowngradeOption('collaborator', false)).to.be.true - }) - it('returns true when on a plan with "collaborator" and without "ann" and trial date is in the past', function () { - expect( - showDowngradeOption('collaborator', false, '2000-02-16T17:59:07.000Z') - ).to.be.true - }) - it('returns false when on a monthly collaborator plan with a pending pause', function () { - expect( - showDowngradeOption( - 'collaborator', - false, - null, - '2030-01-01T12:00:00.000Z' - ) - ).to.be.false - }) - it('returns false when on a monthly collaborator plan with an active pause', function () { - expect( - showDowngradeOption( - 'collaborator', - false, - null, - '2030-01-01T12:00:00.000Z', - 5 - ) - ).to.be.false - }) -}) diff --git a/services/web/test/unit/src/Subscription/SubscriptionViewModelBuilderTests.js b/services/web/test/unit/src/Subscription/SubscriptionViewModelBuilderTests.js index e969cf381c..0f666b888a 100644 --- a/services/web/test/unit/src/Subscription/SubscriptionViewModelBuilderTests.js +++ b/services/web/test/unit/src/Subscription/SubscriptionViewModelBuilderTests.js @@ -25,6 +25,11 @@ describe('SubscriptionViewModelBuilder', function () { planCode: this.planCode, features: this.planFeatures, } + this.annualPlanCode = 'collaborator_annual' + this.annualPlan = { + planCode: this.annualPlanCode, + features: this.planFeatures, + } this.individualSubscription = { planCode: this.planCode, plan: this.plan, @@ -74,6 +79,7 @@ describe('SubscriptionViewModelBuilder', function () { features: this.groupPlanFeatures, membersLimit: 4, membersLimitAddOn: 'additional-license', + groupPlan: true, } this.groupSubscription = { planCode: this.groupPlanCode, @@ -166,6 +172,8 @@ describe('SubscriptionViewModelBuilder', function () { this.PlansLocator.findLocalPlanInSettings .withArgs(this.planCode) .returns(this.plan) + .withArgs(this.annualPlanCode) + .returns(this.annualPlan) .withArgs(this.groupPlanCode) .returns(this.groupPlan) .withArgs(this.commonsPlanCode) @@ -575,6 +583,7 @@ describe('SubscriptionViewModelBuilder', function () { }, isEligibleForGroupPlan: true, isEligibleForPause: false, + isEligibleForDowngradeUpsell: true, }) }) @@ -689,6 +698,96 @@ describe('SubscriptionViewModelBuilder', function () { }) }) + describe('isEligibleForDowngradeUpsell', function () { + it('is true for eligible individual subscriptions', async function () { + this.paymentRecord.pausePeriodStart = null + this.paymentRecord.remainingPauseCycles = null + this.paymentRecord.trialPeriodEnd = null + this.paymentRecord.service = 'recurly' + const result = + await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel( + this.user + ) + assert.isTrue( + result.personalSubscription.payment.isEligibleForDowngradeUpsell + ) + }) + + it('is false for group plans', async function () { + this.individualSubscription.planCode = this.groupPlanCode + this.paymentRecord.pausePeriodStart = null + this.paymentRecord.remainingPauseCycles = null + this.paymentRecord.trialPeriodEnd = null + this.paymentRecord.service = 'recurly' + const result = + await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel( + this.user + ) + assert.isFalse( + result.personalSubscription.payment.isEligibleForDowngradeUpsell + ) + }) + + it('is false for annual individual plans', async function () { + this.individualSubscription.planCode = this.annualPlanCode + this.paymentRecord.pausePeriodStart = null + this.paymentRecord.remainingPauseCycles = null + this.paymentRecord.trialPeriodEnd = null + this.paymentRecord.service = 'recurly' + const result = + await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel( + this.user + ) + assert.isFalse( + result.personalSubscription.payment.isEligibleForDowngradeUpsell + ) + }) + + it('is false for paused plans', async function () { + this.paymentRecord.pausePeriodStart = new Date() + this.paymentRecord.remainingPauseCycles = 1 + this.paymentRecord.trialPeriodEnd = null + this.paymentRecord.service = 'recurly' + const result = + await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel( + this.user + ) + assert.isFalse( + result.personalSubscription.payment.isEligibleForDowngradeUpsell + ) + }) + + it('is false for plans in free trial period', async function () { + this.paymentRecord.pausePeriodStart = null + this.paymentRecord.remainingPauseCycles = null + this.paymentRecord.trialPeriodEnd = new Date( + Date.now() + 24 * 60 * 60 * 1000 // tomorrow + ) + this.paymentRecord.service = 'recurly' + const result = + await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel( + this.user + ) + assert.isFalse( + result.personalSubscription.payment.isEligibleForDowngradeUpsell + ) + }) + + it('is false for Stripe subscriptions', async function () { + this.paymentRecord.pausePeriodStart = null + this.paymentRecord.remainingPauseCycles = null + this.paymentRecord.trialPeriodEnd = null + this.paymentRecord.service = 'stripe' + const result = + await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel( + this.user + ) + assert.isFalse( + result.personalSubscription.payment.isEligibleForDowngradeUpsell + ) + }) + }) + it('includes pending changes', async function () { this.paymentRecord.pendingChange = new PaymentProviderSubscriptionChange({ diff --git a/services/web/types/subscription/dashboard/subscription.ts b/services/web/types/subscription/dashboard/subscription.ts index 4b9c9de57c..a1ee934423 100644 --- a/services/web/types/subscription/dashboard/subscription.ts +++ b/services/web/types/subscription/dashboard/subscription.ts @@ -46,6 +46,7 @@ type PaymentProviderRecord = { remainingPauseCycles?: Nullable isEligibleForPause: boolean isEligibleForGroupPlan: boolean + isEligibleForDowngradeUpsell: boolean } export type GroupPolicy = {