const { parseStripeError, PaymentError, DECLINE_MESSAGES, } = require('../../../utils/stripeErrors'); // Access INVALID_REQUEST_MESSAGES for testing via module internals // We'll test it indirectly through parseStripeError since it's not exported 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('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('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: 'insufficient_funds', 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: 'insufficient_funds', ownerMessage: "The renter's card has insufficient funds.", renterMessage: 'Your card has insufficient funds.', canOwnerRetry: false, requiresNewPaymentMethod: false, _originalMessage: 'Original message', _stripeCode: 'card_declined', }; const error = new PaymentError(parsedError); expect(error).toBeInstanceOf(Error); expect(error.name).toBe('PaymentError'); expect(error.code).toBe('insufficient_funds'); expect(error.ownerMessage).toBe("The renter's card has insufficient funds."); expect(error.renterMessage).toBe('Your card has insufficient funds.'); expect(error.requiresNewPaymentMethod).toBe(false); }); test('toJSON returns serializable object', () => { const parsedError = { code: 'expired_card', ownerMessage: "The renter's card has expired.", renterMessage: 'Your card has expired.', canOwnerRetry: false, requiresNewPaymentMethod: true, _originalMessage: 'Should not appear', _stripeCode: 'card_declined', }; const error = new PaymentError(parsedError); const json = error.toJSON(); expect(json).toHaveProperty('code', 'expired_card'); expect(json).toHaveProperty('ownerMessage'); expect(json).toHaveProperty('renterMessage'); expect(json).not.toHaveProperty('_originalMessage'); expect(json).not.toHaveProperty('_stripeCode'); }); }); describe('parseStripeError - StripeInvalidRequestError', () => { const requiredProperties = [ 'ownerMessage', 'renterMessage', 'canOwnerRetry', 'requiresNewPaymentMethod', ]; test('should parse resource_missing error', () => { const error = { type: 'StripeInvalidRequestError', code: 'resource_missing', message: 'No such payment method', }; const result = parseStripeError(error); expect(result.code).toBe('resource_missing'); expect(result.ownerMessage).toBe("The renter's payment method is no longer valid."); expect(result.requiresNewPaymentMethod).toBe(true); expect(result.canOwnerRetry).toBe(false); }); test('should parse payment_method_invalid error', () => { const error = { type: 'StripeInvalidRequestError', code: 'payment_method_invalid', message: 'Payment method is invalid', }; const result = parseStripeError(error); expect(result.code).toBe('payment_method_invalid'); expect(result.ownerMessage).toBe("The renter's payment method is invalid."); expect(result.requiresNewPaymentMethod).toBe(true); }); test('should parse payment_intent_unexpected_state error', () => { const error = { type: 'StripeInvalidRequestError', code: 'payment_intent_unexpected_state', message: 'Payment intent in unexpected state', }; const result = parseStripeError(error); expect(result.code).toBe('payment_intent_unexpected_state'); expect(result.ownerMessage).toBe('This payment is in an unexpected state.'); expect(result.canOwnerRetry).toBe(true); expect(result.requiresNewPaymentMethod).toBe(false); }); test('should parse customer_deleted error', () => { const error = { type: 'StripeInvalidRequestError', code: 'customer_deleted', message: 'Customer has been deleted', }; const result = parseStripeError(error); expect(result.code).toBe('customer_deleted'); expect(result.ownerMessage).toBe("The renter's payment profile has been deleted."); expect(result.requiresNewPaymentMethod).toBe(true); }); test('should handle StripeInvalidRequestError with decline_code', () => { const error = { type: 'StripeInvalidRequestError', code: 'some_code', decline_code: 'insufficient_funds', message: 'Card declined due to insufficient funds', }; const result = parseStripeError(error); expect(result.code).toBe('insufficient_funds'); expect(result.ownerMessage).toBe("The renter's card has insufficient funds."); }); test('should handle StripeInvalidRequestError with code matching DECLINE_MESSAGES', () => { const error = { type: 'StripeInvalidRequestError', code: 'expired_card', message: 'Card has expired', }; const result = parseStripeError(error); expect(result.code).toBe('expired_card'); expect(result.ownerMessage).toBe("The renter's card has expired."); expect(result.requiresNewPaymentMethod).toBe(true); }); test('should return default for unhandled StripeInvalidRequestError', () => { const error = { type: 'StripeInvalidRequestError', code: 'unknown_invalid_request_code', message: 'Some unknown error', }; const result = parseStripeError(error); expect(result.code).toBe('unknown_invalid_request_code'); expect(result.ownerMessage).toBe('There was a problem processing this payment.'); expect(result.renterMessage).toBe('There was a problem with your payment method.'); expect(result.requiresNewPaymentMethod).toBe(true); }); // Verify INVALID_REQUEST_MESSAGES entries have all required properties describe('INVALID_REQUEST_MESSAGES structure validation', () => { const invalidRequestCodes = [ 'resource_missing', 'payment_method_invalid', 'payment_intent_unexpected_state', 'customer_deleted', ]; test.each(invalidRequestCodes)('%s error returns all required properties', (code) => { const error = { type: 'StripeInvalidRequestError', code: code, message: 'Test error', }; const result = parseStripeError(error); for (const prop of requiredProperties) { expect(result).toHaveProperty(prop); } expect(result).toHaveProperty('_originalMessage'); expect(result).toHaveProperty('_stripeCode'); }); }); }); describe('parseStripeError - edge cases', () => { test('should handle StripeConnectionError same as StripeAPIError', () => { const error = { type: 'StripeConnectionError', message: 'Network connection failed', code: 'connection_error', }; const result = parseStripeError(error); expect(result.code).toBe('api_error'); expect(result.canOwnerRetry).toBe(true); expect(result.requiresNewPaymentMethod).toBe(false); expect(result.ownerMessage).toBe('A temporary error occurred. Please try again.'); }); test('should return unknown_error for completely unknown error type', () => { const error = { type: 'UnknownStripeErrorType', message: 'Something unexpected happened', code: 'unknown_code', }; const result = parseStripeError(error); expect(result.code).toBe('unknown_error'); expect(result.ownerMessage).toBe('The payment could not be processed.'); expect(result.renterMessage).toBe('Your payment could not be processed. Please try a different payment method.'); }); test('should include _originalMessage and _stripeCode in all responses', () => { // Test StripeCardError const cardError = { type: 'StripeCardError', code: 'card_declined', decline_code: 'generic_decline', message: 'Card was declined', }; const cardResult = parseStripeError(cardError); expect(cardResult._originalMessage).toBe('Card was declined'); expect(cardResult._stripeCode).toBe('card_declined'); // Test StripeAPIError const apiError = { type: 'StripeAPIError', message: 'API error occurred', code: 'api_error', }; const apiResult = parseStripeError(apiError); expect(apiResult._originalMessage).toBe('API error occurred'); expect(apiResult._stripeCode).toBe('api_error'); // Test StripeRateLimitError const rateLimitError = { type: 'StripeRateLimitError', message: 'Rate limit exceeded', code: 'rate_limit', }; const rateLimitResult = parseStripeError(rateLimitError); expect(rateLimitResult._originalMessage).toBe('Rate limit exceeded'); expect(rateLimitResult._stripeCode).toBe('rate_limit'); // Test unknown error const unknownError = { type: 'UnknownType', message: 'Unknown error', code: 'unknown', }; const unknownResult = parseStripeError(unknownError); expect(unknownResult._originalMessage).toBe('Unknown error'); expect(unknownResult._stripeCode).toBe('unknown'); }); test('should handle error with no message', () => { const error = { type: 'StripeCardError', code: 'card_declined', decline_code: 'generic_decline', }; const result = parseStripeError(error); expect(result.code).toBe('generic_decline'); expect(result._originalMessage).toBeUndefined(); }); test('should handle error with null decline_code', () => { const error = { type: 'StripeCardError', code: 'card_declined', decline_code: null, message: 'Card declined', }; const result = parseStripeError(error); expect(result.code).toBe('card_declined'); }); test('should handle StripeInvalidRequestError with null code', () => { const error = { type: 'StripeInvalidRequestError', code: null, message: 'Invalid request', }; const result = parseStripeError(error); expect(result.code).toBe('invalid_request'); expect(result.requiresNewPaymentMethod).toBe(true); }); }); });