From cff35c743f5144a6932882556b99441c8e8fdeb1 Mon Sep 17 00:00:00 2001 From: Antoine Clausse Date: Tue, 5 May 2026 11:25:50 +0200 Subject: [PATCH] [web] Fix wrong price shown in future payments preview when upgrading over a pending downgrade (#33305) * fix(web): show correct plan in future payments preview when upgrading over a pending downgrade When a user had a scheduled plan downgrade and then immediately upgraded to a higher plan, makeChangePreview() always used the pending (stale) plan code/name/price for the future payments display rather than the newly selected plan. Check whether the current change is a plan change (premium-subscription or group-plan-upgrade type) and if so use subscriptionChange's plan details instead of pendingChange's, since the immediate upgrade overrides the scheduled downgrade. Closes #33299 Co-Authored-By: Claude Sonnet 4.6 * test(web): add unit tests for makeChangePreview pending-change plan override Covers the four cases: premium-subscription and group-plan-upgrade types use subscriptionChange plan (not pendingChange), add-on-purchase type defers to pendingChange plan, and no-pending-change falls back to subscriptionChange as before. Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Claude Sonnet 4.6 GitOrigin-RevId: cc2f9c88e5dfdfb89370798e857ea98caf8fcf85 --- .../Subscription/SubscriptionController.mjs | 18 +++- .../SubscriptionController.test.mjs | 82 +++++++++++++++++++ 2 files changed, 97 insertions(+), 3 deletions(-) diff --git a/services/web/app/src/Features/Subscription/SubscriptionController.mjs b/services/web/app/src/Features/Subscription/SubscriptionController.mjs index eca26e4ab6..f24e5a8dc8 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionController.mjs +++ b/services/web/app/src/Features/Subscription/SubscriptionController.mjs @@ -1229,11 +1229,23 @@ function makeChangePreview( } } + // If the current change is a plan change, it overrides the pending scheduled + // plan change — use the new plan for future payments, not the stale pending one. + const isPlanChange = + subscriptionChangeDescription.type === 'premium-subscription' || + subscriptionChangeDescription.type === 'group-plan-upgrade' + futureInvoiceChange = new PaymentProviderSubscriptionChange({ subscription, - nextPlanCode: pendingChange.nextPlanCode, - nextPlanName: pendingChange.nextPlanName, - nextPlanPrice: pendingChange.nextPlanPrice, + nextPlanCode: isPlanChange + ? subscriptionChange.nextPlanCode + : pendingChange.nextPlanCode, + nextPlanName: isPlanChange + ? subscriptionChange.nextPlanName + : pendingChange.nextPlanName, + nextPlanPrice: isPlanChange + ? subscriptionChange.nextPlanPrice + : pendingChange.nextPlanPrice, nextAddOns: mergedAddOns, }) } else { diff --git a/services/web/test/unit/src/Subscription/SubscriptionController.test.mjs b/services/web/test/unit/src/Subscription/SubscriptionController.test.mjs index 9a46ce0100..3902d5c8d4 100644 --- a/services/web/test/unit/src/Subscription/SubscriptionController.test.mjs +++ b/services/web/test/unit/src/Subscription/SubscriptionController.test.mjs @@ -1429,6 +1429,88 @@ describe('SubscriptionController', function () { }) }) + describe('makeChangePreview', function () { + let pendingChange, baseSubscription, subscriptionChange + + beforeEach(function (ctx) { + pendingChange = { + nextPlanCode: 'student', + nextPlanName: 'Student', + nextPlanPrice: 1000, + nextAddOns: [], + } + + baseSubscription = { + currency: 'USD', + netTerms: 0, + periodEnd: new Date('2027-04-29'), + taxRate: 0, + pendingChange, + } + + subscriptionChange = { + subscription: baseSubscription, + nextPlanCode: 'professional', + nextPlanName: 'Professional', + nextPlanPrice: 2000, + nextAddOns: [], + immediateCharge: { + subtotal: 0, + tax: 0, + total: 0, + discount: 0, + lineItems: [], + }, + } + }) + + it('uses subscriptionChange plan for future invoice when type is premium-subscription', function (ctx) { + const preview = ctx.SubscriptionController.makeChangePreview( + { + type: 'premium-subscription', + plan: { code: 'professional', name: 'Professional' }, + }, + subscriptionChange + ) + expect(preview.nextInvoice.plan.name).to.equal('Professional') + expect(preview.nextInvoice.plan.amount).to.equal(2000) + }) + + it('uses subscriptionChange plan for future invoice when type is group-plan-upgrade', function (ctx) { + const preview = ctx.SubscriptionController.makeChangePreview( + { type: 'group-plan-upgrade', prevPlan: { name: 'Standard' } }, + subscriptionChange + ) + expect(preview.nextInvoice.plan.name).to.equal('Professional') + expect(preview.nextInvoice.plan.amount).to.equal(2000) + }) + + it('uses pendingChange plan for future invoice when type is add-on-purchase', function (ctx) { + const preview = ctx.SubscriptionController.makeChangePreview( + { + type: 'add-on-purchase', + addOn: { code: 'ai-assist', name: 'AI Assist' }, + }, + subscriptionChange + ) + expect(preview.nextInvoice.plan.name).to.equal('Student') + expect(preview.nextInvoice.plan.amount).to.equal(1000) + }) + + it('uses subscriptionChange plan for future invoice when there is no pending change', function (ctx) { + baseSubscription.pendingChange = undefined + const preview = ctx.SubscriptionController.makeChangePreview( + { + type: 'premium-subscription', + plan: { code: 'professional', name: 'Professional' }, + }, + subscriptionChange + ) + expect(preview.nextInvoice.plan.name).to.equal('Professional') + expect(preview.nextInvoice.plan.amount).to.equal(2000) + }) + }) + describe('previewAddonPurchase', function () { beforeEach(function (ctx) { ctx.req = new MockRequest(vi)