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 +143 -12 1.3.25 → 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
@@ -111,8 +176,12 @@
111 176 add_action('fluent_cart/async_mail/subscription_activated', function ($subscriptionId, $mailName = '') {
112 177 (new static())->sendAsyncSubscriptionMail($mailName, $subscriptionId);
113 178 }, 10, 2);
114 179
180 + add_action('fluent_cart/async_mail/subscription_reactivated', function ($subscriptionId, $mailName = '') {
181 + (new static())->sendAsyncSubscriptionMail($mailName, $subscriptionId);
182 + }, 10, 2);
183 +
115 184 add_action('fluent_cart/async_mail/subscription_renewed', function ($subscriptionId, $mailName = '') {
116 185 (new static())->sendAsyncSubscriptionMail($mailName, $subscriptionId);
117 186 }, 10, 2);
118 187
@@ -187,8 +256,16 @@
187 256 $mailer->addAttachment($pdfPath);
188 257 }
189 258 }
190 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 +
191 268 $mailer->send(true);
192 269
193 270 // Clean up temp PDF file after sending
194 271 if ($pdfPath && file_exists($pdfPath)) {
@@ -200,12 +277,13 @@
200 277 }
201 278
202 279 public function mailByEmailName($emailName, $data)
203 280 {
204 - // 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)
205 284 $orderModel = Arr::get($data, 'order');
206 285
207 - $data = $this->formatParsable($data);
208 286 $notification = EmailNotifications::getNotification($emailName);
209 287 $notification = EmailNotifications::formatNotification($notification, $data);
210 288 list($body, $subject, $to) = $this->parseEmailContent($notification, $data);
211 289
@@ -222,8 +300,16 @@
222 300 $mailer->addAttachment($pdfPath);
223 301 }
224 302 }
225 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 +
226 312 $mailer->send(true);
227 313
228 314 if ($pdfPath && file_exists($pdfPath)) {
229 315 @unlink($pdfPath);
@@ -229,8 +315,23 @@
229 315 @unlink($pdfPath);
230 316 }
231 317 }
232 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 +
233 334 public function getEmailFooter(): string
234 335 {
235 336
236 337 $footer = "";
@@ -270,9 +371,18 @@
270 371 ]);
271 372 }
272 373
273 374 if (empty($body)) {
274 - $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 +
275 385 $body = (string)App::make('view')->make('emails.general_template', [
276 386 'emailBody' => $rawBody,
277 387 'preheader' => Arr::get($notification, 'pre_header', ''),
278 388 'header' => $header,
@@ -282,11 +392,32 @@
282 392
283 393 $body = ShortcodeTemplateBuilder::make($body, $data);
284 394
285 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.
286 398 $to = Arr::get($notification, 'to', '');
287 - $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 + }
288 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 + }
419 +
289 420 return [
290 421 0 => $body,
291 422 1 => $subject,
292 423 2 => $to
@@ -294,9 +425,9 @@
294 425 }
295 426
296 427 public function sendAsyncOrderMail($emailName, $orderId)
297 428 {
298 - $order = Order::query()->with(['customer', 'shipping_address', 'billing_address', 'transactions'])->find($orderId);
429 + $order = Order::query()->with(['customer', 'shipping_address', 'billing_address', 'transactions', 'orderTaxRates'])->find($orderId);
299 430
300 431 if ($order) {
301 432 $transaction = [];
302 433 if (!empty($order->transactions)) {