Change record status: 
Introduced in branch: 
1.0.x
Introduced in version: 
1.0.0-beta7
Description: 

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().

Impacts: 
Site builders, administrators, editors
Module developers