508 lines
16 KiB
JavaScript
508 lines
16 KiB
JavaScript
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);
|
|
});
|
|
});
|
|
});
|