Skip to content

Commit

Permalink
feat: handle new card when card expired scenario
Browse files Browse the repository at this point in the history
  • Loading branch information
MrMuzyk committed Jul 2, 2024
1 parent ee8aeba commit 0ee9313
Show file tree
Hide file tree
Showing 5 changed files with 72 additions and 18 deletions.
14 changes: 14 additions & 0 deletions src/libs/DateUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -850,6 +863,7 @@ const DateUtils = {
getFormattedReservationRangeDate,
getFormattedTransportDate,
doesDateBelongToAPastYear,
isCardExpired,
};

export default DateUtils;
18 changes: 15 additions & 3 deletions src/pages/settings/Subscription/CardSection/CardSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<BillingStatusResult | undefined>(CardSectionUtils.getBillingStatus(translate, defaultCard?.accountData?.cardNumber ?? ''));
const [billingStatus, setBillingStatus] = useState<BillingStatusResult | undefined>(
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();
Expand Down
6 changes: 5 additions & 1 deletion src/pages/settings/Subscription/CardSection/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ type BillingStatusResult = {
function getBillingStatus(
translate: <TKey extends TranslationPaths>(phraseKey: TKey, ...phraseParameters: PhraseParameters<Phrase<TKey>>) => string,
cardEnding: string,
cardMonth: number,
cardYear: number,
): BillingStatusResult | undefined {
const amountOwed = SubscriptionUtils.getAmountOwed();

Expand All @@ -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 {
Expand Down Expand Up @@ -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:
Expand Down
9 changes: 8 additions & 1 deletion src/pages/settings/Subscription/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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};
43 changes: 30 additions & 13 deletions tests/unit/CardsSectionUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ function translateMock<TKey extends TranslationPaths>(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();

Expand Down Expand Up @@ -68,18 +70,26 @@ 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', () => {
mockGetSubscriptionStatus.mockReturnValue({
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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -156,20 +166,27 @@ 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,
isRetryAvailable: true,
});
});

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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down

0 comments on commit 0ee9313

Please sign in to comment.