Files
WooNooW/includes/Modules/Subscription/SubscriptionModule.php
2026-01-29 11:54:42 +07:00

564 lines
20 KiB
PHP

<?php
/**
* Subscription Module Bootstrap
*
* @package WooNooW\Modules\Subscription
*/
namespace WooNooW\Modules\Subscription;
if (!defined('ABSPATH')) exit;
use WooNooW\Core\ModuleRegistry;
use WooNooW\Modules\SubscriptionSettings;
class SubscriptionModule
{
/**
* Initialize the subscription module
*/
public static function init()
{
// Register settings schema
SubscriptionSettings::init();
// Initialize manager immediately since we're already in plugins_loaded
self::maybe_init_manager();
// Install tables on module enable
add_action('woonoow/module/enabled', [__CLASS__, 'on_module_enabled']);
// Add product meta fields
add_action('woocommerce_product_options_general_product_data', [__CLASS__, 'add_product_subscription_fields']);
add_action('woocommerce_process_product_meta', [__CLASS__, 'save_product_subscription_fields']);
// Hook into order completion to create subscriptions
add_action('woocommerce_order_status_completed', [__CLASS__, 'maybe_create_subscription'], 10, 1);
add_action('woocommerce_order_status_processing', [__CLASS__, 'maybe_create_subscription'], 10, 1);
// Hook into order status change to handle manual renewal payments
add_action('woocommerce_order_status_changed', [__CLASS__, 'on_order_status_changed'], 10, 3);
// Modify add to cart button text for subscription products
add_filter('woocommerce_product_single_add_to_cart_text', [__CLASS__, 'subscription_add_to_cart_text'], 10, 2);
add_filter('woocommerce_product_add_to_cart_text', [__CLASS__, 'subscription_add_to_cart_text'], 10, 2);
// Register subscription notification events
add_filter('woonoow_notification_events_registry', [__CLASS__, 'register_notification_events']);
// Hook subscription lifecycle events to send notifications
add_action('woonoow/subscription/pending_cancel', [__CLASS__, 'on_pending_cancel'], 10, 2);
add_action('woonoow/subscription/cancelled', [__CLASS__, 'on_cancelled'], 10, 2);
add_action('woonoow/subscription/expired', [__CLASS__, 'on_expired'], 10, 2);
add_action('woonoow/subscription/paused', [__CLASS__, 'on_paused'], 10, 1);
add_action('woonoow/subscription/resumed', [__CLASS__, 'on_resumed'], 10, 1);
add_action('woonoow/subscription/renewal_failed', [__CLASS__, 'on_renewal_failed'], 10, 2);
add_action('woonoow/subscription/renewal_payment_due', [__CLASS__, 'on_renewal_payment_due'], 10, 2);
add_action('woonoow/subscription/renewal_reminder', [__CLASS__, 'on_renewal_reminder'], 10, 1);
}
/**
* Initialize manager if module is enabled
*/
public static function maybe_init_manager()
{
if (ModuleRegistry::is_enabled('subscription')) {
// Ensure tables exist
self::ensure_tables();
SubscriptionManager::init();
SubscriptionScheduler::init();
}
}
/**
* Ensure database tables exist
*/
private static function ensure_tables()
{
global $wpdb;
$table = $wpdb->prefix . 'woonoow_subscriptions';
// Check if table exists
if ($wpdb->get_var("SHOW TABLES LIKE '$table'") !== $table) {
SubscriptionManager::create_tables();
}
}
/**
* Handle module enable
*/
public static function on_module_enabled($module_id)
{
if ($module_id === 'subscription') {
SubscriptionManager::create_tables();
}
}
/**
* Add subscription fields to product edit page
*/
public static function add_product_subscription_fields()
{
global $post;
if (!ModuleRegistry::is_enabled('subscription')) {
return;
}
echo '<div class="options_group show_if_simple show_if_variable">';
woocommerce_wp_checkbox([
'id' => '_woonoow_subscription_enabled',
'label' => __('Enable Subscription', 'woonoow'),
'description' => __('Enable recurring subscription billing for this product', 'woonoow'),
]);
echo '<div class="woonoow-subscription-options" style="display:none;">';
woocommerce_wp_select([
'id' => '_woonoow_subscription_period',
'label' => __('Billing Period', 'woonoow'),
'description' => __('How often to bill the customer', 'woonoow'),
'options' => [
'day' => __('Daily', 'woonoow'),
'week' => __('Weekly', 'woonoow'),
'month' => __('Monthly', 'woonoow'),
'year' => __('Yearly', 'woonoow'),
],
'value' => get_post_meta($post->ID, '_woonoow_subscription_period', true) ?: 'month',
]);
woocommerce_wp_text_input([
'id' => '_woonoow_subscription_interval',
'label' => __('Billing Interval', 'woonoow'),
'description' => __('Bill every X periods (e.g., 2 = every 2 months)', 'woonoow'),
'type' => 'number',
'value' => get_post_meta($post->ID, '_woonoow_subscription_interval', true) ?: 1,
'custom_attributes' => [
'min' => '1',
'max' => '365',
'step' => '1',
],
]);
woocommerce_wp_text_input([
'id' => '_woonoow_subscription_trial_days',
'label' => __('Free Trial Days', 'woonoow'),
'description' => __('Number of free trial days before first billing (0 = no trial)', 'woonoow'),
'type' => 'number',
'value' => get_post_meta($post->ID, '_woonoow_subscription_trial_days', true) ?: 0,
'custom_attributes' => [
'min' => '0',
'step' => '1',
],
]);
woocommerce_wp_text_input([
'id' => '_woonoow_subscription_signup_fee',
'label' => __('Sign-up Fee', 'woonoow') . ' (' . get_woocommerce_currency_symbol() . ')',
'description' => __('One-time fee charged on first subscription order', 'woonoow'),
'type' => 'text',
'value' => get_post_meta($post->ID, '_woonoow_subscription_signup_fee', true) ?: '',
'data_type' => 'price',
]);
woocommerce_wp_text_input([
'id' => '_woonoow_subscription_length',
'label' => __('Subscription Length', 'woonoow'),
'description' => __('Number of billing periods (0 = unlimited/until cancelled)', 'woonoow'),
'type' => 'number',
'value' => get_post_meta($post->ID, '_woonoow_subscription_length', true) ?: 0,
'custom_attributes' => [
'min' => '0',
'step' => '1',
],
]);
echo '</div>'; // .woonoow-subscription-options
// Add inline script to show/hide options based on checkbox
?>
<script type="text/javascript">
jQuery(function($) {
function toggleSubscriptionOptions() {
if ($('#_woonoow_subscription_enabled').is(':checked')) {
$('.woonoow-subscription-options').show();
} else {
$('.woonoow-subscription-options').hide();
}
}
toggleSubscriptionOptions();
$('#_woonoow_subscription_enabled').on('change', toggleSubscriptionOptions);
});
</script>
<?php
echo '</div>'; // .options_group
}
/**
* Save subscription fields
*/
public static function save_product_subscription_fields($post_id)
{
$subscription_enabled = isset($_POST['_woonoow_subscription_enabled']) ? 'yes' : 'no';
update_post_meta($post_id, '_woonoow_subscription_enabled', $subscription_enabled);
if (isset($_POST['_woonoow_subscription_period'])) {
update_post_meta($post_id, '_woonoow_subscription_period', sanitize_text_field($_POST['_woonoow_subscription_period']));
}
if (isset($_POST['_woonoow_subscription_interval'])) {
update_post_meta($post_id, '_woonoow_subscription_interval', absint($_POST['_woonoow_subscription_interval']));
}
if (isset($_POST['_woonoow_subscription_trial_days'])) {
update_post_meta($post_id, '_woonoow_subscription_trial_days', absint($_POST['_woonoow_subscription_trial_days']));
}
if (isset($_POST['_woonoow_subscription_signup_fee'])) {
update_post_meta($post_id, '_woonoow_subscription_signup_fee', wc_format_decimal($_POST['_woonoow_subscription_signup_fee']));
}
if (isset($_POST['_woonoow_subscription_length'])) {
update_post_meta($post_id, '_woonoow_subscription_length', absint($_POST['_woonoow_subscription_length']));
}
}
/**
* Maybe create subscription from completed order
*/
public static function maybe_create_subscription($order_id)
{
if (!ModuleRegistry::is_enabled('subscription')) {
return;
}
$order = wc_get_order($order_id);
if (!$order) {
return;
}
// Check if subscription already created for this order
if ($order->get_meta('_woonoow_subscription_created')) {
return;
}
foreach ($order->get_items() as $item) {
$product_id = $item->get_product_id();
$variation_id = $item->get_variation_id();
// Check if product has subscription enabled
if (get_post_meta($product_id, '_woonoow_subscription_enabled', true) !== 'yes') {
continue;
}
// Create subscription for this product
SubscriptionManager::create_from_order($order, $item);
}
// Mark order as processed
$order->update_meta_data('_woonoow_subscription_created', 'yes');
$order->save();
}
/**
* Modify add to cart button text for subscription products
*/
public static function subscription_add_to_cart_text($text, $product)
{
if (!ModuleRegistry::is_enabled('subscription')) {
return $text;
}
$product_id = $product->get_id();
if (get_post_meta($product_id, '_woonoow_subscription_enabled', true) === 'yes') {
$settings = ModuleRegistry::get_settings('subscription');
return $settings['button_text_subscribe'] ?? __('Subscribe Now', 'woonoow');
}
return $text;
}
/**
* Register subscription notification events
*
* @param array $events Existing events
* @return array Updated events
*/
public static function register_notification_events($events)
{
// Customer notifications
$events['subscription_pending_cancel'] = [
'id' => 'subscription_pending_cancel',
'label' => __('Subscription Pending Cancellation', 'woonoow'),
'description' => __('When a subscription is scheduled for cancellation at period end', 'woonoow'),
'category' => 'subscriptions',
'recipient_type' => 'customer',
'wc_email' => '',
'enabled' => true,
'variables' => self::get_subscription_variables(),
];
$events['subscription_cancelled'] = [
'id' => 'subscription_cancelled',
'label' => __('Subscription Cancelled', 'woonoow'),
'description' => __('When a subscription is cancelled and access ends', 'woonoow'),
'category' => 'subscriptions',
'recipient_type' => 'customer',
'wc_email' => '',
'enabled' => true,
'variables' => self::get_subscription_variables(),
];
$events['subscription_expired'] = [
'id' => 'subscription_expired',
'label' => __('Subscription Expired', 'woonoow'),
'description' => __('When a subscription expires due to end date or failed payments', 'woonoow'),
'category' => 'subscriptions',
'recipient_type' => 'customer',
'wc_email' => '',
'enabled' => true,
'variables' => self::get_subscription_variables(),
];
$events['subscription_paused'] = [
'id' => 'subscription_paused',
'label' => __('Subscription Paused', 'woonoow'),
'description' => __('When a subscription is put on hold', 'woonoow'),
'category' => 'subscriptions',
'recipient_type' => 'customer',
'wc_email' => '',
'enabled' => true,
'variables' => self::get_subscription_variables(),
];
$events['subscription_resumed'] = [
'id' => 'subscription_resumed',
'label' => __('Subscription Resumed', 'woonoow'),
'description' => __('When a subscription is resumed from pause', 'woonoow'),
'category' => 'subscriptions',
'recipient_type' => 'customer',
'wc_email' => '',
'enabled' => true,
'variables' => self::get_subscription_variables(),
];
$events['subscription_renewal_failed'] = [
'id' => 'subscription_renewal_failed',
'label' => __('Subscription Renewal Failed', 'woonoow'),
'description' => __('When a renewal payment fails', 'woonoow'),
'category' => 'subscriptions',
'recipient_type' => 'customer',
'wc_email' => '',
'enabled' => true,
'variables' => self::get_subscription_variables(),
];
$events['subscription_renewal_payment_due'] = [
'id' => 'subscription_renewal_payment_due',
'label' => __('Subscription Renewal Payment Due', 'woonoow'),
'description' => __('When a manual payment is required for subscription renewal', 'woonoow'),
'category' => 'subscriptions',
'recipient_type' => 'customer',
'wc_email' => '',
'enabled' => true,
'variables' => array_merge(self::get_subscription_variables(), [
'{payment_link}' => __('Link to payment page', 'woonoow'),
]),
];
$events['subscription_renewal_reminder'] = [
'id' => 'subscription_renewal_reminder',
'label' => __('Subscription Renewal Reminder', 'woonoow'),
'description' => __('Reminder before subscription renewal', 'woonoow'),
'category' => 'subscriptions',
'recipient_type' => 'customer',
'wc_email' => '',
'enabled' => true,
'variables' => self::get_subscription_variables(),
];
// Staff notifications
$events['subscription_cancelled_admin'] = [
'id' => 'subscription_cancelled',
'label' => __('Subscription Cancelled', 'woonoow'),
'description' => __('When a customer cancels their subscription', 'woonoow'),
'category' => 'subscriptions',
'recipient_type' => 'staff',
'wc_email' => '',
'enabled' => true,
'variables' => self::get_subscription_variables(),
];
$events['subscription_renewal_failed_admin'] = [
'id' => 'subscription_renewal_failed',
'label' => __('Subscription Renewal Failed', 'woonoow'),
'description' => __('When a subscription renewal payment fails', 'woonoow'),
'category' => 'subscriptions',
'recipient_type' => 'staff',
'wc_email' => '',
'enabled' => true,
'variables' => self::get_subscription_variables(),
];
return $events;
}
/**
* Get subscription-specific template variables
*
* @return array
*/
private static function get_subscription_variables()
{
return [
'{subscription_id}' => __('Subscription ID', 'woonoow'),
'{subscription_status}' => __('Subscription status', 'woonoow'),
'{product_name}' => __('Product name', 'woonoow'),
'{billing_period}' => __('Billing period (e.g., monthly)', 'woonoow'),
'{recurring_amount}' => __('Recurring payment amount', 'woonoow'),
'{next_payment_date}' => __('Next payment date', 'woonoow'),
'{end_date}' => __('Subscription end date', 'woonoow'),
'{cancel_reason}' => __('Cancellation reason', 'woonoow'),
];
}
/**
* Handle pending cancellation notification
*/
public static function on_pending_cancel($subscription_id, $reason = '')
{
self::send_subscription_notification('subscription_pending_cancel', $subscription_id, $reason);
}
/**
* Handle cancellation notification
*/
public static function on_cancelled($subscription_id, $reason = '')
{
self::send_subscription_notification('subscription_cancelled', $subscription_id, $reason);
}
/**
* Handle expiration notification
*/
public static function on_expired($subscription_id, $reason = '')
{
self::send_subscription_notification('subscription_expired', $subscription_id, $reason);
}
/**
* Handle pause notification
*/
public static function on_paused($subscription_id)
{
self::send_subscription_notification('subscription_paused', $subscription_id);
}
/**
* Handle resume notification
*/
public static function on_resumed($subscription_id)
{
self::send_subscription_notification('subscription_resumed', $subscription_id);
}
/**
* Handle renewal failed notification
*/
public static function on_renewal_failed($subscription_id, $failed_count)
{
self::send_subscription_notification('subscription_renewal_failed', $subscription_id, '', $failed_count);
}
/**
* Handle renewal payment due notification
*/
public static function on_renewal_payment_due($subscription_id, $order = null)
{
$payment_link = '';
if ($order && is_a($order, 'WC_Order')) {
$payment_link = $order->get_checkout_payment_url();
}
self::send_subscription_notification('subscription_renewal_payment_due', $subscription_id, '', 0, ['payment_link' => $payment_link]);
}
/**
* Handle renewal reminder notification
*/
public static function on_renewal_reminder($subscription)
{
if (!$subscription || !isset($subscription->id)) {
return;
}
self::send_subscription_notification('subscription_renewal_reminder', $subscription->id);
}
/**
* Send subscription notification
*
* @param string $event_id Event ID
* @param int $subscription_id Subscription ID
* @param string $reason Optional reason
* @param int $failed_count Optional failed payment count
* @param array $extra_data Optional extra data variables
*/
private static function send_subscription_notification($event_id, $subscription_id, $reason = '', $failed_count = 0, $extra_data = [])
{
$subscription = SubscriptionManager::get($subscription_id);
if (!$subscription) {
return;
}
$user = get_user_by('id', $subscription->user_id);
$product = wc_get_product($subscription->product_id);
$data = [
'subscription' => $subscription,
'customer' => $user,
'product' => $product,
'reason' => $reason,
'failed_count' => $failed_count,
'payment_link' => $extra_data['payment_link'] ?? '',
];
// Send via NotificationManager
if (class_exists('\\WooNooW\\Core\\Notifications\\NotificationManager')) {
\WooNooW\Core\Notifications\NotificationManager::send($event_id, 'email', $data);
}
}
/**
* Handle manual renewal payment completion
*/
public static function on_order_status_changed($order_id, $old_status, $new_status)
{
if (!ModuleRegistry::is_enabled('subscription')) {
return;
}
if (!in_array($new_status, ['processing', 'completed'])) {
return;
}
// Check if this is a subscription renewal order
global $wpdb;
$table_orders = $wpdb->prefix . 'woonoow_subscription_orders';
$link = $wpdb->get_row($wpdb->prepare(
"SELECT subscription_id, order_type FROM $table_orders WHERE order_id = %d",
$order_id
));
if ($link && $link->order_type === 'renewal') {
$order = wc_get_order($order_id);
if ($order) {
SubscriptionManager::handle_renewal_success($link->subscription_id, $order);
}
}
}
}