PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.5
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.5
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
← All changes | app/Services/Email/EmailNotificationMailer.php +138 -11 1.5.0 → 1.6.5 View file →
@@ -30,14 +30,21 @@
30 30 );
31 31
32 32 }, 999, 1);
33 33 // To Admin
34 + // 999 like the rest of this file: custom smartcodes in a customised admin
35 + // body resolve against the payload as it stands when the mail is built, so
36 + // the mail has to run after everything else bound here writes its data —
37 + // integration feeds (11), FluentCRM (20), affiliate referral status (99).
38 + // Cost of running last: this hook forces every feed to run realtime, so the
39 + // mail waits on their outbound HTTP, and IntegrationEventListener catches
40 + // only \Exception — a \Error in a feed loses the mail.
34 41 add_action('fluent_cart/order_paid_done', function ($data) {
35 42 $this->mailEmailsOfEvent(
36 43 'order_paid_done',
37 44 $data
38 45 );
39 - }, 10, 1);
46 + }, 999, 1);
40 47
41 48 // to customer and admin
42 49 add_action('fluent_cart/subscription_renewed', function ($data) {
43 50 $this->mailEmailsOfEvent(
@@ -46,8 +53,16 @@
46 53 );
47 54 }, 999, 1);
48 55
49 56 // to customer and admin
57 + add_action('fluent_cart/subscription_renewal_failed', function ($data) {
58 + $this->mailEmailsOfEvent(
59 + 'subscription_renewal_failed',
60 + $data
61 + );
62 + }, 999, 1);
63 +
64 + // to customer and admin
50 65 add_action('fluent_cart/subscription_canceled', function ($data) {
51 66 $this->mailEmailsOfEvent(
52 67 'subscription_canceled',
53 68 $data
@@ -65,17 +80,67 @@
65 80 add_action('fluent_cart/shipping_status_changed_to_delivered', function ($data) {
66 81 $this->mailEmailsOfEvent('shipping_status_changed_to_delivered', $data);
67 82 }, 999, 1);
68 83
69 - // @todo uncomment when invoice feature is deployed
70 - // add_action('fluent_cart/invoice_reminder_due', function ($data) {
71 - // $this->mailEmailsOfEvent('invoice_reminder_due', $data);
72 - // }, 999, 1);
84 + add_action('fluent_cart/renewal_created', function ($data) {
85 + $this->mailEmailsOfEvent('renewal_created', $data);
86 + }, 999, 1);
73 87
74 - add_action('fluent_cart/invoice_reminder_overdue', function ($data) {
75 - $this->mailEmailsOfEvent('invoice_reminder_overdue', $data);
88 + add_action('fluent_cart/renewal_payment_reminder', function ($data) {
89 + $this->mailEmailsOfEvent('renewal_payment_reminder', $data);
76 90 }, 999, 1);
77 91
92 + // Fired by RenewalService when a system renewal order is created ahead of
93 + // its automatic charge — the customer's advance notice of amount and date.
94 + add_action('fluent_cart/subscriptions/system_renewal_scheduled', function ($data) {
95 + $shouldSend = apply_filters('fluent_cart/subscriptions/upcoming_charge_notification', true, $data);
96 +
97 + if ($shouldSend) {
98 + $this->mailEmailsOfEvent('system_upcoming_charge', $data);
99 + }
100 + }, 999, 1);
101 +
102 + // Fired by SystemChargeService when an automatic (system) renewal charge
103 + // fails and the customer should be notified (first failure by default).
104 + add_action('fluent_cart/subscriptions/system_charge_failed_notification', function ($data) {
105 + $this->mailEmailsOfEvent('system_charge_failed', $data);
106 + }, 999, 1);
107 +
108 + add_action('fluent_cart/subscription_period_skipped', function ($data) {
109 + $this->mailEmailsOfEvent('subscription_period_skipped', $data);
110 + }, 999, 1);
111 +
112 + add_action('fluent_cart/subscription_past_due', function ($data) {
113 + $this->mailEmailsOfEvent('subscription_past_due', $data);
114 + }, 999, 1);
115 +
116 + add_action('fluent_cart/renewal_reminder_due', function ($data) {
117 + $this->mailEmailsOfEvent('renewal_reminder_due', $data);
118 + }, 999, 1);
119 +
120 + add_action('fluent_cart/renewal_reminder_overdue', function ($data) {
121 + // Set only by the deprecated bridge in RenewalReminderService::send():
122 + // the staged email (renewal_overdue_first/followup/final) already went
123 + // out for this payload, so mailing here would duplicate it. Direct
124 + // dispatches of this hook never carry the flag and still deliver.
125 + if (!empty(Arr::get($data, 'staged_email_dispatched'))) {
126 + return;
127 + }
128 + $this->mailEmailsOfEvent('renewal_reminder_overdue', $data);
129 + }, 999, 1);
130 +
131 + add_action('fluent_cart/renewal_overdue_first', function ($data) {
132 + $this->mailEmailsOfEvent('renewal_overdue_first', $data);
133 + }, 999, 1);
134 +
135 + add_action('fluent_cart/renewal_overdue_followup', function ($data) {
136 + $this->mailEmailsOfEvent('renewal_overdue_followup', $data);
137 + }, 999, 1);
138 +
139 + add_action('fluent_cart/renewal_overdue_final', function ($data) {
140 + $this->mailEmailsOfEvent('renewal_overdue_final', $data);
141 + }, 999, 1);
142 +
78 143 add_action('fluent_cart/subscription_renewal_reminder', function ($data) {
79 144 $this->mailEmailsOfEvent('subscription_renewal_reminder', $data);
80 145 }, 999, 1);
81 146
@@ -191,8 +256,16 @@
191 256 $mailer->addAttachment($pdfPath);
192 257 }
193 258 }
194 259
260 + $mailer = $this->applyMailerFilter($mailer, [
261 + 'event' => $event,
262 + 'mail_name' => $mailName,
263 + 'recipient' => Arr::get($notification, 'recipient'),
264 + 'notification' => $notification,
265 + 'data' => $data,
266 + ]);
267 +
195 268 $mailer->send(true);
196 269
197 270 // Clean up temp PDF file after sending
198 271 if ($pdfPath && file_exists($pdfPath)) {
@@ -204,12 +277,13 @@
204 277 }
205 278
206 279 public function mailByEmailName($emailName, $data)
207 280 {
208 - // Extract Order model before formatParsable converts it to array
281 + // $data keeps its Models: parseEmailContent renders emails.parts.order_header,
282 + // which reads $order->invoice_no / $order->orderTaxRates — an array here
283 + // warns and then fatals inside the view (regression of 201a14885 via 44a39aa1d)
209 284 $orderModel = Arr::get($data, 'order');
210 285
211 - $data = $this->formatParsable($data);
212 286 $notification = EmailNotifications::getNotification($emailName);
213 287 $notification = EmailNotifications::formatNotification($notification, $data);
214 288 list($body, $subject, $to) = $this->parseEmailContent($notification, $data);
215 289
@@ -226,8 +300,16 @@
226 300 $mailer->addAttachment($pdfPath);
227 301 }
228 302 }
229 303
304 + $mailer = $this->applyMailerFilter($mailer, [
305 + 'event' => Arr::get($notification, 'event', ''),
306 + 'mail_name' => $emailName,
307 + 'recipient' => Arr::get($notification, 'recipient'),
308 + 'notification' => $notification,
309 + 'data' => $data,
310 + ]);
311 +
230 312 $mailer->send(true);
231 313
232 314 if ($pdfPath && file_exists($pdfPath)) {
233 315 @unlink($pdfPath);
@@ -233,8 +315,23 @@
233 315 @unlink($pdfPath);
234 316 }
235 317 }
236 318
319 + /**
320 + * Let third-party code adjust the fully prepared Mailer (recipients,
321 + * subject, body, attachments already set) immediately before it sends.
322 + *
323 + * @param Mailer $mailer
324 + * @param array $context event, mail_name, recipient, notification, data
325 + * @return Mailer
326 + */
327 + private function applyMailerFilter(Mailer $mailer, array $context): Mailer
328 + {
329 + $filtered = apply_filters('fluent_cart/email_notification/mailer', $mailer, $context);
330 +
331 + return $filtered instanceof Mailer ? $filtered : $mailer;
332 + }
333 +
237 334 public function getEmailFooter(): string
238 335 {
239 336
240 337 $footer = "";
@@ -274,9 +371,18 @@
274 371 ]);
275 372 }
276 373
277 374 if (empty($body)) {
278 - $header = App::make('view')->make('emails.parts.order_header', $data);
375 + // order_header reads $order->invoice_no and $order->orderTaxRates, so
376 + // anything that is not an Order model fatals inside the view — an
377 + // array warns and then dies on ->first(). Render it only for a real
378 + // model: notifications that legitimately have no order still get a
379 + // body, they just get no order header.
380 + $orderModel = Arr::get($data, 'order');
381 + $header = ($orderModel instanceof Order)
382 + ? (string)App::make('view')->make('emails.parts.order_header', $data)
383 + : '';
384 +
279 385 $body = (string)App::make('view')->make('emails.general_template', [
280 386 'emailBody' => $rawBody,
281 387 'preheader' => Arr::get($notification, 'pre_header', ''),
282 388 'header' => $header,
@@ -286,10 +392,31 @@
286 392
287 393 $body = ShortcodeTemplateBuilder::make($body, $data);
288 394
289 395 $subject = ShortcodeTemplateBuilder::make(Arr::get($notification, 'subject', ''), $data);
396 + // A notification may declare an array of recipients — wp_mail() accepts
397 + // one — so expand element by element rather than flattening to a string.
290 398 $to = Arr::get($notification, 'to', '');
291 - $to = ShortcodeTemplateBuilder::make($to, $data);
399 + if (is_array($to)) {
400 + foreach ($to as $index => $address) {
401 + $to[$index] = ShortcodeTemplateBuilder::make((string)$address, $data);
402 + }
403 + } else {
404 + $to = ShortcodeTemplateBuilder::make((string)$to, $data);
405 + }
406 +
407 + // Admin-bound notifications only — stores route these to a helpdesk or
408 + // accounting inbox. Customer-bound mail must keep going to the customer,
409 + // so it is deliberately not filterable here. Applied after shortcode
410 + // resolution, so listeners see the address the notification resolved to.
411 + if (Arr::get($notification, 'recipient') === 'admin') {
412 + $to = apply_filters('fluent_cart/admin_email/notification_recipient', $to, [
413 + 'event' => Arr::get($notification, 'event', ''),
414 + 'mail_name' => Arr::get($notification, 'name', ''),
415 + 'notification' => $notification,
416 + 'data' => $data,
417 + ]);
418 + }
292 419
293 420 return [
294 421 0 => $body,
295 422 1 => $subject,