| @@ -3,9 +3,9 @@ | ||
| 3 | 3 | namespace FluentCart\App\Hooks\Handlers; |
| 4 | 4 | |
| 5 | 5 | use FluentCart\App\Models\Order; |
| 6 | 6 | use FluentCart\App\Models\Subscription; |
| 7 | -use FluentCart\App\Services\Reminders\InvoiceReminderService; | |
| 7 | +use FluentCart\App\Services\Reminders\RenewalReminderService; | |
| 8 | 8 | use FluentCart\App\Services\Reminders\ReminderService; |
| 9 | 9 | use FluentCart\App\Services\Reminders\SubscriptionReminderService; |
| 10 | 10 | use FluentCart\Framework\Support\Arr; |
| 11 | 11 | |
| @@ -16,8 +16,9 @@ | ||
| 16 | 16 | add_action('fluent_cart/scheduler/hourly_tasks', [$this, 'scan'], 40); |
| 17 | 17 | |
| 18 | 18 | add_action(SubscriptionReminderService::RENEWAL_ASYNC_HOOK, [$this, 'sendSubscriptionRenewalReminder'], 10, 3); |
| 19 | 19 | add_action(SubscriptionReminderService::TRIAL_ASYNC_HOOK, [$this, 'sendSubscriptionTrialReminder'], 10, 3); |
| 20 | + add_action(RenewalReminderService::ASYNC_HOOK, [$this, 'sendRenewalReminder'], 10, 3); | |
| 20 | 21 | |
| 21 | 22 | add_filter('fluent_cart/store_settings/values', [$this, 'addDefaultStoreSettings'], 10, 2); |
| 22 | 23 | add_filter('fluent_cart/store_settings/fields', [$this, 'addStoreSettingFields'], 10, 2); |
| 23 | 24 | add_filter('fluent_cart/store_settings/sanitizer', [$this, 'addStoreSettingSanitizers']); |
| @@ -26,8 +27,14 @@ | ||
| 26 | 27 | add_action('fluent_cart/order_refunded', [$this, 'clearOrderReminderState'], 10, 1); |
| 27 | 28 | |
| 28 | 29 | add_action('fluent_cart/payments/subscription_canceled', [$this, 'clearSubscriptionReminderState'], 10, 1); |
| 29 | 30 | add_action('fluent_cart/payments/subscription_expired', [$this, 'clearSubscriptionReminderState'], 10, 1); |
| 31 | + | |
| 32 | + // Also listen on the native events so reminder state clears on the paths the | |
| 33 | + // raw status bus misses (cancel button, model cancel, cron expiry). clearState | |
| 34 | + // is idempotent, so the edit path clearing twice is harmless. | |
| 35 | + add_action('fluent_cart/subscription_canceled', [$this, 'clearSubscriptionReminderState'], 10, 1); | |
| 36 | + add_action('fluent_cart/subscription_expired_validity', [$this, 'clearSubscriptionReminderState'], 10, 1); | |
| 30 | 37 | } |
| 31 | 38 | |
| 32 | 39 | public function scan(): void |
| 33 | 40 | { |
| @@ -43,8 +50,13 @@ | ||
| 43 | 50 | { |
| 44 | 51 | (new SubscriptionReminderService())->sendTrial($subscriptionId, $stage, $cycleKey); |
| 45 | 52 | } |
| 46 | 53 | |
| 54 | + public function sendRenewalReminder($orderId, $stage, $cycleKey): void | |
| 55 | + { | |
| 56 | + (new RenewalReminderService())->send($orderId, $stage, $cycleKey); | |
| 57 | + } | |
| 58 | + | |
| 47 | 59 | public function addDefaultStoreSettings($defaults): array |
| 48 | 60 | { |
| 49 | 61 | if (!is_array($defaults)) { |
| 50 | 62 | $defaults = []; |
| @@ -64,8 +76,11 @@ | ||
| 64 | 76 | 'quarterly_renewal_reminders_enabled' => 'no', |
| 65 | 77 | 'quarterly_renewal_reminder_days' => '14', |
| 66 | 78 | 'half_yearly_renewal_reminders_enabled' => 'no', |
| 67 | 79 | 'half_yearly_renewal_reminder_days' => '21', |
| 80 | + // Invoice reminders for manual subscriptions | |
| 81 | + 'renewal_reminders_enabled' => 'no', | |
| 82 | + 'renewal_reminder_overdue_days' => '1,3,7', | |
| 68 | 83 | ]); |
| 69 | 84 | } |
| 70 | 85 | |
| 71 | 86 | public function addStoreSettingFields($fields): array |
| @@ -124,9 +139,9 @@ | ||
| 124 | 139 | |
| 125 | 140 | 'email_notice' => [ |
| 126 | 141 | 'conditions' => $enabledCondition, |
| 127 | 142 | 'type' => 'html', |
| 128 | - 'value' => '<div class="fc_reminder_email_notice" style="background: #f0f6ff; border: 1px solid #d0e2ff; border-radius: 6px; padding: 10px 14px; font-size: 13px; color: #1e3a5f;">' | |
| 143 | + 'value' => '<div class="fct-alert">' | |
| 129 | 144 | . sprintf( |
| 130 | 145 | /* translators: %s is the opening and closing anchor tag */ |
| 131 | 146 | __('Reminder emails must also be enabled in %1$sEmail Notification Settings%2$s under "Scheduler / Reminder Actions" to be delivered.', 'fluent-cart'), |
| 132 | 147 | '<a href="#/settings/email_notifications" style="color: #017EF3; text-decoration: underline;">', |
| @@ -269,8 +284,63 @@ | ||
| 269 | 284 | ], |
| 270 | 285 | ], |
| 271 | 286 | ], |
| 272 | 287 | ], |
| 288 | + | |
| 289 | + // ── Invoice Reminders Group ── | |
| 290 | + 'invoice_hr' => [ | |
| 291 | + 'conditions' => $enabledCondition, | |
| 292 | + 'type' => 'html', | |
| 293 | + 'value' => '<hr class="settings-divider">' | |
| 294 | + ], | |
| 295 | + 'invoice_group_label' => [ | |
| 296 | + 'conditions' => $enabledCondition, | |
| 297 | + 'type' => 'html', | |
| 298 | + 'wrapperClass' => 'mb-4', | |
| 299 | + 'value' => '<span class="setting-label" style="font-size: 15px;">' . __('Invoice Reminders', 'fluent-cart') . '</span> | |
| 300 | + <div class="form-note">' . __('Send overdue reminders for unpaid renewal orders on manual subscriptions.', 'fluent-cart') . '</div>' | |
| 301 | + ], | |
| 302 | + 'invoice_inner_hr' => [ | |
| 303 | + 'conditions' => $enabledCondition, | |
| 304 | + 'type' => 'html', | |
| 305 | + 'value' => '<hr class="settings-divider">' | |
| 306 | + ], | |
| 307 | + 'invoice_grid' => [ | |
| 308 | + 'conditions' => $enabledCondition, | |
| 309 | + 'type' => 'grid', | |
| 310 | + 'columns' => [ | |
| 311 | + 'default' => 1, | |
| 312 | + 'md' => 3 | |
| 313 | + ], | |
| 314 | + 'disable_nesting' => true, | |
| 315 | + 'schema' => [ | |
| 316 | + 'invoice_label' => [ | |
| 317 | + 'type' => 'html', | |
| 318 | + 'value' => '<span class="setting-label">' . __('Overdue Invoice Reminders', 'fluent-cart') . '</span> | |
| 319 | + <div class="form-note">' . __('Remind customers about unpaid invoices after the due date.', 'fluent-cart') . '</div>' | |
| 320 | + ], | |
| 321 | + 'invoice_input_grid' => [ | |
| 322 | + 'type' => 'grid', | |
| 323 | + 'columns' => ['default' => 1, 'md' => 1], | |
| 324 | + 'disable_nesting' => true, | |
| 325 | + 'class' => 'col-span-2', | |
| 326 | + 'schema' => [ | |
| 327 | + 'renewal_reminders_enabled' => [ | |
| 328 | + 'label' => __('Enable', 'fluent-cart'), | |
| 329 | + 'type' => 'checkbox', | |
| 330 | + 'value' => 'yes', | |
| 331 | + ], | |
| 332 | + 'renewal_reminder_overdue_days' => [ | |
| 333 | + 'label' => false, | |
| 334 | + 'type' => 'input', | |
| 335 | + 'value' => '1,3,7', | |
| 336 | + 'note' => __('Days after due date to send overdue reminders. Comma-separated (e.g. 1,3,7). Default: 1,3,7', 'fluent-cart'), | |
| 337 | + 'conditions' => [['key' => 'renewal_reminders_enabled', 'operator' => '==', 'value' => 'yes']], | |
| 338 | + ], | |
| 339 | + ], | |
| 340 | + ], | |
| 341 | + ], | |
| 342 | + ], | |
| 273 | 343 | ], |
| 274 | 344 | ]; |
| 275 | 345 | |
| 276 | 346 | Arr::set($fields, $tabsPath, $tabs); |
| @@ -300,8 +370,12 @@ | ||
| 300 | 370 | $sanitizers['quarterly_renewal_reminder_days'] = 'intval'; |
| 301 | 371 | $sanitizers['half_yearly_renewal_reminders_enabled'] = 'sanitize_text_field'; |
| 302 | 372 | $sanitizers['half_yearly_renewal_reminder_days'] = 'intval'; |
| 303 | 373 | |
| 374 | + // Invoice reminder sanitizers | |
| 375 | + $sanitizers['renewal_reminders_enabled'] = 'sanitize_text_field'; | |
| 376 | + $sanitizers['renewal_reminder_overdue_days'] = 'sanitize_text_field'; | |
| 377 | + | |
| 304 | 378 | return $sanitizers; |
| 305 | 379 | } |
| 306 | 380 | |
| 307 | 381 | public function clearOrderReminderState($data): void |
| @@ -311,9 +385,9 @@ | ||
| 311 | 385 | if (!$order instanceof Order) { |
| 312 | 386 | return; |
| 313 | 387 | } |
| 314 | 388 | |
| 315 | - (new InvoiceReminderService())->clearState($order); | |
| 389 | + (new RenewalReminderService())->clearState($order); | |
| 316 | 390 | } catch (\Throwable $e) { |
| 317 | 391 | fluent_cart_error_log('Reminder state clear error', $e->getMessage()); |
| 318 | 392 | } |
| 319 | 393 | } |