[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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
GitOrigin-RevId: cc2f9c88e5dfdfb89370798e857ea98caf8fcf85
This commit is contained in:
Antoine Clausse
2026-05-05 11:25:50 +02:00
committed by Copybot
parent f434b1fc28
commit cff35c743f
2 changed files with 97 additions and 3 deletions

View File

@@ -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 {

View File

@@ -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)