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

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

678 lines 27.8 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\ProductReview;
9 use FluentCart\App\Models\Subscription;
10 use FluentCart\App\Services\OrderService;
11 use FluentCart\App\Services\ProductReviewService;
12 use FluentCart\App\Services\ShortCodeParser\ShortcodeTemplateBuilder;
13 use FluentCart\Framework\Support\Arr;
14
15 class EmailNotificationMailer
16 {
17 public function register()
18 {
19 // $this->registerAsyncMails();
20 add_action('fluent_cart/order_placed_offline', function ($data) {
21 $this->mailEmailsOfEvent(
22 'order_placed_offline',
23 $data
24 );
25
26 }, 999, 1);
27 // To Customer
28 add_action('fluent_cart/order_paid', function ($data) {
29 $this->mailEmailsOfEvent(
30 'order_paid',
31 $data
32 );
33
34 }, 999, 1);
35 // To Admin
36 // 999 like the rest of this file: custom smartcodes in a customised admin
37 // body resolve against the payload as it stands when the mail is built, so
38 // the mail has to run after everything else bound here writes its data —
39 // integration feeds (11), FluentCRM (20), affiliate referral status (99).
40 // Cost of running last: this hook forces every feed to run realtime, so the
41 // mail waits on their outbound HTTP, and IntegrationEventListener catches
42 // only \Exception — a \Error in a feed loses the mail.
43 add_action('fluent_cart/order_paid_done', function ($data) {
44 $this->mailEmailsOfEvent(
45 'order_paid_done',
46 $data
47 );
48 }, 999, 1);
49
50 // to customer and admin
51 add_action('fluent_cart/subscription_renewed', function ($data) {
52 $this->mailEmailsOfEvent(
53 'subscription_renewed',
54 $data
55 );
56 }, 999, 1);
57
58 // to customer and admin
59 add_action('fluent_cart/subscription_renewal_failed', function ($data) {
60 $this->mailEmailsOfEvent(
61 'subscription_renewal_failed',
62 $data
63 );
64 }, 999, 1);
65
66 // to customer and admin
67 add_action('fluent_cart/subscription_canceled', function ($data) {
68 $this->mailEmailsOfEvent(
69 'subscription_canceled',
70 $data
71 );
72 }, 999, 1);
73
74 add_action('fluent_cart/order_refunded', function ($data) {
75 $this->mailEmailsOfEvent('order_refunded', $data);
76 }, 999, 1);
77
78 add_action('fluent_cart/shipping_status_changed_to_shipped', function ($data) {
79 $this->mailEmailsOfEvent('shipping_status_changed_to_shipped', $data);
80 }, 999, 1);
81
82 add_action('fluent_cart/shipping_status_changed_to_delivered', function ($data) {
83 $this->mailEmailsOfEvent('shipping_status_changed_to_delivered', $data);
84 }, 999, 1);
85
86 add_action('fluent_cart/renewal_created', function ($data) {
87 $this->mailEmailsOfEvent('renewal_created', $data);
88 }, 999, 1);
89
90 add_action('fluent_cart/renewal_payment_reminder', function ($data) {
91 $this->mailEmailsOfEvent('renewal_payment_reminder', $data);
92 }, 999, 1);
93
94 // Fired by RenewalService when a system renewal order is created ahead of
95 // its automatic charge — the customer's advance notice of amount and date.
96 add_action('fluent_cart/subscriptions/system_renewal_scheduled', function ($data) {
97 $shouldSend = apply_filters('fluent_cart/subscriptions/upcoming_charge_notification', true, $data);
98
99 if ($shouldSend) {
100 $this->mailEmailsOfEvent('system_upcoming_charge', $data);
101 }
102 }, 999, 1);
103
104 // Fired by SystemChargeService when an automatic (system) renewal charge
105 // fails and the customer should be notified (first failure by default).
106 add_action('fluent_cart/subscriptions/system_charge_failed_notification', function ($data) {
107 $this->mailEmailsOfEvent('system_charge_failed', $data);
108 }, 999, 1);
109
110 add_action('fluent_cart/subscription_period_skipped', function ($data) {
111 $this->mailEmailsOfEvent('subscription_period_skipped', $data);
112 }, 999, 1);
113
114 add_action('fluent_cart/subscription_past_due', function ($data) {
115 $this->mailEmailsOfEvent('subscription_past_due', $data);
116 }, 999, 1);
117
118 add_action('fluent_cart/renewal_reminder_due', function ($data) {
119 $this->mailEmailsOfEvent('renewal_reminder_due', $data);
120 }, 999, 1);
121
122 add_action('fluent_cart/renewal_reminder_overdue', function ($data) {
123 // Set only by the deprecated bridge in RenewalReminderService::send():
124 // the staged email (renewal_overdue_first/followup/final) already went
125 // out for this payload, so mailing here would duplicate it. Direct
126 // dispatches of this hook never carry the flag and still deliver.
127 if (!empty(Arr::get($data, 'staged_email_dispatched'))) {
128 return;
129 }
130 $this->mailEmailsOfEvent('renewal_reminder_overdue', $data);
131 }, 999, 1);
132
133 add_action('fluent_cart/renewal_overdue_first', function ($data) {
134 $this->mailEmailsOfEvent('renewal_overdue_first', $data);
135 }, 999, 1);
136
137 add_action('fluent_cart/renewal_overdue_followup', function ($data) {
138 $this->mailEmailsOfEvent('renewal_overdue_followup', $data);
139 }, 999, 1);
140
141 add_action('fluent_cart/renewal_overdue_final', function ($data) {
142 $this->mailEmailsOfEvent('renewal_overdue_final', $data);
143 }, 999, 1);
144
145 add_action('fluent_cart/subscription_renewal_reminder', function ($data) {
146 $this->mailEmailsOfEvent('subscription_renewal_reminder', $data);
147 }, 999, 1);
148
149 add_action('fluent_cart/subscription_trial_end_reminder', function ($data) {
150 $this->mailEmailsOfEvent('subscription_trial_end_reminder', $data);
151 }, 999, 1);
152
153 add_action('fluent_cart/review_created', function ($data) {
154 $review = Arr::get($data, 'review');
155 if ($review && empty($review->parent_id)) {
156 $this->mailEmailsOfEvent('review_created', $data);
157 }
158 }, 999, 1);
159
160 // To the reviewer, on the settled hook — the queued job has re-read
161 // the review and it is still approved. The job fires once per
162 // approval, and two requests can make the same approval; the author
163 // is told once — barring a worker killed in the one UPDATE between
164 // the transport accepting a message and the record of it, which
165 // repeats that one message after the lease timeout: at-least-once at
166 // that edge, once everywhere else. So the send is behind a lease on
167 // the review's
168 // other_info: one conditional UPDATE, handed to exactly one caller,
169 // taken before the send and settled after it — delivered closes the
170 // notice, anything else releases the lease so a retry can send. Taken
171 // only when a notification is switched on and there is someone to
172 // send to — a lease spent on either could never be told about later.
173 add_action('fluent_cart/review_approved_done', function ($data) {
174 $review = Arr::get($data, 'review');
175 if (!$review instanceof ProductReview || $review->parent_id) {
176 return;
177 }
178
179 $mailNames = EmailNotifications::activeNotificationNamesOfEvent('review_approved_done');
180 if (!$mailNames) {
181 return;
182 }
183
184 if (ProductReviewService::resolveNotificationRecipient($review) === '') {
185 // Nobody to send to. Not leased, so a corrected address and
186 // a fresh approval can still send.
187 return;
188 }
189
190 $leaseToken = ProductReviewService::claimApprovalNotice($review);
191 if (!$leaseToken) {
192 return;
193 }
194
195 $this->sendUnderNoticeLease(
196 $mailNames,
197 $data,
198 ProductReviewService::deliveredApprovalNotices((int) $review->id),
199 function (array $deliveredNames) use ($review, $leaseToken) {
200 ProductReviewService::recordApprovalNoticeDeliveries((int) $review->id, $leaseToken, $deliveredNames);
201 },
202 function (bool $allDelivered, array $deliveredNames) use ($review, $leaseToken) {
203 ProductReviewService::settleApprovalNotice((int) $review->id, $leaseToken, $allDelivered, $deliveredNames);
204 }
205 );
206 }, 999, 1);
207
208 // To the reviewer, when the store answers them. Same shape as the
209 // approval notice: on the settled hook, behind a once-per-reply
210 // lease, taken only when there is a notification switched on and
211 // someone to send to. One more reason not to send: the person who
212 // wrote the reply is the reviewer — a moderator answering their own
213 // review is not told about it.
214 add_action('fluent_cart/review_replied_done', function ($data) {
215 $reply = Arr::get($data, 'reply');
216 $review = Arr::get($data, 'review');
217 if (!$reply instanceof ProductReview || !$review instanceof ProductReview || $review->parent_id) {
218 return;
219 }
220
221 $mailNames = EmailNotifications::activeNotificationNamesOfEvent('review_replied_done');
222 if (!$mailNames) {
223 return;
224 }
225
226 $recipient = ProductReviewService::resolveNotificationRecipient($review);
227 if ($recipient === '') {
228 return;
229 }
230
231 // The reply's author, compared as who they are — the account the
232 // reply was written from, or failing that the address it
233 // carries — never as a role: a customer who happens to be an
234 // administrator still gets told about replies to their review.
235 $sameAccount = (int) $reply->user_id && (int) $reply->user_id === (int) $review->user_id;
236 $sameAddress = strcasecmp(trim((string) $reply->reviewer_email), $recipient) === 0;
237 if ($sameAccount || $sameAddress) {
238 return;
239 }
240
241 $leaseToken = ProductReviewService::claimReplyNotice($reply);
242 if (!$leaseToken) {
243 return;
244 }
245
246 $this->sendUnderNoticeLease(
247 $mailNames,
248 $data,
249 ProductReviewService::deliveredReplyNotices((int) $reply->id),
250 function (array $deliveredNames) use ($reply, $leaseToken) {
251 ProductReviewService::recordReplyNoticeDeliveries((int) $reply->id, $leaseToken, $deliveredNames);
252 },
253 function (bool $allDelivered, array $deliveredNames) use ($reply, $leaseToken) {
254 ProductReviewService::settleReplyNotice((int) $reply->id, $leaseToken, $allDelivered, $deliveredNames);
255 }
256 );
257 }, 999, 1);
258
259 }
260
261 /**
262 * Send a set of notifications under a once-only lease, and settle it.
263 *
264 * Delivery is the transport's own word. wp_mail() fires wp_mail_succeeded
265 * once the message is accepted and wp_mail_failed when it is not — and
266 * fires neither when something short-circuits it, a pre_wp_mail filter
267 * returning false, say, or a mailer filter that stripped every recipient.
268 * So a send counts as delivered only when the success action fired for
269 * it; silence is not success. (The action is WordPress 5.9's; the plugin
270 * requires 6.7.) Watched only for the duration of this send, so another
271 * email elsewhere in the request is not read as ours.
272 *
273 * One lease covers every notification switched on for the event — an
274 * add-on can register a second — so each is judged on its own: the ones
275 * already delivered under an earlier, released lease are skipped; each
276 * one that goes out is recorded at once, before the next is tried, so a
277 * worker killed after a delivery leaves at most that one delivery
278 * unrecorded; and the loop stops at the first failure to leave the rest
279 * for the retry. A throw anywhere in a send still reaches the settle on
280 * the way out, with what went out before it, and then the caller: the
281 * queued job records the failure and a re-run can send.
282 *
283 * @param string[] $mailNames notifications to send, in order
284 * @param array $data the event payload the templates read
285 * @param string[] $alreadyDelivered names delivered under an earlier lease
286 * @param callable $record fn(string[] $deliveredNames): records deliveries so far, lease still held
287 * @param callable $settle fn(bool $allDelivered, string[] $deliveredNames): settles the lease
288 */
289 protected function sendUnderNoticeLease(array $mailNames, array $data, array $alreadyDelivered, callable $record, callable $settle): void
290 {
291 $deliveredNow = [];
292 $allDelivered = false;
293 $deliveryFailed = false;
294 $deliverySucceeded = false;
295
296 $onMailFailed = function () use (&$deliveryFailed) {
297 $deliveryFailed = true;
298 };
299 $onMailSucceeded = function () use (&$deliverySucceeded) {
300 $deliverySucceeded = true;
301 };
302 add_action('wp_mail_failed', $onMailFailed);
303 add_action('wp_mail_succeeded', $onMailSucceeded);
304
305 try {
306 foreach ($mailNames as $mailName) {
307 if (in_array($mailName, $alreadyDelivered, true)) {
308 continue;
309 }
310
311 $deliveryFailed = false;
312 $deliverySucceeded = false;
313 $this->mailByEmailName($mailName, $data);
314
315 if (!$deliverySucceeded || $deliveryFailed) {
316 $deliveryFailed = true;
317 break;
318 }
319
320 $deliveredNow[] = $mailName;
321 $record(array_merge($alreadyDelivered, $deliveredNow));
322 }
323 $allDelivered = !$deliveryFailed;
324 } finally {
325 remove_action('wp_mail_failed', $onMailFailed);
326 remove_action('wp_mail_succeeded', $onMailSucceeded);
327 $settle($allDelivered, array_merge($alreadyDelivered, $deliveredNow));
328 }
329 }
330
331 public function registerAsyncMails()
332 {
333 //For Async Actions
334 add_action('fluent_cart/async_mail/order_created', function ($orderId, $mailName = '') {
335 (new static())->sendAsyncOrderMail($mailName, $orderId);
336 }, 10, 2);
337
338 add_action('fluent_cart/async_mail/order_placed_offline', function ($orderId, $mailName = '') {
339 (new static())->sendAsyncOrderMail($mailName, $orderId);
340 }, 10, 2);
341
342 add_action('fluent_cart/async_mail/order_paid', function ($orderId, $mailName = '') {
343 (new static())->sendAsyncOrderMail($mailName, $orderId);
344 }, 10, 2);
345
346 add_action('fluent_cart/async_mail/order_updated', function ($orderId, $mailName = '') {
347 (new static())->sendAsyncOrderMail($mailName, $orderId);
348 }, 10, 2);
349
350 add_action('fluent_cart/async_mail/order_refunded', function ($orderId, $mailName = '') {
351 (new static())->sendAsyncOrderMail($mailName, $orderId);
352 }, 10, 2);
353
354 add_action('fluent_cart/async_mail/subscription_activated', function ($subscriptionId, $mailName = '') {
355 (new static())->sendAsyncSubscriptionMail($mailName, $subscriptionId);
356 }, 10, 2);
357
358 add_action('fluent_cart/async_mail/subscription_reactivated', function ($subscriptionId, $mailName = '') {
359 (new static())->sendAsyncSubscriptionMail($mailName, $subscriptionId);
360 }, 10, 2);
361
362 add_action('fluent_cart/async_mail/subscription_renewed', function ($subscriptionId, $mailName = '') {
363 (new static())->sendAsyncSubscriptionMail($mailName, $subscriptionId);
364 }, 10, 2);
365
366 add_action('fluent_cart/async_mail/subscription_eot', function ($subscriptionId, $mailName = '') {
367 (new static())->sendAsyncSubscriptionMail($mailName, $subscriptionId);
368 }, 10, 2);
369
370 add_action('fluent_cart/async_mail/subscription_canceled', function ($subscriptionId, $mailName = '') {
371 (new static())->sendAsyncSubscriptionMail($mailName, $subscriptionId);
372 }, 10, 2);
373
374 add_action('fluent_cart/async_mail/subscription_expired', function ($subscriptionId, $mailName = '') {
375 (new static())->sendAsyncSubscriptionMail($mailName, $subscriptionId);
376 }, 10, 2);
377
378 }
379
380
381 public function formatParsable($parsable)
382 {
383 foreach ($parsable as &$item) {
384 if ($item instanceof Model) {
385 $item = $item->toArray();
386 }
387 }
388
389 if (!Arr::has($parsable, 'order.customer')) {
390 $parsable['order']['customer'] = Arr::get(
391 $parsable, 'customer', []
392 );
393 }
394
395 return $parsable;
396 }
397
398 public function mailEmailsOfEvent($event, $data, $asyncHook = '', $asyncData = [])
399 {
400 $parsedData = $this->formatParsable($data);
401
402 $order = Arr::get($data, 'order');
403
404 // Pass original Model data for template rendering (templates use $order->method())
405 $notifications = EmailNotifications::getNotificationsOfEvent($event, $data);
406
407 foreach ($notifications as $mailName => $notification) {
408 if ($order instanceof Order && !apply_filters('fluent_cart/should_send_email_notification', true, [
409 'event' => $event,
410 'mail_name' => $mailName,
411 'order' => $order,
412 ])) {
413 continue;
414 }
415
416 $isAsync = Arr::get($notification, 'is_async', false);
417 if ($isAsync && !empty($asyncHook)) {
418 $asyncData['mailName'] = $mailName;
419 as_enqueue_async_action($asyncHook, $asyncData);
420 } else {
421 list($body, $subject, $to) = $this->parseEmailContent($notification, $data);
422
423 $mailer = Mailer::make()->to($to)->subject($subject)->body($body);
424
425 $pdfPath = null;
426 $templateId = Arr::get($notification, 'settings.attach_pdf_template', '');
427 // Backward compat: old attach_pdf='yes' maps to order_receipt
428 if (empty($templateId) && Arr::get($notification, 'settings.attach_pdf', 'no') === 'yes') {
429 $templateId = 'order_receipt';
430 }
431 if (!empty($templateId) && defined('FLUENT_PDF')) {
432 $pdfPath = $this->generateOrderPdf($data, $templateId);
433 if ($pdfPath) {
434 $mailer->addAttachment($pdfPath);
435 }
436 }
437
438 $mailer = $this->applyMailerFilter($mailer, [
439 'event' => $event,
440 'mail_name' => $mailName,
441 'recipient' => Arr::get($notification, 'recipient'),
442 'notification' => $notification,
443 'data' => $data,
444 ]);
445
446 $mailer->send(true);
447
448 // Clean up temp PDF file after sending
449 if ($pdfPath && file_exists($pdfPath)) {
450 @unlink($pdfPath);
451 }
452 }
453 }
454
455 }
456
457 public function mailByEmailName($emailName, $data)
458 {
459 // $data keeps its Models: parseEmailContent renders emails.parts.order_header,
460 // which reads $order->invoice_no / $order->orderTaxRates — an array here
461 // warns and then fatals inside the view (regression of 201a14885 via 44a39aa1d)
462 $orderModel = Arr::get($data, 'order');
463
464 $notification = EmailNotifications::getNotification($emailName);
465 $notification = EmailNotifications::formatNotification($notification, $data);
466 list($body, $subject, $to) = $this->parseEmailContent($notification, $data);
467
468 $mailer = Mailer::make()->to($to)->subject($subject)->body($body);
469
470 $pdfPath = null;
471 $templateId = Arr::get($notification, 'settings.attach_pdf_template', '');
472 if (empty($templateId) && Arr::get($notification, 'settings.attach_pdf', 'no') === 'yes') {
473 $templateId = 'order_receipt';
474 }
475 if (!empty($templateId) && defined('FLUENT_PDF')) {
476 $pdfPath = $this->generateOrderPdf(['order' => $orderModel], $templateId);
477 if ($pdfPath) {
478 $mailer->addAttachment($pdfPath);
479 }
480 }
481
482 $mailer = $this->applyMailerFilter($mailer, [
483 'event' => Arr::get($notification, 'event', ''),
484 'mail_name' => $emailName,
485 'recipient' => Arr::get($notification, 'recipient'),
486 'notification' => $notification,
487 'data' => $data,
488 ]);
489
490 $mailer->send(true);
491
492 if ($pdfPath && file_exists($pdfPath)) {
493 @unlink($pdfPath);
494 }
495 }
496
497 /**
498 * Let third-party code adjust the fully prepared Mailer (recipients,
499 * subject, body, attachments already set) immediately before it sends.
500 *
501 * @param Mailer $mailer
502 * @param array $context event, mail_name, recipient, notification, data
503 * @return Mailer
504 */
505 private function applyMailerFilter(Mailer $mailer, array $context): Mailer
506 {
507 $filtered = apply_filters('fluent_cart/email_notification/mailer', $mailer, $context);
508
509 return $filtered instanceof Mailer ? $filtered : $mailer;
510 }
511
512 public function getEmailFooter(): string
513 {
514
515 $footer = "";
516 $settings = EmailNotifications::getSettings();
517 $emailFooter = Arr::get($settings, 'email_footer', '');
518 if (!empty($emailFooter)) {
519 $footer .= ShortcodeTemplateBuilder::make($emailFooter, []);
520 }
521 $isEmailFooter = EmailNotifications::getSettings('show_email_footer');
522 if (!App::isProActive() || $isEmailFooter === 'yes') {
523 $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>";
524 $footer .= $cartFooter;
525 }
526 return $footer;
527
528 }
529
530 public function parseEmailContent($notification, $data, $templateData = null): array
531 {
532 $templateData = $templateData ?: $data;
533
534 $rawBody = Arr::get($notification, 'body', '');
535 $isCustom = (bool) Arr::get($notification, 'is_custom', false);
536
537 // Let pro parse block content; returns empty string if no pro
538 $parsedBody = apply_filters('fluent_cart/parse_email_block_content', '', $rawBody, $data);
539
540 $body = '';
541
542 // Custom block emails use block_editor_template (pro);
543 // default templates use general_template with legacy order_header.
544 if ($isCustom && $parsedBody) {
545 $body = apply_filters('fluent_cart/render_block_email_template', '', [
546 'emailBody' => $parsedBody,
547 'preheader' => Arr::get($notification, 'pre_header', ''),
548 'emailFooter' => $this->getEmailFooter(),
549 ]);
550 }
551
552 if (empty($body)) {
553 // order_header reads $order->invoice_no and $order->orderTaxRates, so
554 // anything that is not an Order model fatals inside the view — an
555 // array warns and then dies on ->first(). Render it only for a real
556 // model: notifications that legitimately have no order still get a
557 // body, they just get no order header.
558 $orderModel = Arr::get($data, 'order');
559 $header = ($orderModel instanceof Order)
560 ? (string)App::make('view')->make('emails.parts.order_header', $data)
561 : '';
562
563 $body = (string)App::make('view')->make('emails.general_template', [
564 'emailBody' => $rawBody,
565 'preheader' => Arr::get($notification, 'pre_header', ''),
566 'header' => $header,
567 'emailFooter' => $this->getEmailFooter(),
568 ]);
569 }
570
571 $body = ShortcodeTemplateBuilder::make($body, $data);
572
573 $subject = ShortcodeTemplateBuilder::make(Arr::get($notification, 'subject', ''), $data);
574 // A notification may declare an array of recipients — wp_mail() accepts
575 // one — so expand element by element rather than flattening to a string.
576 $to = Arr::get($notification, 'to', '');
577 if (is_array($to)) {
578 foreach ($to as $index => $address) {
579 $to[$index] = ShortcodeTemplateBuilder::make((string)$address, $data);
580 }
581 } else {
582 $to = ShortcodeTemplateBuilder::make((string)$to, $data);
583 }
584
585 // Admin-bound notifications only — stores route these to a helpdesk or
586 // accounting inbox. Customer-bound mail must keep going to the customer,
587 // so it is deliberately not filterable here. Applied after shortcode
588 // resolution, so listeners see the address the notification resolved to.
589 if (Arr::get($notification, 'recipient') === 'admin') {
590 $to = apply_filters('fluent_cart/admin_email/notification_recipient', $to, [
591 'event' => Arr::get($notification, 'event', ''),
592 'mail_name' => Arr::get($notification, 'name', ''),
593 'notification' => $notification,
594 'data' => $data,
595 ]);
596 }
597
598 return [
599 0 => $body,
600 1 => $subject,
601 2 => $to
602 ];
603 }
604
605 public function sendAsyncOrderMail($emailName, $orderId)
606 {
607 $order = Order::query()->with(['customer', 'shipping_address', 'billing_address', 'transactions', 'orderTaxRates'])->find($orderId);
608
609 if ($order) {
610 $transaction = [];
611 if (!empty($order->transactions)) {
612 $transaction = $order->transactions->first();
613 }
614 $this->mailByEmailName($emailName, [
615 'order' => $order,
616 'customer' => $order->customer !== null ? $order->customer : [],
617 'transaction' => $transaction
618 ]);
619 }
620 }
621
622 public function sendAsyncSubscriptionMail($emailName, $subscriptionId)
623 {
624 $subscription = Subscription::query()->with([
625 'customer',
626 'transactions',
627 'order'
628 ])->find($subscriptionId);
629
630 if ($subscription) {
631 $order = $subscription->order;
632 if (!$order) {
633 return;
634 }
635 $this->mailByEmailName($emailName, [
636 'subscription' => $subscription,
637 'order' => $order,
638 'customer' => $subscription->customer !== null ? $subscription->customer : [],
639 'transactions' => $subscription->transactions
640 ]);
641 }
642
643 }
644
645 /**
646 * Generate a PDF receipt for the order in the given data array.
647 *
648 * @param array $data Event data containing 'order' key
649 * @return string|null Path to generated PDF or null
650 */
651 private function generateOrderPdf(array $data, string $templateId = 'order_receipt'): ?string
652 {
653 if (!OrderService::canGenerateReceiptPdf()) {
654 return null;
655 }
656
657 $order = Arr::get($data, 'order');
658 if (!$order || !($order instanceof Order)) {
659 return null;
660 }
661
662 try {
663 return apply_filters('fluent_cart/pdf/generate_receipt', null, [
664 'order' => $order,
665 'template_id' => $templateId,
666 ]);
667 } catch (\Throwable $e) {
668 fluent_cart_add_log(
669 'PDF Generation Failed',
670 $e->getMessage(),
671 'error'
672 );
673 return null;
674 }
675 }
676
677 }
678