Paddle delivers transaction.paid and transaction.completed (and adjustment.created / .updated) about a second apart, often concurrently, and retries any non-2xx response. The plugin therefore never relies on “check, then act” without a lock.
Locks
Orders::lock( $key, $timeout = 15 ) / Orders::unlock( $key ) use MySQL GET_LOCK( 'wpinterface_paddle_woo_lock_' . md5( $key ), 15 ). Orders::with_order_lock( $order_id, $callback ) takes order:{id}, passes a fresh order to the callback and always releases. If the lock times out the work still goes ahead, except for renewals.
| Key | Used by |
|---|---|
order:{id} | Orders::complete(), Orders::record_refund(), Billing adjustments, Classic refund alerts, currency/underpaid notes |
renewal:{transaction} | Subscriptions::record_renewal(): if the lock is not acquired it throws, the webhook returns 500, and Paddle retries |
Billing adjustments and Classic refund alerts call Orders::record_refund() while already holding order:{id}, so the same name is locked twice by one connection (re-entrant on MySQL 5.7+ / MariaDB 10.0.2+).
Fresh reads
A request that waited for a lock may hold a stale order in the object cache. Orders::fresh_order( $id ) removes the order from WooCommerce’s OrderCache (HPOS), deletes the orders and post_meta cache entries, runs clean_post_cache(), and reloads with wc_get_order().
Markers
_wpinterface_paddle_adjustment_ids (order meta, a list) records processed references, through Orders::has_adjustment() / add_adjustment():
- Billing: the adjustment ID,
{id}:approved,{id}:pending,{id}:rejected,superseded:{txn},currency:{txn},underpaid:{txn}. - Classic:
alert:{alert_id},dispute:{alert_id},duplicate:{paddle order},currency:{paddle order},underpaid:{paddle order},stale-link:{hash},plan-mismatch:{subscription}.
Billing event IDs use the transient wpinterface_paddle_woo_evt_{md5} instead.
Completing an order
Orders::complete works under order:{id} with a fresh read. It returns false if the order is paid or refunded. Orders in pending, failed, on-hold, cancelled or checkout-draft are completed with payment_complete( $transaction_id ); a cancelled order gets a “reopened, check the stock” note. Any other status gets a note and is left unchanged.
Recording a renewal
lock renewal:{txn} (not acquired → throw → HTTP 500 → Paddle retries)
existing = Orders::find_by_transaction( txn )
existing and paid → return false (already recorded)
existing and unpaid → finish it (a retry after a crash)
none → wcs_create_renewal_order( subscription )
set payment method, transaction ID, Paddle meta, save, note
payment_complete( txn ) with Subscriptions::$from_paddle = true
set_next_payment( Paddle's next billing time )
unlock$from_paddle stops the resulting status change (for example on-hold → active) from being sent back to Paddle.
Store refund ledger
Refunds that WooCommerce already knows about are kept in order meta _wpinterface_paddle_refund_requests so Paddle’s later report isn’t recorded twice. Entries hold marker, amount (null for a full refund), full and status.
- Refund via Paddle:
Orders::add_refund_request()writes a[WC refund {refund_id}]entry with statussendingbefore Paddle is called; the marker is appended to the reason sent to Paddle. Whether the call succeeds or fails, the entry is then removed. - Refund manually:
Refunds::remember_manual_refund()(onwoocommerce_order_refunded) writes a[WC manual refund {id}]entry with statusmanual. Orders::take_refund_request( $order, $reason, $wc_amount, $full )decides whether a refund Paddle reports is already in WooCommerce: first a non-manual entry whose marker is in Paddle’s reason; then, if the reason contains[WC refund N], whether that refund still exists; then any entry with the same amount (within 0.02) or both full. A matching entry is removed.