From 1bc977b02b5a377d63ab0c4cf98ba2de2eb32bd8 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Wed, 19 Mar 2025 14:16:58 +0100 Subject: [PATCH 01/21] Update policyOwnerAmountOwedOverdue banner text --- src/languages/en.ts | 6 ++++-- src/languages/params.ts | 3 +++ src/pages/settings/Subscription/CardSection/utils.ts | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 9955d6957ea8..bb8c6c0eacb8 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -27,6 +27,7 @@ import type { BillingBannerCardOnDisputeParams, BillingBannerDisputePendingParams, BillingBannerInsufficientFundsParams, + BillingBannerOwnerAmountOwedOverdueParams, BillingBannerSubtitleWithDateParams, CanceledRequestParams, CardEndingParams, @@ -5597,8 +5598,9 @@ const translations = { subtitle: ({date}: BillingBannerSubtitleWithDateParams) => `Update your payment card by ${date} to continue using all of your favorite features.`, }, policyOwnerAmountOwedOverdue: { - title: 'Your payment info is outdated', - subtitle: 'Please update your payment information.', + title: 'No se pudo procesar tu pago', + subtitle: ({date, amountOwed}: BillingBannerOwnerAmountOwedOverdueParams) => + `No se ha podido procesar tu cargo de ${amountOwed} del día ${date}. Por favor, añade una tarjeta de pago para saldar la cantidad adeudada.`, }, policyOwnerUnderInvoicing: { title: 'Your payment info is outdated', diff --git a/src/languages/params.ts b/src/languages/params.ts index d1c6751e123c..1565dbbf369c 100644 --- a/src/languages/params.ts +++ b/src/languages/params.ts @@ -446,6 +446,8 @@ type BadgeFreeTrialParams = {numOfDays: number}; type BillingBannerSubtitleWithDateParams = {date: string}; +type BillingBannerOwnerAmountOwedOverdueParams = {date: string; amountOwed: number}; + type BillingBannerDisputePendingParams = {amountOwed: number; cardEnding: string}; type BillingBannerCardAuthenticationRequiredParams = {cardEnding: string}; @@ -699,6 +701,7 @@ export type { SubscriptionSettingsRenewsOnParams, BadgeFreeTrialParams, BillingBannerSubtitleWithDateParams, + BillingBannerOwnerAmountOwedOverdueParams, BillingBannerDisputePendingParams, BillingBannerCardAuthenticationRequiredParams, BillingBannerInsufficientFundsParams, diff --git a/src/pages/settings/Subscription/CardSection/utils.ts b/src/pages/settings/Subscription/CardSection/utils.ts index 3624241a7140..6af67fe766eb 100644 --- a/src/pages/settings/Subscription/CardSection/utils.ts +++ b/src/pages/settings/Subscription/CardSection/utils.ts @@ -44,7 +44,7 @@ function getBillingStatus(translate: LocaleContextProps['translate'], accountDat case PAYMENT_STATUS.POLICY_OWNER_WITH_AMOUNT_OWED_OVERDUE: return { title: translate('subscription.billingBanner.policyOwnerAmountOwedOverdue.title'), - subtitle: translate('subscription.billingBanner.policyOwnerAmountOwedOverdue.subtitle'), + subtitle: translate('subscription.billingBanner.policyOwnerAmountOwedOverdue.subtitle', {date: endDateFormatted ?? '', amountOwed}), isError: true, isRetryAvailable: true, }; From 196cf7019ebba72fd5381f570e048c3db916e48c Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Wed, 19 Mar 2025 15:16:35 +0100 Subject: [PATCH 02/21] Add PurchaseList type and related keys to ONYXKEYS --- src/ONYXKEYS.ts | 4 + src/types/onyx/PurchaseList.ts | 172 +++++++++++++++++++++++++++++++++ src/types/onyx/index.ts | 2 + 3 files changed, 178 insertions(+) create mode 100644 src/types/onyx/PurchaseList.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index db7b18d865e0..b62769e7ae6a 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -277,6 +277,9 @@ const ONYXKEYS = { /** Stores information about the user's saved statements */ WALLET_STATEMENT: 'walletStatement', + /** Stores information about the user's purchases */ + PURCHASE_LIST: 'purchaseList', + /** Stores information about the active personal bank account being set up */ PERSONAL_BANK_ACCOUNT: 'personalBankAccount', @@ -1010,6 +1013,7 @@ type OnyxValuesMapping = { [ONYXKEYS.FUND_LIST]: OnyxTypes.FundList; [ONYXKEYS.CARD_LIST]: OnyxTypes.CardList; [ONYXKEYS.WALLET_STATEMENT]: OnyxTypes.WalletStatement; + [ONYXKEYS.PURCHASE_LIST]: OnyxTypes.PurchaseList; [ONYXKEYS.PERSONAL_BANK_ACCOUNT]: OnyxTypes.PersonalBankAccount; [ONYXKEYS.REIMBURSEMENT_ACCOUNT]: OnyxTypes.ReimbursementAccount; [ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE]: string | number; diff --git a/src/types/onyx/PurchaseList.ts b/src/types/onyx/PurchaseList.ts new file mode 100644 index 000000000000..38f27905ba2e --- /dev/null +++ b/src/types/onyx/PurchaseList.ts @@ -0,0 +1,172 @@ +/** Subscription type for a purchase */ +type Subscription = { + /** Whether new users are added automatically */ + addNewUsersAutomatically: boolean; + + /** Whether the subscription auto-renews */ + autoRenew: boolean; + + /** Date when auto-renew was last changed */ + autoRenewLastChangedDate: string; + + /** End date of the subscription */ + endDate: string; + + /** Start date of the subscription */ + startDate: string; + + /** Type of subscription */ + type: string; + + /** Number of users in the subscription */ + userCount: number; +}; + +/** Message type for a purchase */ +type Message = { + /** Account manager account ID */ + accountManagerAccountID: number; + + /** List of approved accountant account IDs */ + approvedAccountantAccountIDs: number[]; + + /** Approved spend amounts by currency */ + approvedSpend: Record; + + /** Billable amount */ + billableAmount: number; + + /** Billable amount before free trial discount */ + billableAmountBeforeFreeTrialDiscount: number; + + /** List of billable policies */ + billablePolicies: string[]; + + /** Billing type */ + billingType: string; + + /** Card spend surcharge percentage */ + cardSpendSurchargePercent: number; + + /** Cash back amount */ + cashBackAmount: number; + + /** Cash back percentage */ + cashBackPercentage: number; + + /** Chat only actor list */ + chatOnlyActorList: string; + + /** Corporate actor count */ + corporateActorCount: number; + + /** Corporate revenue */ + corporateRevenue: number; + + /** Expensify card monthly spend */ + expensifyCardMonthlySpend: number; + + /** Expensify card spend by currency */ + expensifyCardSpend: Record; + + /** Free actor count */ + freeActorCount: number; + + /** Free trial days */ + freeTrialDays: number; + + /** Free trial discount amount */ + freeTrialDiscountAmount: number; + + /** Free trial discount percentage */ + freeTrialDiscountPercentage: number; + + /** Freebie credits used */ + freebieCreditsUsed: number; + + /** Guide account ID */ + guideAccountID: number; + + /** Whether the user is an approved accountant */ + isApprovedAccountant: boolean; + + /** Whether the user is an approved accountant client */ + isApprovedAccountantClient: boolean; + + /** Paid actor count */ + paidActorCount: number; + + /** Partner manager account ID */ + partnerManagerAccountID: number; + + /** Per policy total members count */ + perPolicyTotalMembersCount: Record; + + /** Potential cash back amount */ + potentialCashBackAmount: number; + + /** Potential cash back percentage */ + potentialCashBackPercentage: number; + + /** Subscription details */ + subscription: Subscription; + + /** Team actor count */ + teamActorCount: number; + + /** Team revenue */ + teamRevenue: number; + + /** Total actor count */ + totalActorCount: number; + + /** Total freebie credits */ + totalFreebieCredits: number; + + /** Total platform spend */ + totalPlatformSpend: number; + + /** Total revenue */ + totalRevenue: number; + + /** Total unique members count */ + totalUniqueMembersCount: number; + + /** Whether domain billing was used */ + wasDomainBillingUsed: boolean; + + /** Yearly overage surcharge */ + yearlyOverageSurcharge: number; + + /** Yearly subscription overage cost */ + yearlySubscriptionOverageCost: number; + + /** Yearly subscription surcharge */ + yearlySubscriptionSurcharge: number; + + /** Yearly subscription user count cost */ + yearlySubscriptionUserCountCost: number; +}; + +/** Purchase type */ +type Purchase = { + /** Amount of the purchase */ + amount: number; + + /** Creation date of the purchase */ + created: string; + + /** Currency of the purchase */ + currency: string; + + /** Message containing purchase details */ + message: Message; + + /** ID of the purchase */ + purchaseID: number; +}; + +/** Record of purchases, indexed by purchase ID */ +type PurchaseList = Purchase[]; + +export default PurchaseList; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 46d6a2108fc7..e477989c489b 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -115,6 +115,7 @@ import type WalletOnfido from './WalletOnfido'; import type WalletStatement from './WalletStatement'; import type WalletTerms from './WalletTerms'; import type WalletTransfer from './WalletTransfer'; +import type PurchaseList from './PurchaseList'; export type { TryNewDot, @@ -218,6 +219,7 @@ export type { WalletStatement, WalletTerms, WalletTransfer, + PurchaseList, ReportUserIsTyping, PolicyReportField, RecentlyUsedReportFields, From 8252fec37d1ef703c47ae4c9ca749c207897627d Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Fri, 21 Mar 2025 13:14:10 +0100 Subject: [PATCH 03/21] Update payment processing messages in English and Spanish translations --- src/languages/en.ts | 5 +++-- src/languages/es.ts | 7 +++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index bb8c6c0eacb8..87d415c25a6c 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -5598,9 +5598,10 @@ const translations = { subtitle: ({date}: BillingBannerSubtitleWithDateParams) => `Update your payment card by ${date} to continue using all of your favorite features.`, }, policyOwnerAmountOwedOverdue: { - title: 'No se pudo procesar tu pago', + title: 'Your payment could not be processed', + generalSubtitle: 'Please add a payment card to clear the amount owed.', subtitle: ({date, amountOwed}: BillingBannerOwnerAmountOwedOverdueParams) => - `No se ha podido procesar tu cargo de ${amountOwed} del día ${date}. Por favor, añade una tarjeta de pago para saldar la cantidad adeudada.`, + `Your ${date} charge of ${amountOwed} could not be processed. Please add a payment card to clear the amount owed.`, }, policyOwnerUnderInvoicing: { title: 'Your payment info is outdated', diff --git a/src/languages/es.ts b/src/languages/es.ts index 6f8d22970367..de71714813a2 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -27,6 +27,7 @@ import type { BillingBannerDisputePendingParams, BillingBannerInsufficientFundsParams, BillingBannerSubtitleWithDateParams, + BillingBannerOwnerAmountOwedOverdueParams, CanceledRequestParams, CardEndingParams, CardInfoParams, @@ -6113,8 +6114,10 @@ const translations = { subtitle: ({date}: BillingBannerSubtitleWithDateParams) => `Actualiza tu tarjeta de pago antes del ${date} para continuar utilizando todas tus herramientas favoritas`, }, policyOwnerAmountOwedOverdue: { - title: 'Tu información de pago está desactualizada', - subtitle: 'Por favor, actualiza tu información de pago.', + title: 'No se pudo procesar tu pago', + generalSubtitle: 'Por favor, añade una tarjeta de pago para saldar la cantidad adeudada.', + subtitle: ({date, amountOwed}: BillingBannerOwnerAmountOwedOverdueParams) => + `No se ha podido procesar tu cargo de ${amountOwed} del día ${date}. Por favor, añade una tarjeta de pago para saldar la cantidad adeudada.`, }, policyOwnerUnderInvoicing: { title: 'Tu información de pago está desactualizada', From dbaebec3f59da8cb1f1657740fdc78a9bf72eaf2 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Fri, 21 Mar 2025 13:20:13 +0100 Subject: [PATCH 04/21] Update billing banner subtitle parameter from amountOwed to purchaseAmountOwed in English and Spanish translations --- src/languages/en.ts | 4 ++-- src/languages/es.ts | 4 ++-- src/languages/params.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 87d415c25a6c..b7b01134b197 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -5600,8 +5600,8 @@ const translations = { policyOwnerAmountOwedOverdue: { title: 'Your payment could not be processed', generalSubtitle: 'Please add a payment card to clear the amount owed.', - subtitle: ({date, amountOwed}: BillingBannerOwnerAmountOwedOverdueParams) => - `Your ${date} charge of ${amountOwed} could not be processed. Please add a payment card to clear the amount owed.`, + subtitle: ({date, purchaseAmountOwed}: BillingBannerOwnerAmountOwedOverdueParams) => + `Your ${date} charge of ${purchaseAmountOwed} could not be processed. Please add a payment card to clear the amount owed.`, }, policyOwnerUnderInvoicing: { title: 'Your payment info is outdated', diff --git a/src/languages/es.ts b/src/languages/es.ts index de71714813a2..247e40e09aa6 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -6116,8 +6116,8 @@ const translations = { policyOwnerAmountOwedOverdue: { title: 'No se pudo procesar tu pago', generalSubtitle: 'Por favor, añade una tarjeta de pago para saldar la cantidad adeudada.', - subtitle: ({date, amountOwed}: BillingBannerOwnerAmountOwedOverdueParams) => - `No se ha podido procesar tu cargo de ${amountOwed} del día ${date}. Por favor, añade una tarjeta de pago para saldar la cantidad adeudada.`, + subtitle: ({date, purchaseAmountOwed}: BillingBannerOwnerAmountOwedOverdueParams) => + `No se ha podido procesar tu cargo de ${purchaseAmountOwed} del día ${date}. Por favor, añade una tarjeta de pago para saldar la cantidad adeudada.`, }, policyOwnerUnderInvoicing: { title: 'Tu información de pago está desactualizada', diff --git a/src/languages/params.ts b/src/languages/params.ts index 1565dbbf369c..f006cc5b3070 100644 --- a/src/languages/params.ts +++ b/src/languages/params.ts @@ -446,7 +446,7 @@ type BadgeFreeTrialParams = {numOfDays: number}; type BillingBannerSubtitleWithDateParams = {date: string}; -type BillingBannerOwnerAmountOwedOverdueParams = {date: string; amountOwed: number}; +type BillingBannerOwnerAmountOwedOverdueParams = {date: string; purchaseAmountOwed: string}; type BillingBannerDisputePendingParams = {amountOwed: number; cardEnding: string}; From 65118177e96d15dad8fec7dd28b6ef33db817d4b Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Fri, 21 Mar 2025 13:24:48 +0100 Subject: [PATCH 05/21] Add purchaseList to CardSection and update billing status logic --- .../Subscription/CardSection/CardSection.tsx | 21 ++++++++++-- .../Subscription/CardSection/utils.ts | 34 +++++++++++++++++-- src/types/onyx/PurchaseList.ts | 2 ++ 3 files changed, 52 insertions(+), 5 deletions(-) diff --git a/src/pages/settings/Subscription/CardSection/CardSection.tsx b/src/pages/settings/Subscription/CardSection/CardSection.tsx index 74e7301bfa29..8a25f5bd5f54 100644 --- a/src/pages/settings/Subscription/CardSection/CardSection.tsx +++ b/src/pages/settings/Subscription/CardSection/CardSection.tsx @@ -47,6 +47,7 @@ function CardSection() { const [authenticationLink] = useOnyx(ONYXKEYS.VERIFY_3DS_SUBSCRIPTION); const [session] = useOnyx(ONYXKEYS.SESSION); const [fundList] = useOnyx(ONYXKEYS.FUND_LIST); + const [purchaseList] = useOnyx(ONYXKEYS.PURCHASE_LIST); const subscriptionPlan = useSubscriptionPlan(); const [subscriptionRetryBillingStatusPending] = useOnyx(ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_PENDING); const [subscriptionRetryBillingStatusSuccessful] = useOnyx(ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_SUCCESSFUL); @@ -66,15 +67,29 @@ function CardSection() { Navigation.navigate(ROUTES.SEARCH_ROOT.getRoute({query})); }, []); - const [billingStatus, setBillingStatus] = useState(() => CardSectionUtils.getBillingStatus(translate, defaultCard?.accountData ?? {})); + const [billingStatus, setBillingStatus] = useState(() => CardSectionUtils.getBillingStatus({translate, accountData: defaultCard?.accountData ?? {}})); const nextPaymentDate = !isEmptyObject(privateSubscription) ? CardSectionUtils.getNextBillingDate() : undefined; const sectionSubtitle = defaultCard && !!nextPaymentDate ? translate('subscription.cardSection.cardNextPayment', {nextPaymentDate}) : translate('subscription.cardSection.subtitle'); useEffect(() => { - setBillingStatus(CardSectionUtils.getBillingStatus(translate, defaultCard?.accountData ?? {})); - }, [subscriptionRetryBillingStatusPending, subscriptionRetryBillingStatusSuccessful, subscriptionRetryBillingStatusFailed, translate, defaultCard?.accountData, privateStripeCustomerID]); + setBillingStatus( + CardSectionUtils.getBillingStatus({ + translate, + accountData: defaultCard?.accountData ?? {}, + purchase: purchaseList?.[0], + }), + ); + }, [ + subscriptionRetryBillingStatusPending, + subscriptionRetryBillingStatusSuccessful, + subscriptionRetryBillingStatusFailed, + translate, + defaultCard?.accountData, + privateStripeCustomerID, + purchaseList, + ]); const handleRetryPayment = () => { clearOutstandingBalance(); diff --git a/src/pages/settings/Subscription/CardSection/utils.ts b/src/pages/settings/Subscription/CardSection/utils.ts index 6af67fe766eb..69f269a50f5c 100644 --- a/src/pages/settings/Subscription/CardSection/utils.ts +++ b/src/pages/settings/Subscription/CardSection/utils.ts @@ -2,10 +2,12 @@ import {addMonths, format, fromUnixTime, startOfMonth} from 'date-fns'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; import type {LocaleContextProps} from '@components/LocaleContextProvider'; +import * as CurrencyUtils from '@libs/CurrencyUtils'; import DateUtils from '@libs/DateUtils'; import {getAmountOwed, getOverdueGracePeriodDate, getSubscriptionStatus, PAYMENT_STATUS} from '@libs/SubscriptionUtils'; import CONST from '@src/CONST'; import type {AccountData} from '@src/types/onyx/Fund'; +import type {Purchase} from '@src/types/onyx/PurchaseList'; import type IconAsset from '@src/types/utils/IconAsset'; type BillingStatusResult = { @@ -19,7 +21,13 @@ type BillingStatusResult = { rightIcon?: IconAsset; }; -function getBillingStatus(translate: LocaleContextProps['translate'], accountData?: AccountData): BillingStatusResult | undefined { +type GetBillingStatusProps = { + translate: LocaleContextProps['translate']; + accountData?: AccountData; + purchase?: Purchase; +}; + +function getBillingStatus({translate, accountData, purchase}: GetBillingStatusProps): BillingStatusResult | undefined { const cardEnding = (accountData?.cardNumber ?? '')?.slice(-4); const amountOwed = getAmountOwed(); @@ -31,6 +39,7 @@ function getBillingStatus(translate: LocaleContextProps['translate'], accountDat const endDateFormatted = endDate ? DateUtils.formatWithUTCTimeZone(fromUnixTime(endDate).toUTCString(), CONST.DATE.MONTH_DAY_YEAR_FORMAT) : null; const isCurrentCardExpired = DateUtils.isCardExpired(accountData?.cardMonth ?? 0, accountData?.cardYear ?? 0); + const {purchaseAmountWithCurrency, isBillingTypeProper, purchaseDateFormatted, isPurchase} = getPurchaseDetails(purchase); switch (subscriptionStatus?.status) { case PAYMENT_STATUS.POLICY_OWNER_WITH_AMOUNT_OWED: @@ -44,7 +53,10 @@ function getBillingStatus(translate: LocaleContextProps['translate'], accountDat case PAYMENT_STATUS.POLICY_OWNER_WITH_AMOUNT_OWED_OVERDUE: return { title: translate('subscription.billingBanner.policyOwnerAmountOwedOverdue.title'), - subtitle: translate('subscription.billingBanner.policyOwnerAmountOwedOverdue.subtitle', {date: endDateFormatted ?? '', amountOwed}), + subtitle: + isBillingTypeProper && isPurchase + ? translate('subscription.billingBanner.policyOwnerAmountOwedOverdue.subtitle', {date: purchaseDateFormatted ?? '', purchaseAmountOwed: purchaseAmountWithCurrency}) + : translate('subscription.billingBanner.policyOwnerAmountOwedOverdue.generalSubtitle'), isError: true, isRetryAvailable: true, }; @@ -139,5 +151,23 @@ function getNextBillingDate(): string { return format(nextBillingDate, CONST.DATE.MONTH_DAY_YEAR_FORMAT); } +function getPurchaseDetails(purchase?: Purchase) { + const purchaseAmount = purchase?.message.billableAmount; + const purchaseCurrency = purchase?.currency; + const isBillingTypeProper = purchase?.message.billingType === 'failed_2018'; + const purchaseCurrencySymbol = CurrencyUtils.getLocalizedCurrencySymbol(purchaseCurrency ?? CONST.CURRENCY.USD); + const purchaseDate = purchase?.created; + const purchaseDateFormatted = purchaseDate ? DateUtils.formatWithUTCTimeZone(purchaseDate, CONST.DATE.MONTH_DAY_YEAR_FORMAT) : null; + + const formattedAmount = CurrencyUtils.convertToFrontendAmountAsString(purchaseAmount, purchaseCurrency); + + return { + purchaseAmountWithCurrency: purchaseCurrencySymbol + formattedAmount, + isBillingTypeProper, + purchaseDateFormatted, + isPurchase: !!purchase, + }; +} + export default {getBillingStatus, getNextBillingDate}; export type {BillingStatusResult}; diff --git a/src/types/onyx/PurchaseList.ts b/src/types/onyx/PurchaseList.ts index 38f27905ba2e..0cde04538901 100644 --- a/src/types/onyx/PurchaseList.ts +++ b/src/types/onyx/PurchaseList.ts @@ -170,3 +170,5 @@ type Purchase = { type PurchaseList = Purchase[]; export default PurchaseList; + +export type {Purchase}; From 243fe04f75d1f6333d73af5e178b70902d135c85 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Mon, 24 Mar 2025 13:02:41 +0100 Subject: [PATCH 06/21] Refactor getBillingStatus tests to use a single object parameter --- tests/unit/CardsSectionUtilsTest.ts | 39 +++++++++++++++++++---------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/tests/unit/CardsSectionUtilsTest.ts b/tests/unit/CardsSectionUtilsTest.ts index 5293d385ddbb..98c3ba91146e 100644 --- a/tests/unit/CardsSectionUtilsTest.ts +++ b/tests/unit/CardsSectionUtilsTest.ts @@ -84,7 +84,7 @@ describe('CardSectionUtils', () => { }); it('should return undefined by default', () => { - expect(CardSectionUtils.getBillingStatus(translateMock, ACCOUNT_DATA)).toBeUndefined(); + expect(CardSectionUtils.getBillingStatus({translate: translateMock, accountData: ACCOUNT_DATA})).toBeUndefined(); }); it('should return POLICY_OWNER_WITH_AMOUNT_OWED variant', () => { @@ -92,7 +92,7 @@ describe('CardSectionUtils', () => { status: PAYMENT_STATUS.POLICY_OWNER_WITH_AMOUNT_OWED, }); - expect(CardSectionUtils.getBillingStatus(translateMock, ACCOUNT_DATA)).toEqual({ + expect(CardSectionUtils.getBillingStatus({translate: translateMock, accountData: ACCOUNT_DATA})).toEqual({ title: 'subscription.billingBanner.policyOwnerAmountOwed.title', subtitle: 'subscription.billingBanner.policyOwnerAmountOwed.subtitle', isError: true, @@ -105,7 +105,7 @@ describe('CardSectionUtils', () => { status: PAYMENT_STATUS.POLICY_OWNER_WITH_AMOUNT_OWED_OVERDUE, }); - expect(CardSectionUtils.getBillingStatus(translateMock, ACCOUNT_DATA)).toEqual({ + expect(CardSectionUtils.getBillingStatus({translate: translateMock, accountData: ACCOUNT_DATA})).toEqual({ title: 'subscription.billingBanner.policyOwnerAmountOwedOverdue.title', subtitle: 'subscription.billingBanner.policyOwnerAmountOwedOverdue.subtitle', isError: true, @@ -113,12 +113,25 @@ describe('CardSectionUtils', () => { }); }); + it('should return POLICY_OWNER_WITH_AMOUNT_OWED_OVERDUE variant with generalSubtitle', () => { + mockGetSubscriptionStatus.mockReturnValue({ + status: PAYMENT_STATUS.POLICY_OWNER_WITH_AMOUNT_OWED_OVERDUE, + }); + + expect(CardSectionUtils.getBillingStatus({translate: translateMock, accountData: ACCOUNT_DATA, purchase: undefined})).toEqual({ + title: 'subscription.billingBanner.policyOwnerAmountOwedOverdue.title', + subtitle: 'subscription.billingBanner.policyOwnerAmountOwedOverdue.generalSubtitle', + isError: true, + isRetryAvailable: true, + }); + }); + it('should return OWNER_OF_POLICY_UNDER_INVOICING variant', () => { mockGetSubscriptionStatus.mockReturnValue({ status: PAYMENT_STATUS.OWNER_OF_POLICY_UNDER_INVOICING, }); - expect(CardSectionUtils.getBillingStatus(translateMock, ACCOUNT_DATA)).toEqual({ + expect(CardSectionUtils.getBillingStatus({translate: translateMock, accountData: ACCOUNT_DATA})).toEqual({ title: 'subscription.billingBanner.policyOwnerUnderInvoicing.title', subtitle: 'subscription.billingBanner.policyOwnerUnderInvoicing.subtitle', isError: true, @@ -131,7 +144,7 @@ describe('CardSectionUtils', () => { status: PAYMENT_STATUS.OWNER_OF_POLICY_UNDER_INVOICING_OVERDUE, }); - expect(CardSectionUtils.getBillingStatus(translateMock, ACCOUNT_DATA)).toEqual({ + expect(CardSectionUtils.getBillingStatus({translate: translateMock, accountData: ACCOUNT_DATA})).toEqual({ title: 'subscription.billingBanner.policyOwnerUnderInvoicingOverdue.title', subtitle: 'subscription.billingBanner.policyOwnerUnderInvoicingOverdue.subtitle', isError: true, @@ -144,7 +157,7 @@ describe('CardSectionUtils', () => { status: PAYMENT_STATUS.BILLING_DISPUTE_PENDING, }); - expect(CardSectionUtils.getBillingStatus(translateMock, ACCOUNT_DATA)).toEqual({ + expect(CardSectionUtils.getBillingStatus({translate: translateMock, accountData: ACCOUNT_DATA})).toEqual({ title: 'subscription.billingBanner.billingDisputePending.title', subtitle: 'subscription.billingBanner.billingDisputePending.subtitle', isError: true, @@ -157,7 +170,7 @@ describe('CardSectionUtils', () => { status: PAYMENT_STATUS.CARD_AUTHENTICATION_REQUIRED, }); - expect(CardSectionUtils.getBillingStatus(translateMock, ACCOUNT_DATA)).toEqual({ + expect(CardSectionUtils.getBillingStatus({translate: translateMock, accountData: ACCOUNT_DATA})).toEqual({ title: 'subscription.billingBanner.cardAuthenticationRequired.title', subtitle: 'subscription.billingBanner.cardAuthenticationRequired.subtitle', isError: true, @@ -170,7 +183,7 @@ describe('CardSectionUtils', () => { status: PAYMENT_STATUS.INSUFFICIENT_FUNDS, }); - expect(CardSectionUtils.getBillingStatus(translateMock, ACCOUNT_DATA)).toEqual({ + expect(CardSectionUtils.getBillingStatus({translate: translateMock, accountData: ACCOUNT_DATA})).toEqual({ title: 'subscription.billingBanner.insufficientFunds.title', subtitle: 'subscription.billingBanner.insufficientFunds.subtitle', isError: true, @@ -183,14 +196,14 @@ describe('CardSectionUtils', () => { status: PAYMENT_STATUS.CARD_EXPIRED, }); - expect(CardSectionUtils.getBillingStatus(translateMock, {...ACCOUNT_DATA, cardYear: 2023})).toEqual({ + expect(CardSectionUtils.getBillingStatus({translate: translateMock, accountData: {...ACCOUNT_DATA, cardYear: 2023}})).toEqual({ title: 'subscription.billingBanner.cardExpired.title', subtitle: 'subscription.billingBanner.cardExpired.subtitle', isError: true, isRetryAvailable: false, }); - expect(CardSectionUtils.getBillingStatus(translateMock, ACCOUNT_DATA)).toEqual({ + expect(CardSectionUtils.getBillingStatus({translate: translateMock, accountData: ACCOUNT_DATA})).toEqual({ title: 'subscription.billingBanner.cardExpired.title', subtitle: 'subscription.billingBanner.cardExpired.subtitle', isError: true, @@ -203,7 +216,7 @@ describe('CardSectionUtils', () => { status: PAYMENT_STATUS.CARD_EXPIRE_SOON, }); - expect(CardSectionUtils.getBillingStatus(translateMock, ACCOUNT_DATA)).toEqual({ + expect(CardSectionUtils.getBillingStatus({translate: translateMock, accountData: ACCOUNT_DATA})).toEqual({ title: 'subscription.billingBanner.cardExpireSoon.title', subtitle: 'subscription.billingBanner.cardExpireSoon.subtitle', isError: false, @@ -216,7 +229,7 @@ describe('CardSectionUtils', () => { status: PAYMENT_STATUS.RETRY_BILLING_SUCCESS, }); - expect(CardSectionUtils.getBillingStatus(translateMock, ACCOUNT_DATA)).toEqual({ + expect(CardSectionUtils.getBillingStatus({translate: translateMock, accountData: ACCOUNT_DATA})).toEqual({ title: 'subscription.billingBanner.retryBillingSuccess.title', subtitle: 'subscription.billingBanner.retryBillingSuccess.subtitle', isError: false, @@ -229,7 +242,7 @@ describe('CardSectionUtils', () => { status: PAYMENT_STATUS.RETRY_BILLING_ERROR, }); - expect(CardSectionUtils.getBillingStatus(translateMock, ACCOUNT_DATA)).toEqual({ + expect(CardSectionUtils.getBillingStatus({translate: translateMock, accountData: ACCOUNT_DATA})).toEqual({ title: 'subscription.billingBanner.retryBillingError.title', subtitle: 'subscription.billingBanner.retryBillingError.subtitle', isError: true, From 2005b3bd4a89d8eca5b79414ba998689f7e9915c Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Mon, 24 Mar 2025 13:08:41 +0100 Subject: [PATCH 07/21] Add mockPurchase object to getBillingStatus test for improved coverage --- tests/unit/CardsSectionUtilsTest.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/unit/CardsSectionUtilsTest.ts b/tests/unit/CardsSectionUtilsTest.ts index 98c3ba91146e..604a7d91e796 100644 --- a/tests/unit/CardsSectionUtilsTest.ts +++ b/tests/unit/CardsSectionUtilsTest.ts @@ -6,6 +6,7 @@ import {PAYMENT_STATUS} from '@libs/SubscriptionUtils'; import type {TranslationParameters, TranslationPaths} from '@src/languages/types'; import type {BillingStatusResult} from '@src/pages/settings/Subscription/CardSection/utils'; import CardSectionUtils from '@src/pages/settings/Subscription/CardSection/utils'; +import type {Purchase} from '@src/types/onyx/PurchaseList'; // eslint-disable-next-line @typescript-eslint/no-unused-vars -- this param is required for the mock function translateMock(path: TPath, ...phraseParameters: TranslationParameters): string { @@ -105,7 +106,18 @@ describe('CardSectionUtils', () => { status: PAYMENT_STATUS.POLICY_OWNER_WITH_AMOUNT_OWED_OVERDUE, }); - expect(CardSectionUtils.getBillingStatus({translate: translateMock, accountData: ACCOUNT_DATA})).toEqual({ + const mockPurchase = { + message: { + billingType: 'failed_2018', + billableAmount: 1000, + }, + currency: 'USD', + created: '2023-01-01', + amount: 1000, + purchaseID: 12345, + } as Purchase; + + expect(CardSectionUtils.getBillingStatus({translate: translateMock, accountData: ACCOUNT_DATA, purchase: mockPurchase})).toEqual({ title: 'subscription.billingBanner.policyOwnerAmountOwedOverdue.title', subtitle: 'subscription.billingBanner.policyOwnerAmountOwedOverdue.subtitle', isError: true, From 1675fdbe73b3c8f67e7d04f2920f6c7755d69c96 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Mon, 24 Mar 2025 13:09:30 +0100 Subject: [PATCH 08/21] Refactor currency utility imports in Subscription CardSection --- src/pages/settings/Subscription/CardSection/utils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/settings/Subscription/CardSection/utils.ts b/src/pages/settings/Subscription/CardSection/utils.ts index 69f269a50f5c..d9ba14185d94 100644 --- a/src/pages/settings/Subscription/CardSection/utils.ts +++ b/src/pages/settings/Subscription/CardSection/utils.ts @@ -2,7 +2,7 @@ import {addMonths, format, fromUnixTime, startOfMonth} from 'date-fns'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; import type {LocaleContextProps} from '@components/LocaleContextProvider'; -import * as CurrencyUtils from '@libs/CurrencyUtils'; +import {getLocalizedCurrencySymbol, convertToFrontendAmountAsString} from '@libs/CurrencyUtils'; import DateUtils from '@libs/DateUtils'; import {getAmountOwed, getOverdueGracePeriodDate, getSubscriptionStatus, PAYMENT_STATUS} from '@libs/SubscriptionUtils'; import CONST from '@src/CONST'; @@ -155,11 +155,11 @@ function getPurchaseDetails(purchase?: Purchase) { const purchaseAmount = purchase?.message.billableAmount; const purchaseCurrency = purchase?.currency; const isBillingTypeProper = purchase?.message.billingType === 'failed_2018'; - const purchaseCurrencySymbol = CurrencyUtils.getLocalizedCurrencySymbol(purchaseCurrency ?? CONST.CURRENCY.USD); + const purchaseCurrencySymbol = getLocalizedCurrencySymbol(purchaseCurrency ?? CONST.CURRENCY.USD); const purchaseDate = purchase?.created; const purchaseDateFormatted = purchaseDate ? DateUtils.formatWithUTCTimeZone(purchaseDate, CONST.DATE.MONTH_DAY_YEAR_FORMAT) : null; - const formattedAmount = CurrencyUtils.convertToFrontendAmountAsString(purchaseAmount, purchaseCurrency); + const formattedAmount = convertToFrontendAmountAsString(purchaseAmount, purchaseCurrency); return { purchaseAmountWithCurrency: purchaseCurrencySymbol + formattedAmount, From 4d04b100e3612aebe5b143c983ee4c952d568f1c Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Mon, 24 Mar 2025 13:11:22 +0100 Subject: [PATCH 09/21] Fix prettier --- src/languages/es.ts | 2 +- src/pages/settings/Subscription/CardSection/utils.ts | 2 +- src/types/onyx/index.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 8fef7b52a341..b844ae24009e 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -26,8 +26,8 @@ import type { BillingBannerCardOnDisputeParams, BillingBannerDisputePendingParams, BillingBannerInsufficientFundsParams, - BillingBannerSubtitleWithDateParams, BillingBannerOwnerAmountOwedOverdueParams, + BillingBannerSubtitleWithDateParams, CanceledRequestParams, CardEndingParams, CardInfoParams, diff --git a/src/pages/settings/Subscription/CardSection/utils.ts b/src/pages/settings/Subscription/CardSection/utils.ts index d9ba14185d94..ee161828d313 100644 --- a/src/pages/settings/Subscription/CardSection/utils.ts +++ b/src/pages/settings/Subscription/CardSection/utils.ts @@ -2,7 +2,7 @@ import {addMonths, format, fromUnixTime, startOfMonth} from 'date-fns'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; import type {LocaleContextProps} from '@components/LocaleContextProvider'; -import {getLocalizedCurrencySymbol, convertToFrontendAmountAsString} from '@libs/CurrencyUtils'; +import {convertToFrontendAmountAsString, getLocalizedCurrencySymbol} from '@libs/CurrencyUtils'; import DateUtils from '@libs/DateUtils'; import {getAmountOwed, getOverdueGracePeriodDate, getSubscriptionStatus, PAYMENT_STATUS} from '@libs/SubscriptionUtils'; import CONST from '@src/CONST'; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index e477989c489b..c43bb4216d5b 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -67,6 +67,7 @@ import type PreferredTheme from './PreferredTheme'; import type PriorityMode from './PriorityMode'; import type PrivatePersonalDetails from './PrivatePersonalDetails'; import type PrivateSubscription from './PrivateSubscription'; +import type PurchaseList from './PurchaseList'; import type QuickAction from './QuickAction'; import type RecentlyUsedCategories from './RecentlyUsedCategories'; import type RecentlyUsedReportFields from './RecentlyUsedReportFields'; @@ -115,7 +116,6 @@ import type WalletOnfido from './WalletOnfido'; import type WalletStatement from './WalletStatement'; import type WalletTerms from './WalletTerms'; import type WalletTransfer from './WalletTransfer'; -import type PurchaseList from './PurchaseList'; export type { TryNewDot, From 6544c0e9e38b2f7685ba2cbc35d6aa6261bc8079 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Fri, 28 Mar 2025 16:14:57 +0100 Subject: [PATCH 10/21] Update billing status handling and refactor currency formatting in CardSection --- .../settings/Subscription/CardSection/CardSection.tsx | 4 +++- src/pages/settings/Subscription/CardSection/utils.ts | 7 +++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/pages/settings/Subscription/CardSection/CardSection.tsx b/src/pages/settings/Subscription/CardSection/CardSection.tsx index 8a25f5bd5f54..ca8028ff42ed 100644 --- a/src/pages/settings/Subscription/CardSection/CardSection.tsx +++ b/src/pages/settings/Subscription/CardSection/CardSection.tsx @@ -67,7 +67,9 @@ function CardSection() { Navigation.navigate(ROUTES.SEARCH_ROOT.getRoute({query})); }, []); - const [billingStatus, setBillingStatus] = useState(() => CardSectionUtils.getBillingStatus({translate, accountData: defaultCard?.accountData ?? {}})); + const [billingStatus, setBillingStatus] = useState(() => + CardSectionUtils.getBillingStatus({translate, accountData: defaultCard?.accountData ?? {}, purchase: purchaseList?.[0]}), + ); const nextPaymentDate = !isEmptyObject(privateSubscription) ? CardSectionUtils.getNextBillingDate() : undefined; diff --git a/src/pages/settings/Subscription/CardSection/utils.ts b/src/pages/settings/Subscription/CardSection/utils.ts index ee161828d313..e55aaf86b091 100644 --- a/src/pages/settings/Subscription/CardSection/utils.ts +++ b/src/pages/settings/Subscription/CardSection/utils.ts @@ -2,7 +2,7 @@ import {addMonths, format, fromUnixTime, startOfMonth} from 'date-fns'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; import type {LocaleContextProps} from '@components/LocaleContextProvider'; -import {convertToFrontendAmountAsString, getLocalizedCurrencySymbol} from '@libs/CurrencyUtils'; +import {convertAmountToDisplayString} from '@libs/CurrencyUtils'; import DateUtils from '@libs/DateUtils'; import {getAmountOwed, getOverdueGracePeriodDate, getSubscriptionStatus, PAYMENT_STATUS} from '@libs/SubscriptionUtils'; import CONST from '@src/CONST'; @@ -155,14 +155,13 @@ function getPurchaseDetails(purchase?: Purchase) { const purchaseAmount = purchase?.message.billableAmount; const purchaseCurrency = purchase?.currency; const isBillingTypeProper = purchase?.message.billingType === 'failed_2018'; - const purchaseCurrencySymbol = getLocalizedCurrencySymbol(purchaseCurrency ?? CONST.CURRENCY.USD); const purchaseDate = purchase?.created; const purchaseDateFormatted = purchaseDate ? DateUtils.formatWithUTCTimeZone(purchaseDate, CONST.DATE.MONTH_DAY_YEAR_FORMAT) : null; - const formattedAmount = convertToFrontendAmountAsString(purchaseAmount, purchaseCurrency); + const formattedAmount = convertAmountToDisplayString(purchaseAmount, purchaseCurrency); return { - purchaseAmountWithCurrency: purchaseCurrencySymbol + formattedAmount, + purchaseAmountWithCurrency: formattedAmount, isBillingTypeProper, purchaseDateFormatted, isPurchase: !!purchase, From 669c151ebed4a8a16c93991ad2c28beb077c0d86 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Fri, 28 Mar 2025 16:19:01 +0100 Subject: [PATCH 11/21] Refactor getBillingStatus to inline purchase details extraction and remove getPurchaseDetails function --- .../Subscription/CardSection/utils.ts | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/src/pages/settings/Subscription/CardSection/utils.ts b/src/pages/settings/Subscription/CardSection/utils.ts index e55aaf86b091..9358f2d38456 100644 --- a/src/pages/settings/Subscription/CardSection/utils.ts +++ b/src/pages/settings/Subscription/CardSection/utils.ts @@ -39,7 +39,14 @@ function getBillingStatus({translate, accountData, purchase}: GetBillingStatusPr const endDateFormatted = endDate ? DateUtils.formatWithUTCTimeZone(fromUnixTime(endDate).toUTCString(), CONST.DATE.MONTH_DAY_YEAR_FORMAT) : null; const isCurrentCardExpired = DateUtils.isCardExpired(accountData?.cardMonth ?? 0, accountData?.cardYear ?? 0); - const {purchaseAmountWithCurrency, isBillingTypeProper, purchaseDateFormatted, isPurchase} = getPurchaseDetails(purchase); + + const purchaseAmount = purchase?.message.billableAmount; + const purchaseCurrency = purchase?.currency; + const isBillingTypeProper = purchase?.message.billingType === 'failed_2018'; + const purchaseDate = purchase?.created; + const purchaseDateFormatted = purchaseDate ? DateUtils.formatWithUTCTimeZone(purchaseDate, CONST.DATE.MONTH_DAY_YEAR_FORMAT) : null; + const purchaseAmountWithCurrency = convertAmountToDisplayString(purchaseAmount, purchaseCurrency); + const isPurchase = !!purchase; switch (subscriptionStatus?.status) { case PAYMENT_STATUS.POLICY_OWNER_WITH_AMOUNT_OWED: @@ -151,22 +158,5 @@ function getNextBillingDate(): string { return format(nextBillingDate, CONST.DATE.MONTH_DAY_YEAR_FORMAT); } -function getPurchaseDetails(purchase?: Purchase) { - const purchaseAmount = purchase?.message.billableAmount; - const purchaseCurrency = purchase?.currency; - const isBillingTypeProper = purchase?.message.billingType === 'failed_2018'; - const purchaseDate = purchase?.created; - const purchaseDateFormatted = purchaseDate ? DateUtils.formatWithUTCTimeZone(purchaseDate, CONST.DATE.MONTH_DAY_YEAR_FORMAT) : null; - - const formattedAmount = convertAmountToDisplayString(purchaseAmount, purchaseCurrency); - - return { - purchaseAmountWithCurrency: formattedAmount, - isBillingTypeProper, - purchaseDateFormatted, - isPurchase: !!purchase, - }; -} - export default {getBillingStatus, getNextBillingDate}; export type {BillingStatusResult}; From c7071a9cba130d5b8147aaf32f102064c03dc811 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Fri, 28 Mar 2025 16:38:26 +0100 Subject: [PATCH 12/21] Update billing banner subtitles to handle optional date and amount parameters --- src/languages/en.ts | 5 +++-- src/languages/params.ts | 2 +- src/pages/settings/Subscription/CardSection/utils.ts | 10 ++++------ 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index a44a9c359a30..f2b9c0bcf321 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -5683,9 +5683,10 @@ const translations = { }, policyOwnerAmountOwedOverdue: { title: 'Your payment could not be processed', - generalSubtitle: 'Please add a payment card to clear the amount owed.', subtitle: ({date, purchaseAmountOwed}: BillingBannerOwnerAmountOwedOverdueParams) => - `Your ${date} charge of ${purchaseAmountOwed} could not be processed. Please add a payment card to clear the amount owed.`, + date && purchaseAmountOwed + ? `Your ${date} charge of ${purchaseAmountOwed} could not be processed. Please add a payment card to clear the amount owed.` + : 'Please add a payment card to clear the amount owed.', }, policyOwnerUnderInvoicing: { title: 'Your payment info is outdated', diff --git a/src/languages/params.ts b/src/languages/params.ts index b69844b92044..b200bc75a514 100644 --- a/src/languages/params.ts +++ b/src/languages/params.ts @@ -448,7 +448,7 @@ type BadgeFreeTrialParams = {numOfDays: number}; type BillingBannerSubtitleWithDateParams = {date: string}; -type BillingBannerOwnerAmountOwedOverdueParams = {date: string; purchaseAmountOwed: string}; +type BillingBannerOwnerAmountOwedOverdueParams = {date?: string; purchaseAmountOwed?: string}; type BillingBannerDisputePendingParams = {amountOwed: number; cardEnding: string}; diff --git a/src/pages/settings/Subscription/CardSection/utils.ts b/src/pages/settings/Subscription/CardSection/utils.ts index 9358f2d38456..4c349acd8a39 100644 --- a/src/pages/settings/Subscription/CardSection/utils.ts +++ b/src/pages/settings/Subscription/CardSection/utils.ts @@ -42,11 +42,9 @@ function getBillingStatus({translate, accountData, purchase}: GetBillingStatusPr const purchaseAmount = purchase?.message.billableAmount; const purchaseCurrency = purchase?.currency; - const isBillingTypeProper = purchase?.message.billingType === 'failed_2018'; const purchaseDate = purchase?.created; const purchaseDateFormatted = purchaseDate ? DateUtils.formatWithUTCTimeZone(purchaseDate, CONST.DATE.MONTH_DAY_YEAR_FORMAT) : null; const purchaseAmountWithCurrency = convertAmountToDisplayString(purchaseAmount, purchaseCurrency); - const isPurchase = !!purchase; switch (subscriptionStatus?.status) { case PAYMENT_STATUS.POLICY_OWNER_WITH_AMOUNT_OWED: @@ -60,10 +58,10 @@ function getBillingStatus({translate, accountData, purchase}: GetBillingStatusPr case PAYMENT_STATUS.POLICY_OWNER_WITH_AMOUNT_OWED_OVERDUE: return { title: translate('subscription.billingBanner.policyOwnerAmountOwedOverdue.title'), - subtitle: - isBillingTypeProper && isPurchase - ? translate('subscription.billingBanner.policyOwnerAmountOwedOverdue.subtitle', {date: purchaseDateFormatted ?? '', purchaseAmountOwed: purchaseAmountWithCurrency}) - : translate('subscription.billingBanner.policyOwnerAmountOwedOverdue.generalSubtitle'), + subtitle: translate('subscription.billingBanner.policyOwnerAmountOwedOverdue.subtitle', { + date: purchaseDateFormatted ?? '', + purchaseAmountOwed: purchaseAmountWithCurrency, + }), isError: true, isRetryAvailable: true, }; From e65ad3f9fdeb1d4b152ec85aba33fb2a339ef4d4 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Fri, 28 Mar 2025 16:47:36 +0100 Subject: [PATCH 13/21] Update Spanish translations for billing banner to conditionally include date and amount owed --- src/languages/es.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 76318f4c9bfd..8c950e8941ba 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -6199,9 +6199,10 @@ const translations = { }, policyOwnerAmountOwedOverdue: { title: 'No se pudo procesar tu pago', - generalSubtitle: 'Por favor, añade una tarjeta de pago para saldar la cantidad adeudada.', subtitle: ({date, purchaseAmountOwed}: BillingBannerOwnerAmountOwedOverdueParams) => - `No se ha podido procesar tu cargo de ${purchaseAmountOwed} del día ${date}. Por favor, añade una tarjeta de pago para saldar la cantidad adeudada.`, + date && purchaseAmountOwed + ? `No se ha podido procesar tu cargo de ${purchaseAmountOwed} del día ${date}. Por favor, añade una tarjeta de pago para saldar la cantidad adeudada.` + : 'Por favor, añade una tarjeta de pago para saldar la cantidad adeudada.', }, policyOwnerUnderInvoicing: { title: 'Tu información de pago está desactualizada', From 196a5d0cff82a56583eb8f340cbfa03b83245598 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Fri, 28 Mar 2025 18:38:08 +0100 Subject: [PATCH 14/21] Update billing date formatting to conditionally display based on billing type --- src/pages/settings/Subscription/CardSection/utils.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/settings/Subscription/CardSection/utils.ts b/src/pages/settings/Subscription/CardSection/utils.ts index 4c349acd8a39..d6dbfe19c1dd 100644 --- a/src/pages/settings/Subscription/CardSection/utils.ts +++ b/src/pages/settings/Subscription/CardSection/utils.ts @@ -43,7 +43,8 @@ function getBillingStatus({translate, accountData, purchase}: GetBillingStatusPr const purchaseAmount = purchase?.message.billableAmount; const purchaseCurrency = purchase?.currency; const purchaseDate = purchase?.created; - const purchaseDateFormatted = purchaseDate ? DateUtils.formatWithUTCTimeZone(purchaseDate, CONST.DATE.MONTH_DAY_YEAR_FORMAT) : null; + const isBillingTypeProper = purchase?.message.billingType === 'failed_2018'; + const purchaseDateFormatted = purchaseDate && isBillingTypeProper ? DateUtils.formatWithUTCTimeZone(purchaseDate, CONST.DATE.MONTH_DAY_YEAR_FORMAT) : undefined; const purchaseAmountWithCurrency = convertAmountToDisplayString(purchaseAmount, purchaseCurrency); switch (subscriptionStatus?.status) { @@ -59,7 +60,7 @@ function getBillingStatus({translate, accountData, purchase}: GetBillingStatusPr return { title: translate('subscription.billingBanner.policyOwnerAmountOwedOverdue.title'), subtitle: translate('subscription.billingBanner.policyOwnerAmountOwedOverdue.subtitle', { - date: purchaseDateFormatted ?? '', + date: purchaseDateFormatted, purchaseAmountOwed: purchaseAmountWithCurrency, }), isError: true, From 2e7dc7698ae83a1514fc3df3349d729978fdcbc2 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Fri, 28 Mar 2025 18:39:17 +0100 Subject: [PATCH 15/21] Remove test case for POLICY_OWNER_WITH_AMOUNT_OWED_OVERDUE variant in CardSectionUtils --- tests/unit/CardsSectionUtilsTest.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/tests/unit/CardsSectionUtilsTest.ts b/tests/unit/CardsSectionUtilsTest.ts index 604a7d91e796..6a48e9b72a7f 100644 --- a/tests/unit/CardsSectionUtilsTest.ts +++ b/tests/unit/CardsSectionUtilsTest.ts @@ -125,19 +125,6 @@ describe('CardSectionUtils', () => { }); }); - it('should return POLICY_OWNER_WITH_AMOUNT_OWED_OVERDUE variant with generalSubtitle', () => { - mockGetSubscriptionStatus.mockReturnValue({ - status: PAYMENT_STATUS.POLICY_OWNER_WITH_AMOUNT_OWED_OVERDUE, - }); - - expect(CardSectionUtils.getBillingStatus({translate: translateMock, accountData: ACCOUNT_DATA, purchase: undefined})).toEqual({ - title: 'subscription.billingBanner.policyOwnerAmountOwedOverdue.title', - subtitle: 'subscription.billingBanner.policyOwnerAmountOwedOverdue.generalSubtitle', - isError: true, - isRetryAvailable: true, - }); - }); - it('should return OWNER_OF_POLICY_UNDER_INVOICING variant', () => { mockGetSubscriptionStatus.mockReturnValue({ status: PAYMENT_STATUS.OWNER_OF_POLICY_UNDER_INVOICING, From fd6043b3663770f745b604c6030f30be42c31962 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Fri, 28 Mar 2025 18:41:12 +0100 Subject: [PATCH 16/21] Refactor billablePolicies to use a detailed record structure for improved clarity and data management --- src/types/onyx/PurchaseList.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/types/onyx/PurchaseList.ts b/src/types/onyx/PurchaseList.ts index 0cde04538901..8a630f4c2ca0 100644 --- a/src/types/onyx/PurchaseList.ts +++ b/src/types/onyx/PurchaseList.ts @@ -39,8 +39,19 @@ type Message = { /** Billable amount before free trial discount */ billableAmountBeforeFreeTrialDiscount: number; - /** List of billable policies */ - billablePolicies: string[]; + /** Record of billable policies with their details */ + billablePolicies: Record; + /** Whether the policy is corporate */ + corporate: boolean; + /** Expensify card spend by currency */ + expensifyCardSpend: Record; + /** Type of the policy */ + type: string; + }>; /** Billing type */ billingType: string; From c07617a5e45cc22ba386975c607d8d95bf2ad857 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Mon, 31 Mar 2025 12:34:34 +0200 Subject: [PATCH 17/21] Refactor billablePolicies type to use a dedicated BillablePolicy structure for improved clarity and maintainability --- src/types/onyx/PurchaseList.ts | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/types/onyx/PurchaseList.ts b/src/types/onyx/PurchaseList.ts index 8a630f4c2ca0..6b9fe0f4b9b1 100644 --- a/src/types/onyx/PurchaseList.ts +++ b/src/types/onyx/PurchaseList.ts @@ -22,6 +22,20 @@ type Subscription = { userCount: number; }; +/** Type for a billable policy */ +type BillablePolicy = { + /** List of actors in the policy */ + actorList: string; + /** Approved spend amounts by currency */ + approvedSpend: Record; + /** Whether the policy is corporate */ + corporate: boolean; + /** Expensify card spend by currency */ + expensifyCardSpend: Record; + /** Type of the policy */ + type: string; +}; + /** Message type for a purchase */ type Message = { /** Account manager account ID */ @@ -40,18 +54,7 @@ type Message = { billableAmountBeforeFreeTrialDiscount: number; /** Record of billable policies with their details */ - billablePolicies: Record; - /** Whether the policy is corporate */ - corporate: boolean; - /** Expensify card spend by currency */ - expensifyCardSpend: Record; - /** Type of the policy */ - type: string; - }>; + billablePolicies: Record; /** Billing type */ billingType: string; From 5bc191417fc5040e8d22703b115a9846480cc9ff Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Mon, 31 Mar 2025 12:37:46 +0200 Subject: [PATCH 18/21] Update billing status handling to conditionally format subtitle based on billing failure --- .../settings/Subscription/CardSection/utils.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/pages/settings/Subscription/CardSection/utils.ts b/src/pages/settings/Subscription/CardSection/utils.ts index d6dbfe19c1dd..b56d49356386 100644 --- a/src/pages/settings/Subscription/CardSection/utils.ts +++ b/src/pages/settings/Subscription/CardSection/utils.ts @@ -43,8 +43,8 @@ function getBillingStatus({translate, accountData, purchase}: GetBillingStatusPr const purchaseAmount = purchase?.message.billableAmount; const purchaseCurrency = purchase?.currency; const purchaseDate = purchase?.created; - const isBillingTypeProper = purchase?.message.billingType === 'failed_2018'; - const purchaseDateFormatted = purchaseDate && isBillingTypeProper ? DateUtils.formatWithUTCTimeZone(purchaseDate, CONST.DATE.MONTH_DAY_YEAR_FORMAT) : undefined; + const isBillingFailed = purchase?.message.billingType === 'failed_2018'; + const purchaseDateFormatted = purchaseDate ? DateUtils.formatWithUTCTimeZone(purchaseDate, CONST.DATE.MONTH_DAY_YEAR_FORMAT) : undefined; const purchaseAmountWithCurrency = convertAmountToDisplayString(purchaseAmount, purchaseCurrency); switch (subscriptionStatus?.status) { @@ -59,10 +59,12 @@ function getBillingStatus({translate, accountData, purchase}: GetBillingStatusPr case PAYMENT_STATUS.POLICY_OWNER_WITH_AMOUNT_OWED_OVERDUE: return { title: translate('subscription.billingBanner.policyOwnerAmountOwedOverdue.title'), - subtitle: translate('subscription.billingBanner.policyOwnerAmountOwedOverdue.subtitle', { - date: purchaseDateFormatted, - purchaseAmountOwed: purchaseAmountWithCurrency, - }), + subtitle: translate('subscription.billingBanner.policyOwnerAmountOwedOverdue.subtitle', + isBillingFailed ? { + date: purchaseDateFormatted, + purchaseAmountOwed: purchaseAmountWithCurrency, + } : {} + ), isError: true, isRetryAvailable: true, }; From e28cd6d4031f2c34d9beafd189c8e7b95ffa9f40 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Mon, 31 Mar 2025 12:42:00 +0200 Subject: [PATCH 19/21] Fix prettier --- .../settings/Subscription/CardSection/utils.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/pages/settings/Subscription/CardSection/utils.ts b/src/pages/settings/Subscription/CardSection/utils.ts index b56d49356386..dcec9b6691c5 100644 --- a/src/pages/settings/Subscription/CardSection/utils.ts +++ b/src/pages/settings/Subscription/CardSection/utils.ts @@ -59,11 +59,14 @@ function getBillingStatus({translate, accountData, purchase}: GetBillingStatusPr case PAYMENT_STATUS.POLICY_OWNER_WITH_AMOUNT_OWED_OVERDUE: return { title: translate('subscription.billingBanner.policyOwnerAmountOwedOverdue.title'), - subtitle: translate('subscription.billingBanner.policyOwnerAmountOwedOverdue.subtitle', - isBillingFailed ? { - date: purchaseDateFormatted, - purchaseAmountOwed: purchaseAmountWithCurrency, - } : {} + subtitle: translate( + 'subscription.billingBanner.policyOwnerAmountOwedOverdue.subtitle', + isBillingFailed + ? { + date: purchaseDateFormatted, + purchaseAmountOwed: purchaseAmountWithCurrency, + } + : {}, ), isError: true, isRetryAvailable: true, From fa1e98a56c27e85c68c38b8e4b7979eaefc68547 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Tue, 1 Apr 2025 13:41:38 +0200 Subject: [PATCH 20/21] Make subscription and message properties optional for improved flexibility --- src/types/onyx/PurchaseList.ts | 106 ++++++++++++++++----------------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/src/types/onyx/PurchaseList.ts b/src/types/onyx/PurchaseList.ts index 6b9fe0f4b9b1..9695838705c3 100644 --- a/src/types/onyx/PurchaseList.ts +++ b/src/types/onyx/PurchaseList.ts @@ -1,165 +1,165 @@ /** Subscription type for a purchase */ type Subscription = { /** Whether new users are added automatically */ - addNewUsersAutomatically: boolean; + addNewUsersAutomatically?: boolean; /** Whether the subscription auto-renews */ - autoRenew: boolean; + autoRenew?: boolean; /** Date when auto-renew was last changed */ - autoRenewLastChangedDate: string; + autoRenewLastChangedDate?: string; /** End date of the subscription */ - endDate: string; + endDate?: string; /** Start date of the subscription */ - startDate: string; + startDate?: string; /** Type of subscription */ - type: string; + type?: string; /** Number of users in the subscription */ - userCount: number; + userCount?: number; }; /** Type for a billable policy */ type BillablePolicy = { /** List of actors in the policy */ - actorList: string; + actorList?: string; /** Approved spend amounts by currency */ - approvedSpend: Record; + approvedSpend?: Record; /** Whether the policy is corporate */ - corporate: boolean; + corporate?: boolean; /** Expensify card spend by currency */ - expensifyCardSpend: Record; + expensifyCardSpend?: Record; /** Type of the policy */ - type: string; + type?: string; }; /** Message type for a purchase */ type Message = { /** Account manager account ID */ - accountManagerAccountID: number; + accountManagerAccountID?: number; /** List of approved accountant account IDs */ - approvedAccountantAccountIDs: number[]; + approvedAccountantAccountIDs?: number[]; /** Approved spend amounts by currency */ - approvedSpend: Record; + approvedSpend?: Record; /** Billable amount */ - billableAmount: number; + billableAmount?: number; /** Billable amount before free trial discount */ - billableAmountBeforeFreeTrialDiscount: number; + billableAmountBeforeFreeTrialDiscount?: number; /** Record of billable policies with their details */ - billablePolicies: Record; + billablePolicies?: Record; /** Billing type */ - billingType: string; + billingType?: string; /** Card spend surcharge percentage */ - cardSpendSurchargePercent: number; + cardSpendSurchargePercent?: number; /** Cash back amount */ - cashBackAmount: number; + cashBackAmount?: number; /** Cash back percentage */ - cashBackPercentage: number; + cashBackPercentage?: number; /** Chat only actor list */ - chatOnlyActorList: string; + chatOnlyActorList?: string; /** Corporate actor count */ - corporateActorCount: number; + corporateActorCount?: number; /** Corporate revenue */ - corporateRevenue: number; + corporateRevenue?: number; /** Expensify card monthly spend */ - expensifyCardMonthlySpend: number; + expensifyCardMonthlySpend?: number; /** Expensify card spend by currency */ - expensifyCardSpend: Record; + expensifyCardSpend?: Record; /** Free actor count */ - freeActorCount: number; + freeActorCount?: number; /** Free trial days */ - freeTrialDays: number; + freeTrialDays?: number; /** Free trial discount amount */ - freeTrialDiscountAmount: number; + freeTrialDiscountAmount?: number; /** Free trial discount percentage */ - freeTrialDiscountPercentage: number; + freeTrialDiscountPercentage?: number; /** Freebie credits used */ - freebieCreditsUsed: number; + freebieCreditsUsed?: number; /** Guide account ID */ - guideAccountID: number; + guideAccountID?: number; /** Whether the user is an approved accountant */ - isApprovedAccountant: boolean; + isApprovedAccountant?: boolean; /** Whether the user is an approved accountant client */ - isApprovedAccountantClient: boolean; + isApprovedAccountantClient?: boolean; /** Paid actor count */ - paidActorCount: number; + paidActorCount?: number; /** Partner manager account ID */ - partnerManagerAccountID: number; + partnerManagerAccountID?: number; /** Per policy total members count */ - perPolicyTotalMembersCount: Record; + perPolicyTotalMembersCount?: Record; /** Potential cash back amount */ - potentialCashBackAmount: number; + potentialCashBackAmount?: number; /** Potential cash back percentage */ - potentialCashBackPercentage: number; + potentialCashBackPercentage?: number; /** Subscription details */ - subscription: Subscription; + subscription?: Subscription; /** Team actor count */ - teamActorCount: number; + teamActorCount?: number; /** Team revenue */ - teamRevenue: number; + teamRevenue?: number; /** Total actor count */ - totalActorCount: number; + totalActorCount?: number; /** Total freebie credits */ - totalFreebieCredits: number; + totalFreebieCredits?: number; /** Total platform spend */ - totalPlatformSpend: number; + totalPlatformSpend?: number; /** Total revenue */ - totalRevenue: number; + totalRevenue?: number; /** Total unique members count */ - totalUniqueMembersCount: number; + totalUniqueMembersCount?: number; /** Whether domain billing was used */ - wasDomainBillingUsed: boolean; + wasDomainBillingUsed?: boolean; /** Yearly overage surcharge */ - yearlyOverageSurcharge: number; + yearlyOverageSurcharge?: number; /** Yearly subscription overage cost */ - yearlySubscriptionOverageCost: number; + yearlySubscriptionOverageCost?: number; /** Yearly subscription surcharge */ - yearlySubscriptionSurcharge: number; + yearlySubscriptionSurcharge?: number; /** Yearly subscription user count cost */ - yearlySubscriptionUserCountCost: number; + yearlySubscriptionUserCountCost?: number; }; /** Purchase type */ From c34e176a027ad1387c6e5c1aeb80fc8546b36fe8 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Fri, 4 Apr 2025 13:42:02 +0200 Subject: [PATCH 21/21] Add billing type constant and refactor purchase types for improved clarity --- src/CONST.ts | 4 ++ .../Subscription/CardSection/utils.ts | 2 +- src/types/onyx/PurchaseList.ts | 61 +++++++------------ 3 files changed, 27 insertions(+), 40 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 6de65ae74f63..94f540a02e3f 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -6958,6 +6958,10 @@ const CONST = { ILLUSTRATION_ASPECT_RATIO: 39 / 22, OFFLINE_INDICATOR_HEIGHT: 25, + + BILLING: { + TYPE_FAILED_2018: 'typeFailed2018', + }, } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; diff --git a/src/pages/settings/Subscription/CardSection/utils.ts b/src/pages/settings/Subscription/CardSection/utils.ts index dcec9b6691c5..af7f7b635b8d 100644 --- a/src/pages/settings/Subscription/CardSection/utils.ts +++ b/src/pages/settings/Subscription/CardSection/utils.ts @@ -43,7 +43,7 @@ function getBillingStatus({translate, accountData, purchase}: GetBillingStatusPr const purchaseAmount = purchase?.message.billableAmount; const purchaseCurrency = purchase?.currency; const purchaseDate = purchase?.created; - const isBillingFailed = purchase?.message.billingType === 'failed_2018'; + const isBillingFailed = purchase?.message.billingType === CONST.BILLING.TYPE_FAILED_2018; const purchaseDateFormatted = purchaseDate ? DateUtils.formatWithUTCTimeZone(purchaseDate, CONST.DATE.MONTH_DAY_YEAR_FORMAT) : undefined; const purchaseAmountWithCurrency = convertAmountToDisplayString(purchaseAmount, purchaseCurrency); diff --git a/src/types/onyx/PurchaseList.ts b/src/types/onyx/PurchaseList.ts index 9695838705c3..ec5b2b1fde43 100644 --- a/src/types/onyx/PurchaseList.ts +++ b/src/types/onyx/PurchaseList.ts @@ -1,39 +1,25 @@ -/** Subscription type for a purchase */ -type Subscription = { - /** Whether new users are added automatically */ - addNewUsersAutomatically?: boolean; - - /** Whether the subscription auto-renews */ - autoRenew?: boolean; - - /** Date when auto-renew was last changed */ - autoRenewLastChangedDate?: string; - - /** End date of the subscription */ - endDate?: string; +import type CONST from '@src/CONST'; +import type PrivateSubscription from './PrivateSubscription'; - /** Start date of the subscription */ - startDate?: string; - - /** Type of subscription */ - type?: string; - - /** Number of users in the subscription */ - userCount?: number; -}; +/** Subscription type for a purchase */ +type Subscription = Omit; /** Type for a billable policy */ type BillablePolicy = { - /** List of actors in the policy */ + /** Comma separated list of emails for members in the policy */ actorList?: string; - /** Approved spend amounts by currency */ + + /** Amount spent, by currency */ approvedSpend?: Record; + /** Whether the policy is corporate */ corporate?: boolean; + /** Expensify card spend by currency */ expensifyCardSpend?: Record; - /** Type of the policy */ - type?: string; + + /** Policy type */ + type?: typeof CONST.POLICY.TYPE; }; /** Message type for a purchase */ @@ -41,7 +27,7 @@ type Message = { /** Account manager account ID */ accountManagerAccountID?: number; - /** List of approved accountant account IDs */ + /** List of Approved Accountant account IDs */ approvedAccountantAccountIDs?: number[]; /** Approved spend amounts by currency */ @@ -71,21 +57,18 @@ type Message = { /** Chat only actor list */ chatOnlyActorList?: string; - /** Corporate actor count */ + /** Actor count for Corporate policy type */ corporateActorCount?: number; - /** Corporate revenue */ + /** Amount charged for Corporate policy type */ corporateRevenue?: number; - /** Expensify card monthly spend */ + /** Expensify Card monthly spend */ expensifyCardMonthlySpend?: number; - /** Expensify card spend by currency */ + /** Expensify Card spend by currency */ expensifyCardSpend?: Record; - /** Free actor count */ - freeActorCount?: number; - /** Free trial days */ freeTrialDays?: number; @@ -101,10 +84,10 @@ type Message = { /** Guide account ID */ guideAccountID?: number; - /** Whether the user is an approved accountant */ + /** Whether the user is an Approved Accountant */ isApprovedAccountant?: boolean; - /** Whether the user is an approved accountant client */ + /** Whether the user is an Approved Accountant client */ isApprovedAccountantClient?: boolean; /** Paid actor count */ @@ -125,10 +108,10 @@ type Message = { /** Subscription details */ subscription?: Subscription; - /** Team actor count */ + /** Actor count for Team policy type */ teamActorCount?: number; - /** Team revenue */ + /** Amount charged for Team policy type */ teamRevenue?: number; /** Total actor count */ @@ -180,7 +163,7 @@ type Purchase = { purchaseID: number; }; -/** Record of purchases, indexed by purchase ID */ +/** Array of purchases */ type PurchaseList = Purchase[]; export default PurchaseList;