PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.0
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.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 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.0, at app/Services/Email/EmailNotificationMailer.php

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