expireCancelAtPeriodEnd($now); $this->cancelPastDueAfterGrace($now); $this->expireEndedTrials($now); $this->chargeDueRenewals($now); } /** 1. cancel_at_period_end + period ended → canceled. */ private function expireCancelAtPeriodEnd(string $now): void { global $wpdb; $table = Subscription::table(); $ids = $wpdb->get_col($wpdb->prepare( "SELECT id FROM {$table} WHERE status = %s AND cancel_at_period_end = 1 AND current_period_end <= %s", Subscription::STATUS_ACTIVE, $now )); foreach ($ids as $id) { Subscription::transition((int) $id, Subscription::STATUS_CANCELED); } } /** 2. past_due longer than grace window → canceled (dunning exhausted). */ private function cancelPastDueAfterGrace(string $now): void { global $wpdb; $table = Subscription::table(); $graceDays = (int) get_option('abovault_grace_days', Subscription::DEFAULT_GRACE_DAYS); $cutoff = gmdate('Y-m-d H:i:s', strtotime($now) - $graceDays * DAY_IN_SECONDS); $ids = $wpdb->get_col($wpdb->prepare( "SELECT id FROM {$table} WHERE status = %s AND last_payment_failed_at <= %s", Subscription::STATUS_PAST_DUE, $cutoff )); foreach ($ids as $id) { // Retention engine gets a final win-back shot before the hard cancel // (via the transition action), then the sub is closed. Subscription::transition((int) $id, Subscription::STATUS_CANCELED); } } /** 3. trialing past trial_end → expired. */ private function expireEndedTrials(string $now): void { global $wpdb; $table = Subscription::table(); $ids = $wpdb->get_col($wpdb->prepare( "SELECT id FROM {$table} WHERE status = %s AND trial_end IS NOT NULL AND trial_end <= %s", Subscription::STATUS_TRIALING, $now )); foreach ($ids as $id) { Subscription::transition((int) $id, Subscription::STATUS_EXPIRED); } } /** * 4. Active subscriptions due for renewal → attempt charge via RetryPolicy. * On success: advance the period. On failure: → past_due + schedule retry. */ private function chargeDueRenewals(string $now): void { global $wpdb; $table = Subscription::table(); // Skip subs with a renewal already in flight (pending_charge_ref) — the // webhook will resolve those. Prevents re-charging during SEPA settlement. $rows = $wpdb->get_results($wpdb->prepare( "SELECT * FROM {$table} WHERE status = %s AND current_period_end <= %s AND cancel_at_period_end = 0 AND pending_charge_ref = '' LIMIT 100", Subscription::STATUS_ACTIVE, $now ), ARRAY_A); $retry = new RetryPolicy(); foreach ($rows as $sub) { $retry->attemptRenewal($sub); } } /** Re-attempt a single past_due subscription (scheduled retry). */ public function retryOne(array $sub): void { (new RetryPolicy())->attemptRenewal($sub); } }