PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.1
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.1
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
fluent-cart / app / Services / Email / EmailNotificationMailer.php

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

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