894 lines
27 KiB
PHP
894 lines
27 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Subscription Manager
|
|
*
|
|
* Core business logic for subscription management
|
|
*
|
|
* @package WooNooW\Modules\Subscription
|
|
*/
|
|
|
|
namespace WooNooW\Modules\Subscription;
|
|
|
|
if (!defined('ABSPATH')) exit;
|
|
|
|
use WooNooW\Core\ModuleRegistry;
|
|
|
|
class SubscriptionManager
|
|
{
|
|
|
|
/** @var string Subscriptions table name */
|
|
private static $table_subscriptions;
|
|
|
|
/** @var string Subscription orders table name */
|
|
private static $table_subscription_orders;
|
|
|
|
/**
|
|
* Initialize the manager
|
|
*/
|
|
public static function init()
|
|
{
|
|
global $wpdb;
|
|
self::$table_subscriptions = $wpdb->prefix . 'woonoow_subscriptions';
|
|
self::$table_subscription_orders = $wpdb->prefix . 'woonoow_subscription_orders';
|
|
}
|
|
|
|
/**
|
|
* Create database tables
|
|
*/
|
|
public static function create_tables()
|
|
{
|
|
global $wpdb;
|
|
|
|
$charset_collate = $wpdb->get_charset_collate();
|
|
$table_subscriptions = $wpdb->prefix . 'woonoow_subscriptions';
|
|
$table_subscription_orders = $wpdb->prefix . 'woonoow_subscription_orders';
|
|
|
|
$sql_subscriptions = "CREATE TABLE $table_subscriptions (
|
|
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
|
user_id BIGINT UNSIGNED NOT NULL,
|
|
order_id BIGINT UNSIGNED NOT NULL,
|
|
product_id BIGINT UNSIGNED NOT NULL,
|
|
variation_id BIGINT UNSIGNED DEFAULT NULL,
|
|
status ENUM('pending', 'active', 'on-hold', 'cancelled', 'expired', 'pending-cancel') DEFAULT 'pending',
|
|
billing_period ENUM('day', 'week', 'month', 'year') NOT NULL,
|
|
billing_interval INT UNSIGNED DEFAULT 1,
|
|
recurring_amount DECIMAL(12,4) DEFAULT 0,
|
|
start_date DATETIME NOT NULL,
|
|
trial_end_date DATETIME DEFAULT NULL,
|
|
next_payment_date DATETIME DEFAULT NULL,
|
|
end_date DATETIME DEFAULT NULL,
|
|
last_payment_date DATETIME DEFAULT NULL,
|
|
payment_method VARCHAR(100) DEFAULT NULL,
|
|
payment_meta LONGTEXT,
|
|
cancel_reason TEXT DEFAULT NULL,
|
|
pause_count INT UNSIGNED DEFAULT 0,
|
|
failed_payment_count INT UNSIGNED DEFAULT 0,
|
|
reminder_sent_at DATETIME DEFAULT NULL,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
INDEX idx_user_id (user_id),
|
|
INDEX idx_order_id (order_id),
|
|
INDEX idx_product_id (product_id),
|
|
INDEX idx_status (status),
|
|
INDEX idx_next_payment (next_payment_date)
|
|
) $charset_collate;";
|
|
|
|
$sql_orders = "CREATE TABLE $table_subscription_orders (
|
|
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
|
subscription_id BIGINT UNSIGNED NOT NULL,
|
|
order_id BIGINT UNSIGNED NOT NULL,
|
|
order_type ENUM('parent', 'renewal', 'switch', 'resubscribe') DEFAULT 'renewal',
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
INDEX idx_subscription (subscription_id),
|
|
INDEX idx_order (order_id)
|
|
) $charset_collate;";
|
|
|
|
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
|
dbDelta($sql_subscriptions);
|
|
dbDelta($sql_orders);
|
|
}
|
|
|
|
/**
|
|
* Create subscription from order item
|
|
*
|
|
* @param \WC_Order $order
|
|
* @param \WC_Order_Item_Product $item
|
|
* @return int|false Subscription ID or false on failure
|
|
*/
|
|
public static function create_from_order($order, $item)
|
|
{
|
|
global $wpdb;
|
|
|
|
$product_id = $item->get_product_id();
|
|
$variation_id = $item->get_variation_id();
|
|
$user_id = $order->get_user_id();
|
|
|
|
if (!$user_id) {
|
|
// Guest orders not supported for subscriptions
|
|
return false;
|
|
}
|
|
|
|
// Get subscription settings from product
|
|
$billing_period = get_post_meta($product_id, '_woonoow_subscription_period', true) ?: 'month';
|
|
$billing_interval = absint(get_post_meta($product_id, '_woonoow_subscription_interval', true)) ?: 1;
|
|
$trial_days = absint(get_post_meta($product_id, '_woonoow_subscription_trial_days', true));
|
|
$subscription_length = absint(get_post_meta($product_id, '_woonoow_subscription_length', true));
|
|
|
|
// Calculate dates
|
|
$now = current_time('mysql');
|
|
$start_date = $now;
|
|
$trial_end_date = null;
|
|
|
|
if ($trial_days > 0) {
|
|
$trial_end_date = date('Y-m-d H:i:s', strtotime($now . " + $trial_days days"));
|
|
$next_payment_date = $trial_end_date;
|
|
} else {
|
|
$next_payment_date = self::calculate_next_payment_date($now, $billing_period, $billing_interval);
|
|
}
|
|
|
|
// Calculate end date if subscription has fixed length
|
|
$end_date = null;
|
|
if ($subscription_length > 0) {
|
|
$end_date = self::calculate_end_date($start_date, $billing_period, $billing_interval, $subscription_length);
|
|
}
|
|
|
|
// Get recurring amount (product price)
|
|
$product = $item->get_product();
|
|
$recurring_amount = $product ? $product->get_price() : $item->get_total();
|
|
|
|
// Get payment method
|
|
$payment_method = $order->get_payment_method();
|
|
$payment_meta = json_encode([
|
|
'method_title' => $order->get_payment_method_title(),
|
|
'customer_id' => $order->get_customer_id(),
|
|
]);
|
|
|
|
// Insert subscription
|
|
$inserted = $wpdb->insert(
|
|
self::$table_subscriptions,
|
|
[
|
|
'user_id' => $user_id,
|
|
'order_id' => $order->get_id(),
|
|
'product_id' => $product_id,
|
|
'variation_id' => $variation_id ?: null,
|
|
'status' => 'active',
|
|
'billing_period' => $billing_period,
|
|
'billing_interval' => $billing_interval,
|
|
'recurring_amount' => $recurring_amount,
|
|
'start_date' => $start_date,
|
|
'trial_end_date' => $trial_end_date,
|
|
'next_payment_date' => $next_payment_date,
|
|
'end_date' => $end_date,
|
|
'last_payment_date' => $now,
|
|
'payment_method' => $payment_method,
|
|
'payment_meta' => $payment_meta,
|
|
],
|
|
['%d', '%d', '%d', '%d', '%s', '%s', '%d', '%f', '%s', '%s', '%s', '%s', '%s', '%s', '%s']
|
|
);
|
|
|
|
if (!$inserted) {
|
|
return false;
|
|
}
|
|
|
|
$subscription_id = $wpdb->insert_id;
|
|
|
|
// Link parent order to subscription
|
|
$wpdb->insert(
|
|
self::$table_subscription_orders,
|
|
[
|
|
'subscription_id' => $subscription_id,
|
|
'order_id' => $order->get_id(),
|
|
'order_type' => 'parent',
|
|
],
|
|
['%d', '%d', '%s']
|
|
);
|
|
|
|
// Trigger action
|
|
do_action('woonoow/subscription/created', $subscription_id, $order, $item);
|
|
|
|
return $subscription_id;
|
|
}
|
|
|
|
/**
|
|
* Get subscription by ID
|
|
*
|
|
* @param int $subscription_id
|
|
* @return object|null
|
|
*/
|
|
public static function get($subscription_id)
|
|
{
|
|
global $wpdb;
|
|
|
|
return $wpdb->get_row($wpdb->prepare(
|
|
"SELECT * FROM " . self::$table_subscriptions . " WHERE id = %d",
|
|
$subscription_id
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Get subscription by related order ID (parent or renewal)
|
|
*
|
|
* @param int $order_id
|
|
* @return object|null
|
|
*/
|
|
public static function get_by_order_id($order_id)
|
|
{
|
|
global $wpdb;
|
|
|
|
$table_subscriptions = $wpdb->prefix . 'woonoow_subscriptions';
|
|
$table_subscription_orders = $wpdb->prefix . 'woonoow_subscription_orders';
|
|
|
|
// Join subscriptions table to get full subscription data
|
|
return $wpdb->get_row($wpdb->prepare(
|
|
"SELECT s.*
|
|
FROM $table_subscriptions s
|
|
JOIN $table_subscription_orders so ON s.id = so.subscription_id
|
|
WHERE so.order_id = %d",
|
|
$order_id
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Get subscriptions by user
|
|
*
|
|
* @param int $user_id
|
|
* @param array $args
|
|
* @return array
|
|
*/
|
|
public static function get_by_user($user_id, $args = [])
|
|
{
|
|
global $wpdb;
|
|
|
|
$defaults = [
|
|
'status' => null,
|
|
'limit' => 20,
|
|
'offset' => 0,
|
|
];
|
|
$args = wp_parse_args($args, $defaults);
|
|
|
|
$where = "WHERE user_id = %d";
|
|
$params = [$user_id];
|
|
|
|
if ($args['status']) {
|
|
$where .= " AND status = %s";
|
|
$params[] = $args['status'];
|
|
}
|
|
|
|
$sql = $wpdb->prepare(
|
|
"SELECT * FROM " . self::$table_subscriptions . " $where ORDER BY created_at DESC LIMIT %d OFFSET %d",
|
|
array_merge($params, [$args['limit'], $args['offset']])
|
|
);
|
|
|
|
return $wpdb->get_results($sql);
|
|
}
|
|
|
|
/**
|
|
* Get all subscriptions (admin)
|
|
*
|
|
* @param array $args
|
|
* @return array
|
|
*/
|
|
public static function get_all($args = [])
|
|
{
|
|
global $wpdb;
|
|
|
|
$defaults = [
|
|
'status' => null,
|
|
'product_id' => null,
|
|
'user_id' => null,
|
|
'limit' => 20,
|
|
'offset' => 0,
|
|
'search' => null,
|
|
];
|
|
$args = wp_parse_args($args, $defaults);
|
|
|
|
$where = "WHERE 1=1";
|
|
$params = [];
|
|
|
|
if ($args['status']) {
|
|
$where .= " AND status = %s";
|
|
$params[] = $args['status'];
|
|
}
|
|
|
|
if ($args['product_id']) {
|
|
$where .= " AND product_id = %d";
|
|
$params[] = $args['product_id'];
|
|
}
|
|
|
|
if ($args['user_id']) {
|
|
$where .= " AND user_id = %d";
|
|
$params[] = $args['user_id'];
|
|
}
|
|
|
|
$order = "ORDER BY created_at DESC";
|
|
$limit = "LIMIT " . intval($args['limit']) . " OFFSET " . intval($args['offset']);
|
|
|
|
$sql = "SELECT * FROM " . self::$table_subscriptions . " $where $order $limit";
|
|
|
|
if (!empty($params)) {
|
|
$sql = $wpdb->prepare($sql, $params);
|
|
}
|
|
|
|
return $wpdb->get_results($sql);
|
|
}
|
|
|
|
/**
|
|
* Count subscriptions
|
|
*
|
|
* @param array $args
|
|
* @return int
|
|
*/
|
|
public static function count($args = [])
|
|
{
|
|
global $wpdb;
|
|
|
|
$where = "WHERE 1=1";
|
|
$params = [];
|
|
|
|
if (!empty($args['status'])) {
|
|
$where .= " AND status = %s";
|
|
$params[] = $args['status'];
|
|
}
|
|
|
|
$sql = "SELECT COUNT(*) FROM " . self::$table_subscriptions . " $where";
|
|
|
|
if (!empty($params)) {
|
|
$sql = $wpdb->prepare($sql, $params);
|
|
}
|
|
|
|
return (int) $wpdb->get_var($sql);
|
|
}
|
|
|
|
/**
|
|
* Update subscription status
|
|
*
|
|
* @param int $subscription_id
|
|
* @param string $status
|
|
* @param string|null $reason
|
|
* @return bool
|
|
*/
|
|
public static function update_status($subscription_id, $status, $reason = null)
|
|
{
|
|
global $wpdb;
|
|
|
|
$data = ['status' => $status];
|
|
$format = ['%s'];
|
|
|
|
if ($reason !== null) {
|
|
$data['cancel_reason'] = $reason;
|
|
$format[] = '%s';
|
|
}
|
|
|
|
$updated = $wpdb->update(
|
|
self::$table_subscriptions,
|
|
$data,
|
|
['id' => $subscription_id],
|
|
$format,
|
|
['%d']
|
|
);
|
|
|
|
if ($updated !== false) {
|
|
do_action('woonoow/subscription/status_changed', $subscription_id, $status, $reason);
|
|
}
|
|
|
|
return $updated !== false;
|
|
}
|
|
|
|
/**
|
|
* Cancel subscription
|
|
*
|
|
* @param int $subscription_id
|
|
* @param string $reason
|
|
* @param bool $immediate Force immediate cancellation
|
|
* @return bool
|
|
*/
|
|
public static function cancel($subscription_id, $reason = '', $immediate = false)
|
|
{
|
|
$subscription = self::get($subscription_id);
|
|
if (!$subscription || in_array($subscription->status, ['cancelled', 'expired'])) {
|
|
return false;
|
|
}
|
|
|
|
// Default to pending-cancel if there's time left
|
|
$new_status = 'cancelled';
|
|
$now = current_time('mysql');
|
|
|
|
if (!$immediate && $subscription->next_payment_date && $subscription->next_payment_date > $now) {
|
|
$new_status = 'pending-cancel';
|
|
}
|
|
|
|
$success = self::update_status($subscription_id, $new_status, $reason);
|
|
|
|
if ($success) {
|
|
if ($new_status === 'pending-cancel') {
|
|
do_action('woonoow/subscription/pending_cancel', $subscription_id, $reason);
|
|
} else {
|
|
do_action('woonoow/subscription/cancelled', $subscription_id, $reason);
|
|
}
|
|
}
|
|
|
|
return $success;
|
|
}
|
|
|
|
/**
|
|
* Pause subscription
|
|
*
|
|
* @param int $subscription_id
|
|
* @return bool
|
|
*/
|
|
public static function pause($subscription_id)
|
|
{
|
|
global $wpdb;
|
|
|
|
$subscription = self::get($subscription_id);
|
|
if (!$subscription || $subscription->status !== 'active') {
|
|
return false;
|
|
}
|
|
|
|
// Check max pause count
|
|
$settings = ModuleRegistry::get_settings('subscription');
|
|
$max_pause = $settings['max_pause_count'] ?? 3;
|
|
|
|
if ($max_pause > 0 && $subscription->pause_count >= $max_pause) {
|
|
return false;
|
|
}
|
|
|
|
$updated = $wpdb->update(
|
|
self::$table_subscriptions,
|
|
[
|
|
'status' => 'on-hold',
|
|
'pause_count' => $subscription->pause_count + 1,
|
|
],
|
|
['id' => $subscription_id],
|
|
['%s', '%d'],
|
|
['%d']
|
|
);
|
|
|
|
if ($updated !== false) {
|
|
do_action('woonoow/subscription/paused', $subscription_id);
|
|
}
|
|
|
|
return $updated !== false;
|
|
}
|
|
|
|
/**
|
|
* Resume subscription
|
|
*
|
|
* @param int $subscription_id
|
|
* @return bool
|
|
*/
|
|
public static function resume($subscription_id)
|
|
{
|
|
global $wpdb;
|
|
|
|
$subscription = self::get($subscription_id);
|
|
if (!$subscription || !in_array($subscription->status, ['on-hold', 'pending-cancel'])) {
|
|
return false;
|
|
}
|
|
|
|
$update_data = ['status' => 'active'];
|
|
$format = ['%s'];
|
|
|
|
// Only recalculate payment date if resuming from on-hold
|
|
if ($subscription->status === 'on-hold') {
|
|
// Recalculate next payment date from now
|
|
$next_payment = self::calculate_next_payment_date(
|
|
current_time('mysql'),
|
|
$subscription->billing_period,
|
|
$subscription->billing_interval
|
|
);
|
|
$update_data['next_payment_date'] = $next_payment;
|
|
$format[] = '%s';
|
|
}
|
|
|
|
$updated = $wpdb->update(
|
|
self::$table_subscriptions,
|
|
$update_data,
|
|
['id' => $subscription_id],
|
|
$format,
|
|
['%d']
|
|
);
|
|
|
|
if ($updated !== false) {
|
|
do_action('woonoow/subscription/resumed', $subscription_id);
|
|
}
|
|
|
|
return $updated !== false;
|
|
}
|
|
|
|
/**
|
|
* Process renewal for a subscription
|
|
*
|
|
* @param int $subscription_id
|
|
* @return bool
|
|
*/
|
|
/**
|
|
* Process renewal for a subscription
|
|
*
|
|
* @param int $subscription_id
|
|
* @return bool
|
|
*/
|
|
public static function renew($subscription_id)
|
|
{
|
|
global $wpdb;
|
|
|
|
$subscription = self::get($subscription_id);
|
|
if (!$subscription || !in_array($subscription->status, ['active', 'on-hold'])) {
|
|
return false;
|
|
}
|
|
|
|
// Check for existing pending renewal order to prevent duplicates
|
|
$existing_pending = $wpdb->get_row($wpdb->prepare(
|
|
"SELECT so.order_id FROM " . self::$table_subscription_orders . " so
|
|
JOIN {$wpdb->posts} p ON so.order_id = p.ID
|
|
WHERE so.subscription_id = %d
|
|
AND so.order_type = 'renewal'
|
|
AND p.post_status IN ('wc-pending', 'pending', 'wc-on-hold', 'on-hold')",
|
|
$subscription_id
|
|
));
|
|
|
|
if ($existing_pending) {
|
|
return ['success' => true, 'order_id' => (int) $existing_pending->order_id, 'status' => 'existing'];
|
|
}
|
|
|
|
// Create renewal order
|
|
$renewal_order = self::create_renewal_order($subscription);
|
|
if (!$renewal_order) {
|
|
// Failed to create order
|
|
self::handle_renewal_failure($subscription_id);
|
|
return false;
|
|
}
|
|
|
|
// Process payment
|
|
// Result can be: true (paid), false (failed), or 'manual' (waiting for payment)
|
|
$payment_result = self::process_renewal_payment($subscription, $renewal_order);
|
|
|
|
if ($payment_result === true) {
|
|
self::handle_renewal_success($subscription_id, $renewal_order);
|
|
return ['success' => true, 'order_id' => $renewal_order->get_id(), 'status' => 'complete'];
|
|
} elseif ($payment_result === 'manual') {
|
|
// Manual payment required
|
|
|
|
// CHECK: Is this an early renewal? (Next payment date is in future)
|
|
$now = current_time('mysql');
|
|
$is_early_renewal = $subscription->next_payment_date && $subscription->next_payment_date > $now;
|
|
|
|
if ($is_early_renewal) {
|
|
// Early renewal: Keep active, just waiting for payment
|
|
return ['success' => true, 'order_id' => $renewal_order->get_id(), 'status' => 'manual'];
|
|
}
|
|
|
|
// Normal/Overdue renewal: Set to on-hold
|
|
self::update_status($subscription_id, 'on-hold', 'awaiting_payment');
|
|
return ['success' => true, 'order_id' => $renewal_order->get_id(), 'status' => 'manual'];
|
|
} else {
|
|
// Auto-debit failed
|
|
self::handle_renewal_failure($subscription_id);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a renewal order
|
|
*
|
|
* @param object $subscription
|
|
* @return \WC_Order|false
|
|
*/
|
|
private static function create_renewal_order($subscription)
|
|
{
|
|
global $wpdb;
|
|
|
|
// Get original order
|
|
$parent_order = wc_get_order($subscription->order_id);
|
|
if (!$parent_order) {
|
|
return false;
|
|
}
|
|
|
|
// Create new order
|
|
$renewal_order = wc_create_order([
|
|
'customer_id' => $subscription->user_id,
|
|
'status' => 'pending',
|
|
'parent' => $subscription->order_id,
|
|
]);
|
|
|
|
if (is_wp_error($renewal_order)) {
|
|
return false;
|
|
}
|
|
|
|
// Add product
|
|
$product = wc_get_product($subscription->variation_id ?: $subscription->product_id);
|
|
if ($product) {
|
|
$renewal_order->add_product($product, 1, [
|
|
'total' => $subscription->recurring_amount,
|
|
'subtotal' => $subscription->recurring_amount,
|
|
]);
|
|
}
|
|
|
|
// Copy billing/shipping from parent
|
|
$renewal_order->set_address($parent_order->get_address('billing'), 'billing');
|
|
$renewal_order->set_address($parent_order->get_address('shipping'), 'shipping');
|
|
$renewal_order->set_payment_method($subscription->payment_method);
|
|
|
|
// Calculate totals
|
|
$renewal_order->calculate_totals();
|
|
$renewal_order->save();
|
|
|
|
// Link to subscription
|
|
$wpdb->insert(
|
|
self::$table_subscription_orders,
|
|
[
|
|
'subscription_id' => $subscription->id,
|
|
'order_id' => $renewal_order->get_id(),
|
|
'order_type' => 'renewal',
|
|
],
|
|
['%d', '%d', '%s']
|
|
);
|
|
|
|
return $renewal_order;
|
|
}
|
|
|
|
/**
|
|
* Process payment for renewal order
|
|
*
|
|
* @param object $subscription
|
|
* @param \WC_Order $order
|
|
* @return bool
|
|
*/
|
|
/**
|
|
* Process payment for renewal order
|
|
*
|
|
* @param object $subscription
|
|
* @param \WC_Order $order
|
|
* @return bool|string True if paid, false if failed, 'manual' if waiting
|
|
*/
|
|
private static function process_renewal_payment($subscription, $order)
|
|
{
|
|
// Allow plugins to override payment processing completely
|
|
// Return true/false/'manual' to bypass default logic
|
|
$pre = apply_filters('woonoow_pre_process_subscription_payment', null, $subscription, $order);
|
|
if ($pre !== null) {
|
|
return $pre;
|
|
}
|
|
|
|
// Get payment gateway
|
|
$gateways = WC()->payment_gateways()->get_available_payment_gateways();
|
|
$gateway_id = $subscription->payment_method;
|
|
|
|
if (!isset($gateways[$gateway_id])) {
|
|
// Payment method not available - treat as failure so user can fix
|
|
$order->update_status('failed', __('Payment method not available for renewal', 'woonoow'));
|
|
return false;
|
|
}
|
|
|
|
$gateway = $gateways[$gateway_id];
|
|
|
|
// 1. Try Auto-Debit if supported
|
|
if (method_exists($gateway, 'process_subscription_renewal_payment')) {
|
|
$result = $gateway->process_subscription_renewal_payment($order, $subscription);
|
|
if (!is_wp_error($result) && $result) {
|
|
return true;
|
|
}
|
|
// If explicit failure from auto-debit, return false (will trigger retry logic)
|
|
return false;
|
|
}
|
|
|
|
// 2. Allow other plugins to handle auto-debit via filter (e.g. Stripe/PayPal adapters)
|
|
$external_result = apply_filters('woonoow_process_subscription_payment', null, $gateway, $order, $subscription);
|
|
if ($external_result !== null) {
|
|
return $external_result ? true : false;
|
|
}
|
|
|
|
// 3. Fallback: Manual Payment
|
|
// Set order to pending-payment
|
|
$order->update_status('pending', __('Awaiting manual renewal payment', 'woonoow'));
|
|
|
|
// Send renewal payment email to customer
|
|
do_action('woonoow/subscription/renewal_payment_due', $subscription->id, $order);
|
|
|
|
return 'manual'; // Return special status
|
|
}
|
|
|
|
/**
|
|
* Handle successful renewal
|
|
*
|
|
* @param int $subscription_id
|
|
* @param \WC_Order $order
|
|
*/
|
|
public static function handle_renewal_success($subscription_id, $order)
|
|
{
|
|
global $wpdb;
|
|
|
|
$subscription = self::get($subscription_id);
|
|
|
|
// Calculate next payment date
|
|
// For early renewal, start from the current next_payment_date if it's in the future
|
|
// Otherwise start from now (for expired/overdue subscriptions)
|
|
$now = current_time('mysql');
|
|
$base_date = $now;
|
|
|
|
if ($subscription->next_payment_date && $subscription->next_payment_date > $now) {
|
|
$base_date = $subscription->next_payment_date;
|
|
}
|
|
|
|
$next_payment = self::calculate_next_payment_date(
|
|
$base_date,
|
|
$subscription->billing_period,
|
|
$subscription->billing_interval
|
|
);
|
|
|
|
// Check if subscription should end
|
|
if ($subscription->end_date && strtotime($next_payment) > strtotime($subscription->end_date)) {
|
|
$next_payment = null;
|
|
}
|
|
|
|
$wpdb->update(
|
|
self::$table_subscriptions,
|
|
[
|
|
'status' => 'active',
|
|
'next_payment_date' => $next_payment,
|
|
'last_payment_date' => current_time('mysql'),
|
|
'failed_payment_count' => 0,
|
|
],
|
|
['id' => $subscription_id],
|
|
['%s', '%s', '%s', '%d'],
|
|
['%d']
|
|
);
|
|
|
|
// Complete the order
|
|
$order->payment_complete();
|
|
|
|
do_action('woonoow/subscription/renewed', $subscription_id, $order);
|
|
}
|
|
|
|
/**
|
|
* Handle failed renewal
|
|
*
|
|
* @param int $subscription_id
|
|
*/
|
|
private static function handle_renewal_failure($subscription_id)
|
|
{
|
|
global $wpdb;
|
|
|
|
$subscription = self::get($subscription_id);
|
|
$new_failed_count = $subscription->failed_payment_count + 1;
|
|
|
|
// Get settings
|
|
$settings = ModuleRegistry::get_settings('subscription');
|
|
$max_attempts = $settings['expire_after_failed_attempts'] ?? 3;
|
|
|
|
if ($new_failed_count >= $max_attempts) {
|
|
// Mark as expired
|
|
$wpdb->update(
|
|
self::$table_subscriptions,
|
|
[
|
|
'status' => 'expired',
|
|
'failed_payment_count' => $new_failed_count,
|
|
],
|
|
['id' => $subscription_id],
|
|
['%s', '%d'],
|
|
['%d']
|
|
);
|
|
|
|
do_action('woonoow/subscription/expired', $subscription_id, 'payment_failed');
|
|
} else {
|
|
// Just increment failed count
|
|
$wpdb->update(
|
|
self::$table_subscriptions,
|
|
['failed_payment_count' => $new_failed_count],
|
|
['id' => $subscription_id],
|
|
['%d'],
|
|
['%d']
|
|
);
|
|
|
|
do_action('woonoow/subscription/renewal_failed', $subscription_id, $new_failed_count);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calculate next payment date
|
|
*
|
|
* @param string $from_date
|
|
* @param string $period
|
|
* @param int $interval
|
|
* @return string
|
|
*/
|
|
public static function calculate_next_payment_date($from_date, $period, $interval = 1)
|
|
{
|
|
$interval = max(1, $interval);
|
|
|
|
switch ($period) {
|
|
case 'day':
|
|
$modifier = "+ $interval days";
|
|
break;
|
|
case 'week':
|
|
$modifier = "+ $interval weeks";
|
|
break;
|
|
case 'month':
|
|
$modifier = "+ $interval months";
|
|
break;
|
|
case 'year':
|
|
$modifier = "+ $interval years";
|
|
break;
|
|
default:
|
|
$modifier = "+ 1 month";
|
|
}
|
|
|
|
return date('Y-m-d H:i:s', strtotime($from_date . ' ' . $modifier));
|
|
}
|
|
|
|
/**
|
|
* Calculate subscription end date
|
|
*
|
|
* @param string $start_date
|
|
* @param string $period
|
|
* @param int $interval
|
|
* @param int $length
|
|
* @return string
|
|
*/
|
|
private static function calculate_end_date($start_date, $period, $interval, $length)
|
|
{
|
|
$total_periods = $interval * $length;
|
|
|
|
switch ($period) {
|
|
case 'day':
|
|
$modifier = "+ $total_periods days";
|
|
break;
|
|
case 'week':
|
|
$modifier = "+ $total_periods weeks";
|
|
break;
|
|
case 'month':
|
|
$modifier = "+ $total_periods months";
|
|
break;
|
|
case 'year':
|
|
$modifier = "+ $total_periods years";
|
|
break;
|
|
default:
|
|
$modifier = "+ $total_periods months";
|
|
}
|
|
|
|
return date('Y-m-d H:i:s', strtotime($start_date . ' ' . $modifier));
|
|
}
|
|
|
|
/**
|
|
* Get subscriptions due for renewal
|
|
*
|
|
* @return array
|
|
*/
|
|
public static function get_due_renewals()
|
|
{
|
|
global $wpdb;
|
|
|
|
$now = current_time('mysql');
|
|
|
|
return $wpdb->get_results($wpdb->prepare(
|
|
"SELECT * FROM " . self::$table_subscriptions . "
|
|
WHERE status = 'active'
|
|
AND next_payment_date IS NOT NULL
|
|
AND next_payment_date <= %s
|
|
ORDER BY next_payment_date ASC",
|
|
$now
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Get subscription orders
|
|
*
|
|
* @param int $subscription_id
|
|
* @return array
|
|
*/
|
|
public static function get_orders($subscription_id)
|
|
{
|
|
global $wpdb;
|
|
|
|
return $wpdb->get_results($wpdb->prepare(
|
|
"SELECT so.*, p.post_status as order_status
|
|
FROM " . self::$table_subscription_orders . " so
|
|
LEFT JOIN {$wpdb->posts} p ON so.order_id = p.ID
|
|
WHERE so.subscription_id = %d
|
|
ORDER BY so.created_at DESC",
|
|
$subscription_id
|
|
));
|
|
}
|
|
}
|