PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.7.1
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.7.1
1.7.1 1.7.0 1.6.6 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 All 51 releases
← All changes | app/Services/Email/EmailNotifications.php +462 -64 1.5.3 → 1.7.1 View file →
@@ -1,8 +1,9 @@
1 1 <?php
2 2
3 3 namespace FluentCart\App\Services\Email;
4 4
5 +use FluentCart\Api\ModuleSettings;
5 6 use FluentCart\App\Models\Meta;
6 7 use FluentCart\App\Services\Cache;
7 8 use FluentCart\App\Services\TemplateService;
8 9 use FluentCart\Framework\Support\Arr;
@@ -17,12 +18,24 @@
17 18 ];
18 19
19 20 const META_KEY = 'email_notifications_config';
20 21
22 + /** The events the reviews module raises. */
23 + const REVIEW_EVENTS = ['review_created', 'review_approved_done', 'review_replied_done'];
24 +
21 25 public static function getNotifications(): array
22 26 {
23 27 $settings = static::getDefaultNotifications();
24 28 $settings = apply_filters('fluent_cart/email_notifications', $settings);
29 +
30 + // Review notifications are neither offered nor sent while the module
31 + // is off; their stored settings are kept by name.
32 + if (!ModuleSettings::isActive('reviews')) {
33 + $settings = array_filter($settings, function ($setting) {
34 + return !in_array(Arr::get($setting, 'event'), static::REVIEW_EVENTS, true);
35 + });
36 + }
37 +
25 38 $config = Arr::get(static::cachedSettings(), 'notification_config', []);
26 39
27 40 foreach ($settings as $key => &$setting) {
28 41 $setting['name'] = $key;
@@ -57,10 +70,13 @@
57 70 * Order Renewal -> fluent_cart/subscription_renewed (customer + admin)
58 71 * Order Refunded -> fluent_cart/order_fully_refunded (customer + admin)
59 72 * Order Shipping Status Changed => fluent_cart/shipping_status_changed (customer + admin)
60 73 * Reminder Events:
61 - * - fluent_cart/invoice_reminder_due (@todo uncomment when invoice feature is deployed)
62 - * - fluent_cart/invoice_reminder_overdue (used for payment reminders)
74 + * - fluent_cart/renewal_reminder_due
75 + * - fluent_cart/renewal_reminder_overdue (manual/on-demand payment reminder)
76 + * - fluent_cart/renewal_overdue_first (scheduled, first overdue stage)
77 + * - fluent_cart/renewal_overdue_followup (scheduled, intermediate overdue stages)
78 + * - fluent_cart/renewal_overdue_final (scheduled, last overdue stage)
63 79 * - fluent_cart/subscription_renewal_reminder
64 80 * - fluent_cart/subscription_trial_end_reminder
65 81 */
66 82
@@ -138,8 +154,43 @@
138 154 'email_body' => '',
139 155 'attach_pdf_template' => '',
140 156 ]
141 157 ],
158 + 'subscription_renewal_failed_customer' => [
159 + 'event' => 'subscription_renewal_failed',
160 + 'group' => 'subscription',
161 + 'group_label' => __('Subscription Actions', 'fluent-cart'),
162 + 'title' => __('Notify customer when an automatic renewal charge fails', 'fluent-cart'),
163 + 'description' => __('This email will be sent to the customer when their saved payment method could not be charged for a subscription renewal.', 'fluent-cart'),
164 + 'recipient' => 'customer',
165 + 'smartcode_groups' => [],
166 + 'template_path' => 'subscription.renewal_failed.customer',
167 + 'is_async' => false,
168 + 'settings' => [
169 + 'active' => 'yes',
170 + 'subject' => __('Automatic Payment Failed for Renewal #{{order.invoice_no}}', 'fluent-cart'),
171 + 'is_default_body' => 'yes',
172 + 'email_body' => '',
173 + ]
174 + ],
175 + 'subscription_renewal_failed_admin' => [
176 + 'event' => 'subscription_renewal_failed',
177 + 'group' => 'subscription',
178 + 'group_label' => __('Subscription Actions', 'fluent-cart'),
179 + 'title' => __('Notify admin when an automatic renewal charge fails', 'fluent-cart'),
180 + 'description' => __('This email will be sent to the admin when a saved payment method could not be charged for a subscription renewal.', 'fluent-cart'),
181 + 'recipient' => 'admin',
182 + 'smartcode_groups' => [],
183 + 'template_path' => 'subscription.renewal_failed.admin',
184 + 'pre_header' => __('An automatic subscription renewal charge failed. Review the order in the FluentCart Dashboard.', 'fluent-cart'),
185 + 'is_async' => false,
186 + 'settings' => [
187 + 'active' => 'no',
188 + 'subject' => __('Automatic Charge Failed for Renewal #{{order.invoice_no}}', 'fluent-cart'),
189 + 'is_default_body' => 'yes',
190 + 'email_body' => '',
191 + ]
192 + ],
142 193 'subscription_canceled_customer' => [
143 194 'event' => 'subscription_canceled',
144 195 'group' => 'subscription',
145 196 'group_label' => __('Subscription Actions', 'fluent-cart'),
@@ -173,8 +224,25 @@
173 224 'is_default_body' => 'yes',
174 225 'email_body' => '',
175 226 ]
176 227 ],
228 + 'subscription_period_skipped_customer' => [
229 + 'event' => 'subscription_period_skipped',
230 + 'group' => 'subscription',
231 + 'group_label' => __('Subscription Actions', 'fluent-cart'),
232 + 'title' => __('Notify customer when a billing period is skipped', 'fluent-cart'),
233 + 'description' => __('This email will be sent to the customer when an admin skips their next billing period. The internal reason and actor are never included. Off by default.', 'fluent-cart'),
234 + 'recipient' => 'customer',
235 + 'smartcode_groups' => [],
236 + 'template_path' => 'subscription.period_skipped.customer',
237 + 'is_async' => false,
238 + 'settings' => [
239 + 'active' => 'no',
240 + 'subject' => __('Your next payment has been skipped on {{settings.store_name}}', 'fluent-cart'),
241 + 'is_default_body' => 'yes',
242 + 'email_body' => '',
243 + ]
244 + ],
177 245 'order_refunded_admin' => [
178 246 'event' => 'order_refunded',
179 247 'group' => 'order',
180 248 'group_label' => __('Order Actions', 'fluent-cart'),
@@ -284,42 +352,132 @@
284 352 'email_body' => '',
285 353 'attach_pdf_template' => '',
286 354 ]
287 355 ],
288 - // @todo uncomment when invoice feature is deployed
289 - // 'invoice_reminder_due_customer' => [
290 - // 'event' => 'invoice_reminder_due',
291 - // 'title' => __('Invoice due reminder to customer', 'fluent-cart'),
292 - // 'description' => __('This email will be sent before/at invoice due date when payment is pending.', 'fluent-cart'),
293 - // 'recipient' => 'customer',
294 - // 'smartcode_groups' => [],
295 - // 'template_path' => 'order.reminder.due.customer',
296 - // 'is_async' => false,
297 - // 'settings' => [
298 - // 'active' => 'yes',
299 - // 'subject' => __('Payment Reminder #{{order.invoice_no}}', 'fluent-cart'),
300 - // 'is_default_body' => 'yes',
301 - // 'email_body' => '',
302 - // ]
303 - // ],
304 - // 'invoice_reminder_due_admin' => [
305 - // 'event' => 'invoice_reminder_due',
306 - // 'title' => __('Invoice due reminder copy to admin', 'fluent-cart'),
307 - // 'description' => __('This email will be sent to admin when a due reminder is sent to customer.', 'fluent-cart'),
308 - // 'recipient' => 'admin',
309 - // 'smartcode_groups' => [],
310 - // 'template_path' => 'order.reminder.due.admin',
311 - // 'pre_header' => 'An invoice due reminder was sent to a customer. Review the order details in this email or visit FluentCart Dashboard.',
312 - // 'is_async' => false,
313 - // 'settings' => [
314 - // 'active' => 'no',
315 - // 'subject' => __('Invoice Reminder Sent #{{order.invoice_no}}', 'fluent-cart'),
316 - // 'is_default_body' => 'yes',
317 - // 'email_body' => '',
318 - // ]
319 - // ],
320 - 'invoice_reminder_overdue_customer' => [
321 - 'event' => 'invoice_reminder_overdue',
356 + 'renewal_created_admin' => [
357 + 'event' => 'renewal_created',
358 + 'group' => 'manual_subscription',
359 + 'group_label' => __('Store-Managed Renewals', 'fluent-cart'),
360 + 'title' => __('Notify admin when a renewal order is created', 'fluent-cart'),
361 + 'description' => __('This email will be sent to the admin when a renewal order is generated for a store-managed subscription.', 'fluent-cart'),
362 + 'recipient' => 'admin',
363 + 'smartcode_groups' => [],
364 + 'template_path' => 'renewal.created.admin',
365 + 'pre_header' => 'A new renewal order has been created for a store-managed subscription. Review it in the FluentCart Dashboard.',
366 + 'is_async' => false,
367 + 'settings' => [
368 + 'active' => 'yes',
369 + 'subject' => __('New Renewal Order Created on {{settings.store_name}}', 'fluent-cart'),
370 + 'is_default_body' => 'yes',
371 + 'email_body' => '',
372 + ]
373 + ],
374 + 'renewal_created_customer' => [
375 + 'event' => 'renewal_created',
376 + 'group' => 'manual_subscription',
377 + 'group_label' => __('Store-Managed Renewals', 'fluent-cart'),
378 + 'title' => __('Send the renewal order to the customer when a renewal order is created', 'fluent-cart'),
379 + 'description' => __('This email will be sent to the customer when a renewal order is generated for a store-managed subscription.', 'fluent-cart'),
380 + 'recipient' => 'customer',
381 + 'smartcode_groups' => [],
382 + 'template_path' => 'renewal.created.customer',
383 + 'is_async' => false,
384 + 'settings' => [
385 + 'active' => 'yes',
386 + 'subject' => __('Your Subscription Renewal #{{order.invoice_no}}', 'fluent-cart'),
387 + 'is_default_body' => 'yes',
388 + 'email_body' => '',
389 + ]
390 + ],
391 + 'system_upcoming_charge_customer' => [
392 + 'event' => 'system_upcoming_charge',
393 + 'group' => 'system_subscription',
394 + 'group_label' => __('Store-Managed Renewals · Auto-Charge', 'fluent-cart'),
395 + 'title' => __('Notify customer before an automatic renewal charge', 'fluent-cart'),
396 + 'description' => __('This email tells the customer the amount and date of the upcoming automatic charge to their saved payment method, when the renewal order is created ahead of the due date.', 'fluent-cart'),
397 + 'recipient' => 'customer',
398 + 'smartcode_groups' => [],
399 + 'template_path' => 'subscription.upcoming_charge.customer',
400 + 'is_async' => false,
401 + 'settings' => [
402 + 'active' => 'yes',
403 + 'subject' => __('Upcoming automatic payment for your subscription', 'fluent-cart'),
404 + 'is_default_body' => 'yes',
405 + 'email_body' => '',
406 + ]
407 + ],
408 + 'system_charge_failed_customer' => [
409 + 'event' => 'system_charge_failed',
410 + 'group' => 'system_subscription',
411 + 'group_label' => __('Store-Managed Renewals · Auto-Charge', 'fluent-cart'),
412 + 'title' => __('Notify customer when an automatic renewal charge fails', 'fluent-cart'),
413 + 'description' => __('This email will be sent to the customer when the saved payment method could not be charged for a renewal order. It includes the failure reason and a Pay Now link.', 'fluent-cart'),
414 + 'recipient' => 'customer',
415 + 'smartcode_groups' => [],
416 + 'template_path' => 'subscription.charge_failed.customer',
417 + 'is_async' => false,
418 + 'settings' => [
419 + 'active' => 'yes',
420 + 'subject' => __('Automatic Payment Failed for Renewal #{{order.invoice_no}}', 'fluent-cart'),
421 + 'is_default_body' => 'yes',
422 + 'email_body' => '',
423 + ]
424 + ],
425 + 'system_charge_failed_admin' => [
426 + 'event' => 'system_charge_failed',
427 + 'group' => 'system_subscription',
428 + 'group_label' => __('Store-Managed Renewals · Auto-Charge', 'fluent-cart'),
429 + 'title' => __('Notify admin when an automatic renewal charge fails', 'fluent-cart'),
430 + 'description' => __('This email will be sent to the admin when a saved payment method could not be charged for a renewal order.', 'fluent-cart'),
431 + 'recipient' => 'admin',
432 + 'smartcode_groups' => [],
433 + 'template_path' => 'subscription.charge_failed.admin',
434 + 'pre_header' => 'An automatic subscription renewal charge failed. Review the renewal order in the FluentCart Dashboard.',
435 + 'is_async' => false,
436 + 'settings' => [
437 + 'active' => 'no',
438 + 'subject' => __('Automatic Charge Failed for Renewal #{{order.invoice_no}}', 'fluent-cart'),
439 + 'is_default_body' => 'yes',
440 + 'email_body' => '',
441 + ]
442 + ],
443 + 'renewal_reminder_due_admin' => [
444 + 'event' => 'renewal_reminder_due',
445 + 'group' => 'manual_subscription',
446 + 'group_label' => __('Store-Managed Renewals', 'fluent-cart'),
447 + 'title' => __('Renewal due reminder copy to admin', 'fluent-cart'),
448 + 'description' => __('This email will be sent to admin when a due reminder is sent to customer.', 'fluent-cart'),
449 + 'recipient' => 'admin',
450 + 'smartcode_groups' => [],
451 + 'template_path' => 'order.reminder.due.admin',
452 + 'pre_header' => 'A renewal due reminder was sent to a customer. Review the order details in this email or visit FluentCart Dashboard.',
453 + 'is_async' => false,
454 + 'settings' => [
455 + 'active' => 'no',
456 + 'subject' => __('Renewal Reminder Sent #{{order.invoice_no}}', 'fluent-cart'),
457 + 'is_default_body' => 'yes',
458 + 'email_body' => '',
459 + ]
460 + ],
461 + 'renewal_reminder_due_customer' => [
462 + 'event' => 'renewal_reminder_due',
463 + 'group' => 'manual_subscription',
464 + 'group_label' => __('Store-Managed Renewals', 'fluent-cart'),
465 + 'title' => __('Renewal due reminder to customer', 'fluent-cart'),
466 + 'description' => __('This email will be sent before/at renewal due date when payment is pending.', 'fluent-cart'),
467 + 'recipient' => 'customer',
468 + 'smartcode_groups' => [],
469 + 'template_path' => 'order.reminder.due.customer',
470 + 'is_async' => false,
471 + 'settings' => [
472 + 'active' => 'yes',
473 + 'subject' => __('Payment Reminder #{{order.invoice_no}}', 'fluent-cart'),
474 + 'is_default_body' => 'yes',
475 + 'email_body' => '',
476 + ]
477 + ],
478 + 'renewal_reminder_overdue_customer' => [
479 + 'event' => 'renewal_reminder_overdue',
322 480 'group' => 'scheduler',
323 481 'group_label' => __('Scheduler / Reminder Actions', 'fluent-cart'),
324 482 'title' => __('Payment reminder to customer', 'fluent-cart'),
325 483 'description' => __('This email will be sent to remind customer about pending payment.', 'fluent-cart'),
@@ -334,21 +492,39 @@
334 492 'is_default_body' => 'yes',
335 493 'email_body' => '',
336 494 ]
337 495 ],
338 - 'subscription_renewal_reminder_customer' => [
339 - 'event' => 'subscription_renewal_reminder',
340 - 'group' => 'scheduler',
341 - 'group_label' => __('Scheduler / Reminder Actions', 'fluent-cart'),
342 - 'title' => __('Upcoming renewal reminder to customer', 'fluent-cart'),
343 - 'description' => __('This email will be sent before subscription auto-renewal date.', 'fluent-cart'),
496 + 'subscription_past_due_admin' => [
497 + 'event' => 'subscription_past_due',
498 + 'group' => 'subscription',
499 + 'group_label' => __('Subscription Actions', 'fluent-cart'),
500 + 'title' => __('Notify admin when a subscription is past due', 'fluent-cart'),
501 + 'description' => __('This email will be sent to the admin when a subscription is marked as past due.', 'fluent-cart'),
502 + 'recipient' => 'admin',
503 + 'smartcode_groups' => [],
504 + 'template_path' => 'subscription.past_due.admin',
505 + 'pre_header' => 'A subscription payment is past due. Review the subscription details in this email or visit FluentCart Dashboard.',
506 + 'is_async' => false,
507 + 'settings' => [
508 + 'active' => 'no',
509 + 'subject' => __('Subscription Past Due #{{order.invoice_no}}', 'fluent-cart'),
510 + 'is_default_body' => 'yes',
511 + 'email_body' => '',
512 + ]
513 + ],
514 + 'subscription_past_due_customer' => [
515 + 'event' => 'subscription_past_due',
516 + 'group' => 'subscription',
517 + 'group_label' => __('Subscription Actions', 'fluent-cart'),
518 + 'title' => __('Notify customer when subscription is past due', 'fluent-cart'),
519 + 'description' => __('This email will be sent to the customer when their subscription is marked as past due.', 'fluent-cart'),
344 520 'recipient' => 'customer',
345 521 'smartcode_groups' => [],
346 - 'template_path' => 'subscription.reminder.customer',
522 + 'template_path' => 'subscription.past_due.customer',
347 523 'is_async' => false,
348 524 'settings' => [
349 525 'active' => 'yes',
350 - 'subject' => __('Upcoming Renewal Reminder from {{settings.store_name}}', 'fluent-cart'),
526 + 'subject' => __('Your Subscription is Past Due on {{settings.store_name}}', 'fluent-cart'),
351 527 'is_default_body' => 'yes',
352 528 'email_body' => '',
353 529 ]
354 530 ],
@@ -369,26 +545,77 @@
369 545 'is_default_body' => 'yes',
370 546 'email_body' => '',
371 547 ]
372 548 ],
373 - 'subscription_trial_end_reminder_customer' => [
374 - 'event' => 'subscription_trial_end_reminder',
549 + 'subscription_renewal_reminder_customer' => [
550 + 'event' => 'subscription_renewal_reminder',
375 551 'group' => 'scheduler',
376 552 'group_label' => __('Scheduler / Reminder Actions', 'fluent-cart'),
377 - 'title' => __('Trial ending soon reminder to customer', 'fluent-cart'),
378 - 'description' => __('This email will be sent before a trial period ends and converts to a paid subscription.', 'fluent-cart'),
553 + 'title' => __('Upcoming renewal reminder to customer', 'fluent-cart'),
554 + 'description' => __('This email will be sent before subscription auto-renewal date.', 'fluent-cart'),
379 555 'recipient' => 'customer',
380 556 'smartcode_groups' => [],
381 - 'template_path' => 'subscription.trial_end.customer',
557 + 'template_path' => 'subscription.reminder.customer',
382 558 'is_async' => false,
383 559 'settings' => [
384 560 'active' => 'yes',
385 - 'subject' => __('Your Trial is Ending Soon - {{settings.store_name}}', 'fluent-cart'),
561 + 'subject' => __('Upcoming Renewal Reminder from {{settings.store_name}}', 'fluent-cart'),
386 562 'is_default_body' => 'yes',
387 563 'email_body' => '',
388 564 ]
389 565 ],
390 - 'subscription_trial_end_reminder_admin' => [
566 + 'renewal_overdue_first_customer' => [
567 + 'event' => 'renewal_overdue_first',
568 + 'group' => 'scheduler',
569 + 'group_label' => __('Scheduler / Reminder Actions', 'fluent-cart'),
570 + 'title' => __('First overdue renewal reminder to customer', 'fluent-cart'),
571 + 'description' => __('This email will be sent at the first overdue reminder day after a renewal due date while the payment is still pending.', 'fluent-cart'),
572 + 'recipient' => 'customer',
573 + 'smartcode_groups' => [],
574 + 'template_path' => 'order.reminder.renewal_overdue.first.customer',
575 + 'is_async' => false,
576 + 'settings' => [
577 + 'active' => 'yes',
578 + 'subject' => __('Payment Reminder for Renewal #{{order.order_ref}}', 'fluent-cart'),
579 + 'is_default_body' => 'yes',
580 + 'email_body' => '',
581 + ]
582 + ],
583 + 'renewal_overdue_followup_customer' => [
584 + 'event' => 'renewal_overdue_followup',
585 + 'group' => 'scheduler',
586 + 'group_label' => __('Scheduler / Reminder Actions', 'fluent-cart'),
587 + 'title' => __('Follow-up overdue renewal reminder to customer', 'fluent-cart'),
588 + 'description' => __('This email will be sent at the intermediate overdue reminder days while the renewal payment remains pending.', 'fluent-cart'),
589 + 'recipient' => 'customer',
590 + 'smartcode_groups' => [],
591 + 'template_path' => 'order.reminder.renewal_overdue.followup.customer',
592 + 'is_async' => false,
593 + 'settings' => [
594 + 'active' => 'yes',
595 + 'subject' => __('Payment Overdue for Renewal #{{order.order_ref}}', 'fluent-cart'),
596 + 'is_default_body' => 'yes',
597 + 'email_body' => '',
598 + ]
599 + ],
600 + 'renewal_overdue_final_customer' => [
601 + 'event' => 'renewal_overdue_final',
602 + 'group' => 'scheduler',
603 + 'group_label' => __('Scheduler / Reminder Actions', 'fluent-cart'),
604 + 'title' => __('Final overdue renewal notice to customer', 'fluent-cart'),
605 + 'description' => __('This email will be sent at the last configured overdue reminder day while the renewal payment is still pending.', 'fluent-cart'),
606 + 'recipient' => 'customer',
607 + 'smartcode_groups' => [],
608 + 'template_path' => 'order.reminder.renewal_overdue.final.customer',
609 + 'is_async' => false,
610 + 'settings' => [
611 + 'active' => 'yes',
612 + 'subject' => __('Final Notice: Payment Overdue for Renewal #{{order.order_ref}}', 'fluent-cart'),
613 + 'is_default_body' => 'yes',
614 + 'email_body' => '',
615 + ]
616 + ],
617 + 'subscription_trial_end_reminder_admin' => [
391 618 'event' => 'subscription_trial_end_reminder',
392 619 'group' => 'scheduler',
393 620 'group_label' => __('Scheduler / Reminder Actions', 'fluent-cart'),
394 621 'title' => __('Trial ending soon reminder copy to admin', 'fluent-cart'),
@@ -404,12 +631,142 @@
404 631 'is_default_body' => 'yes',
405 632 'email_body' => '',
406 633 ]
407 634 ],
635 + 'subscription_trial_end_reminder_customer' => [
636 + 'event' => 'subscription_trial_end_reminder',
637 + 'group' => 'scheduler',
638 + 'group_label' => __('Scheduler / Reminder Actions', 'fluent-cart'),
639 + 'title' => __('Trial ending soon reminder to customer', 'fluent-cart'),
640 + 'description' => __('This email will be sent before a trial period ends and converts to a paid subscription.', 'fluent-cart'),
641 + 'recipient' => 'customer',
642 + 'smartcode_groups' => [],
643 + 'template_path' => 'subscription.trial_end.customer',
644 + 'is_async' => false,
645 + 'settings' => [
646 + 'active' => 'yes',
647 + 'subject' => __('Your Trial is Ending Soon - {{settings.store_name}}', 'fluent-cart'),
648 + 'is_default_body' => 'yes',
649 + 'email_body' => '',
650 + ]
651 + ],
652 + 'review_submitted_admin' => [
653 + 'event' => 'review_created',
654 + 'group' => 'review',
655 + 'group_label' => __('Review Actions', 'fluent-cart'),
656 + 'title' => __('Send mail to admin when a new review is submitted', 'fluent-cart'),
657 + 'description' => __('Email admin when a customer submits a new review.', 'fluent-cart'),
658 + 'recipient' => 'admin',
659 + 'smartcode_groups' => [],
660 + 'template_path' => 'review.submitted.admin',
661 + 'is_async' => false,
662 + 'pre_header' => __('A new product review has been submitted.', 'fluent-cart'),
663 + 'settings' => [
664 + 'active' => 'yes',
665 + 'subject' => __('New Review Submitted', 'fluent-cart'),
666 + 'is_default_body' => 'yes',
667 + 'email_body' => '',
668 + ]
669 + ],
670 + 'review_approved_customer' => [
671 + // The settled hook, not the transition: fired from a queued
672 + // job once the review has been re-read and is still approved,
673 + // the way order_paid_done is to order_paid. A bulk approve is
674 + // fifty of these, and none of them is sent inside that request.
675 + 'event' => 'review_approved_done',
676 + 'group' => 'review',
677 + 'group_label' => __('Review Actions', 'fluent-cart'),
678 + 'title' => __('Send mail to the reviewer when their review is approved', 'fluent-cart'),
679 + 'description' => __('Email the customer who wrote a review once it is approved and visible on the product page.', 'fluent-cart'),
680 + 'recipient' => 'customer',
681 + // Declared, because the customer recipient resolves through an
682 + // order and a review has none. The resolver behind this code is
683 + // the one every review notification uses.
684 + 'to' => '{{review.recipient_email}}',
685 + 'smartcode_groups' => [],
686 + 'template_path' => 'review.approved.customer',
687 + 'is_async' => false,
688 + 'pre_header' => __('Your review is now live.', 'fluent-cart'),
689 + 'settings' => [
690 + 'active' => 'yes',
691 + 'subject' => __('Your review of {{review.product_title}} is now live', 'fluent-cart'),
692 + 'is_default_body' => 'yes',
693 + 'email_body' => '',
694 + ]
695 + ],
696 + 'review_replied_customer' => [
697 + // Fired from a queued job once the reply and the review it
698 + // answers have both been re-read and are still published.
699 + 'event' => 'review_replied_done',
700 + 'group' => 'review',
701 + 'group_label' => __('Review Actions', 'fluent-cart'),
702 + 'title' => __('Send mail to the reviewer when the store replies to their review', 'fluent-cart'),
703 + 'description' => __('Email the customer who wrote a review when the store posts a reply to it.', 'fluent-cart'),
704 + 'recipient' => 'customer',
705 + // The review's author, resolved the way every review
706 + // notification resolves it — {{review.*}} is the review being
707 + // answered, not the reply.
708 + 'to' => '{{review.recipient_email}}',
709 + 'smartcode_groups' => [],
710 + 'template_path' => 'review.replied.customer',
711 + 'is_async' => false,
712 + 'pre_header' => __('The store has replied to your review.', 'fluent-cart'),
713 + 'settings' => [
714 + 'active' => 'yes',
715 + 'subject' => __('{{settings.store_name}} replied to your review of {{review.product_title}}', 'fluent-cart'),
716 + 'is_default_body' => 'yes',
717 + 'email_body' => '',
718 + ]
719 + ],
408 720 ];
409 721
410 722 }
411 723
724 + /**
725 + * The names of the notifications that would send for an event — the same
726 + * gate getNotificationsOfEvent() applies, without rendering a body.
727 + *
728 + * For a caller that only needs to know whether, and which, before it
729 + * commits to something — a once-only lease, say — and will render at
730 + * send time through mailByEmailName(). Rendering here as well would draw
731 + * every template twice per send.
732 + *
733 + * Kept in step with getNotificationsOfEvent(): the four conditions below
734 + * are that method's, in the same order.
735 + *
736 + * @param string $event
737 + * @return string[] notification names, in registry order
738 + */
739 + public static function activeNotificationNamesOfEvent($event): array
740 + {
741 + $mailingSettings = static::getSettings();
742 + $names = [];
743 +
744 + foreach (static::getNotifications() as $key => $notification) {
745 + if (Arr::get($notification, 'event') !== $event) {
746 + continue;
747 + }
748 +
749 + $settings = Arr::get($notification, 'settings');
750 + if (!in_array($notification['event'], ['order_placed_offline', 'renewal_reminder_overdue']) && Arr::get($settings, 'active') !== 'yes') {
751 + continue;
752 + }
753 +
754 + $recipient = Arr::get($notification, 'recipient');
755 + if (!Arr::get($notification, 'to') && !in_array($recipient, ['admin', 'customer', 'user', 'subscriber'])) {
756 + continue;
757 + }
758 +
759 + if (empty(static::resolveRecipientTemplate($notification, $mailingSettings))) {
760 + continue;
761 + }
762 +
763 + $names[] = (string) $key;
764 + }
765 +
766 + return $names;
767 + }
768 +
412 769 public static function getNotificationsOfEvent($event, $viewData): array
413 770 {
414 771 $notifications = static::getNotifications();
415 772 $notifications = array_filter($notifications, function ($notification) use ($event) {
@@ -427,9 +784,9 @@
427 784
428 785 $settings = Arr::get($notification, 'settings');
429 786
430 787 // Skip active check for on-demand notifications (offline payments, payment reminders)
431 - if (in_array($notification['event'], ['order_placed_offline', 'invoice_reminder_overdue']) || Arr::get($settings, 'active') === 'yes') {
788 + if (in_array($notification['event'], ['order_placed_offline', 'renewal_reminder_overdue']) || Arr::get($settings, 'active') === 'yes') {
432 789 // Continue with normal flow
433 790 } else {
434 791 continue;
435 792 }
@@ -434,17 +791,16 @@
434 791 continue;
435 792 }
436 793 $recipient = Arr::get($notification, 'recipient');
437 794
438 - if (!in_array($recipient, ['admin', 'customer', 'user', 'subscriber'])) {
795 + // A notification that declares its own `to` carries its recipient
796 + // with it, so the whitelist — which only describes the recipients
797 + // core knows how to resolve — does not apply to it.
798 + if (!Arr::get($notification, 'to') && !in_array($recipient, ['admin', 'customer', 'user', 'subscriber'])) {
439 799 continue;
440 800 }
441 801
442 - if ($recipient === 'admin') {
443 - $toEmail = Arr::get($mailingSettings, 'admin_email', '');
444 - } else {
445 - $toEmail = self::EMAIL_RECIPIENT_MAP[$recipient];
446 - }
802 + $toEmail = static::resolveRecipientTemplate($notification, $mailingSettings);
447 803
448 804 if (empty($toEmail)) {
449 805 continue;
450 806 }
@@ -459,15 +815,10 @@
459 815 {
460 816 $settings = Arr::get($notification, 'settings');
461 817 $mailingSettings = static::getSettings();
462 818
463 -
464 819 $recipient = Arr::get($notification, 'recipient');
465 - if ($recipient === 'admin') {
466 - $toEmail = Arr::get($mailingSettings, 'admin_email', '');
467 - } else {
468 - $toEmail = self::EMAIL_RECIPIENT_MAP[$recipient];
469 - }
820 + $toEmail = static::resolveRecipientTemplate($notification, $mailingSettings);
470 821
471 822
472 823 $isDefaultEmailBody = Arr::get($settings, 'is_default_body', 'yes') === 'yes' || empty(Arr::get($settings, 'email_body'));
473 824
@@ -482,9 +833,56 @@
482 833 'is_async' => $notification['is_async'],
483 834 'subject' => Arr::get($settings, 'subject'),
484 835 'is_custom' => !$isDefaultEmailBody,
485 836 'settings' => $settings,
837 + // Carried through so send-time consumers can tell one notification
838 + // from another, and admin-bound mail from customer-bound — see the
839 + // admin recipient filter in parseEmailContent().
840 + 'name' => Arr::get($notification, 'name', ''),
841 + 'event' => Arr::get($notification, 'event', ''),
842 + 'recipient' => $recipient,
486 843 ];
844 + }
845 +
846 + /**
847 + * Resolve the recipient template for a notification.
848 + *
849 + * A notification may declare its own `to` — a smartcode template, or a
850 + * literal address — which wins outright. Add-ons need this: they register
851 + * notifications that are not order-bound, and the built-in customer
852 + * recipient resolves to {{order.customer.email}}, which has nothing to read
853 + * from without an order.
854 + *
855 + * Falling back to the recipient map keeps every core notification, and any
856 + * add-on that does not declare `to`, on exactly the path it used before.
857 + *
858 + * The returned value is a template — parseEmailContent() runs it through
859 + * the shortcode parser before use. A declared `to` is returned as given:
860 + * wp_mail() accepts an array of recipients as well as a comma-separated
861 + * string, so an array is passed through rather than flattened.
862 + *
863 + * @param array $notification
864 + * @param array|null $mailingSettings resolved settings, passed in to avoid a repeat lookup in loops
865 + * @return string|array
866 + */
867 + public static function resolveRecipientTemplate($notification, $mailingSettings = null)
868 + {
869 + $declaredTo = Arr::get($notification, 'to', '');
870 + if (!empty($declaredTo)) {
871 + return $declaredTo;
872 + }
873 +
874 + if ($mailingSettings === null) {
875 + $mailingSettings = static::getSettings();
876 + }
877 +
878 + $recipient = Arr::get($notification, 'recipient');
879 +
880 + if ($recipient === 'admin') {
881 + return (string)Arr::get($mailingSettings, 'admin_email', '');
882 + }
883 +
884 + return (string)Arr::get(self::EMAIL_RECIPIENT_MAP, $recipient, '');
487 885 }
488 886
489 887 public static function getNotification($name)
490 888 {