id); try { $success = SubscriptionManager::renew($subscription->id); if ($success) { do_action('woonoow/subscription/renewal_completed', $subscription->id); } else { // Auto-debit failed (returns false), so schedule retry // Note: 'manual' falls into a separate bucket in SubscriptionManager and returns true (handled) self::schedule_retry($subscription->id); } } catch (\Exception $e) { // Log error error_log('[WooNooW Subscription] Renewal failed for subscription #' . $subscription->id . ': ' . $e->getMessage()); do_action('woonoow/subscription/renewal_error', $subscription->id, $e); // Also schedule retry on exception self::schedule_retry($subscription->id); } } } /** * Check for expired subscriptions */ public static function check_expirations() { global $wpdb; if (!ModuleRegistry::is_enabled('subscription')) { return; } $table = $wpdb->prefix . 'woonoow_subscriptions'; $now = current_time('mysql'); // Find subscriptions that have passed their end date $expired = $wpdb->get_results($wpdb->prepare( "SELECT id FROM $table WHERE status = 'active' AND end_date IS NOT NULL AND end_date <= %s", $now )); foreach ($expired as $subscription) { SubscriptionManager::update_status($subscription->id, 'expired'); do_action('woonoow/subscription/expired', $subscription->id, 'end_date_reached'); } // Also check pending-cancel subscriptions that need to be finalized $pending_cancel = $wpdb->get_results($wpdb->prepare( "SELECT id FROM $table WHERE status = 'pending-cancel' AND next_payment_date IS NOT NULL AND next_payment_date <= %s", $now )); foreach ($pending_cancel as $subscription) { SubscriptionManager::update_status($subscription->id, 'cancelled'); do_action('woonoow/subscription/cancelled', $subscription->id, 'pending_cancel_completed'); } } /** * Send renewal reminder emails */ public static function send_reminders() { global $wpdb; if (!ModuleRegistry::is_enabled('subscription')) { return; } // Check if reminders are enabled $settings = ModuleRegistry::get_settings('subscription'); if (empty($settings['send_renewal_reminder'])) { return; } $days_before = $settings['reminder_days_before'] ?? 3; $reminder_date = date('Y-m-d H:i:s', strtotime("+$days_before days")); $tomorrow = date('Y-m-d H:i:s', strtotime('+' . ($days_before + 1) . ' days')); $table = $wpdb->prefix . 'woonoow_subscriptions'; // Find subscriptions due for reminder (that haven't had reminder sent for this billing cycle) $due_reminders = $wpdb->get_results($wpdb->prepare( "SELECT * FROM $table WHERE status = 'active' AND next_payment_date IS NOT NULL AND next_payment_date >= %s AND next_payment_date < %s AND (reminder_sent_at IS NULL OR reminder_sent_at < last_payment_date OR (last_payment_date IS NULL AND reminder_sent_at < start_date))", $reminder_date, $tomorrow )); foreach ($due_reminders as $subscription) { // Trigger reminder email do_action('woonoow/subscription/renewal_reminder', $subscription); // Mark reminder as sent in database $wpdb->update( $table, ['reminder_sent_at' => current_time('mysql')], ['id' => $subscription->id], ['%s'], ['%d'] ); } } /** * Get retry schedule for failed payments * * @param int $subscription_id * @return string|null Next retry datetime or null if no more retries */ public static function get_next_retry_date($subscription_id) { $subscription = SubscriptionManager::get($subscription_id); if (!$subscription) { return null; } $settings = ModuleRegistry::get_settings('subscription'); if (empty($settings['renewal_retry_enabled'])) { return null; } $retry_days_str = $settings['renewal_retry_days'] ?? '1,3,5'; $retry_days = array_map('intval', array_filter(explode(',', $retry_days_str))); $failed_count = $subscription->failed_payment_count; if ($failed_count >= count($retry_days)) { return null; // No more retries } $days_to_add = $retry_days[$failed_count] ?? 1; return date('Y-m-d H:i:s', strtotime("+$days_to_add days")); } /** * Schedule a retry for failed payment * * @param int $subscription_id */ public static function schedule_retry($subscription_id) { global $wpdb; $next_retry = self::get_next_retry_date($subscription_id); if ($next_retry) { $table = $wpdb->prefix . 'woonoow_subscriptions'; $wpdb->update( $table, ['next_payment_date' => $next_retry], ['id' => $subscription_id], ['%s'], ['%d'] ); } } /** * H5 — Find subscriptions that are on-hold because a manual renewal order * was created and never paid. Send a re-notification once per 24h. After * `unpaid_renewal_max_age_days` (default 7), auto-cancel to prevent * indefinite on-hold state. Auto-cancel is the last-resort safety net; * the admin can resume manually at any time. */ public static function retry_unpaid_renewals() { global $wpdb; if (!ModuleRegistry::is_enabled('subscription')) { return; } $settings = ModuleRegistry::get_settings('subscription'); $max_age_days = isset($settings['unpaid_renewal_max_age_days']) ? max(1, (int) $settings['unpaid_renewal_max_age_days']) : 7; $table_subs = $wpdb->prefix . 'woonoow_subscriptions'; $table_orders = $wpdb->prefix . 'woonoow_subscription_orders'; $posts = $wpdb->posts; $now = current_time('mysql'); $min_age = date('Y-m-d H:i:s', strtotime('-24 hours', strtotime($now))); $max_age_cutoff = date('Y-m-d H:i:s', strtotime("-{$max_age_days} days", strtotime($now))); $reminder_threshold = date('Y-m-d H:i:s', strtotime('-24 hours', strtotime($now))); // Find (subscription, order) pairs where the renewal order is still unpaid // and the order was created at least 24h ago. We rate-limit per-order by // storing the last-notice timestamp in order meta. $candidates = $wpdb->get_results($wpdb->prepare( "SELECT s.id AS subscription_id, o.order_id FROM $table_subs s JOIN $table_orders o ON o.subscription_id = s.id AND o.order_type = 'renewal' JOIN $posts p ON p.ID = o.order_id WHERE s.status = 'on-hold' AND p.post_status IN ('wc-pending', 'pending', 'wc-failed', 'failed') AND p.post_date <= %s AND p.post_date >= %s", $min_age, $max_age_cutoff )); foreach ($candidates as $row) { $order = wc_get_order($row->order_id); if (!$order) { continue; } // Per-order rate limit: don't re-notify more than once per 24h. $last_notice = (string) $order->get_meta('_woonoow_unpaid_notice_at', true); if ($last_notice !== '' && strtotime($last_notice) > strtotime($reminder_threshold)) { continue; } $subscription = SubscriptionManager::get($row->subscription_id); if (!$subscription) { continue; } // Re-fire the same notification that fired at first renewal. Email // templates registered for `renewal_payment_due` will be sent. do_action('woonoow/subscription/renewal_payment_due', $subscription->id, $order); $order->update_meta_data('_woonoow_unpaid_notice_at', $now); $order->save(); } // Auto-cancel: anything older than the cutoff that is still on-hold gets // cancelled outright. The admin can override by resuming manually. $auto_cancel = $wpdb->get_results($wpdb->prepare( "SELECT id FROM $table_subs WHERE status = 'on-hold' AND next_payment_date IS NOT NULL AND next_payment_date <= %s", $max_age_cutoff )); foreach ($auto_cancel as $row) { SubscriptionManager::update_status($row->id, 'cancelled', 'unpaid_renewal_timeout'); do_action('woonoow/subscription/cancelled', $row->id, 'unpaid_renewal_timeout'); } } /** * Auto-resume subscriptions that have been paused beyond the merchant-configured * maximum pause duration. Runs daily. * * Setting: `max_pause_duration_days` (int, 0 = disabled/unlimited). * When a subscription hits the limit it is automatically resumed, giving the * customer a fresh billing cycle from now. */ public static function check_pause_expirations() { global $wpdb; if (!ModuleRegistry::is_enabled('subscription')) { return; } $settings = ModuleRegistry::get_settings('subscription'); $max_days = isset($settings['max_pause_duration_days']) ? (int) $settings['max_pause_duration_days'] : 0; // 0 means unlimited — feature is disabled. if ($max_days <= 0) { return; } $table = $wpdb->prefix . 'woonoow_subscriptions'; $cutoff = date('Y-m-d H:i:s', strtotime("-{$max_days} days")); // Find on-hold subscriptions paused longer than the allowed duration. $expired_pauses = $wpdb->get_results($wpdb->prepare( "SELECT id FROM $table WHERE status = 'on-hold' AND paused_at IS NOT NULL AND paused_at <= %s", $cutoff )); foreach ($expired_pauses as $row) { $resumed = SubscriptionManager::resume($row->id); if ($resumed) { do_action('woonoow/subscription/auto_resumed', $row->id, 'max_pause_duration_exceeded'); } } } }