Files
rentall-app/backend/tests/unit/utils/stripeErrors.test.js
2026-01-08 15:00:12 -05:00

337 lines
11 KiB
JavaScript

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