PluginProbe ʕ •ᴥ•ʔ
VikAppointments Services Booking Calendar / 1.2.20
VikAppointments Services Booking Calendar v1.2.20
1.2.21 1.2.20 trunk 1.2.17 1.2.18 1.2.19
vikappointments / site / helpers / libraries / mail / templates / customer.php
vikappointments / site / helpers / libraries / mail / templates Last commit date
admin.php 1 month ago cancellation.php 1 month ago customer.php 1 month ago employee.php 1 month ago index.html 1 month ago packadmin.php 1 month ago package.php 1 month ago stock.php 1 month ago waitlist.php 1 month ago
customer.php
613 lines
1 <?php
2 /**
3 * @package VikAppointments
4 * @subpackage core
5 * @author E4J s.r.l.
6 * @copyright Copyright (C) 2021 E4J s.r.l. All Rights Reserved.
7 * @license http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL
8 * @link https://vikwp.com
9 */
10
11 // No direct access
12 defined('ABSPATH') or die('No script kiddies please!');
13
14 VAPLoader::import('libraries.mail.template');
15 VAPLoader::import('libraries.order.factory');
16
17 /**
18 * Wrapper used to handle mail notifications for the
19 * customers that purchase an appointment.
20 *
21 * @since 1.7
22 */
23 class VAPMailTemplateCustomer implements VAPMailTemplate
24 {
25 /**
26 * The order object.
27 *
28 * @var VAPOrderAppointment
29 */
30 protected $order;
31
32 /**
33 * An optional template file to use.
34 *
35 * @var string
36 */
37 protected $templateFile;
38
39 /**
40 * An array of options.
41 *
42 * @var array
43 */
44 protected $options;
45
46 /**
47 * An array of fetched attachments.
48 *
49 * @var string[]
50 * @since 1.7.4
51 */
52 protected $attachments = [];
53
54 /**
55 * Class constructor.
56 *
57 * @param mixed $order Either the order ID or the order object.
58 * @param array $options An array of options.
59 */
60 public function __construct($order, array $options = array())
61 {
62 if (empty($options['lang']))
63 {
64 $options['lang'] = null;
65 }
66
67 if (is_numeric($order))
68 {
69 // recover order details for the given language
70 $this->order = VAPOrderFactory::getAppointments($order, $options['lang']);
71 }
72 else
73 {
74 // use order as provided
75 $this->order = $order;
76 }
77
78 if (!$options['lang'])
79 {
80 // use order lang tag in case it was not specified
81 $options['lang'] = $this->order->get('langtag', null);
82
83 if (!$options['lang'])
84 {
85 // the order is not assigned to any lang tag, use the current one
86 $options['lang'] = JFactory::getLanguage()->getTag();
87 }
88 }
89
90 // register options
91 $this->options = $options;
92
93 // load given language to translate template contents
94 VikAppointments::loadLanguage($this->options['lang'], JPATH_SITE);
95 }
96
97 /**
98 * Returns the code of the template before being parsed.
99 *
100 * @param string $file An optional template file to use. If not specified,
101 * the one set in configuration will be used.
102 *
103 * @return void
104 */
105 public function setFile($file)
106 {
107 // use specified template file
108 $this->templateFile = $file;
109
110 // check if a filename or a path was passed
111 if ($file && !is_file($file))
112 {
113 // make sure we have a valid file path
114 $this->templateFile = VAPHELPERS . DIRECTORY_SEPARATOR . 'mail_tmpls' . DIRECTORY_SEPARATOR . $file;
115 }
116 }
117
118 /**
119 * Returns the code of the template before being parsed.
120 *
121 * @return string
122 */
123 public function getTemplate()
124 {
125 // copy order details in a local variable for being used directly
126 // within the template file
127 $order = $this->order;
128
129 if (!$this->templateFile)
130 {
131 // get template file from configuration
132 $file = VAPFactory::getConfig()->get('mailtmpl');
133
134 // build template path
135 $this->templateFile = VAPHELPERS . DIRECTORY_SEPARATOR . 'mail_tmpls' . DIRECTORY_SEPARATOR . $file;
136 }
137
138 // make sure the file exists
139 if (!is_file($this->templateFile))
140 {
141 // missing file, return empty string
142 return '';
143 }
144
145 // start output buffering
146 ob_start();
147 // include file to catch its contents
148 include $this->templateFile;
149 // write template contents within a variable
150 $content = ob_get_contents();
151 // clear output buffer
152 ob_end_clean();
153
154 // free space
155 unset($order);
156
157 return $content;
158 }
159
160 /**
161 * Fetches the subject to be used in the e-mail.
162 *
163 * @return string
164 */
165 public function getSubject()
166 {
167 // get company name
168 $fromname = VAPFactory::getConfig()->getString('agencyname');
169
170 /**
171 * Fetch subject.
172 *
173 * @since 1.7.7 Added the possibility to use a custom subject.
174 */
175 $subject = !empty($this->options['subject']) ? $this->options['subject'] : 'VAPCUSTOMEREMAILSUBJECT';
176 $subject = JText::sprintf($subject, $fromname);
177
178 // let plugins manipulate the subject for this e-mail template
179 $res = VAPMailFactory::letPluginsManipulateMail('customer', 'subject', $subject, $this->order);
180
181 if ($res === false)
182 {
183 // a plugin prevented the e-mail sending
184 return '';
185 }
186
187 /**
188 * Parse e-mail subject to replace tags with the related order details.
189 *
190 * @since 1.6.6
191 */
192 VikAppointments::parseEmailSubject($subject, $this->order);
193
194 return $subject;
195 }
196
197 /**
198 * Parses the HTML of the template and returns it.
199 *
200 * @return string
201 */
202 public function getHtml()
203 {
204 $config = VAPFactory::getConfig();
205 $currency = VAPFactory::getCurrency();
206
207 // load template HTML
208 $tmpl = $this->getTemplate();
209
210 $payment_name = '';
211 $payment_notes = '';
212
213 // fetch payment name and notes
214 if ($this->order->payment)
215 {
216 // use payment name
217 $payment_name = $this->order->payment->name;
218
219 if ($this->order->statusRole == 'PENDING')
220 {
221 // show notes before purchase when waiting for the payment
222 $payment_notes = $this->order->payment->notes->beforePurchase;
223 }
224 else if ($this->order->statusRole == 'APPROVED')
225 {
226 // show notes after purchase when the order has been confirmed
227 $payment_notes = $this->order->payment->notes->afterPurchase;
228 }
229 }
230
231 // fetch coupon string
232 if ($this->order->coupon)
233 {
234 $coupon_str = $this->order->coupon->code;
235
236 if ($this->order->coupon->amount > 0)
237 {
238 $coupon_str .= ' : ';
239
240 if ($this->order->coupon->type == 1)
241 {
242 $coupon_str .= $this->order->coupon->amount . '%';
243 }
244 else
245 {
246 $coupon_str .= $currency->format($this->order->coupon->amount);
247 }
248 }
249 }
250 else
251 {
252 $coupon_str = '';
253 }
254
255 $vik = VAPApplication::getInstance();
256
257 // fetch order link HREF
258 $order_link_href = "index.php?option=com_vikappointments&view=order&ordnum={$this->order->id}&ordkey={$this->order->sid}&lang={$this->options['lang']}";
259 $order_link_href = $vik->routeForExternalUse($order_link_href);
260
261 // fetch cancellation link HREF
262 $cancellation_link_href = $order_link_href . '#cancel';
263
264 // fetch confirmation link HREF
265 $confirmation_link_href = "index.php?option=com_vikappointments&view=order&task=order.confirm&id={$this->order->id}&conf_key={$this->order->conf_key}&lang={$this->options['lang']}";
266 $confirmation_link_href = $vik->routeForExternalUse($confirmation_link_href);
267
268 // fetch company logo image
269 $logo_str = $config->get('companylogo');
270
271 if ($logo_str && is_file(VAPMEDIA . DIRECTORY_SEPARATOR . $logo_str))
272 {
273 $logo_str = JHtml::fetch('vaphtml.media.display', $logo_str, [
274 'alt' => $config->get('agencyname'),
275 'small' => false,
276 'style' => 'max-width: 100%;',
277 ]);
278 }
279 else
280 {
281 $logo_str = '';
282 }
283
284 // get name chunks
285 $customerNameChunks = preg_split("/\s+/", (string) $this->order->purchaser_nominative);
286
287 // extract last name from the list
288 $customerLastName = array_pop($customerNameChunks);
289 // join remaining chunks into the first name
290 $customerFirstName = implode(' ', $customerNameChunks);
291
292 if (!$customerFirstName)
293 {
294 // only one name provided, use it as first name
295 $customerFirstName = $customerLastName;
296 $customerLastName = '';
297 }
298
299 // build placeholders lookup
300 $placeholders = array(
301 'logo' => $logo_str,
302 'company_name' => $config->get('agencyname'),
303 'order_number' => $this->order->id,
304 'order_key' => $this->order->sid,
305 'order_status' => JHtml::fetch('vaphtml.status.display', $this->order->status),
306 'order_payment' => $payment_name,
307 'order_payment_notes' => $payment_notes,
308 'order_total_cost' => $currency->format($this->order->totals->gross),
309 'order_total_net' => $currency->format($this->order->totals->net),
310 'order_total_tax' => $currency->format($this->order->totals->tax),
311 'order_total_discount' => $currency->format($this->order->totals->discount),
312 'order_coupon_code' => $coupon_str,
313 'order_link' => $order_link_href,
314 'cancellation_link' => $cancellation_link_href,
315 'confirmation_link' => $confirmation_link_href,
316 'user_name' => $this->order->author ? $this->order->author->name : '',
317 'user_username' => $this->order->author ? $this->order->author->username : '',
318 'user_email' => $this->order->author ? $this->order->author->email : '',
319 'customer_full_name' => $this->order->purchaser_nominative,
320 'customer_first_name' => $customerFirstName,
321 'customer_last_name' => $customerLastName,
322 );
323
324 // get e-mail custom text model
325 $model = JModelVAP::getInstance('mailtext');
326
327 // set up options
328 $options = $this->options;
329 $options['file'] = $this->templateFile;
330
331 // parse e-mail template
332 $tmpl = $model->parseTemplate($tmpl, $this->order, $options);
333
334 // parse e-mail template placeholders
335 foreach ($placeholders as $tag => $value)
336 {
337 $tmpl = str_replace("{{$tag}}", $value, $tmpl);
338 }
339
340 /**
341 * Fetch the attachments configured through the e-mail custom texts.
342 *
343 * @since 1.7.4
344 */
345 $this->attachments = $model->getAttachments();
346
347 // let plugins manipulate the content for this e-mail template
348 $res = VAPMailFactory::letPluginsManipulateMail('customer', 'content', $tmpl, $this->order);
349
350 if ($res === false)
351 {
352 // a plugin prevented the e-mail sending
353 return '';
354 }
355
356 return $tmpl;
357 }
358
359 /**
360 * Sends the HTML contents via e-mail.
361 *
362 * @return boolean
363 */
364 public function send()
365 {
366 $config = VAPFactory::getConfig();
367
368 // get recipient from order details
369 $recipient = $this->order->purchaser_mail;
370
371 if (!$recipient)
372 {
373 // missing recipient
374 return false;
375 }
376
377 // get administrator e-mail
378 // $adminmail = VikAppointments::getAdminMail();
379 // get sender e-mail address
380 $sendermail = VikAppointments::getSenderMail();
381 // get company name
382 $fromname = $config->getString('agencyname');
383
384 // fetch subject
385 $subject = $this->getSubject();
386
387 // parse e-mail template
388 $html = $this->getHtml();
389
390 if (empty($subject) || empty($html))
391 {
392 // do not send e-mail in case the subject or
393 // the content are empty
394 return false;
395 }
396
397 $attachments = array();
398
399 // get all documents that should be included as attachments
400 foreach (VikAppointments::getMailAttachmentsURL() as $file)
401 {
402 // register into the list with status 1, meaning that
403 // the file is permanent and should not be deleted
404 $attachments[$file] = 1;
405 }
406
407 /**
408 * Scan all booked services and include the related attachments.
409 *
410 * @since 1.7
411 */
412 foreach ($this->order->appointments as $appointment)
413 {
414 // iterate attachments
415 foreach ($appointment->service->attachments as $file)
416 {
417 // build full path
418 $file = VAPMAIL_ATTACHMENTS . DIRECTORY_SEPARATOR . $file;
419
420 // Register into the list with status 1, meaning that
421 // the file is permanent and should not be deleted.
422 // We don't need to check whether the file has been already
423 // registered because the array maps all the files by key.
424 // So, in case the file already exists, it will be replaced
425 // with the new one.
426 $attachments[$file] = 1;
427 }
428 }
429
430 /**
431 * Attach all the files configured through the e-mail custom texts.
432 *
433 * @since 1.7.4
434 */
435 foreach ($this->attachments as $file)
436 {
437 // build full path
438 $file = VAPMAIL_ATTACHMENTS . DIRECTORY_SEPARATOR . $file;
439
440 // Register into the list with status 1, meaning that
441 // the file is permanent and should not be deleted.
442 // We don't need to check whether the file has been already
443 // registered because the array maps all the files by key.
444 // So, in case the file already exists, it will be replaced
445 // with the new one.
446 $attachments[$file] = 1;
447 }
448
449 /**
450 * ICS attachment is included only for confirmed appointments.
451 *
452 * @since 1.7
453 */
454 if ($this->order->statusRole == 'APPROVED' && VikAppointments::getAttachmentPropertiesICS('customer'))
455 {
456 // get export ICS file
457 $file = VikAppointments::composeFileICS($this->order->id);
458
459 if ($file)
460 {
461 // register into the list with status 0, meaning that
462 // the file is volatile and should be deleted
463 $attachments[$file] = 0;
464 }
465 }
466
467 /**
468 * CSV attachment is included only for confirmed appointments.
469 *
470 * @since 1.7
471 */
472 if ($this->order->statusRole == 'APPROVED' && VikAppointments::getAttachmentPropertiesCSV('customer'))
473 {
474 // get export CSV file
475 $file = VikAppointments::composeFileCSV($this->order->id);
476
477 if ($file)
478 {
479 // register into the list with status 0, meaning that
480 // the file is volatile and should be deleted
481 $attachments[$file] = 0;
482 }
483 }
484
485 /**
486 * Scan all the user notes and take the last update one to include the
487 * attachments within the e-mail message.
488 *
489 * @since 1.7
490 */
491 $notes = $this->order->getUserNotes();
492
493 // check whether the last modified note owns any attachments
494 if ($notes && $notes[0]->attachments)
495 {
496 // include the note attachments within the e-mail
497 foreach ($notes[0]->attachments as $noteFile)
498 {
499 // register into the list with status 1, meaning that
500 // the file is permanent and should not be deleted
501 $attachments[$noteFile->path] = 1;
502 }
503 }
504
505 // let plugins manipulate the attachments for this e-mail template
506 $res = VAPMailFactory::letPluginsManipulateMail('customer', 'attachment', $attachments, $this->order);
507
508 $sent = false;
509
510 // make sure the plugin didn't prevent e-mail sending
511 if ($res !== false)
512 {
513 // send the e-mail notification
514 $sent = VAPApplication::getInstance()->sendMail(
515 $sendermail,
516 $fromname,
517 $recipient,
518 $this->getReplyTo(),
519 $subject,
520 $html,
521 array_keys($attachments)
522 );
523 }
524
525 // destroy any temporary attachment here
526 foreach ($attachments as $file => $keep)
527 {
528 // check if the file exists and it should be deleted
529 if (!$keep && is_file($file))
530 {
531 // permanently delete file
532 unlink($file);
533 }
534 }
535
536 return $sent;
537 }
538
539 /**
540 * Checks whether the notification should be sent.
541 *
542 * @return boolean
543 */
544 public function shouldSend()
545 {
546 // check if the customer is allowed to self-confirm its appointment
547 if (VikAppointments::canUserApproveOrder($this->order))
548 {
549 // self-confirmation is allowed, force the e-mail sending
550 // because the confirmation link can be found only within
551 // the notification e-mail sent
552 return true;
553 }
554
555 // get list of statuses for which the notification should be sent
556 $list = VAPFactory::getConfig()->getArray('mailcustwhen');
557
558 // make sure the order status is contained within the list
559 return in_array($this->order->status, $list);
560 }
561
562 /**
563 * Helper function used to obtain the most suitable reply-to mail address.
564 *
565 * @return string|null
566 */
567 protected function getReplyTo()
568 {
569 /**
570 * Use the reply-to address specified from the global configuration.
571 *
572 * @since 1.7.7
573 */
574 $defaultReplyTo = VAPFactory::getConfig()->get('replytoemail', null);
575
576 $first = null;
577
578 foreach ($this->order->appointments as $app)
579 {
580 if ($first === null)
581 {
582 $first = $app;
583 }
584 else if ($app->employee->id != $first->employee->id)
585 {
586 // the booked appointments are assigned to different employees,
587 // so we don't need to use the reply-to address
588 return $defaultReplyTo;
589 }
590 }
591
592 /**
593 * Use the email address of the employee only in case all the following conditions are verified:
594 * - the e-mail address must not be empty;
595 * - the employee decided to use its own e-mail as reply-to;
596 * - the appointment must be approved.
597 *
598 * Before the 1.7.7 version the e-mail was always used as reply-to in case the customer
599 * explicitly selected the employee and in case the appointment was confirmed.
600 *
601 * @since 1.7.7
602 */
603 if ($first->employee->email && $first->employee->replyto && $first->statusRole === 'APPROVED')
604 {
605 // use the employee e-mail as reply-to address
606 return $app->employee->email;
607 }
608
609 // fallback to the global reply-to
610 return $defaultReplyTo;
611 }
612 }
613