Dunning
When a charge fails, OSub_Dunning opens a run on wp_osub_dunning_runs and schedules retries according to the configured policy.
Default retry schedule
Four retries at days 1, 3, 5, and 7 after the original failure. OSub_Dunning_Policy::next_retry_at() computes each retry time using:
- A base offset (1, 3, 5, 7 days)
- Day-of-week heuristics that push retries away from weekends and onto post-payday windows for
insufficient_fundsdeclines - Time-of-day push to mid-morning local time, when issuer fraud systems are more lenient
Per-failure-reason policy
Different decline reasons get different treatment. Soft declines (insufficient_funds, try_again_later) retry on the standard schedule. Hard declines (card_declined, expired_card) skip ahead to a payment-method-update magic link.
Run outcomes
Each dunning run carries an outcome column:
recovering- retries in flightrecovered- a retry succeeded; the subscription returns toactiveexhausted- all retries failed; final action runs
Final action
When retries exhaust, OSub_Dunning::exhaust() performs the configured final action. Two options:
pause- pause the subscription with reasondunning_exhaustedcancel(default) - transition tocancelledwith reasondunning_exhausted
Magic-link recovery
Every failed charge attaches a magic link via OSub_Magic_Links. The customer clicks the link, lands on a hosted payment-method-update page, swaps to a working card, and the worker picks up the next retry.
Tick cleanup
OSub_Dunning::tick() runs hourly. It walks recovering runs older than 60 days with no progress and marks them exhausted, so the table does not grow unbounded.
Recovery rate
OSub_Analytics::dunning_recovery_rate($days) returns the percentage of dunning runs that ended in recovered over the window. Surfaced on the Dashboard and the Reports page.
ML pipeline
OSub_ML_Pipeline collects features (decline reason, attempt number, day-of-week, hour) on every retry outcome. The collected data feeds a future model that picks the next retry time per customer.

