Paddle delivers transaction.paid and transaction.completed (and adjustment.created / .updated) about a second apart, often concurrently, and retries on 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_lock_' . md5( $key )).
| Key | Used by |
|---|---|
order:{id} | Orders::complete(), Orders::record_refund(), Orders::with_order_lock() (Billing adjustments, underpaid notes) |
order:{id}:subscriptions | Orders::activate_subscriptions() |
renewal:{transaction} | Orders::record_renewal(): if the lock is not acquired it throws, so the webhook returns 500 and Paddle retries |
refund-alert:{order id} | Classic refund alerts |
Fresh reads
A request that waited for a lock may hold a stale copy of the order in the object cache. Orders::fresh_order( $id ) deletes the BerlinDB cache entries (edd-orders, orders) and order meta (edd_order_meta) before re-reading. Every locked section starts with a fresh read.
Markers
_wpinterface_paddle_adjustment_ids (order meta, a list) records processed references: Billing adjustment IDs and {id}:approved, {id}:pending, {id}:rejected; Classic alert:{alert_id}, dispute:…, duplicate:{paddle order}, stale-link:…, plan-mismatch:…, superseded:{txn}, underpaid:{txn}, unrecorded:…. Use Orders::has_adjustment() / add_adjustment(). Billing event IDs are kept in the transient wpinterface_paddle_evt_{md5}.
Renewal state machine
Orders::record_renewal:
lock renewal:{txn} (not acquired → throw → HTTP 500 → Paddle retries)
option wpinterface_paddle_renewing_{md5 txn} set unless present ← "this plugin started it"
add_payment() → new renewal order …… or false (transaction already recorded)
false + no "started" option → return false (recorded earlier, or by hand in EDD Recurring)
false + "started" option → resume with the existing order of this subscription
if no _paddle_renewal_applied on the order:
renew() (expiration forced to Paddle's period end; Software Licensing extends licenses at priority 10)
edd_subscription_post_renew priority 11 → _paddle_renewal_applied = 1
delete option; return renewal order ID → driver stores Paddle totals, tax, transaction ID, noteThis makes every combination safe: duplicate deliveries, a crash inside add_payment() hooks, a crash after Software Licensing already extended the license, and renewals an admin recorded by hand.
Store refund ledger
Refunds started in EDD are recorded in order meta _wpinterface_paddle_refund_requests before Paddle is called (Orders::add_refund_request()). Each entry has a marker ([EDD refund {id}], appended to the refund reason sent to Paddle), the amount in EDD terms, full, and status (sending, sent, failed). When Paddle reports a refund, Orders::take_refund_request() matches it by marker, or else by amount or full refund. A match means it is already recorded in EDD. This covers timeouts after Paddle accepted the request, and refunds completed by hand in Paddle after a failure. Billing also records the adjustment ID immediately on success.