runHourlyScan(); } public function sendSubscriptionRenewalReminder($subscriptionId, $stage, $cycleKey): void { (new SubscriptionReminderService())->sendRenewal($subscriptionId, $stage, $cycleKey); } public function sendSubscriptionTrialReminder($subscriptionId, $stage, $cycleKey): void { (new SubscriptionReminderService())->sendTrial($subscriptionId, $stage, $cycleKey); } public function sendRenewalReminder($orderId, $stage, $cycleKey): void { (new RenewalReminderService())->send($orderId, $stage, $cycleKey); } public function addDefaultStoreSettings($defaults): array { if (!is_array($defaults)) { $defaults = []; } return wp_parse_args($defaults, [ 'reminders_enabled' => 'no', // Yearly renewal reminders (enabled by default - compliance) 'yearly_renewal_reminders_enabled' => 'yes', 'yearly_renewal_reminder_days' => '30', // Trial end reminders (enabled by default - conversion) 'trial_end_reminders_enabled' => 'yes', 'trial_end_reminder_days' => '3', // Optional additional billing cycles 'monthly_renewal_reminders_enabled' => 'no', 'monthly_renewal_reminder_days' => '7', 'quarterly_renewal_reminders_enabled' => 'no', 'quarterly_renewal_reminder_days' => '14', 'half_yearly_renewal_reminders_enabled' => 'no', 'half_yearly_renewal_reminder_days' => '21', // Invoice reminders for manual subscriptions 'renewal_reminders_enabled' => 'no', 'renewal_reminder_overdue_days' => '1,3,7', ]); } public function addStoreSettingFields($fields): array { $tabsPath = 'setting_tabs.schema'; $tabs = Arr::get($fields, $tabsPath, []); if (!is_array($tabs)) { return $fields; } $enabledCondition = [ [ 'key' => 'reminders_enabled', 'operator' => '==', 'value' => 'yes', ], ]; $tabs['reminders'] = [ 'title' => __('Reminder Emails', 'fluent-cart'), 'show_title' => false, 'type' => 'section', 'columns' => [ 'default' => 1, 'md' => 1 ], 'disable_nesting' => true, 'schema' => [ // Master switch row 'intro_grid' => [ 'type' => 'grid', 'columns' => [ 'default' => 1, 'md' => 3 ], 'disable_nesting' => true, 'schema' => [ 'label' => [ 'type' => 'html', 'value' => '' . __('Reminder Emails', 'fluent-cart') . '
' . __('Send automated reminders for Subscription trial ending and upcoming subscription renewals.', 'fluent-cart') . '
' ], 'reminders_enabled' => [ 'wrapperClass' => 'col-span-2 flex items-start justify-end', 'label' => false, 'type' => 'switch', 'value' => 'no', 'attributes' => [ 'active-value' => 'yes', 'inactive-value' => 'no', ], ], ], ], 'email_notice' => [ 'conditions' => $enabledCondition, 'type' => 'html', 'value' => '
' . sprintf( /* translators: %s is the opening and closing anchor tag */ __('Reminder emails must also be enabled in %1$sEmail Notification Settings%2$s under "Scheduler / Reminder Actions" to be delivered.', 'fluent-cart'), '', '' ) . '
' ], // ── Subscription Reminders Group ── 'subscription_hr' => [ 'conditions' => $enabledCondition, 'type' => 'html', 'value' => '
' ], 'subscription_group_label' => [ 'conditions' => $enabledCondition, 'type' => 'html', 'wrapperClass' => 'mb-4', 'value' => '' . __('Subscription Reminders', 'fluent-cart') . '
' . __('Send reminders before subscription renewals and trial expirations.', 'fluent-cart') . '
' ], 'trial_inner_hr' => [ 'conditions' => $enabledCondition, 'type' => 'html', 'value' => '
' ], 'trial_grid' => [ 'conditions' => $enabledCondition, 'type' => 'grid', 'columns' => [ 'default' => 1, 'md' => 3 ], 'disable_nesting' => true, 'schema' => [ 'label' => [ 'type' => 'html', 'value' => '' . __('Trial Ending', 'fluent-cart') . '
' . __('Notify customers before their trial ends and billing begins.', 'fluent-cart') . '
' ], 'trial_input_grid' => [ 'type' => 'grid', 'columns' => ['default' => 1, 'md' => 1], 'disable_nesting' => true, 'class' => 'col-span-2', 'schema' => [ 'trial_end_reminders_enabled' => [ 'label' => __('Enable', 'fluent-cart'), 'type' => 'checkbox', 'value' => 'yes', ], 'trial_end_reminder_days' => [ 'label' => false, 'type' => 'input', 'value' => '3', 'note' => __('Days before trial ends. Min: 1, Max: 14. Default: 3', 'fluent-cart'), 'conditions' => [['key' => 'trial_end_reminders_enabled', 'operator' => '==', 'value' => 'yes']], ], ], ], ], ], 'renewal_inner_hr' => [ 'conditions' => $enabledCondition, 'type' => 'html', 'value' => '
' ], 'renewal_grid' => [ 'conditions' => $enabledCondition, 'type' => 'grid', 'columns' => [ 'default' => 1, 'md' => 3 ], 'disable_nesting' => true, 'schema' => [ 'label' => [ 'type' => 'html', 'value' => '' . __('Renewal Reminders', 'fluent-cart') . '
' . __('Notify customers before upcoming subscription renewals.', 'fluent-cart') . '
' ], 'renewal_input_grid' => [ 'type' => 'grid', 'columns' => ['default' => 1, 'md' => 1], 'disable_nesting' => true, 'class' => 'col-span-2', 'schema' => [ 'yearly_renewal_reminders_enabled' => [ 'label' => __('Yearly (Recommended)', 'fluent-cart'), 'type' => 'checkbox', 'value' => 'yes', ], 'yearly_renewal_reminder_days' => [ 'label' => false, 'type' => 'input', 'value' => '30', 'wrapperClass' => 'mb-6', 'note' => __('Days before billing date. Min: 7, Max: 90. Default: 30', 'fluent-cart'), 'conditions' => [['key' => 'yearly_renewal_reminders_enabled', 'operator' => '==', 'value' => 'yes']], ], 'half_yearly_renewal_reminders_enabled' => [ 'label' => __('Half Yearly', 'fluent-cart'), 'type' => 'checkbox', 'value' => 'no', ], 'half_yearly_renewal_reminder_days' => [ 'label' => false, 'type' => 'input', 'value' => '21', 'wrapperClass' => 'mb-6', 'note' => __('Days before half yearly renewal. Min: 7, Max: 60. Default: 21', 'fluent-cart'), 'conditions' => [['key' => 'half_yearly_renewal_reminders_enabled', 'operator' => '==', 'value' => 'yes']], ], 'quarterly_renewal_reminders_enabled' => [ 'label' => __('Quarterly', 'fluent-cart'), 'type' => 'checkbox', 'value' => 'no', ], 'quarterly_renewal_reminder_days' => [ 'label' => false, 'type' => 'input', 'value' => '14', 'wrapperClass' => 'mb-6', 'note' => __('Days before quarterly renewal. Min: 7, Max: 60. Default: 14', 'fluent-cart'), 'conditions' => [['key' => 'quarterly_renewal_reminders_enabled', 'operator' => '==', 'value' => 'yes']], ], 'monthly_renewal_reminders_enabled' => [ 'label' => __('Monthly', 'fluent-cart'), 'type' => 'checkbox', 'value' => 'no', ], 'monthly_renewal_reminder_days' => [ 'label' => false, 'type' => 'input', 'value' => '7', 'wrapperClass' => 'mb-6', 'note' => __('Days before monthly renewal. Min: 3, Max: 28. Default: 7', 'fluent-cart'), 'conditions' => [['key' => 'monthly_renewal_reminders_enabled', 'operator' => '==', 'value' => 'yes']], ], ], ], ], ], // ── Invoice Reminders Group ── 'invoice_hr' => [ 'conditions' => $enabledCondition, 'type' => 'html', 'value' => '
' ], 'invoice_group_label' => [ 'conditions' => $enabledCondition, 'type' => 'html', 'wrapperClass' => 'mb-4', 'value' => '' . __('Invoice Reminders', 'fluent-cart') . '
' . __('Send overdue reminders for unpaid renewal orders on manual subscriptions.', 'fluent-cart') . '
' ], 'invoice_inner_hr' => [ 'conditions' => $enabledCondition, 'type' => 'html', 'value' => '
' ], 'invoice_grid' => [ 'conditions' => $enabledCondition, 'type' => 'grid', 'columns' => [ 'default' => 1, 'md' => 3 ], 'disable_nesting' => true, 'schema' => [ 'invoice_label' => [ 'type' => 'html', 'value' => '' . __('Overdue Invoice Reminders', 'fluent-cart') . '
' . __('Remind customers about unpaid invoices after the due date.', 'fluent-cart') . '
' ], 'invoice_input_grid' => [ 'type' => 'grid', 'columns' => ['default' => 1, 'md' => 1], 'disable_nesting' => true, 'class' => 'col-span-2', 'schema' => [ 'renewal_reminders_enabled' => [ 'label' => __('Enable', 'fluent-cart'), 'type' => 'checkbox', 'value' => 'yes', ], 'renewal_reminder_overdue_days' => [ 'label' => false, 'type' => 'input', 'value' => '1,3,7', 'note' => __('Days after due date to send overdue reminders. Comma-separated (e.g. 1,3,7). Default: 1,3,7', 'fluent-cart'), 'conditions' => [['key' => 'renewal_reminders_enabled', 'operator' => '==', 'value' => 'yes']], ], ], ], ], ], ], ]; Arr::set($fields, $tabsPath, $tabs); return $fields; } public function addStoreSettingSanitizers($sanitizers): array { if (!is_array($sanitizers)) { $sanitizers = []; } $sanitizers['reminders_enabled'] = 'sanitize_text_field'; // Yearly renewal reminder sanitizers (default) $sanitizers['yearly_renewal_reminders_enabled'] = 'sanitize_text_field'; $sanitizers['yearly_renewal_reminder_days'] = 'intval'; // Trial end reminder sanitizers (default) $sanitizers['trial_end_reminders_enabled'] = 'sanitize_text_field'; $sanitizers['trial_end_reminder_days'] = 'intval'; // Optional billing cycle reminder sanitizers $sanitizers['monthly_renewal_reminders_enabled'] = 'sanitize_text_field'; $sanitizers['monthly_renewal_reminder_days'] = 'intval'; $sanitizers['quarterly_renewal_reminders_enabled'] = 'sanitize_text_field'; $sanitizers['quarterly_renewal_reminder_days'] = 'intval'; $sanitizers['half_yearly_renewal_reminders_enabled'] = 'sanitize_text_field'; $sanitizers['half_yearly_renewal_reminder_days'] = 'intval'; // Invoice reminder sanitizers $sanitizers['renewal_reminders_enabled'] = 'sanitize_text_field'; $sanitizers['renewal_reminder_overdue_days'] = 'sanitize_text_field'; return $sanitizers; } public function clearOrderReminderState($data): void { try { $order = Arr::get($data, 'order'); if (!$order instanceof Order) { return; } (new RenewalReminderService())->clearState($order); } catch (\Throwable $e) { fluent_cart_error_log('Reminder state clear error', $e->getMessage()); } } public function clearSubscriptionReminderState($data): void { try { $subscription = Arr::get($data, 'subscription'); if (!$subscription instanceof Subscription) { return; } (new SubscriptionReminderService())->clearState($subscription); } catch (\Throwable $e) { fluent_cart_error_log('Subscription reminder state clear error', $e->getMessage()); } } }