diff --git a/services/web/app/src/Features/Subscription/PaymentProviderEntities.js b/services/web/app/src/Features/Subscription/PaymentProviderEntities.js index 12600175fd..21bd504caf 100644 --- a/services/web/app/src/Features/Subscription/PaymentProviderEntities.js +++ b/services/web/app/src/Features/Subscription/PaymentProviderEntities.js @@ -134,9 +134,11 @@ class PaymentProviderSubscription { if (newPlan == null) { throw new OError('Unable to find plan in settings', { planCode }) } + const isInTrial = SubscriptionHelper.isInTrial(this.trialPeriodEnd) const shouldChangeAtTermEnd = SubscriptionHelper.shouldPlanChangeAtTermEnd( currentPlan, - newPlan + newPlan, + isInTrial ) const changeRequest = new PaymentProviderSubscriptionChangeRequest({ @@ -250,9 +252,10 @@ class PaymentProviderSubscription { const addOnUpdates = this.addOns .filter(addOn => addOn.code !== code) .map(addOn => addOn.toAddOnUpdate()) + const isInTrial = SubscriptionHelper.isInTrial(this.trialPeriodEnd) return new PaymentProviderSubscriptionChangeRequest({ subscription: this, - timeframe: 'term_end', + timeframe: isInTrial ? 'now' : 'term_end', addOnUpdates, }) } diff --git a/services/web/app/src/Features/Subscription/SubscriptionController.js b/services/web/app/src/Features/Subscription/SubscriptionController.js index 4159ab5b93..5856682166 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionController.js +++ b/services/web/app/src/Features/Subscription/SubscriptionController.js @@ -88,9 +88,13 @@ async function userSubscriptionPage(req, res) { await Modules.promises.hooks.fire('userCanExtendTrial', user) )?.[0] const fromPlansPage = req.query.hasSubscription + const isInTrial = SubscriptionHelper.isInTrial( + personalSubscription?.payment?.trialEndsAt + ) const plansData = SubscriptionViewModelBuilder.buildPlansListForSubscriptionDash( - personalSubscription?.plan + personalSubscription?.plan, + isInTrial ) AnalyticsManager.recordEventForSession(req.session, 'subscription-page-view') diff --git a/services/web/app/src/Features/Subscription/SubscriptionHelper.js b/services/web/app/src/Features/Subscription/SubscriptionHelper.js index 059314723a..429432349d 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionHelper.js +++ b/services/web/app/src/Features/Subscription/SubscriptionHelper.js @@ -6,7 +6,12 @@ const { isStandaloneAiAddOnPlanCode } = require('./PaymentProviderEntities') * If the user changes to a less expensive plan, we shouldn't apply the change immediately. * This is to avoid unintended/artifical credits on users Recurly accounts. */ -function shouldPlanChangeAtTermEnd(oldPlan, newPlan) { +function shouldPlanChangeAtTermEnd(oldPlan, newPlan, isInTrial) { + if (isInTrial) { + // we should always upgrade or downgrade immediately if actively in trial + return false + } + if ( oldPlan.annual === newPlan.annual && isStandaloneAiAddOnPlanCode(oldPlan.planCode) && @@ -148,6 +153,14 @@ function getSubscriptionTrialEndsAt(subscription) { return subscription?.paymentProvider?.trialEndsAt } +function isInTrial(trialEndsAt) { + if (!trialEndsAt) { + return false + } + + return trialEndsAt.getTime() > Date.now() +} + module.exports = { shouldPlanChangeAtTermEnd, generateInitialLocalizedGroupPrice, @@ -157,4 +170,5 @@ module.exports = { getPaidSubscriptionState, getSubscriptionTrialStartedAt, getSubscriptionTrialEndsAt, + isInTrial, } diff --git a/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js b/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js index 25b00c28a5..3681975a38 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js +++ b/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js @@ -470,7 +470,7 @@ async function getUsersSubscriptionDetails(user) { return { bestSubscription, individualSubscription, memberGroupSubscriptions } } -function buildPlansList(currentPlan) { +function buildPlansList(currentPlan, isInTrial) { const { plans } = Settings const allPlans = {} @@ -484,7 +484,11 @@ function buildPlansList(currentPlan) { result.planCodesChangingAtTermEnd = _.map( _.filter(plans, plan => { if (!plan.hideFromUsers) { - return SubscriptionHelper.shouldPlanChangeAtTermEnd(currentPlan, plan) + return SubscriptionHelper.shouldPlanChangeAtTermEnd( + currentPlan, + plan, + isInTrial + ) } }), 'planCode' @@ -569,8 +573,8 @@ function buildGroupSubscriptionForView(groupSubscription) { } } -function buildPlansListForSubscriptionDash(currentPlan) { - const allPlansData = buildPlansList(currentPlan) +function buildPlansListForSubscriptionDash(currentPlan, isInTrial) { + const allPlansData = buildPlansList(currentPlan, isInTrial) const plans = [] // only list individual and visible plans for "change plans" UI if (allPlansData.studentAccounts) { diff --git a/services/web/test/unit/src/Subscription/PaymentProviderEntitiesTest.js b/services/web/test/unit/src/Subscription/PaymentProviderEntitiesTest.js index d19e8808a6..07c401dfb8 100644 --- a/services/web/test/unit/src/Subscription/PaymentProviderEntitiesTest.js +++ b/services/web/test/unit/src/Subscription/PaymentProviderEntitiesTest.js @@ -104,6 +104,23 @@ describe('PaymentProviderEntities', function () { ) }) + 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') + 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 @@ -282,6 +299,22 @@ describe('PaymentProviderEntities', function () { ) }) + 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') diff --git a/services/web/test/unit/src/Subscription/SubscriptionHelperTests.js b/services/web/test/unit/src/Subscription/SubscriptionHelperTests.js index d22f877646..fb667ca451 100644 --- a/services/web/test/unit/src/Subscription/SubscriptionHelperTests.js +++ b/services/web/test/unit/src/Subscription/SubscriptionHelperTests.js @@ -102,38 +102,65 @@ describe('SubscriptionHelper', function () { }) describe('shouldPlanChangeAtTermEnd', function () { - it('should return true if the new plan is less expensive', function () { + it('should return false if isInTrial is true', function () { + const isInTrial = true const changeAtTermEnd = this.SubscriptionHelper.shouldPlanChangeAtTermEnd( plans.expensive, - plans.cheaper + plans.cheaper, + isInTrial + ) + expect(changeAtTermEnd).to.be.false + }) + + it('should return true if the new plan is less expensive', function () { + const isInTrial = false + const changeAtTermEnd = this.SubscriptionHelper.shouldPlanChangeAtTermEnd( + plans.expensive, + plans.cheaper, + isInTrial ) expect(changeAtTermEnd).to.be.true }) + it('should return false if the new plan is more exepensive', function () { + const isInTrial = false const changeAtTermEnd = this.SubscriptionHelper.shouldPlanChangeAtTermEnd( plans.cheaper, - plans.expensive + plans.expensive, + isInTrial ) expect(changeAtTermEnd).to.be.false }) + it('should return false if the new plan is the same price', function () { + const isInTrial = false + const changeAtTermEnd = this.SubscriptionHelper.shouldPlanChangeAtTermEnd( plans.cheaper, - plans.alsoCheap + plans.alsoCheap, + isInTrial ) expect(changeAtTermEnd).to.be.false }) + it('should return false if the change is from an individual plan to a more expensive group plan', function () { + const isInTrial = false + const changeAtTermEnd = this.SubscriptionHelper.shouldPlanChangeAtTermEnd( plans.expensive, - plans.expensiveGroup + plans.expensiveGroup, + isInTrial ) expect(changeAtTermEnd).to.be.false }) + it('should return true if the change is from an individual plan to a cheaper group plan', function () { + const isInTrial = false + const changeAtTermEnd = this.SubscriptionHelper.shouldPlanChangeAtTermEnd( plans.expensive, - plans.cheapGroup + plans.cheapGroup, + isInTrial ) expect(changeAtTermEnd).to.be.true }) @@ -471,4 +498,25 @@ describe('SubscriptionHelper', function () { expect(result).to.be.undefined }) }) + + describe('isInTrial', function () { + it('should return false if trialEndsAt is null', function () { + const result = this.SubscriptionHelper.isInTrial(null) + expect(result).to.be.false + }) + + it('should return false if trialEndsAt is before now', function () { + const tenDaysAgo = new Date() + tenDaysAgo.setDate(tenDaysAgo.getDate() - 10) + const result = this.SubscriptionHelper.isInTrial(tenDaysAgo) + expect(result).to.be.false + }) + + it('should return true if trialEndsAt is after now', function () { + const tenDaysFromNow = new Date() + tenDaysFromNow.setDate(tenDaysFromNow.getDate() + 10) + const result = this.SubscriptionHelper.isInTrial(tenDaysFromNow) + expect(result).to.be.true + }) + }) })