woocommerce Case study
Recover MU Plugin
Two things were leaking revenue: carts abandoned before checkout, and orders that reached the gateway and stopped. This plugin handles both, and along the way it fixes a WooCommerce behaviour that was leaving unpaid orders stuck in pending indefinitely.
The business problem
Existing recovery plugins wanted to own the customer record, add their own tracking and send on schedules that ignore how this store actually operates. The unpaid-order half was a separate problem: WooCommerce's own cancellation of unpaid orders did not reliably fire here, so pending orders accumulated and both the reporting and the stock position drifted.
What I delivered
- a2-recover-sms-mu.php, a 1,295-line MU plugin scheduling recovery messages through Action Scheduler in its own group rather than on wp-cron.
- Recovery triggers at four distinct points: order processed at checkout, order entering pending, order entering on-hold, and add-to-cart or checkout-view for carts that never became orders.
- A pending-to-failed enforcement path with a 31-minute timeout, which is deliberately just past the gateway's own window.
- A five-minute sweep with a lock, plus a watchdog on wp_loaded, so the transition still happens on a site where scheduled events are unreliable.
- An interception of WooCommerce's own unpaid-order cancellation, so the two mechanisms cannot both act on the same order.
- Short links that resolve to specific category or gift-card destinations, so a recovery message can point at something more useful than the cart.
- Coupon application from a cookie at three separate cart and checkout hooks, so an incentive attached to a recovery message survives the customer's route back.
Technical approach
- Action Scheduler rather than wp-cron, in a dedicated group, because recovery messages must not be dropped when a cron run is missed and must be inspectable when a customer asks why they received one.
- The 31-minute timeout is one minute past the gateway window on purpose, so the plugin never races the payment provider for the same order.
- The sweep takes a lock before running, since two overlapping sweeps on a slow site is how a customer receives the same message twice.
- The wp_loaded watchdog exists because scheduled events on a store behind aggressive caching cannot be relied on; the sweep is the primary path and the watchdog is the backstop.
- The SMS template name and API key are read from constants defined elsewhere, so no credential lives in this file.
Result and evidence
Recovery messaging runs on the store, and pending orders now transition to failed on a predictable timetable instead of accumulating. I do not have an isolated recovered-revenue figure, because the coupon and the message went live together and I cannot separate them honestly; the operational result I can state is that the pending backlog stopped growing.
Commercial value
Both halves pay for themselves in ordinary operation: recovered carts on one side, and accurate order status on the other, which is what stock levels and daily reporting depend on.
Readable implementation brief
implementation_brief {
project: "Recover MU Plugin"
file: "mu-plugins/a2-recover-sms-mu.php (1295 lines, v0.2.0)"
queue: "Action Scheduler, dedicated group, not wp-cron"
triggers: "checkout processed, order pending, order on-hold,
add-to-cart, checkout viewed"
pending_rule: "pending -> failed after 31 minutes
(one minute past the gateway window)"
reliability: "5-minute locked sweep + wp_loaded watchdog"
conflict: "intercepts Woo's own unpaid-order cancellation"
return_path: "short links to category/gift-card targets,
coupon restored from cookie at 3 hooks"
secrets: "API key and template read from external constants"
}What this project shows
The part I would defend in review is the timeout choice and the lock. Recovery messaging is a system where the failure mode is messaging a real customer twice, or messaging someone who already paid.
Building the watchdog as a backstop to the sweep rather than as the main path is the same instinct: the primary mechanism should be the correct one, and the backstop should only matter when the environment misbehaves.