From 65b7574be2b596ce3e851a11720281e57c7c9b51 Mon Sep 17 00:00:00 2001 From: jackiettran <41605212+jackiettran@users.noreply.github.com> Date: Thu, 8 Jan 2026 15:00:12 -0500 Subject: [PATCH] updated card and bank error handling messages --- backend/tests/unit/utils/stripeErrors.test.js | 336 ++++++++++++++++++ backend/utils/stripeErrors.js | 159 ++++++++- 2 files changed, 482 insertions(+), 13 deletions(-) create mode 100644 backend/tests/unit/utils/stripeErrors.test.js diff --git a/backend/tests/unit/utils/stripeErrors.test.js b/backend/tests/unit/utils/stripeErrors.test.js new file mode 100644 index 0000000..34d9bae --- /dev/null +++ b/backend/tests/unit/utils/stripeErrors.test.js @@ -0,0 +1,336 @@ +const { + parseStripeError, + PaymentError, + DECLINE_MESSAGES, +} = require('../../../utils/stripeErrors'); + +describe('Stripe Errors Utility', () => { + describe('DECLINE_MESSAGES', () => { + const requiredProperties = [ + 'ownerMessage', + 'renterMessage', + 'canOwnerRetry', + 'requiresNewPaymentMethod', + ]; + + test('each decline code has all required properties', () => { + for (const [code, messages] of Object.entries(DECLINE_MESSAGES)) { + for (const prop of requiredProperties) { + expect(messages).toHaveProperty(prop); + } + } + }); + + describe('ACH decline codes', () => { + const achCodes = [ + 'bank_account_closed', + 'bank_account_frozen', + 'bank_account_restricted', + 'bank_account_unusable', + 'bank_account_invalid_details', + 'debit_not_authorized', + ]; + + test.each(achCodes)('%s exists in DECLINE_MESSAGES', (code) => { + expect(DECLINE_MESSAGES).toHaveProperty(code); + }); + + test.each(achCodes)('%s has all required properties', (code) => { + const messages = DECLINE_MESSAGES[code]; + expect(messages).toHaveProperty('ownerMessage'); + expect(messages).toHaveProperty('renterMessage'); + expect(messages).toHaveProperty('canOwnerRetry'); + expect(messages).toHaveProperty('requiresNewPaymentMethod'); + }); + + test.each(achCodes)('%s requires new payment method', (code) => { + expect(DECLINE_MESSAGES[code].requiresNewPaymentMethod).toBe(true); + }); + + test.each(achCodes)('%s does not allow owner retry', (code) => { + expect(DECLINE_MESSAGES[code].canOwnerRetry).toBe(false); + }); + + test.each(achCodes)('%s has non-empty messages', (code) => { + expect(DECLINE_MESSAGES[code].ownerMessage.length).toBeGreaterThan(0); + expect(DECLINE_MESSAGES[code].renterMessage.length).toBeGreaterThan(0); + }); + }); + + describe('Additional card decline codes', () => { + const additionalCardCodes = [ + 'call_issuer', + 'do_not_try_again', + 'duplicate_transaction', + 'issuer_not_available', + 'restricted_card', + 'withdrawal_count_limit_exceeded', + 'not_permitted', + 'invalid_amount', + 'security_violation', + 'stop_payment_order', + 'transaction_not_allowed', + ]; + + test.each(additionalCardCodes)('%s exists in DECLINE_MESSAGES', (code) => { + expect(DECLINE_MESSAGES).toHaveProperty(code); + }); + + test.each(additionalCardCodes)('%s has all required properties', (code) => { + const messages = DECLINE_MESSAGES[code]; + expect(messages).toHaveProperty('ownerMessage'); + expect(messages).toHaveProperty('renterMessage'); + expect(messages).toHaveProperty('canOwnerRetry'); + expect(messages).toHaveProperty('requiresNewPaymentMethod'); + }); + + test.each(additionalCardCodes)('%s has non-empty messages', (code) => { + expect(DECLINE_MESSAGES[code].ownerMessage.length).toBeGreaterThan(0); + expect(DECLINE_MESSAGES[code].renterMessage.length).toBeGreaterThan(0); + }); + + // Codes that allow retry (temporary issues) + const retryableCodes = [ + 'call_issuer', + 'duplicate_transaction', + 'issuer_not_available', + 'withdrawal_count_limit_exceeded', + ]; + + test.each(retryableCodes)('%s allows owner retry', (code) => { + expect(DECLINE_MESSAGES[code].canOwnerRetry).toBe(true); + }); + + // Codes that require new payment method (permanent issues) + const permanentCodes = [ + 'do_not_try_again', + 'restricted_card', + 'not_permitted', + 'security_violation', + 'stop_payment_order', + 'transaction_not_allowed', + ]; + + test.each(permanentCodes)('%s requires new payment method', (code) => { + expect(DECLINE_MESSAGES[code].requiresNewPaymentMethod).toBe(true); + }); + + test('do_not_try_again has isPermanent flag', () => { + expect(DECLINE_MESSAGES.do_not_try_again.isPermanent).toBe(true); + }); + + test('invalid_amount does not require new payment method', () => { + // This is a platform/amount issue, not a card issue + expect(DECLINE_MESSAGES.invalid_amount.requiresNewPaymentMethod).toBe(false); + }); + }); + }); + + describe('parseStripeError', () => { + test('parses card decline error with known code', () => { + const error = { + type: 'StripeCardError', + code: 'card_declined', + decline_code: 'insufficient_funds', + message: 'Your card has insufficient funds.', + }; + + const result = parseStripeError(error); + + expect(result.code).toBe('insufficient_funds'); + expect(result.ownerMessage).toBe("The renter's card has insufficient funds."); + expect(result.renterMessage).toBe('Your card has insufficient funds.'); + expect(result.canOwnerRetry).toBe(false); + expect(result.requiresNewPaymentMethod).toBe(false); + }); + + test('parses card decline error - do_not_try_again (permanent)', () => { + const error = { + type: 'StripeCardError', + code: 'card_declined', + decline_code: 'do_not_try_again', + message: 'Do not try again.', + }; + + const result = parseStripeError(error); + + expect(result.code).toBe('do_not_try_again'); + expect(result.ownerMessage).toBe("The renter's card has been permanently declined."); + expect(result.requiresNewPaymentMethod).toBe(true); + }); + + test('parses card decline error - call_issuer (retryable)', () => { + const error = { + type: 'StripeCardError', + code: 'card_declined', + decline_code: 'call_issuer', + message: 'Call issuer.', + }; + + const result = parseStripeError(error); + + expect(result.code).toBe('call_issuer'); + expect(result.canOwnerRetry).toBe(true); + expect(result.requiresNewPaymentMethod).toBe(false); + }); + + test('parses card decline error - withdrawal_count_limit_exceeded', () => { + const error = { + type: 'StripeCardError', + code: 'card_declined', + decline_code: 'withdrawal_count_limit_exceeded', + message: 'Withdrawal count limit exceeded.', + }; + + const result = parseStripeError(error); + + expect(result.code).toBe('withdrawal_count_limit_exceeded'); + expect(result.renterMessage).toContain('daily limit'); + expect(result.canOwnerRetry).toBe(true); + }); + + test('parses ACH decline error - bank_account_closed', () => { + const error = { + type: 'StripeCardError', + code: 'card_declined', + decline_code: 'bank_account_closed', + message: 'The bank account has been closed.', + }; + + const result = parseStripeError(error); + + expect(result.code).toBe('bank_account_closed'); + expect(result.ownerMessage).toBe("The renter's bank account has been closed."); + expect(result.renterMessage).toBe('Your bank account has been closed. Please add a different payment method.'); + expect(result.requiresNewPaymentMethod).toBe(true); + }); + + test('parses ACH decline error - bank_account_frozen', () => { + const error = { + type: 'StripeCardError', + code: 'card_declined', + decline_code: 'bank_account_frozen', + message: 'The bank account is frozen.', + }; + + const result = parseStripeError(error); + + expect(result.code).toBe('bank_account_frozen'); + expect(result.ownerMessage).toBe("The renter's bank account is frozen."); + expect(result.renterMessage).toContain('frozen'); + }); + + test('parses ACH decline error - debit_not_authorized', () => { + const error = { + type: 'StripeCardError', + code: 'card_declined', + decline_code: 'debit_not_authorized', + message: 'Debit not authorized.', + }; + + const result = parseStripeError(error); + + expect(result.code).toBe('debit_not_authorized'); + expect(result.renterMessage).toContain('not authorized'); + }); + + test('returns default error for unknown decline code', () => { + const error = { + type: 'StripeCardError', + code: 'card_declined', + decline_code: 'unknown_code_xyz', + message: 'Unknown error', + }; + + const result = parseStripeError(error); + + expect(result.code).toBe('unknown_code_xyz'); + expect(result.ownerMessage).toBe('The payment could not be processed.'); + expect(result.requiresNewPaymentMethod).toBe(true); + }); + + test('handles StripeAPIError', () => { + const error = { + type: 'StripeAPIError', + message: 'API error', + code: 'api_error', + }; + + const result = parseStripeError(error); + + expect(result.code).toBe('api_error'); + expect(result.canOwnerRetry).toBe(true); + }); + + test('handles StripeRateLimitError', () => { + const error = { + type: 'StripeRateLimitError', + message: 'Rate limit', + code: 'rate_limit_error', + }; + + const result = parseStripeError(error); + + expect(result.code).toBe('rate_limit'); + expect(result.canOwnerRetry).toBe(true); + }); + + test('includes original error info for logging', () => { + const error = { + type: 'StripeCardError', + code: 'card_declined', + decline_code: 'bank_account_frozen', + message: 'Original Stripe message', + }; + + const result = parseStripeError(error); + + expect(result._originalMessage).toBe('Original Stripe message'); + expect(result._stripeCode).toBe('card_declined'); + }); + }); + + describe('PaymentError', () => { + test('creates PaymentError with all properties', () => { + const parsedError = { + code: 'bank_account_frozen', + ownerMessage: "The renter's bank account is frozen.", + renterMessage: 'Your bank account is frozen.', + canOwnerRetry: false, + requiresNewPaymentMethod: true, + _originalMessage: 'Original message', + _stripeCode: 'card_declined', + }; + + const error = new PaymentError(parsedError); + + expect(error).toBeInstanceOf(Error); + expect(error.name).toBe('PaymentError'); + expect(error.code).toBe('bank_account_frozen'); + expect(error.ownerMessage).toBe("The renter's bank account is frozen."); + expect(error.renterMessage).toBe('Your bank account is frozen.'); + expect(error.requiresNewPaymentMethod).toBe(true); + }); + + test('toJSON returns serializable object', () => { + const parsedError = { + code: 'bank_account_closed', + ownerMessage: "The renter's bank account has been closed.", + renterMessage: 'Your bank account has been closed.', + canOwnerRetry: false, + requiresNewPaymentMethod: true, + _originalMessage: 'Should not appear', + _stripeCode: 'card_declined', + }; + + const error = new PaymentError(parsedError); + const json = error.toJSON(); + + expect(json).toHaveProperty('code', 'bank_account_closed'); + expect(json).toHaveProperty('ownerMessage'); + expect(json).toHaveProperty('renterMessage'); + expect(json).not.toHaveProperty('_originalMessage'); + expect(json).not.toHaveProperty('_stripeCode'); + }); + }); +}); diff --git a/backend/utils/stripeErrors.js b/backend/utils/stripeErrors.js index 927a895..16b70a5 100644 --- a/backend/utils/stripeErrors.js +++ b/backend/utils/stripeErrors.js @@ -8,7 +8,8 @@ const DECLINE_MESSAGES = { authentication_required: { ownerMessage: "The renter's card requires additional authentication.", - renterMessage: "Your card requires authentication to complete this payment.", + renterMessage: + "Your card requires authentication to complete this payment.", canOwnerRetry: false, requiresNewPaymentMethod: false, requires3DS: true, @@ -20,7 +21,7 @@ const DECLINE_MESSAGES = { requiresNewPaymentMethod: false, // renter might add funds }, card_declined: { - ownerMessage: "The renter's card was declined by their bank.", + ownerMessage: "The renter's card was declined.", renterMessage: "Your card was declined. Please contact your bank or try a different card.", canOwnerRetry: false, @@ -81,7 +82,7 @@ const DECLINE_MESSAGES = { requiresNewPaymentMethod: true, }, incorrect_zip: { - ownerMessage: "Billing address verification failed.", + ownerMessage: "Renter's billing address couldn't be verified.", renterMessage: "Your billing address couldn't be verified. Please update your payment method.", canOwnerRetry: false, @@ -96,7 +97,7 @@ const DECLINE_MESSAGES = { requiresNewPaymentMethod: false, }, do_not_honor: { - ownerMessage: "The renter's card was declined by their bank.", + ownerMessage: "The renter's card was declined.", renterMessage: "Your card was declined. Please contact your bank or try a different card.", canOwnerRetry: false, @@ -104,41 +105,170 @@ const DECLINE_MESSAGES = { }, invalid_account: { ownerMessage: "The renter's card account is invalid.", - renterMessage: - "Your card account is invalid. Please use a different card.", + renterMessage: "Your card account is invalid. Please use a different card.", canOwnerRetry: false, requiresNewPaymentMethod: true, }, new_account_information_available: { ownerMessage: "The renter's card information needs to be updated.", - renterMessage: "Your card information needs to be updated. Please re-enter your payment details.", + renterMessage: + "Your card information needs to be updated. Please re-enter your payment details.", canOwnerRetry: false, requiresNewPaymentMethod: true, }, card_not_supported: { ownerMessage: "The renter's card type is not supported.", - renterMessage: "This card type is not supported. Please use a different card.", + renterMessage: + "This card type is not supported. Please use a different card.", canOwnerRetry: false, requiresNewPaymentMethod: true, }, currency_not_supported: { ownerMessage: "The renter's card doesn't support this currency.", - renterMessage: "Your card doesn't support USD payments. Please use a different card.", + renterMessage: + "Your card doesn't support USD payments. Please use a different card.", canOwnerRetry: false, requiresNewPaymentMethod: true, }, try_again_later: { - ownerMessage: "The payment processor is temporarily unavailable. Please try again.", - renterMessage: "A temporary error occurred. The owner can try again shortly.", + ownerMessage: + "The payment processor is temporarily unavailable. Please try again.", + renterMessage: + "A temporary error occurred. The owner can try again shortly.", canOwnerRetry: true, requiresNewPaymentMethod: false, }, + + // Additional Card Declines + call_issuer: { + ownerMessage: "The renter needs to contact their bank.", + renterMessage: + "Please call your bank to authorize this transaction, then try again.", + canOwnerRetry: true, + requiresNewPaymentMethod: false, + }, + do_not_try_again: { + ownerMessage: "The renter's card has been permanently declined.", + renterMessage: + "This card cannot be used. Please add a different payment method.", + canOwnerRetry: false, + requiresNewPaymentMethod: true, + isPermanent: true, + }, + duplicate_transaction: { + ownerMessage: "This appears to be a duplicate charge attempt.", + renterMessage: + "This looks like a duplicate charge. Please wait a moment before trying again.", + canOwnerRetry: true, + requiresNewPaymentMethod: false, + }, + issuer_not_available: { + ownerMessage: "The renter's bank is temporarily unavailable.", + renterMessage: + "Your bank is temporarily unavailable. Please try again in a few minutes.", + canOwnerRetry: true, + requiresNewPaymentMethod: false, + }, + restricted_card: { + ownerMessage: "The renter's card has restrictions.", + renterMessage: + "This card has restrictions. Please contact your bank or use a different card.", + canOwnerRetry: false, + requiresNewPaymentMethod: true, + }, + withdrawal_count_limit_exceeded: { + ownerMessage: "The renter has exceeded their card's daily limit.", + renterMessage: + "You've reached your card's daily limit. Please try tomorrow or use a different card.", + canOwnerRetry: true, + requiresNewPaymentMethod: false, + }, + not_permitted: { + ownerMessage: "This transaction is not permitted on the renter's card.", + renterMessage: + "This transaction is not permitted on your card. Please use a different card.", + canOwnerRetry: false, + requiresNewPaymentMethod: true, + }, + invalid_amount: { + ownerMessage: "The payment amount is invalid.", + renterMessage: + "There was an issue with the payment amount. Please contact support.", + canOwnerRetry: false, + requiresNewPaymentMethod: false, + }, + security_violation: { + ownerMessage: "The payment was blocked for security reasons.", + renterMessage: + "Your payment was blocked for security reasons. Please contact your bank.", + canOwnerRetry: false, + requiresNewPaymentMethod: true, + }, + stop_payment_order: { + ownerMessage: "A stop payment order exists on this card.", + renterMessage: + "A stop payment has been placed. Please contact your bank or use a different card.", + canOwnerRetry: false, + requiresNewPaymentMethod: true, + }, + transaction_not_allowed: { + ownerMessage: "Renter's card does not allow this transaction type.", + renterMessage: + "This transaction is not allowed on your card. Please use a different card.", + canOwnerRetry: false, + requiresNewPaymentMethod: true, + }, + + // ACH/Bank Account Declines + bank_account_closed: { + ownerMessage: "The renter's bank account has been closed.", + renterMessage: + "Your bank account has been closed. Please add a different payment method.", + canOwnerRetry: false, + requiresNewPaymentMethod: true, + }, + bank_account_frozen: { + ownerMessage: "The renter's bank account is frozen.", + renterMessage: + "Your bank account is frozen. Please contact your bank or use a different payment method.", + canOwnerRetry: false, + requiresNewPaymentMethod: true, + }, + bank_account_restricted: { + ownerMessage: "The renter's bank account has restrictions.", + renterMessage: + "Your bank account has restrictions. Please contact your bank or use a different payment method.", + canOwnerRetry: false, + requiresNewPaymentMethod: true, + }, + bank_account_unusable: { + ownerMessage: "The renter's bank account cannot be used.", + renterMessage: + "Your bank account cannot be used for payments. Please add a different payment method.", + canOwnerRetry: false, + requiresNewPaymentMethod: true, + }, + bank_account_invalid_details: { + ownerMessage: "The renter's bank account details are invalid.", + renterMessage: + "Your bank account details appear to be invalid. Please re-enter your bank information.", + canOwnerRetry: false, + requiresNewPaymentMethod: true, + }, + debit_not_authorized: { + ownerMessage: "The renter's bank account is not authorized for debits.", + renterMessage: + "Your bank account is not authorized for automatic debits. Please enable ACH payments or use a card.", + canOwnerRetry: false, + requiresNewPaymentMethod: true, + }, }; // Default error for unknown decline codes const DEFAULT_ERROR = { ownerMessage: "The payment could not be processed.", - renterMessage: "Your payment could not be processed. Please try a different payment method.", + renterMessage: + "Your payment could not be processed. Please try a different payment method.", canOwnerRetry: false, requiresNewPaymentMethod: true, }; @@ -179,7 +309,10 @@ function parseStripeError(error) { }; } - if (error.type === "StripeAPIError" || error.type === "StripeConnectionError") { + if ( + error.type === "StripeAPIError" || + error.type === "StripeConnectionError" + ) { return { code: "api_error", ownerMessage: "A temporary error occurred. Please try again.",