/** * 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, };