From 0ee9313eef6bcefce1628b0366aecc8777d98326 Mon Sep 17 00:00:00 2001 From: Michal Muzyk Date: Tue, 2 Jul 2024 14:02:43 +0200 Subject: [PATCH] feat: handle new card when card expired scenario --- src/libs/DateUtils.ts | 14 ++++++ .../Subscription/CardSection/CardSection.tsx | 18 ++++++-- .../Subscription/CardSection/utils.ts | 6 ++- src/pages/settings/Subscription/utils.ts | 9 +++- tests/unit/CardsSectionUtilsTest.ts | 43 +++++++++++++------ 5 files changed, 72 insertions(+), 18 deletions(-) diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts index f538e5e719e2..f2e929fe16c6 100644 --- a/src/libs/DateUtils.ts +++ b/src/libs/DateUtils.ts @@ -806,6 +806,19 @@ function doesDateBelongToAPastYear(date: string): boolean { return transactionYear !== new Date().getFullYear(); } +/** + * Returns a boolean value indicating whether the card has expired. + * @param expiryMonth month when card expires + * @param expiryYear year when card expires + */ + +function isCardExpired(expiryMonth: number, expiryYear: number): boolean { + const currentYear = new Date().getFullYear(); + const currentMonth = new Date().getMonth() + 1; + + return expiryYear < currentYear || (expiryYear === currentYear && expiryMonth < currentMonth); +} + const DateUtils = { isDate, formatToDayOfWeek, @@ -850,6 +863,7 @@ const DateUtils = { getFormattedReservationRangeDate, getFormattedTransportDate, doesDateBelongToAPastYear, + isCardExpired, }; export default DateUtils; diff --git a/src/pages/settings/Subscription/CardSection/CardSection.tsx b/src/pages/settings/Subscription/CardSection/CardSection.tsx index 9a97621e8d3d..05a3841444fd 100644 --- a/src/pages/settings/Subscription/CardSection/CardSection.tsx +++ b/src/pages/settings/Subscription/CardSection/CardSection.tsx @@ -40,15 +40,27 @@ function CardSection() { const cardMonth = useMemo(() => DateUtils.getMonthNames(preferredLocale)[(defaultCard?.accountData?.cardMonth ?? 1) - 1], [defaultCard?.accountData?.cardMonth, preferredLocale]); - const [billingStatus, setBillingStatus] = useState(CardSectionUtils.getBillingStatus(translate, defaultCard?.accountData?.cardNumber ?? '')); + const [billingStatus, setBillingStatus] = useState( + CardSectionUtils.getBillingStatus(translate, defaultCard?.accountData?.cardNumber ?? '', defaultCard?.accountData?.cardMonth ?? 0, defaultCard?.accountData?.cardYear ?? 0), + ); 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?.cardNumber ?? '')); - }, [subscriptionRetryBillingStatusPending, subscriptionRetryBillingStatusSuccessful, subscriptionRetryBillingStatusFailed, defaultCard?.accountData?.cardNumber, translate]); + setBillingStatus( + CardSectionUtils.getBillingStatus(translate, defaultCard?.accountData?.cardNumber ?? '', defaultCard?.accountData?.cardMonth ?? 0, defaultCard?.accountData?.cardYear ?? 0), + ); + }, [ + subscriptionRetryBillingStatusPending, + subscriptionRetryBillingStatusSuccessful, + subscriptionRetryBillingStatusFailed, + translate, + defaultCard?.accountData?.cardNumber, + defaultCard?.accountData?.cardMonth, + defaultCard?.accountData?.cardYear, + ]); const handleRetryPayment = () => { Subscription.clearOutstandingBalance(); diff --git a/src/pages/settings/Subscription/CardSection/utils.ts b/src/pages/settings/Subscription/CardSection/utils.ts index a56e34d34e9c..970496ae4a6f 100644 --- a/src/pages/settings/Subscription/CardSection/utils.ts +++ b/src/pages/settings/Subscription/CardSection/utils.ts @@ -22,6 +22,8 @@ type BillingStatusResult = { function getBillingStatus( translate: (phraseKey: TKey, ...phraseParameters: PhraseParameters>) => string, cardEnding: string, + cardMonth: number, + cardYear: number, ): BillingStatusResult | undefined { const amountOwed = SubscriptionUtils.getAmountOwed(); @@ -31,6 +33,8 @@ function getBillingStatus( const endDateFormatted = endDate ? DateUtils.formatWithUTCTimeZone(fromUnixTime(endDate).toUTCString(), CONST.DATE.MONTH_DAY_YEAR_FORMAT) : null; + const isCurrentCardExpired = DateUtils.isCardExpired(cardMonth, cardYear); + switch (subscriptionStatus?.status) { case SubscriptionUtils.PAYMENT_STATUS.POLICY_OWNER_WITH_AMOUNT_OWED: return { @@ -92,7 +96,7 @@ function getBillingStatus( title: translate('subscription.billingBanner.cardExpired.title'), subtitle: translate('subscription.billingBanner.cardExpired.subtitle', {amountOwed}), isError: true, - isRetryAvailable: true, + isRetryAvailable: !isCurrentCardExpired, }; case SubscriptionUtils.PAYMENT_STATUS.CARD_EXPIRE_SOON: diff --git a/src/pages/settings/Subscription/utils.ts b/src/pages/settings/Subscription/utils.ts index e9649eb3748d..b39d9e54a257 100644 --- a/src/pages/settings/Subscription/utils.ts +++ b/src/pages/settings/Subscription/utils.ts @@ -19,4 +19,11 @@ function getNewSubscriptionRenewalDate(): string { return format(startOfMonth(addMonths(new Date(), 12)), CONST.DATE.MONTH_DAY_YEAR_ABBR_FORMAT); } -export {getNewSubscriptionRenewalDate, formatSubscriptionEndDate}; +function isCardExpired(expiryMonth: number, expiryYear: number): boolean { + const currentYear = new Date().getFullYear(); + const currentMonth = new Date().getMonth() + 1; + + return expiryYear < currentYear || (expiryYear === currentYear && expiryMonth < currentMonth); +} + +export {getNewSubscriptionRenewalDate, formatSubscriptionEndDate, isCardExpired}; diff --git a/tests/unit/CardsSectionUtilsTest.ts b/tests/unit/CardsSectionUtilsTest.ts index 6182c5f49126..9318bced5fda 100644 --- a/tests/unit/CardsSectionUtilsTest.ts +++ b/tests/unit/CardsSectionUtilsTest.ts @@ -15,6 +15,8 @@ function translateMock(key: TKey, ...phraseParame const CARD_ENDING = '1234'; const AMOUNT_OWED = 100; const GRACE_PERIOD_DATE = 1750819200; +const CARD_MONTH = 12; +const CARD_YEAR = 2024; const mockGetSubscriptionStatus = jest.fn(); @@ -68,10 +70,18 @@ describe('CardSectionUtils', () => { beforeAll(() => { mockGetSubscriptionStatus.mockReturnValue(''); + + jest.useFakeTimers(); + // Month is zero indexed, so this is July 5th 2024 + jest.setSystemTime(new Date(2024, 6, 5)); + }); + + afterAll(() => { + jest.useRealTimers(); }); it('should return undefined by default', () => { - expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING)).toBeUndefined(); + expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING, CARD_MONTH, CARD_YEAR)).toBeUndefined(); }); it('should return POLICY_OWNER_WITH_AMOUNT_OWED variant', () => { @@ -79,7 +89,7 @@ describe('CardSectionUtils', () => { status: PAYMENT_STATUS.POLICY_OWNER_WITH_AMOUNT_OWED, }); - expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING)).toEqual({ + expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING, CARD_MONTH, CARD_YEAR)).toEqual({ title: 'subscription.billingBanner.policyOwnerAmountOwed.title', subtitle: 'subscription.billingBanner.policyOwnerAmountOwed.subtitle', isError: true, @@ -92,7 +102,7 @@ describe('CardSectionUtils', () => { status: PAYMENT_STATUS.POLICY_OWNER_WITH_AMOUNT_OWED_OVERDUE, }); - expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING)).toEqual({ + expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING, CARD_MONTH, CARD_YEAR)).toEqual({ title: 'subscription.billingBanner.policyOwnerAmountOwedOverdue.title', subtitle: 'subscription.billingBanner.policyOwnerAmountOwedOverdue.subtitle', isError: true, @@ -104,7 +114,7 @@ describe('CardSectionUtils', () => { status: PAYMENT_STATUS.OWNER_OF_POLICY_UNDER_INVOICING, }); - expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING)).toEqual({ + expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING, CARD_MONTH, CARD_YEAR)).toEqual({ title: 'subscription.billingBanner.policyOwnerUnderInvoicing.title', subtitle: 'subscription.billingBanner.policyOwnerUnderInvoicing.subtitle', isError: true, @@ -117,7 +127,7 @@ describe('CardSectionUtils', () => { status: PAYMENT_STATUS.OWNER_OF_POLICY_UNDER_INVOICING_OVERDUE, }); - expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING)).toEqual({ + expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING, CARD_MONTH, CARD_YEAR)).toEqual({ title: 'subscription.billingBanner.policyOwnerUnderInvoicingOverdue.title', subtitle: 'subscription.billingBanner.policyOwnerUnderInvoicingOverdue.subtitle', isError: true, @@ -130,7 +140,7 @@ describe('CardSectionUtils', () => { status: PAYMENT_STATUS.BILLING_DISPUTE_PENDING, }); - expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING)).toEqual({ + expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING, CARD_MONTH, CARD_YEAR)).toEqual({ title: 'subscription.billingBanner.billingDisputePending.title', subtitle: 'subscription.billingBanner.billingDisputePending.subtitle', isError: true, @@ -143,7 +153,7 @@ describe('CardSectionUtils', () => { status: PAYMENT_STATUS.CARD_AUTHENTICATION_REQUIRED, }); - expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING)).toEqual({ + expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING, CARD_MONTH, CARD_YEAR)).toEqual({ title: 'subscription.billingBanner.cardAuthenticationRequired.title', subtitle: 'subscription.billingBanner.cardAuthenticationRequired.subtitle', isError: true, @@ -156,7 +166,7 @@ describe('CardSectionUtils', () => { status: PAYMENT_STATUS.INSUFFICIENT_FUNDS, }); - expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING)).toEqual({ + expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING, CARD_MONTH, CARD_YEAR)).toEqual({ title: 'subscription.billingBanner.insufficientFunds.title', subtitle: 'subscription.billingBanner.insufficientFunds.subtitle', isError: true, @@ -164,12 +174,19 @@ describe('CardSectionUtils', () => { }); }); - it('should return CARD_EXPIRED variant', () => { + it('should return CARD_EXPIRED variant with correct isRetryAvailableStatus for expired and unexpired card', () => { mockGetSubscriptionStatus.mockReturnValue({ status: PAYMENT_STATUS.CARD_EXPIRED, }); - expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING)).toEqual({ + expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING, CARD_MONTH, CARD_YEAR - 1)).toEqual({ + title: 'subscription.billingBanner.cardExpired.title', + subtitle: 'subscription.billingBanner.cardExpired.subtitle', + isError: true, + isRetryAvailable: false, + }); + + expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING, CARD_MONTH, CARD_YEAR)).toEqual({ title: 'subscription.billingBanner.cardExpired.title', subtitle: 'subscription.billingBanner.cardExpired.subtitle', isError: true, @@ -182,7 +199,7 @@ describe('CardSectionUtils', () => { status: PAYMENT_STATUS.CARD_EXPIRE_SOON, }); - expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING)).toEqual({ + expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING, CARD_MONTH, CARD_YEAR)).toEqual({ title: 'subscription.billingBanner.cardExpireSoon.title', subtitle: 'subscription.billingBanner.cardExpireSoon.subtitle', isError: false, @@ -195,7 +212,7 @@ describe('CardSectionUtils', () => { status: PAYMENT_STATUS.RETRY_BILLING_SUCCESS, }); - expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING)).toEqual({ + expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING, CARD_MONTH, CARD_YEAR)).toEqual({ title: 'subscription.billingBanner.retryBillingSuccess.title', subtitle: 'subscription.billingBanner.retryBillingSuccess.subtitle', isError: false, @@ -208,7 +225,7 @@ describe('CardSectionUtils', () => { status: PAYMENT_STATUS.RETRY_BILLING_ERROR, }); - expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING)).toEqual({ + expect(CardSectionUtils.getBillingStatus(translateMock, CARD_ENDING, CARD_MONTH, CARD_YEAR)).toEqual({ title: 'subscription.billingBanner.retryBillingError.title', subtitle: 'subscription.billingBanner.retryBillingError.subtitle', isError: true,