PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.1
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.1
1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 trunk All 48 releases
fluent-cart / app / Hooks / Handlers / ReminderHandler.php

ReminderHandler.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.6.1, at app/Hooks/Handlers/ReminderHandler.php

409 lines 20.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Hooks\Handlers;
4
5 use FluentCart\App\Models\Order;
6 use FluentCart\App\Models\Subscription;
7 use FluentCart\App\Services\Reminders\RenewalReminderService;
8 use FluentCart\App\Services\Reminders\ReminderService;
9 use FluentCart\App\Services\Reminders\SubscriptionReminderService;
10 use FluentCart\Framework\Support\Arr;
11
12 class ReminderHandler
13 {
14 public function register(): void
15 {
16 add_action('fluent_cart/scheduler/hourly_tasks', [$this, 'scan'], 40);
17
18 add_action(SubscriptionReminderService::RENEWAL_ASYNC_HOOK, [$this, 'sendSubscriptionRenewalReminder'], 10, 3);
19 add_action(SubscriptionReminderService::TRIAL_ASYNC_HOOK, [$this, 'sendSubscriptionTrialReminder'], 10, 3);
20 add_action(RenewalReminderService::ASYNC_HOOK, [$this, 'sendRenewalReminder'], 10, 3);
21
22 add_filter('fluent_cart/store_settings/values', [$this, 'addDefaultStoreSettings'], 10, 2);
23 add_filter('fluent_cart/store_settings/fields', [$this, 'addStoreSettingFields'], 10, 2);
24 add_filter('fluent_cart/store_settings/sanitizer', [$this, 'addStoreSettingSanitizers']);
25
26 add_action('fluent_cart/order_paid', [$this, 'clearOrderReminderState'], 10, 1);
27 add_action('fluent_cart/order_refunded', [$this, 'clearOrderReminderState'], 10, 1);
28
29 add_action('fluent_cart/payments/subscription_canceled', [$this, 'clearSubscriptionReminderState'], 10, 1);
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);
37 }
38
39 public function scan(): void
40 {
41 (new ReminderService())->runHourlyScan();
42 }
43
44 public function sendSubscriptionRenewalReminder($subscriptionId, $stage, $cycleKey): void
45 {
46 (new SubscriptionReminderService())->sendRenewal($subscriptionId, $stage, $cycleKey);
47 }
48
49 public function sendSubscriptionTrialReminder($subscriptionId, $stage, $cycleKey): void
50 {
51 (new SubscriptionReminderService())->sendTrial($subscriptionId, $stage, $cycleKey);
52 }
53
54 public function sendRenewalReminder($orderId, $stage, $cycleKey): void
55 {
56 (new RenewalReminderService())->send($orderId, $stage, $cycleKey);
57 }
58
59 public function addDefaultStoreSettings($defaults): array
60 {
61 if (!is_array($defaults)) {
62 $defaults = [];
63 }
64
65 return wp_parse_args($defaults, [
66 'reminders_enabled' => 'no',
67 // Yearly renewal reminders (enabled by default - compliance)
68 'yearly_renewal_reminders_enabled' => 'yes',
69 'yearly_renewal_reminder_days' => '30',
70 // Trial end reminders (enabled by default - conversion)
71 'trial_end_reminders_enabled' => 'yes',
72 'trial_end_reminder_days' => '3',
73 // Optional additional billing cycles
74 'monthly_renewal_reminders_enabled' => 'no',
75 'monthly_renewal_reminder_days' => '7',
76 'quarterly_renewal_reminders_enabled' => 'no',
77 'quarterly_renewal_reminder_days' => '14',
78 'half_yearly_renewal_reminders_enabled' => 'no',
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',
83 ]);
84 }
85
86 public function addStoreSettingFields($fields): array
87 {
88 $tabsPath = 'setting_tabs.schema';
89 $tabs = Arr::get($fields, $tabsPath, []);
90
91 if (!is_array($tabs)) {
92 return $fields;
93 }
94
95 $enabledCondition = [
96 [
97 'key' => 'reminders_enabled',
98 'operator' => '==',
99 'value' => 'yes',
100 ],
101 ];
102
103 $tabs['reminders'] = [
104 'title' => __('Reminder Emails', 'fluent-cart'),
105 'show_title' => false,
106 'type' => 'section',
107 'columns' => [
108 'default' => 1,
109 'md' => 1
110 ],
111 'disable_nesting' => true,
112 'schema' => [
113 // Master switch row
114 'intro_grid' => [
115 'type' => 'grid',
116 'columns' => [
117 'default' => 1,
118 'md' => 3
119 ],
120 'disable_nesting' => true,
121 'schema' => [
122 'label' => [
123 'type' => 'html',
124 'value' => '<span class="setting-label">' . __('Reminder Emails', 'fluent-cart') . '</span>
125 <div class="form-note">' . __('Send automated reminders for Subscription trial ending and upcoming subscription renewals.', 'fluent-cart') . '</div>'
126 ],
127 'reminders_enabled' => [
128 'wrapperClass' => 'col-span-2 flex items-start justify-end',
129 'label' => false,
130 'type' => 'switch',
131 'value' => 'no',
132 'attributes' => [
133 'active-value' => 'yes',
134 'inactive-value' => 'no',
135 ],
136 ],
137 ],
138 ],
139
140 'email_notice' => [
141 'conditions' => $enabledCondition,
142 'type' => 'html',
143 'value' => '<div class="fct-alert">'
144 . sprintf(
145 /* translators: %s is the opening and closing anchor tag */
146 __('Reminder emails must also be enabled in %1$sEmail Notification Settings%2$s under "Scheduler / Reminder Actions" to be delivered.', 'fluent-cart'),
147 '<a href="#/settings/email_notifications" style="color: #017EF3; text-decoration: underline;">',
148 '</a>'
149 )
150 . '</div>'
151 ],
152
153 // ── Subscription Reminders Group ──
154 'subscription_hr' => [
155 'conditions' => $enabledCondition,
156 'type' => 'html',
157 'value' => '<hr class="settings-divider">'
158 ],
159 'subscription_group_label' => [
160 'conditions' => $enabledCondition,
161 'type' => 'html',
162 'wrapperClass' => 'mb-4',
163 'value' => '<span class="setting-label" style="font-size: 15px;">' . __('Subscription Reminders', 'fluent-cart') . '</span>
164 <div class="form-note">' . __('Send reminders before subscription renewals and trial expirations.', 'fluent-cart') . '</div>'
165 ],
166 'trial_inner_hr' => [
167 'conditions' => $enabledCondition,
168 'type' => 'html',
169 'value' => '<hr class="settings-divider">'
170 ],
171 'trial_grid' => [
172 'conditions' => $enabledCondition,
173 'type' => 'grid',
174 'columns' => [
175 'default' => 1,
176 'md' => 3
177 ],
178 'disable_nesting' => true,
179 'schema' => [
180 'label' => [
181 'type' => 'html',
182 'value' => '<span class="setting-label">' . __('Trial Ending', 'fluent-cart') . '</span>
183 <div class="form-note">' . __('Notify customers before their trial ends and billing begins.', 'fluent-cart') . '</div>'
184 ],
185 'trial_input_grid' => [
186 'type' => 'grid',
187 'columns' => ['default' => 1, 'md' => 1],
188 'disable_nesting' => true,
189 'class' => 'col-span-2',
190 'schema' => [
191 'trial_end_reminders_enabled' => [
192 'label' => __('Enable', 'fluent-cart'),
193 'type' => 'checkbox',
194 'value' => 'yes',
195 ],
196 'trial_end_reminder_days' => [
197 'label' => false,
198 'type' => 'input',
199 'value' => '3',
200 'note' => __('Days before trial ends. Min: 1, Max: 14. Default: 3', 'fluent-cart'),
201 'conditions' => [['key' => 'trial_end_reminders_enabled', 'operator' => '==', 'value' => 'yes']],
202 ],
203 ],
204 ],
205 ],
206 ],
207 'renewal_inner_hr' => [
208 'conditions' => $enabledCondition,
209 'type' => 'html',
210 'value' => '<hr class="settings-divider">'
211 ],
212 'renewal_grid' => [
213 'conditions' => $enabledCondition,
214 'type' => 'grid',
215 'columns' => [
216 'default' => 1,
217 'md' => 3
218 ],
219 'disable_nesting' => true,
220 'schema' => [
221 'label' => [
222 'type' => 'html',
223 'value' => '<span class="setting-label">' . __('Renewal Reminders', 'fluent-cart') . '</span>
224 <div class="form-note">' . __('Notify customers before upcoming subscription renewals.', 'fluent-cart') . '</div>'
225 ],
226 'renewal_input_grid' => [
227 'type' => 'grid',
228 'columns' => ['default' => 1, 'md' => 1],
229 'disable_nesting' => true,
230 'class' => 'col-span-2',
231 'schema' => [
232 'yearly_renewal_reminders_enabled' => [
233 'label' => __('Yearly (Recommended)', 'fluent-cart'),
234 'type' => 'checkbox',
235 'value' => 'yes',
236 ],
237 'yearly_renewal_reminder_days' => [
238 'label' => false,
239 'type' => 'input',
240 'value' => '30',
241 'wrapperClass' => 'mb-6',
242 'note' => __('Days before billing date. Min: 7, Max: 90. Default: 30', 'fluent-cart'),
243 'conditions' => [['key' => 'yearly_renewal_reminders_enabled', 'operator' => '==', 'value' => 'yes']],
244 ],
245 'half_yearly_renewal_reminders_enabled' => [
246 'label' => __('Half Yearly', 'fluent-cart'),
247 'type' => 'checkbox',
248 'value' => 'no',
249 ],
250 'half_yearly_renewal_reminder_days' => [
251 'label' => false,
252 'type' => 'input',
253 'value' => '21',
254 'wrapperClass' => 'mb-6',
255 'note' => __('Days before half yearly renewal. Min: 7, Max: 60. Default: 21', 'fluent-cart'),
256 'conditions' => [['key' => 'half_yearly_renewal_reminders_enabled', 'operator' => '==', 'value' => 'yes']],
257 ],
258 'quarterly_renewal_reminders_enabled' => [
259 'label' => __('Quarterly', 'fluent-cart'),
260 'type' => 'checkbox',
261 'value' => 'no',
262 ],
263 'quarterly_renewal_reminder_days' => [
264 'label' => false,
265 'type' => 'input',
266 'value' => '14',
267 'wrapperClass' => 'mb-6',
268 'note' => __('Days before quarterly renewal. Min: 7, Max: 60. Default: 14', 'fluent-cart'),
269 'conditions' => [['key' => 'quarterly_renewal_reminders_enabled', 'operator' => '==', 'value' => 'yes']],
270 ],
271 'monthly_renewal_reminders_enabled' => [
272 'label' => __('Monthly', 'fluent-cart'),
273 'type' => 'checkbox',
274 'value' => 'no',
275 ],
276 'monthly_renewal_reminder_days' => [
277 'label' => false,
278 'type' => 'input',
279 'value' => '7',
280 'wrapperClass' => 'mb-6',
281 'note' => __('Days before monthly renewal. Min: 3, Max: 28. Default: 7', 'fluent-cart'),
282 'conditions' => [['key' => 'monthly_renewal_reminders_enabled', 'operator' => '==', 'value' => 'yes']],
283 ],
284 ],
285 ],
286 ],
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 ],
343 ],
344 ];
345
346 Arr::set($fields, $tabsPath, $tabs);
347 return $fields;
348 }
349
350 public function addStoreSettingSanitizers($sanitizers): array
351 {
352 if (!is_array($sanitizers)) {
353 $sanitizers = [];
354 }
355
356 $sanitizers['reminders_enabled'] = 'sanitize_text_field';
357
358 // Yearly renewal reminder sanitizers (default)
359 $sanitizers['yearly_renewal_reminders_enabled'] = 'sanitize_text_field';
360 $sanitizers['yearly_renewal_reminder_days'] = 'intval';
361
362 // Trial end reminder sanitizers (default)
363 $sanitizers['trial_end_reminders_enabled'] = 'sanitize_text_field';
364 $sanitizers['trial_end_reminder_days'] = 'intval';
365
366 // Optional billing cycle reminder sanitizers
367 $sanitizers['monthly_renewal_reminders_enabled'] = 'sanitize_text_field';
368 $sanitizers['monthly_renewal_reminder_days'] = 'intval';
369 $sanitizers['quarterly_renewal_reminders_enabled'] = 'sanitize_text_field';
370 $sanitizers['quarterly_renewal_reminder_days'] = 'intval';
371 $sanitizers['half_yearly_renewal_reminders_enabled'] = 'sanitize_text_field';
372 $sanitizers['half_yearly_renewal_reminder_days'] = 'intval';
373
374 // Invoice reminder sanitizers
375 $sanitizers['renewal_reminders_enabled'] = 'sanitize_text_field';
376 $sanitizers['renewal_reminder_overdue_days'] = 'sanitize_text_field';
377
378 return $sanitizers;
379 }
380
381 public function clearOrderReminderState($data): void
382 {
383 try {
384 $order = Arr::get($data, 'order');
385 if (!$order instanceof Order) {
386 return;
387 }
388
389 (new RenewalReminderService())->clearState($order);
390 } catch (\Throwable $e) {
391 fluent_cart_error_log('Reminder state clear error', $e->getMessage());
392 }
393 }
394
395 public function clearSubscriptionReminderState($data): void
396 {
397 try {
398 $subscription = Arr::get($data, 'subscription');
399 if (!$subscription instanceof Subscription) {
400 return;
401 }
402
403 (new SubscriptionReminderService())->clearState($subscription);
404 } catch (\Throwable $e) {
405 fluent_cart_error_log('Subscription reminder state clear error', $e->getMessage());
406 }
407 }
408 }
409