PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.4
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.4
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 1.2.0 All 47 releases
fluent-cart / app / Services / Email / EmailNotificationMailer.php

EmailNotificationMailer.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.6.4, at app/Services/Email/EmailNotificationMailer.php

500 lines 19.4 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\Services\Email;
4
5 use FluentCart\App\App;
6 use FluentCart\App\Models\Model;
7 use FluentCart\App\Models\Order;
8 use FluentCart\App\Models\Subscription;
9 use FluentCart\App\Services\OrderService;
10 use FluentCart\App\Services\ShortCodeParser\ShortcodeTemplateBuilder;
11 use FluentCart\Framework\Support\Arr;
12
13 class EmailNotificationMailer
14 {
15 public function register()
16 {
17 // $this->registerAsyncMails();
18 add_action('fluent_cart/order_placed_offline', function ($data) {
19 $this->mailEmailsOfEvent(
20 'order_placed_offline',
21 $data
22 );
23
24 }, 999, 1);
25 // To Customer
26 add_action('fluent_cart/order_paid', function ($data) {
27 $this->mailEmailsOfEvent(
28 'order_paid',
29 $data
30 );
31
32 }, 999, 1);
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.
41 add_action('fluent_cart/order_paid_done', function ($data) {
42 $this->mailEmailsOfEvent(
43 'order_paid_done',
44 $data
45 );
46 }, 999, 1);
47
48 // to customer and admin
49 add_action('fluent_cart/subscription_renewed', function ($data) {
50 $this->mailEmailsOfEvent(
51 'subscription_renewed',
52 $data
53 );
54 }, 999, 1);
55
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
65 add_action('fluent_cart/subscription_canceled', function ($data) {
66 $this->mailEmailsOfEvent(
67 'subscription_canceled',
68 $data
69 );
70 }, 999, 1);
71
72 add_action('fluent_cart/order_refunded', function ($data) {
73 $this->mailEmailsOfEvent('order_refunded', $data);
74 }, 999, 1);
75
76 add_action('fluent_cart/shipping_status_changed_to_shipped', function ($data) {
77 $this->mailEmailsOfEvent('shipping_status_changed_to_shipped', $data);
78 }, 999, 1);
79
80 add_action('fluent_cart/shipping_status_changed_to_delivered', function ($data) {
81 $this->mailEmailsOfEvent('shipping_status_changed_to_delivered', $data);
82 }, 999, 1);
83
84 add_action('fluent_cart/renewal_created', function ($data) {
85 $this->mailEmailsOfEvent('renewal_created', $data);
86 }, 999, 1);
87
88 add_action('fluent_cart/renewal_payment_reminder', function ($data) {
89 $this->mailEmailsOfEvent('renewal_payment_reminder', $data);
90 }, 999, 1);
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
143 add_action('fluent_cart/subscription_renewal_reminder', function ($data) {
144 $this->mailEmailsOfEvent('subscription_renewal_reminder', $data);
145 }, 999, 1);
146
147 add_action('fluent_cart/subscription_trial_end_reminder', function ($data) {
148 $this->mailEmailsOfEvent('subscription_trial_end_reminder', $data);
149 }, 999, 1);
150
151 }
152
153 public function registerAsyncMails()
154 {
155 //For Async Actions
156 add_action('fluent_cart/async_mail/order_created', function ($orderId, $mailName = '') {
157 (new static())->sendAsyncOrderMail($mailName, $orderId);
158 }, 10, 2);
159
160 add_action('fluent_cart/async_mail/order_placed_offline', function ($orderId, $mailName = '') {
161 (new static())->sendAsyncOrderMail($mailName, $orderId);
162 }, 10, 2);
163
164 add_action('fluent_cart/async_mail/order_paid', function ($orderId, $mailName = '') {
165 (new static())->sendAsyncOrderMail($mailName, $orderId);
166 }, 10, 2);
167
168 add_action('fluent_cart/async_mail/order_updated', function ($orderId, $mailName = '') {
169 (new static())->sendAsyncOrderMail($mailName, $orderId);
170 }, 10, 2);
171
172 add_action('fluent_cart/async_mail/order_refunded', function ($orderId, $mailName = '') {
173 (new static())->sendAsyncOrderMail($mailName, $orderId);
174 }, 10, 2);
175
176 add_action('fluent_cart/async_mail/subscription_activated', function ($subscriptionId, $mailName = '') {
177 (new static())->sendAsyncSubscriptionMail($mailName, $subscriptionId);
178 }, 10, 2);
179
180 add_action('fluent_cart/async_mail/subscription_reactivated', function ($subscriptionId, $mailName = '') {
181 (new static())->sendAsyncSubscriptionMail($mailName, $subscriptionId);
182 }, 10, 2);
183
184 add_action('fluent_cart/async_mail/subscription_renewed', function ($subscriptionId, $mailName = '') {
185 (new static())->sendAsyncSubscriptionMail($mailName, $subscriptionId);
186 }, 10, 2);
187
188 add_action('fluent_cart/async_mail/subscription_eot', function ($subscriptionId, $mailName = '') {
189 (new static())->sendAsyncSubscriptionMail($mailName, $subscriptionId);
190 }, 10, 2);
191
192 add_action('fluent_cart/async_mail/subscription_canceled', function ($subscriptionId, $mailName = '') {
193 (new static())->sendAsyncSubscriptionMail($mailName, $subscriptionId);
194 }, 10, 2);
195
196 add_action('fluent_cart/async_mail/subscription_expired', function ($subscriptionId, $mailName = '') {
197 (new static())->sendAsyncSubscriptionMail($mailName, $subscriptionId);
198 }, 10, 2);
199
200 }
201
202
203 public function formatParsable($parsable)
204 {
205 foreach ($parsable as &$item) {
206 if ($item instanceof Model) {
207 $item = $item->toArray();
208 }
209 }
210
211 if (!Arr::has($parsable, 'order.customer')) {
212 $parsable['order']['customer'] = Arr::get(
213 $parsable, 'customer', []
214 );
215 }
216
217 return $parsable;
218 }
219
220 public function mailEmailsOfEvent($event, $data, $asyncHook = '', $asyncData = [])
221 {
222 $parsedData = $this->formatParsable($data);
223
224 $order = Arr::get($data, 'order');
225
226 // Pass original Model data for template rendering (templates use $order->method())
227 $notifications = EmailNotifications::getNotificationsOfEvent($event, $data);
228
229 foreach ($notifications as $mailName => $notification) {
230 if ($order instanceof Order && !apply_filters('fluent_cart/should_send_email_notification', true, [
231 'event' => $event,
232 'mail_name' => $mailName,
233 'order' => $order,
234 ])) {
235 continue;
236 }
237
238 $isAsync = Arr::get($notification, 'is_async', false);
239 if ($isAsync && !empty($asyncHook)) {
240 $asyncData['mailName'] = $mailName;
241 as_enqueue_async_action($asyncHook, $asyncData);
242 } else {
243 list($body, $subject, $to) = $this->parseEmailContent($notification, $data);
244
245 $mailer = Mailer::make()->to($to)->subject($subject)->body($body);
246
247 $pdfPath = null;
248 $templateId = Arr::get($notification, 'settings.attach_pdf_template', '');
249 // Backward compat: old attach_pdf='yes' maps to order_receipt
250 if (empty($templateId) && Arr::get($notification, 'settings.attach_pdf', 'no') === 'yes') {
251 $templateId = 'order_receipt';
252 }
253 if (!empty($templateId) && defined('FLUENT_PDF')) {
254 $pdfPath = $this->generateOrderPdf($data, $templateId);
255 if ($pdfPath) {
256 $mailer->addAttachment($pdfPath);
257 }
258 }
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
268 $mailer->send(true);
269
270 // Clean up temp PDF file after sending
271 if ($pdfPath && file_exists($pdfPath)) {
272 @unlink($pdfPath);
273 }
274 }
275 }
276
277 }
278
279 public function mailByEmailName($emailName, $data)
280 {
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)
284 $orderModel = Arr::get($data, 'order');
285
286 $notification = EmailNotifications::getNotification($emailName);
287 $notification = EmailNotifications::formatNotification($notification, $data);
288 list($body, $subject, $to) = $this->parseEmailContent($notification, $data);
289
290 $mailer = Mailer::make()->to($to)->subject($subject)->body($body);
291
292 $pdfPath = null;
293 $templateId = Arr::get($notification, 'settings.attach_pdf_template', '');
294 if (empty($templateId) && Arr::get($notification, 'settings.attach_pdf', 'no') === 'yes') {
295 $templateId = 'order_receipt';
296 }
297 if (!empty($templateId) && defined('FLUENT_PDF')) {
298 $pdfPath = $this->generateOrderPdf(['order' => $orderModel], $templateId);
299 if ($pdfPath) {
300 $mailer->addAttachment($pdfPath);
301 }
302 }
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
312 $mailer->send(true);
313
314 if ($pdfPath && file_exists($pdfPath)) {
315 @unlink($pdfPath);
316 }
317 }
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
334 public function getEmailFooter(): string
335 {
336
337 $footer = "";
338 $settings = EmailNotifications::getSettings();
339 $emailFooter = Arr::get($settings, 'email_footer', '');
340 if (!empty($emailFooter)) {
341 $footer .= ShortcodeTemplateBuilder::make($emailFooter, []);
342 }
343 $isEmailFooter = EmailNotifications::getSettings('show_email_footer');
344 if (!App::isProActive() || $isEmailFooter === 'yes') {
345 $cartFooter = "<div style='padding: 15px; text-align: center; font-size: 16px; color: #2F3448;'>Powered by <a href='https://fluentcart.com' style='color: #017EF3; text-decoration: none;'>FluentCart</a></div>";
346 $footer .= $cartFooter;
347 }
348 return $footer;
349
350 }
351
352 public function parseEmailContent($notification, $data, $templateData = null): array
353 {
354 $templateData = $templateData ?: $data;
355
356 $rawBody = Arr::get($notification, 'body', '');
357 $isCustom = (bool) Arr::get($notification, 'is_custom', false);
358
359 // Let pro parse block content; returns empty string if no pro
360 $parsedBody = apply_filters('fluent_cart/parse_email_block_content', '', $rawBody, $data);
361
362 $body = '';
363
364 // Custom block emails use block_editor_template (pro);
365 // default templates use general_template with legacy order_header.
366 if ($isCustom && $parsedBody) {
367 $body = apply_filters('fluent_cart/render_block_email_template', '', [
368 'emailBody' => $parsedBody,
369 'preheader' => Arr::get($notification, 'pre_header', ''),
370 'emailFooter' => $this->getEmailFooter(),
371 ]);
372 }
373
374 if (empty($body)) {
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
385 $body = (string)App::make('view')->make('emails.general_template', [
386 'emailBody' => $rawBody,
387 'preheader' => Arr::get($notification, 'pre_header', ''),
388 'header' => $header,
389 'emailFooter' => $this->getEmailFooter(),
390 ]);
391 }
392
393 $body = ShortcodeTemplateBuilder::make($body, $data);
394
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.
398 $to = Arr::get($notification, 'to', '');
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 }
419
420 return [
421 0 => $body,
422 1 => $subject,
423 2 => $to
424 ];
425 }
426
427 public function sendAsyncOrderMail($emailName, $orderId)
428 {
429 $order = Order::query()->with(['customer', 'shipping_address', 'billing_address', 'transactions', 'orderTaxRates'])->find($orderId);
430
431 if ($order) {
432 $transaction = [];
433 if (!empty($order->transactions)) {
434 $transaction = $order->transactions->first();
435 }
436 $this->mailByEmailName($emailName, [
437 'order' => $order,
438 'customer' => $order->customer !== null ? $order->customer : [],
439 'transaction' => $transaction
440 ]);
441 }
442 }
443
444 public function sendAsyncSubscriptionMail($emailName, $subscriptionId)
445 {
446 $subscription = Subscription::query()->with([
447 'customer',
448 'transactions',
449 'order'
450 ])->find($subscriptionId);
451
452 if ($subscription) {
453 $order = $subscription->order;
454 if (!$order) {
455 return;
456 }
457 $this->mailByEmailName($emailName, [
458 'subscription' => $subscription,
459 'order' => $order,
460 'customer' => $subscription->customer !== null ? $subscription->customer : [],
461 'transactions' => $subscription->transactions
462 ]);
463 }
464
465 }
466
467 /**
468 * Generate a PDF receipt for the order in the given data array.
469 *
470 * @param array $data Event data containing 'order' key
471 * @return string|null Path to generated PDF or null
472 */
473 private function generateOrderPdf(array $data, string $templateId = 'order_receipt'): ?string
474 {
475 if (!OrderService::canGenerateReceiptPdf()) {
476 return null;
477 }
478
479 $order = Arr::get($data, 'order');
480 if (!$order || !($order instanceof Order)) {
481 return null;
482 }
483
484 try {
485 return apply_filters('fluent_cart/pdf/generate_receipt', null, [
486 'order' => $order,
487 'template_id' => $templateId,
488 ]);
489 } catch (\Throwable $e) {
490 fluent_cart_add_log(
491 'PDF Generation Failed',
492 $e->getMessage(),
493 'error'
494 );
495 return null;
496 }
497 }
498
499 }
500