فهرست منبع

[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
Antoine Clausse 3 ماه پیش
والد
کامیت
cff35c743f

+ 15 - 3
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 {

+ 82 - 0
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)