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

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

450 lines 17.3 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 $this->mailEmailsOfEvent('renewal_reminder_overdue', $data);
122 }, 999, 1);
123
124 add_action('fluent_cart/subscription_renewal_reminder', function ($data) {
125 $this->mailEmailsOfEvent('subscription_renewal_reminder', $data);
126 }, 999, 1);
127
128 add_action('fluent_cart/subscription_trial_end_reminder', function ($data) {
129 $this->mailEmailsOfEvent('subscription_trial_end_reminder', $data);
130 }, 999, 1);
131
132 }
133
134 public function registerAsyncMails()
135 {
136 //For Async Actions
137 add_action('fluent_cart/async_mail/order_created', function ($orderId, $mailName = '') {
138 (new static())->sendAsyncOrderMail($mailName, $orderId);
139 }, 10, 2);
140
141 add_action('fluent_cart/async_mail/order_placed_offline', function ($orderId, $mailName = '') {
142 (new static())->sendAsyncOrderMail($mailName, $orderId);
143 }, 10, 2);
144
145 add_action('fluent_cart/async_mail/order_paid', function ($orderId, $mailName = '') {
146 (new static())->sendAsyncOrderMail($mailName, $orderId);
147 }, 10, 2);
148
149 add_action('fluent_cart/async_mail/order_updated', function ($orderId, $mailName = '') {
150 (new static())->sendAsyncOrderMail($mailName, $orderId);
151 }, 10, 2);
152
153 add_action('fluent_cart/async_mail/order_refunded', function ($orderId, $mailName = '') {
154 (new static())->sendAsyncOrderMail($mailName, $orderId);
155 }, 10, 2);
156
157 add_action('fluent_cart/async_mail/subscription_activated', function ($subscriptionId, $mailName = '') {
158 (new static())->sendAsyncSubscriptionMail($mailName, $subscriptionId);
159 }, 10, 2);
160
161 add_action('fluent_cart/async_mail/subscription_reactivated', function ($subscriptionId, $mailName = '') {
162 (new static())->sendAsyncSubscriptionMail($mailName, $subscriptionId);
163 }, 10, 2);
164
165 add_action('fluent_cart/async_mail/subscription_renewed', function ($subscriptionId, $mailName = '') {
166 (new static())->sendAsyncSubscriptionMail($mailName, $subscriptionId);
167 }, 10, 2);
168
169 add_action('fluent_cart/async_mail/subscription_eot', function ($subscriptionId, $mailName = '') {
170 (new static())->sendAsyncSubscriptionMail($mailName, $subscriptionId);
171 }, 10, 2);
172
173 add_action('fluent_cart/async_mail/subscription_canceled', function ($subscriptionId, $mailName = '') {
174 (new static())->sendAsyncSubscriptionMail($mailName, $subscriptionId);
175 }, 10, 2);
176
177 add_action('fluent_cart/async_mail/subscription_expired', function ($subscriptionId, $mailName = '') {
178 (new static())->sendAsyncSubscriptionMail($mailName, $subscriptionId);
179 }, 10, 2);
180
181 }
182
183
184 public function formatParsable($parsable)
185 {
186 foreach ($parsable as &$item) {
187 if ($item instanceof Model) {
188 $item = $item->toArray();
189 }
190 }
191
192 if (!Arr::has($parsable, 'order.customer')) {
193 $parsable['order']['customer'] = Arr::get(
194 $parsable, 'customer', []
195 );
196 }
197
198 return $parsable;
199 }
200
201 public function mailEmailsOfEvent($event, $data, $asyncHook = '', $asyncData = [])
202 {
203 $parsedData = $this->formatParsable($data);
204
205 $order = Arr::get($data, 'order');
206
207 // Pass original Model data for template rendering (templates use $order->method())
208 $notifications = EmailNotifications::getNotificationsOfEvent($event, $data);
209
210 foreach ($notifications as $mailName => $notification) {
211 if ($order instanceof Order && !apply_filters('fluent_cart/should_send_email_notification', true, [
212 'event' => $event,
213 'mail_name' => $mailName,
214 'order' => $order,
215 ])) {
216 continue;
217 }
218
219 $isAsync = Arr::get($notification, 'is_async', false);
220 if ($isAsync && !empty($asyncHook)) {
221 $asyncData['mailName'] = $mailName;
222 as_enqueue_async_action($asyncHook, $asyncData);
223 } else {
224 list($body, $subject, $to) = $this->parseEmailContent($notification, $data);
225
226 $mailer = Mailer::make()->to($to)->subject($subject)->body($body);
227
228 $pdfPath = null;
229 $templateId = Arr::get($notification, 'settings.attach_pdf_template', '');
230 // Backward compat: old attach_pdf='yes' maps to order_receipt
231 if (empty($templateId) && Arr::get($notification, 'settings.attach_pdf', 'no') === 'yes') {
232 $templateId = 'order_receipt';
233 }
234 if (!empty($templateId) && defined('FLUENT_PDF')) {
235 $pdfPath = $this->generateOrderPdf($data, $templateId);
236 if ($pdfPath) {
237 $mailer->addAttachment($pdfPath);
238 }
239 }
240
241 $mailer->send(true);
242
243 // Clean up temp PDF file after sending
244 if ($pdfPath && file_exists($pdfPath)) {
245 @unlink($pdfPath);
246 }
247 }
248 }
249
250 }
251
252 public function mailByEmailName($emailName, $data)
253 {
254 // $data keeps its Models: parseEmailContent renders emails.parts.order_header,
255 // which reads $order->invoice_no / $order->orderTaxRates — an array here
256 // warns and then fatals inside the view (regression of 201a14885 via 44a39aa1d)
257 $orderModel = Arr::get($data, 'order');
258
259 $notification = EmailNotifications::getNotification($emailName);
260 $notification = EmailNotifications::formatNotification($notification, $data);
261 list($body, $subject, $to) = $this->parseEmailContent($notification, $data);
262
263 $mailer = Mailer::make()->to($to)->subject($subject)->body($body);
264
265 $pdfPath = null;
266 $templateId = Arr::get($notification, 'settings.attach_pdf_template', '');
267 if (empty($templateId) && Arr::get($notification, 'settings.attach_pdf', 'no') === 'yes') {
268 $templateId = 'order_receipt';
269 }
270 if (!empty($templateId) && defined('FLUENT_PDF')) {
271 $pdfPath = $this->generateOrderPdf(['order' => $orderModel], $templateId);
272 if ($pdfPath) {
273 $mailer->addAttachment($pdfPath);
274 }
275 }
276
277 $mailer->send(true);
278
279 if ($pdfPath && file_exists($pdfPath)) {
280 @unlink($pdfPath);
281 }
282 }
283
284 public function getEmailFooter(): string
285 {
286
287 $footer = "";
288 $settings = EmailNotifications::getSettings();
289 $emailFooter = Arr::get($settings, 'email_footer', '');
290 if (!empty($emailFooter)) {
291 $footer .= ShortcodeTemplateBuilder::make($emailFooter, []);
292 }
293 $isEmailFooter = EmailNotifications::getSettings('show_email_footer');
294 if (!App::isProActive() || $isEmailFooter === 'yes') {
295 $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>";
296 $footer .= $cartFooter;
297 }
298 return $footer;
299
300 }
301
302 public function parseEmailContent($notification, $data, $templateData = null): array
303 {
304 $templateData = $templateData ?: $data;
305
306 $rawBody = Arr::get($notification, 'body', '');
307 $isCustom = (bool) Arr::get($notification, 'is_custom', false);
308
309 // Let pro parse block content; returns empty string if no pro
310 $parsedBody = apply_filters('fluent_cart/parse_email_block_content', '', $rawBody, $data);
311
312 $body = '';
313
314 // Custom block emails use block_editor_template (pro);
315 // default templates use general_template with legacy order_header.
316 if ($isCustom && $parsedBody) {
317 $body = apply_filters('fluent_cart/render_block_email_template', '', [
318 'emailBody' => $parsedBody,
319 'preheader' => Arr::get($notification, 'pre_header', ''),
320 'emailFooter' => $this->getEmailFooter(),
321 ]);
322 }
323
324 if (empty($body)) {
325 // order_header reads $order->invoice_no and $order->orderTaxRates, so
326 // anything that is not an Order model fatals inside the view — an
327 // array warns and then dies on ->first(). Render it only for a real
328 // model: notifications that legitimately have no order still get a
329 // body, they just get no order header.
330 $orderModel = Arr::get($data, 'order');
331 $header = ($orderModel instanceof Order)
332 ? (string)App::make('view')->make('emails.parts.order_header', $data)
333 : '';
334
335 $body = (string)App::make('view')->make('emails.general_template', [
336 'emailBody' => $rawBody,
337 'preheader' => Arr::get($notification, 'pre_header', ''),
338 'header' => $header,
339 'emailFooter' => $this->getEmailFooter(),
340 ]);
341 }
342
343 $body = ShortcodeTemplateBuilder::make($body, $data);
344
345 $subject = ShortcodeTemplateBuilder::make(Arr::get($notification, 'subject', ''), $data);
346 // A notification may declare an array of recipients — wp_mail() accepts
347 // one — so expand element by element rather than flattening to a string.
348 $to = Arr::get($notification, 'to', '');
349 if (is_array($to)) {
350 foreach ($to as $index => $address) {
351 $to[$index] = ShortcodeTemplateBuilder::make((string)$address, $data);
352 }
353 } else {
354 $to = ShortcodeTemplateBuilder::make((string)$to, $data);
355 }
356
357 // Admin-bound notifications only — stores route these to a helpdesk or
358 // accounting inbox. Customer-bound mail must keep going to the customer,
359 // so it is deliberately not filterable here. Applied after shortcode
360 // resolution, so listeners see the address the notification resolved to.
361 if (Arr::get($notification, 'recipient') === 'admin') {
362 $to = apply_filters('fluent_cart/admin_email/notification_recipient', $to, [
363 'event' => Arr::get($notification, 'event', ''),
364 'mail_name' => Arr::get($notification, 'name', ''),
365 'notification' => $notification,
366 'data' => $data,
367 ]);
368 }
369
370 return [
371 0 => $body,
372 1 => $subject,
373 2 => $to
374 ];
375 }
376
377 public function sendAsyncOrderMail($emailName, $orderId)
378 {
379 $order = Order::query()->with(['customer', 'shipping_address', 'billing_address', 'transactions', 'orderTaxRates'])->find($orderId);
380
381 if ($order) {
382 $transaction = [];
383 if (!empty($order->transactions)) {
384 $transaction = $order->transactions->first();
385 }
386 $this->mailByEmailName($emailName, [
387 'order' => $order,
388 'customer' => $order->customer !== null ? $order->customer : [],
389 'transaction' => $transaction
390 ]);
391 }
392 }
393
394 public function sendAsyncSubscriptionMail($emailName, $subscriptionId)
395 {
396 $subscription = Subscription::query()->with([
397 'customer',
398 'transactions',
399 'order'
400 ])->find($subscriptionId);
401
402 if ($subscription) {
403 $order = $subscription->order;
404 if (!$order) {
405 return;
406 }
407 $this->mailByEmailName($emailName, [
408 'subscription' => $subscription,
409 'order' => $order,
410 'customer' => $subscription->customer !== null ? $subscription->customer : [],
411 'transactions' => $subscription->transactions
412 ]);
413 }
414
415 }
416
417 /**
418 * Generate a PDF receipt for the order in the given data array.
419 *
420 * @param array $data Event data containing 'order' key
421 * @return string|null Path to generated PDF or null
422 */
423 private function generateOrderPdf(array $data, string $templateId = 'order_receipt'): ?string
424 {
425 if (!OrderService::canGenerateReceiptPdf()) {
426 return null;
427 }
428
429 $order = Arr::get($data, 'order');
430 if (!$order || !($order instanceof Order)) {
431 return null;
432 }
433
434 try {
435 return apply_filters('fluent_cart/pdf/generate_receipt', null, [
436 'order' => $order,
437 'template_id' => $templateId,
438 ]);
439 } catch (\Throwable $e) {
440 fluent_cart_add_log(
441 'PDF Generation Failed',
442 $e->getMessage(),
443 'error'
444 );
445 return null;
446 }
447 }
448
449 }
450