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'); }); }); });