Payment Retries
The retry system automatically processes charge attempts for overdue invoices.
Attempt Statuses
| Status | Description |
|---|---|
pending | Awaiting processing |
processing | Being processed (claimed by worker) |
succeeded | Payment successful |
failed | Payment declined, more attempts available |
exhausted | Maximum attempts reached |
Attempt Fields
| Field | Description |
|---|---|
| Invoice | Associated invoice |
| Payment | Created payment record |
| Attempt number | Sequential (1, 2, 3...) |
| Payment method | Method used for the attempt |
| Amount (cents) | Amount to be charged |
| Failure code | Decline reason |
| Failure message | Detailed error description |
| Scheduled for | Attempt date/time |
| Processed at | Processing date/time |
| Next retry at | When the next attempt will be made |
Retry Worker
The worker runs every 6 hours and:
- Finds attempts with
status=pendingandscheduledFor ≤ now - Atomically claims each attempt (
pending → processing) to avoid duplication - For each attempt:
- Checks if the invoice is already paid (skips)
- Creates a payment record
- Processes the payment via gateway
- On success:
- Marks the invoice as paid
- Creates a recovery record
- Removes the account from the delinquent list
- On failure:
- If attempts remain: schedules the next one
- If attempts exhausted: marks as
exhaustedand suspends the account (if configured)
Race Condition Prevention
The system uses atomic updateMany with status verification to ensure only one worker processes each attempt:
WHERE id = X AND status = 'pending'
SET status = 'processing'
If count = 0, another worker has already claimed the attempt.
Retry for Installments
Retries can also target specific installments:
- The
metadata.installmentIdidentifies the installment - The retry amount is for the installment, not the entire invoice