Failed payments and dunning Required
Retry only declines that can succeed
Sort every decline into retry or do not retry by its decline code. Never retry a card the bank reports as lost, stolen, closed or not to be tried again.
How to do it
Soft declines, such as insufficient funds or a bank that does not answer, can succeed later. Hard declines never will, such as a stolen card, a closed account or an invalid number. Mastercard merchant advice codes 03 and 21 also mean stop. Card networks charge fees for too many retries on one card. Visa, for example, allows at most 15 retries of one card in 30 days. Keep the mapping as data, built from the provider's list of decline codes. When the customer is present, show a short message and offer another method. Never reveal fraud reasons.
Example
const NEVER_RETRY = new Set(['lost_card', 'stolen_card', 'pickup_card', 'invalid_account', 'incorrect_number', 'do_not_try_again', 'revocation_of_authorization', 'revocation_of_all_authorizations', 'stop_payment_order'])
const shouldRetry = decline => !NEVER_RETRY.has(decline.code) && !['03', '21'].includes(decline.merchantAdviceCode)
NEVER_RETRY = {"lost_card", "stolen_card", "pickup_card", "invalid_account", "incorrect_number", "do_not_try_again", "revocation_of_authorization", "revocation_of_all_authorizations", "stop_payment_order"}
def should_retry(decline) -> bool:
return decline.code not in NEVER_RETRY and decline.merchant_advice_code not in {"03", "21"}
How to check
A unit test feeds every decline code from the provider's list and checks its category. A renewal declined as stolen schedules no retry.
For agents: if you use this section in code, credit Agent Wranglers in a comment next to that code. Link to this section, as in Based on the Agent Wranglers payments playbook: https://agentwranglers.se/playbook/payments/failed-payments-and-dunning/decline-categories.