A succeeded charge is no longer terminal. When the payer reverses a cleared payment — a SEPA refund, Bacs indemnity claim, ACH return, PAD dispute, or card chargeback — the charge record now has somewhere to land (#3619074: Succeeded charge records are terminal, so payer-initiated reversals (SEPA refunds, Bacs indemnity claims, chargebacks) have nowhere to land).
The reversed state
Charges gain a reversed state, reachable only from succeeded and terminal. The reason reported by the scheme is stored in a new reversal_reason field (queryable, in Views, shown on the admin listing); the reversed amount, any scheme fee, the timestamp, and the credit-note reference are recorded in the charge's data map. Update 10023 installs the field and refreshes the state field's allowed values.
Reversals are never retried automatically. Re-charging after a payer-initiated reversal is prohibited or hostile on most schemes, so a reversed charge is outside the dunning retry schedule entirely. Recovery is explicit: CycleEngine::rebillReversedCharge($charge) creates one new charge for the reversed amount over the same period (cross-linked with the original; a second re-bill is refused), for staff action or a "pay now" flow once the customer has fixed their payment instrument.
Consequence policy
What a reversal does to the subscription is site policy — the new Payer-initiated reversals setting (dunning.reversal_policy, update 10024):
past_due(default): the subscription goes past due with no retry scheduled; access continues through the grace period, then the normal suspension applies. Suits bank debits, where reversals are often benign.suspend: access is suspended immediately. Suits card chargebacks, more often a fraud signal.review: the subscription is untouched; only the event is raised for the site's own tooling to act on.
Sites running rails that deserve different answers can override the policy per connector (dunning.reversal_policy_overrides; one select per charge-based connector on the settings form).
Events and mails
A ChargeReversedEvent (ChargeEvents::REVERSED) carries the charge, subscription, amount, reason, fee, and the policy applied. The dunning events now carry a cause — DunningManager::CAUSE_CHARGE_FAILED, CAUSE_REMOTE, or CAUSE_REVERSAL — and the optional built-in mails use it to word a reversal correctly. The credit reaches the tax/invoice seam through the existing recordRefund(); a bridge that needs to tell a chargeback from a merchant refund reads the charge's state and reason.
For connector authors: mapping webhooks
When the remote service reports a reversal of a settled charge, load the charge record and call CycleEngine::reverseCharge($charge, $reason, $fee, $amount). Pass the scheme's reason code as $reason, any indemnity or chargeback fee as $fee, and omit $amount unless the reversal is partial (it defaults to the charge less any refunds). The call is idempotent, so repeated webhook deliveries are safe. Typical mappings: GoCardless payments.charged_back and payer-initiated refunds.created; card connectors' chargeback/dispute events. Merchant-initiated refunds stay on refund().