440 lines
15 KiB
JavaScript
440 lines
15 KiB
JavaScript
/**
|
|
* Stripe Payment Error Handling Utility
|
|
*
|
|
* Maps Stripe decline codes to user-friendly messages for both owners and renters.
|
|
* Provides structured error information for frontend handling.
|
|
*/
|
|
|
|
const logger = require('./logger');
|
|
|
|
const DECLINE_MESSAGES = {
|
|
authentication_required: {
|
|
ownerMessage: "The renter's card requires additional authentication.",
|
|
renterMessage:
|
|
"Your card requires authentication to complete this payment.",
|
|
canOwnerRetry: false,
|
|
requiresNewPaymentMethod: false,
|
|
requires3DS: true,
|
|
},
|
|
insufficient_funds: {
|
|
ownerMessage: "The renter's card has insufficient funds.",
|
|
renterMessage: "Your card has insufficient funds.",
|
|
canOwnerRetry: false,
|
|
requiresNewPaymentMethod: false, // renter might add funds
|
|
},
|
|
card_declined: {
|
|
ownerMessage: "The renter's card was declined.",
|
|
renterMessage:
|
|
"Your card was declined. Please contact your bank or try a different card.",
|
|
canOwnerRetry: false,
|
|
requiresNewPaymentMethod: true,
|
|
},
|
|
generic_decline: {
|
|
ownerMessage: "The renter's card was declined.",
|
|
renterMessage: "Your card was declined. Please try a different card.",
|
|
canOwnerRetry: false,
|
|
requiresNewPaymentMethod: true,
|
|
},
|
|
expired_card: {
|
|
ownerMessage: "The renter's card has expired.",
|
|
renterMessage: "Your card has expired. Please add a new payment method.",
|
|
canOwnerRetry: false,
|
|
requiresNewPaymentMethod: true,
|
|
},
|
|
processing_error: {
|
|
ownerMessage:
|
|
"A payment processing error occurred. This is usually temporary.",
|
|
renterMessage: "A temporary error occurred processing your payment.",
|
|
canOwnerRetry: true, // Owner can retry immediately
|
|
requiresNewPaymentMethod: false,
|
|
},
|
|
lost_card: {
|
|
ownerMessage: "The renter's card cannot be used.",
|
|
renterMessage:
|
|
"Your card cannot be used. Please add a different payment method.",
|
|
canOwnerRetry: false,
|
|
requiresNewPaymentMethod: true,
|
|
},
|
|
stolen_card: {
|
|
ownerMessage: "The renter's card cannot be used.",
|
|
renterMessage:
|
|
"Your card cannot be used. Please add a different payment method.",
|
|
canOwnerRetry: false,
|
|
requiresNewPaymentMethod: true,
|
|
},
|
|
incorrect_cvc: {
|
|
ownerMessage: "Payment verification failed.",
|
|
renterMessage:
|
|
"Your card couldn't be verified. Please re-enter your payment details.",
|
|
canOwnerRetry: false,
|
|
requiresNewPaymentMethod: true,
|
|
},
|
|
invalid_cvc: {
|
|
ownerMessage: "Payment verification failed.",
|
|
renterMessage:
|
|
"Your card couldn't be verified. Please re-enter your payment details.",
|
|
canOwnerRetry: false,
|
|
requiresNewPaymentMethod: true,
|
|
},
|
|
fraudulent: {
|
|
ownerMessage: "This payment was blocked for security reasons.",
|
|
renterMessage:
|
|
"Your payment was blocked by our security system. Please try a different card.",
|
|
canOwnerRetry: false,
|
|
requiresNewPaymentMethod: true,
|
|
},
|
|
incorrect_zip: {
|
|
ownerMessage: "Renter's billing address couldn't be verified.",
|
|
renterMessage:
|
|
"Your billing address couldn't be verified. Please update your payment method.",
|
|
canOwnerRetry: false,
|
|
requiresNewPaymentMethod: true,
|
|
},
|
|
card_velocity_exceeded: {
|
|
ownerMessage:
|
|
"Too many payment attempts on this card. Please try again later.",
|
|
renterMessage:
|
|
"Too many attempts on your card. Please wait or try a different card.",
|
|
canOwnerRetry: true, // After delay
|
|
requiresNewPaymentMethod: false,
|
|
},
|
|
do_not_honor: {
|
|
ownerMessage: "The renter's card was declined.",
|
|
renterMessage:
|
|
"Your card was declined. Please contact your bank or try a different card.",
|
|
canOwnerRetry: false,
|
|
requiresNewPaymentMethod: true,
|
|
},
|
|
invalid_account: {
|
|
ownerMessage: "The renter's card account is invalid.",
|
|
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.",
|
|
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.",
|
|
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.",
|
|
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.",
|
|
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,
|
|
},
|
|
};
|
|
|
|
// 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.",
|
|
canOwnerRetry: false,
|
|
requiresNewPaymentMethod: true,
|
|
};
|
|
|
|
// Mapping for StripeInvalidRequestError codes
|
|
const INVALID_REQUEST_MESSAGES = {
|
|
resource_missing: {
|
|
ownerMessage: "The renter's payment method is no longer valid.",
|
|
renterMessage:
|
|
"Your payment method is no longer valid. Please add a new payment method.",
|
|
canOwnerRetry: false,
|
|
requiresNewPaymentMethod: true,
|
|
},
|
|
payment_method_invalid: {
|
|
ownerMessage: "The renter's payment method is invalid.",
|
|
renterMessage:
|
|
"Your payment method is invalid. Please add a new payment method.",
|
|
canOwnerRetry: false,
|
|
requiresNewPaymentMethod: true,
|
|
},
|
|
payment_intent_unexpected_state: {
|
|
ownerMessage: "This payment is in an unexpected state.",
|
|
renterMessage: "There was an issue with your payment. Please try again.",
|
|
canOwnerRetry: true,
|
|
requiresNewPaymentMethod: false,
|
|
},
|
|
customer_deleted: {
|
|
ownerMessage: "The renter's payment profile has been deleted.",
|
|
renterMessage:
|
|
"Your payment profile needs to be set up again. Please add a new payment method.",
|
|
canOwnerRetry: false,
|
|
requiresNewPaymentMethod: true,
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Parse a Stripe error and return structured error information
|
|
* @param {Error} error - The error object from Stripe
|
|
* @returns {Object} Structured error with code, messages, and retry info
|
|
*/
|
|
function parseStripeError(error) {
|
|
// Check if this is a Stripe error
|
|
if (error.type === "StripeCardError" || error.code === "card_declined") {
|
|
const declineCode = error.decline_code || error.code || "card_declined";
|
|
const errorInfo = DECLINE_MESSAGES[declineCode] || DEFAULT_ERROR;
|
|
|
|
// Log if we're falling back to default for an unknown decline code
|
|
if (!DECLINE_MESSAGES[declineCode]) {
|
|
logger.warn("[StripeErrors] Unknown decline code - please add mapping", {
|
|
declineCode,
|
|
errorCode: error.code,
|
|
errorType: error.type,
|
|
errorMessage: error.message,
|
|
});
|
|
}
|
|
|
|
return {
|
|
code: declineCode,
|
|
ownerMessage: errorInfo.ownerMessage,
|
|
renterMessage: errorInfo.renterMessage,
|
|
canOwnerRetry: errorInfo.canOwnerRetry,
|
|
requiresNewPaymentMethod: errorInfo.requiresNewPaymentMethod,
|
|
// Include original error info for logging (not for users)
|
|
_originalMessage: error.message,
|
|
_stripeCode: error.code,
|
|
};
|
|
}
|
|
|
|
// Handle other Stripe error types
|
|
if (error.type === "StripeInvalidRequestError") {
|
|
// First, check if there's a decline_code in the error (some bank errors include this)
|
|
if (error.decline_code && DECLINE_MESSAGES[error.decline_code]) {
|
|
const errorInfo = DECLINE_MESSAGES[error.decline_code];
|
|
return {
|
|
code: error.decline_code,
|
|
ownerMessage: errorInfo.ownerMessage,
|
|
renterMessage: errorInfo.renterMessage,
|
|
canOwnerRetry: errorInfo.canOwnerRetry,
|
|
requiresNewPaymentMethod: errorInfo.requiresNewPaymentMethod,
|
|
_originalMessage: error.message,
|
|
_stripeCode: error.code,
|
|
};
|
|
}
|
|
|
|
// Check if error.code has a specific mapping
|
|
const errorCode = error.code;
|
|
if (errorCode && INVALID_REQUEST_MESSAGES[errorCode]) {
|
|
const errorInfo = INVALID_REQUEST_MESSAGES[errorCode];
|
|
return {
|
|
code: errorCode,
|
|
ownerMessage: errorInfo.ownerMessage,
|
|
renterMessage: errorInfo.renterMessage,
|
|
canOwnerRetry: errorInfo.canOwnerRetry,
|
|
requiresNewPaymentMethod: errorInfo.requiresNewPaymentMethod,
|
|
_originalMessage: error.message,
|
|
_stripeCode: error.code,
|
|
};
|
|
}
|
|
|
|
// Also check DECLINE_MESSAGES for the error code (some codes appear in both contexts)
|
|
if (errorCode && DECLINE_MESSAGES[errorCode]) {
|
|
const errorInfo = DECLINE_MESSAGES[errorCode];
|
|
return {
|
|
code: errorCode,
|
|
ownerMessage: errorInfo.ownerMessage,
|
|
renterMessage: errorInfo.renterMessage,
|
|
canOwnerRetry: errorInfo.canOwnerRetry,
|
|
requiresNewPaymentMethod: errorInfo.requiresNewPaymentMethod,
|
|
_originalMessage: error.message,
|
|
_stripeCode: error.code,
|
|
};
|
|
}
|
|
|
|
// Log unhandled StripeInvalidRequestError codes for future mapping
|
|
logger.warn(
|
|
"[StripeErrors] Unhandled StripeInvalidRequestError - please add mapping",
|
|
{
|
|
errorCode: error.code,
|
|
declineCode: error.decline_code,
|
|
errorMessage: error.message,
|
|
param: error.param,
|
|
}
|
|
);
|
|
|
|
return {
|
|
code: errorCode || "invalid_request",
|
|
ownerMessage: "There was a problem processing this payment.",
|
|
renterMessage: "There was a problem with your payment method.",
|
|
canOwnerRetry: false,
|
|
requiresNewPaymentMethod: true,
|
|
_originalMessage: error.message,
|
|
_stripeCode: error.code,
|
|
};
|
|
}
|
|
|
|
if (
|
|
error.type === "StripeAPIError" ||
|
|
error.type === "StripeConnectionError"
|
|
) {
|
|
return {
|
|
code: "api_error",
|
|
ownerMessage: "A temporary error occurred. Please try again.",
|
|
renterMessage: "A temporary error occurred processing your payment.",
|
|
canOwnerRetry: true,
|
|
requiresNewPaymentMethod: false,
|
|
_originalMessage: error.message,
|
|
_stripeCode: error.code,
|
|
};
|
|
}
|
|
|
|
if (error.type === "StripeRateLimitError") {
|
|
return {
|
|
code: "rate_limit",
|
|
ownerMessage: "Too many requests. Please wait a moment and try again.",
|
|
renterMessage: "Please wait a moment and try again.",
|
|
canOwnerRetry: true,
|
|
requiresNewPaymentMethod: false,
|
|
_originalMessage: error.message,
|
|
_stripeCode: error.code,
|
|
};
|
|
}
|
|
|
|
// Default fallback for unknown errors
|
|
logger.warn("[StripeErrors] Unknown error type - please investigate", {
|
|
errorType: error.type,
|
|
errorCode: error.code,
|
|
declineCode: error.decline_code,
|
|
errorMessage: error.message,
|
|
});
|
|
|
|
return {
|
|
code: "unknown_error",
|
|
...DEFAULT_ERROR,
|
|
_originalMessage: error.message,
|
|
_stripeCode: error.code,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Custom PaymentError class for structured payment failures
|
|
*/
|
|
class PaymentError extends Error {
|
|
constructor(parsedError) {
|
|
super(parsedError.ownerMessage);
|
|
this.name = "PaymentError";
|
|
this.code = parsedError.code;
|
|
this.ownerMessage = parsedError.ownerMessage;
|
|
this.renterMessage = parsedError.renterMessage;
|
|
this.canOwnerRetry = parsedError.canOwnerRetry;
|
|
this.requiresNewPaymentMethod = parsedError.requiresNewPaymentMethod;
|
|
this._originalMessage = parsedError._originalMessage;
|
|
this._stripeCode = parsedError._stripeCode;
|
|
}
|
|
|
|
toJSON() {
|
|
return {
|
|
code: this.code,
|
|
ownerMessage: this.ownerMessage,
|
|
renterMessage: this.renterMessage,
|
|
canOwnerRetry: this.canOwnerRetry,
|
|
requiresNewPaymentMethod: this.requiresNewPaymentMethod,
|
|
};
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
parseStripeError,
|
|
PaymentError,
|
|
DECLINE_MESSAGES,
|
|
};
|