Skip to content

Refunds & chargebacks

Refunds & chargebacks

Refunds and chargebacks both reverse money, so both are ledger reversals — not mutations of a posted entry (the ledger is append-only).

Refunds

A RefundRequest carries the amount, a RefundReason, and a ReversalKind; Refunder produces a Refund:

interface Refunder
{
    public function refund(RefundRequest $request): Refund;
}

DefaultRefunder posts the reversal to the ledger via ReversalPosting and records it to a RefundRepository. It raises CannotRefund when the request exceeds the refundable amount, so you cannot refund more than was charged. Refunds against the gateway go through the PaymentGateway::refund seam.

Chargebacks

A chargeback is a bank-initiated reversal. A ChargebackNotice drives ChargebackHandler:

interface ChargebackHandler
{
    public function handle(ChargebackNotice $notice): Chargeback;
}

DefaultChargebackHandler records the Chargeback to a ChargebackRegister and posts the reversal, and can flag the account Disputed so standing reflects the open dispute. Because the register is separate from refunds, a chargeback that follows a refund (or vice versa) is reconcilable rather than double-counted.

Testing

InteractsWithRefunds, FakeRefunder, and FakeChargebackHandler drive the reversal postings and the over-refund guard.