Refund internals

Updated

Refunds travel in both directions. Store-side requests go through the ledger described in Concurrency and idempotency.

WooCommerce → Paddle

WooCommerce calls Gateway::process_refund( $order_id, $amount, $reason ) for Refund via Paddle after it has created the WC_Order_Refund. The plugin finds that refund, sets $full = count( refunds ) <= 1 && amount >= order total − 0.01, and calls driver->refund(). On success it adds a note with Paddle’s reference and returns true; on failure it returns a WP_Error.

Why a failed refund deletes the WooCommerce refund

When process_refund() returns an error, WooCommerce’s wc_create_refund() deletes the refund it just created and shows the error. The ledger entry is removed too. If Paddle made the refund after all (a timeout), its webhook finds no matching entry and the refund is recorded then.

  • Billing: GET /adjustments?transaction_id=…&action=refund, keeping pending and approved ones. type: full when $full and nothing was refunded before. Otherwise type: partial with items: the Paddle lines of the refunded order lines first (through _wpinterface_paddle_line_map and each refund item’s _refunded_item_id), then the rest over lines that still have money left. WooCommerce amounts are scaled by Paddle total ÷ WooCommerce total, because Paddle may have added tax. Throws if more is asked than is left. The adjustment ID is added to the markers immediately.
  • Classic: payment/refund with the Paddle order ID, no amount for full refunds, or the amount scaled by _wpinterface_paddle_total ÷ order total.

Paddle → WooCommerce

Orders::record_refund( $order, $amount, $full, $note, $targets = array() ) runs under the order lock with a fresh read:

  • Adds the note. Returns false if nothing is left to refund.
  • If $full or the amount covers what is left, refunds what is left.
  • Calls wc_create_refund() with refund_payment => false, restock_items => false and reason “Refunded in Paddle”.
  • $targets become line items only when the order has no WooCommerce tax and the lines add up to the refund; otherwise the refund is recorded as an amount.
  • If WooCommerce refuses, a note asks the store owner to use Refund manually.

Webhook handling

Billing adjustment.* (found by transaction_id, same environment, under the order lock):

Action / statusResult
refund, rejectedOnce: a note; for store refunds a loud note that the customer was not refunded although WooCommerce shows the refund
refund, pending_approvalNote, unless it is a store refund
refund, approved, known IDApproved note once
refund, approved, unknown IDMarkers added; if take_refund_request() matches, a note only; otherwise record_refund()
chargeback, approvedOrder → on-hold
chargeback_warning, chargeback_reverseNote

Classic payment_refunded / subscription_payment_refunded (found by order ID, same environment, under the order lock): deduplicated by alert:{alert_id}; the amount is scaled to WooCommerce terms; take_refund_request() with the refund reason; VAT refunds only add a note; otherwise record_refund() (always an amount, never line targets).

Chargebacks

Billing chargeback (approved) and Classic payment_dispute_created put the order on hold with a note; each is processed once. Warnings and reversals only add notes. Nothing is refunded in WooCommerce automatically.

Was this page helpful?