Refunds travel in both directions. Store-side requests go through the ledger described in Concurrency and idempotency.
EDD → Paddle
- A checkbox Refund the payment in Paddle is added to EDD’s refund modal (
edd_after_submit_refund_table), ticked by default. edd_refund_order(10, 3) runs only for admin form submissions ($_POST['data'], capabilityedit_shop_payments). It callsdriver->refund( $order, $amount, $full, '', $refund )with$full = $all_refunded && first refund.- Billing:
GET /adjustments?transaction_id=…&action=refundsubtracts earlier pending or approved refunds per line. Then typefull, or partialitems[]: the refunded EDD items’ lines first (via_wpinterface_paddle_line_mapand the refund record’s items), fees to the fee line, and the rest spread over lines that still have money left. EDD amounts are scaled by Paddle total ÷ EDD total, because Paddle may have added tax. Throws if more is asked than is left. - Classic:
payment/refundwith the order’s Paddle order ID; partial amounts are scaled the same way. - On success, an
edd_order_transactionsrow is added to the refund record and a note to the order. On failure, a note explains how to finish the refund in Paddle.
Paddle → EDD
Orders::record_refund( $order, $amount, $full, $note, $targets = array() ) runs under the order lock with a fresh read:
- If the refund covers what is left, or is full:
edd_refund_order( $id ). - Otherwise
partial_refund_lines()buildsedd_refund_order( $id, $items, $adjustments ). Lines Paddle named come first ($targets:{type: item, cart_index, amount}or{type: fee, amount}, built byBilling_Driver::refund_targets()from_wpinterface_paddle_line_itemsand_wpinterface_paddle_line_map), then items, then fees. It respects each line’sget_refundable_amounts()and emits one entry per line. edd_is_order_refundable_by_overrideis forced to true for the call, because webhooks have no user and EDD’s refund window would otherwise refuse.- Software Licensing revokes licenses when items become refunded (full refunds).