Просмотр исходного кода

Show Recurly's line items breakdown in subscription change preview (#27809)

* Show Recurly's line items breakdown in subscription change preview

* fix rounding, filter items that cancel each other out

GitOrigin-RevId: 0f5d71b3917ce8a52ff36608a6ec6280fe7d38ce
Domagoj Kriskovic 1 год назад
Родитель
Сommit
eac4a5cb13

+ 1 - 1
services/web/app/src/Features/Subscription/AiHelper.js

@@ -9,7 +9,7 @@ const AI_ADD_ON_CODE = 'assistant'
 /**
  * Returns whether the given plan code is a standalone AI plan
  *
- * @param {string} planCode
+ * @param {string | null | undefined} planCode
  * @return {boolean}
  */
 function isStandaloneAiAddOnPlanCode(planCode) {

+ 12 - 0
services/web/app/src/Features/Subscription/PaymentProviderEntities.js

@@ -6,6 +6,16 @@
  * @import { AddOn } from '../../../../types/subscription/plan'
  */
 
+/**
+ * @typedef {object} ImmediateChargeLineItem
+ * @property {string | null | undefined} planCode
+ * @property {string} description
+ * @property {number} subtotal
+ * @property {number} discount
+ * @property {number} tax
+ * @property {boolean} isAiAssist
+ */
+
 const OError = require('@overleaf/o-error')
 const { DuplicateAddOnError, AddOnNotPresentError } = require('./Errors')
 const PlansLocator = require('./PlansLocator')
@@ -539,12 +549,14 @@ class PaymentProviderImmediateCharge {
    * @param {number} props.tax
    * @param {number} props.total
    * @param {number} props.discount
+   * @param {ImmediateChargeLineItem[]} [props.lineItems]
    */
   constructor(props) {
     this.subtotal = props.subtotal
     this.tax = props.tax
     this.total = props.total
     this.discount = props.discount
+    this.lineItems = props.lineItems ?? []
   }
 }
 

+ 32 - 0
services/web/app/src/Features/Subscription/RecurlyClient.js

@@ -23,6 +23,7 @@ const {
   SubtotalLimitExceededError,
 } = require('./Errors')
 const RecurlyMetrics = require('./RecurlyMetrics')
+const { isStandaloneAiAddOnPlanCode, AI_ADD_ON_CODE } = require('./AiHelper')
 
 /**
  * @import { PaymentProviderSubscriptionChangeRequest } from './PaymentProviderEntities'
@@ -585,6 +586,23 @@ function computeImmediateCharge(subscriptionChange) {
   let total = subscriptionChange.invoiceCollection?.chargeInvoice?.total ?? 0
   let discount =
     subscriptionChange.invoiceCollection?.chargeInvoice?.discount ?? 0
+
+  const lineItems = []
+
+  for (const lineItem of subscriptionChange.invoiceCollection?.chargeInvoice
+    ?.lineItems || []) {
+    lineItems.push({
+      planCode: lineItem.planCode,
+      isAiAssist:
+        lineItem.addOnCode === AI_ADD_ON_CODE ||
+        isStandaloneAiAddOnPlanCode(lineItem.planCode),
+      description: lineItem.description ?? '',
+      subtotal: roundToTwoDecimal(lineItem.subtotal ?? 0),
+      discount: roundToTwoDecimal(lineItem.discount ?? 0),
+      tax: roundToTwoDecimal(lineItem.tax ?? 0),
+    })
+  }
+
   for (const creditInvoice of subscriptionChange.invoiceCollection
     ?.creditInvoices ?? []) {
     // The credit invoice numbers are already negative
@@ -593,12 +611,26 @@ function computeImmediateCharge(subscriptionChange) {
     // Tax rate can be different in credit invoice if a user relocates
     tax = roundToTwoDecimal(tax + (creditInvoice.tax ?? 0))
     discount = roundToTwoDecimal(discount + (creditInvoice.discount ?? 0))
+
+    for (const lineItem of creditInvoice.lineItems || []) {
+      lineItems.push({
+        planCode: lineItem.planCode,
+        isAiAssist:
+          lineItem.addOnCode === AI_ADD_ON_CODE ||
+          isStandaloneAiAddOnPlanCode(lineItem.planCode),
+        description: lineItem.description ?? '',
+        subtotal: roundToTwoDecimal(lineItem.subtotal ?? 0),
+        discount: roundToTwoDecimal(lineItem.discount ?? 0),
+        tax: roundToTwoDecimal(lineItem.tax ?? 0),
+      })
+    }
   }
   return new PaymentProviderImmediateCharge({
     subtotal,
     total,
     tax,
     discount,
+    lineItems,
   })
 }
 

+ 48 - 11
services/web/frontend/js/features/subscription/components/preview-subscription-change/root.tsx

@@ -33,6 +33,22 @@ function PreviewSubscriptionChange() {
   const location = useLocation()
   const aiAssistEnabled = useFeatureFlag('overleaf-assist-bundle')
 
+  // Filter out items that cancel each other out (AI assist items with subtotals that sum to 0)
+  const filteredLineItems = preview.immediateCharge.lineItems.filter(
+    (item, index, arr) => {
+      if (!item.isAiAssist) return true
+
+      const isCanceledByAnotherItem = arr.some(
+        (otherItem, otherIndex) =>
+          otherIndex !== index &&
+          otherItem.isAiAssist &&
+          otherItem.subtotal + item.subtotal === 0
+      )
+
+      return !isCanceledByAnotherItem
+    }
+  )
+
   useEffect(() => {
     if (preview.change.type === 'add-on-purchase') {
       eventTracking.sendMB('preview-subscription-change-view', {
@@ -139,17 +155,38 @@ function PreviewSubscriptionChange() {
 
             <OLCard className="payment-summary-card mt-5">
               <h3>{t('due_today')}:</h3>
-              <OLRow>
-                <OLCol xs={9}>{changeName}</OLCol>
-                <OLCol xs={3} className="text-end">
-                  <strong>
-                    {formatCurrency(
-                      preview.immediateCharge.subtotal,
-                      preview.currency
-                    )}
-                  </strong>
-                </OLCol>
-              </OLRow>
+              {filteredLineItems.length > 1 ? (
+                <>
+                  {filteredLineItems.map((item, index) => (
+                    <OLRow key={index}>
+                      <OLCol xs={9}>
+                        {item.subtotal < 0
+                          ? `Refund: ${item.description}`
+                          : item.description}
+                      </OLCol>
+                      <OLCol xs={3} className="text-end">
+                        <strong>
+                          {formatCurrency(item.subtotal, preview.currency)}
+                        </strong>
+                      </OLCol>
+                    </OLRow>
+                  ))}
+                </>
+              ) : (
+                <>
+                  <OLRow>
+                    <OLCol xs={9}>{changeName}</OLCol>
+                    <OLCol xs={3} className="text-end">
+                      <strong>
+                        {formatCurrency(
+                          preview.immediateCharge.subtotal,
+                          preview.currency
+                        )}
+                      </strong>
+                    </OLCol>
+                  </OLRow>
+                </>
+              )}
 
               {preview.immediateCharge.tax > 0 && (
                 <OLRow className="mt-1">

+ 8 - 0
services/web/types/subscription/subscription-change-preview.ts

@@ -11,6 +11,14 @@ export type SubscriptionChangePreview = {
     tax: number
     total: number
     discount: number
+    lineItems: {
+      planCode: string | null | undefined
+      description: string
+      subtotal: number
+      discount: number
+      tax: number
+      isAiAssist: boolean
+    }[]
   }
   nextInvoice: {
     date: string