PluginProbe ʕ •ᴥ•ʔ
VikAppointments Services Booking Calendar / trunk
VikAppointments Services Booking Calendar vtrunk
trunk 1.2.17 1.2.18 1.2.19
vikappointments / site / controllers / order.php
vikappointments / site / controllers Last commit date
calendarweek.php 3 years ago cart.php 1 month ago confirmapp.php 2 years ago empattachser.php 4 years ago empeditcoupon.php 4 years ago empeditcustfield.php 4 years ago empeditlocation.php 4 years ago empeditpay.php 4 years ago empeditprofile.php 4 months ago empeditservice.php 4 years ago empeditwdays.php 4 years ago emplocwdays.php 4 years ago emplogin.php 2 years ago employeesearch.php 2 years ago employeeslist.php 4 years ago empmakerecur.php 1 month ago empmanres.php 1 month ago empsettings.php 2 years ago empsubscr.php 4 years ago empsubscrorder.php 1 year ago index.html 6 years ago modules.php 1 year ago order.php 4 months ago packages.php 4 years ago packagesconfirm.php 4 years ago packagesorder.php 1 year ago servicesearch.php 2 years ago subscriptions.php 4 years ago subscrpayment.php 1 year ago userprofile.php 4 months ago waitinglist.php 4 years ago
order.php
737 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.mvc.controllers.admin');
15
16 /**
17 * VikAppointments order controller.
18 *
19 * @since 1.7
20 */
21 class VikAppointmentsControllerOrder extends VAPControllerAdmin
22 {
23 /**
24 * This is the end-point used by the gateway to validate a payment transaction.
25 * It is mandatory to send the following parameters (via GET or POST) in order to
26 * retrieve the correct details of the order transaction.
27 *
28 * @param integer ordnum The order number (ID).
29 * @param string ordkey The order key (SID).
30 *
31 * @return void
32 */
33 public function notifypayment()
34 {
35 $dispatcher = VAPFactory::getEventDispatcher();
36
37 $app = JFactory::getApplication();
38 $input = $app->input;
39
40 $oid = $input->getUint('ordnum');
41 $sid = $input->getAlnum('ordkey');
42
43 // Get order details (filter by ID and SID).
44 // In case the order doesn't exist, an exception will be thrown.
45 VAPLoader::import('libraries.order.factory');
46 $order = VAPOrderFactory::getAppointments($oid, null, array('sid' => $sid));
47
48 /**
49 * This event is triggered every time a payment tries to validate a transaction made.
50 *
51 * DOES NOT trigger in case the order doesn't exist.
52 *
53 * @param mixed $order The details of the booked appointments.
54 *
55 * @return void
56 *
57 * @since 1.7
58 */
59 $dispatcher->trigger('onReceivePaymentNotification', array($order));
60
61 // build return and error URL
62 $return_url = "index.php?option=com_vikappointments&view=order&ordnum={$oid}&ordkey={$sid}";
63 $error_url = "index.php?option=com_vikappointments&view=order&ordnum={$oid}&ordkey={$sid}";
64
65 /**
66 * If we are trying to validate an order already paid/confirmed, auto-redirect to
67 * the return URL instead of throwing an exception.
68 *
69 * @since 1.7.1
70 */
71 if ($order->statusRole == 'APPROVED')
72 {
73 $this->setRedirect(JRoute::rewrite($return_url, false));
74 return;
75 }
76
77 /**
78 * Allow the payment for REMOVED reservations because they
79 * have been probably paid while they were PENDING.
80 *
81 * @since 1.7
82 */
83 $accepted = array(
84 'PENDING',
85 'REMOVED',
86 );
87
88 // make sure the order can be paid
89 if (!in_array($order->statusRole, $accepted))
90 {
91 // status not allowed
92 throw new Exception('The current status of the order does not allow any payments.', 403);
93 }
94
95 if (!$order->payment)
96 {
97 // payment method not found
98 throw new Exception('The selected payment does not exist', 404);
99 }
100
101 // reload payment details to access the parameters
102 $payment = JModelVAP::getInstance('payment')->getItem($order->payment->id);
103
104 $vik = VAPApplication::getInstance();
105
106 $config = VAPFactory::getConfig();
107
108 // fetch transaction data
109 $paymentData = array();
110
111 /**
112 * The payment URLs are correctly routed for external usage.
113 *
114 * @since 1.6
115 */
116 $return_url = $vik->routeForExternalUse($return_url, false);
117 $error_url = $vik->routeForExternalUse($error_url, false);
118
119 /**
120 * Include the Notification URL in both the PLAIN and ROUTED formats.
121 *
122 * @since 1.7
123 */
124 $notify_url = "index.php?option=com_vikappointments&task=order.notifypayment&ordnum={$oid}&ordkey={$sid}";
125
126 /**
127 * Calculate the total amount to pay.
128 *
129 * @since 1.7.8
130 */
131 $total_to_pay = VikAppointments::getTotalBeforePayment($order);
132
133 $paymentData['type'] = 'appointments';
134 $paymentData['action'] = 'validate';
135 $paymentData['oid'] = $order->id;
136 $paymentData['sid'] = $order->sid;
137 $paymentData['attempt'] = $order->payment_attempt;
138 $paymentData['transaction_name'] = JText::sprintf('VAPTRANSACTIONNAME', $config->get('agencyname'));
139 $paymentData['transaction_currency'] = $config->get('currencyname');
140 $paymentData['currency_symb'] = $config->get('currencysymb');
141 $paymentData['tax'] = 0;
142 $paymentData['return_url'] = $return_url;
143 $paymentData['error_url'] = $error_url;
144 $paymentData['notify_url'] = $vik->routeForExternalUse($notify_url, false);
145 $paymentData['notify_url_plain'] = JUri::root() . $notify_url;
146 $paymentData['total_to_pay'] = $total_to_pay;
147 $paymentData['total_net_price'] = $total_to_pay;
148 $paymentData['total_tax'] = 0;
149 $paymentData['leavedeposit'] = false; // @deprecated
150 $paymentData['payfull'] = true; // @deprecated
151 $paymentData['payment_info'] = $payment;
152 $paymentData['details'] = array(
153 'purchaser_nominative' => $order->purchaser_nominative,
154 'purchaser_mail' => $order->purchaser_mail,
155 'purchaser_phone' => $order->purchaser_phone,
156 );
157
158 /**
159 * Added support for customer billing details.
160 *
161 * @since 1.7
162 */
163 $paymentData['billing'] = $order->billing;
164
165 /**
166 * Trigger event to manipulate the payment details.
167 *
168 * @param array &$order The transaction details.
169 * @param array &$params The payment configuration array.
170 *
171 * @return void
172 *
173 * @since 1.6
174 */
175 $dispatcher->trigger('onInitPaymentTransaction', array(&$paymentData, &$payment->params));
176
177 /**
178 * Instantiate the payment using the platform handler.
179 *
180 * @since 1.6.3
181 */
182 $obj = $vik->getPaymentInstance($payment->file, $paymentData, $payment->params);
183
184 try
185 {
186 // validate payment transaction
187 $result = $obj->validatePayment();
188 }
189 catch (Exception $e)
190 {
191 // catch any exceptions that might have been thrown by the gateway
192 $result = [];
193 $result['verified'] = 0;
194 $result['log'] = $e->getMessage();
195 }
196
197 // get multi-order model
198 $model = JModelVAP::getInstance('multiorder');
199
200 // successful response
201 if ($result['verified'])
202 {
203 if (!empty($result['tot_paid']))
204 {
205 // increase total amount paid
206 $order->totals->paid += (float) $result['tot_paid'];
207 }
208
209 if ($order->totals->paid >= $order->totals->gross)
210 {
211 // the whole amount has been paid, use the apposite PAID status
212 $order->status = JHtml::fetch('vaphtml.status.paid', 'appointments', 'code');
213 // flag the old "paid" flag for BC
214 $order->paid = 1;
215 }
216 else
217 {
218 // a deposit have been left, use CONFIRMED status
219 $order->status = JHtml::fetch('vaphtml.status.confirmed', 'appointments', 'code');
220 // keep old "paid" flag disabled for BC
221 $order->paid = 0;
222 }
223
224 // prepare data to save
225 $data = array(
226 'id' => $order->id,
227 'status' => $order->status,
228 'status_comment' => 'VAP_STATUS_CHANGED_FROM_PAY',
229 'tot_paid' => $order->totals->paid,
230 'paid' => $order->paid,
231 );
232
233 $model->save($data);
234
235 // try to send e-mail notifications
236 VikAppointments::sendMailAction($order->id);
237
238 // try to send SMS notifications
239 VikAppointments::sendSmsAction($order->id);
240
241 // try to auto-generate the invoice
242 VikAppointments::generateInvoice($order->id);
243
244 /**
245 * Trigger event after the validation of a successful transaction.
246 *
247 * @param array $order The transaction details.
248 * @param array $args The response array.
249 *
250 * @return void
251 *
252 * @since 1.6
253 */
254 $dispatcher->trigger('onSuccessPaymentTransaction', array($paymentData, $result));
255 }
256 // failure response
257 else
258 {
259 // check if the payment registered any logs
260 if (!empty($result['log']))
261 {
262 $text = array(
263 'Order #' . $order->id . '-' . $order->sid . ' (Appointment)',
264 nl2br($result['log']),
265 );
266
267 // send error logs to administrator(s)
268 VikAppointments::sendAdminMailPaymentFailed($order->id, $text);
269
270 // get current date and time
271 $timeformat = preg_replace("/:i/", ':i:s', $config->get('timeformat'));
272 $now = JHtml::fetch('date', 'now', $config->get('dateformat') . ' ' . $timeformat, $app->get('offset', 'UTC'));
273
274 // build log string
275 $log = str_repeat('-', strlen($now) + 4) . "\n";
276 $log .= "| $now |\n";
277 $log .= str_repeat('-', strlen($now) + 4) . "\n";
278 $log .= "\n" . $result['log'];
279
280 if (!empty($order->log))
281 {
282 // always prepend new logs at the beginning
283 $log = $log . "\n\n" . $order->log;
284 }
285
286 // prepare save data
287 $data = array(
288 'id' => $order->id,
289 'log' => $log,
290 'payment_attempt' => ++$order->payment_attempt,
291 );
292
293 // update order logs
294 $model->save($data);
295 }
296
297 /**
298 * Trigger event after the validation of a failed transaction.
299 *
300 * @param array $order The transaction details.
301 * @param array $args The response array.
302 *
303 * @return void
304 *
305 * @since 1.6
306 */
307 $dispatcher->trigger('onFailPaymentTransaction', array($paymentData, $result));
308 }
309
310 // check whether the payment instance supports a method
311 // to be executed after the validation
312 if (method_exists($obj, 'afterValidation'))
313 {
314 $obj->afterValidation($result['verified'] ? 1 : 0);
315 }
316 }
317
318 /**
319 * This task is used to confirm an order (only PENDING status).
320 * After a successful confirmation, the owner of the appointment will be notified via e-mail.
321 *
322 * The response of this action is echoed directly.
323 *
324 * This method expects the following parameters to be sent via POST or GET.
325 *
326 * NOTE: this task MUST not use security tokens to prevent CSRF, because this link is included
327 * within the e-mail of the administrators/employees, letting them to access this resource
328 * without having to log in first.
329 *
330 * @param integer id The order number.
331 * @param string conf_key The confirmation key.
332 *
333 * @return boolean
334 */
335 public function confirm()
336 {
337 $input = JFactory::getApplication()->input;
338
339 $id = $input->getUint('id', 0);
340 $key = $input->getAlnum('conf_key');
341
342 if (empty($key))
343 {
344 // missing confirmation key
345 echo '<div class="vap-confirmpage order-error">' . JText::translate('VAPCONFORDNOROWS') . '</div>';
346 return false;
347 }
348
349 VAPLoader::import('libraries.order.factory');
350
351 try
352 {
353 // get order details (search by confirmation key)
354 $order = VAPOrderFactory::getAppointments($id, null, array('conf_key' => $key));
355 }
356 catch (Exception $e)
357 {
358 // order not found
359 echo '<div class="vap-confirmpage order-error">' . JText::translate('VAPCONFORDNOROWS') . '</div>';
360 return false;
361 }
362
363 if ($order->statusRole != 'PENDING')
364 {
365 if ($order->statusRole == 'APPROVED')
366 {
367 // this order has been already confirmed
368 echo '<div class="vap-confirmpage order-notice">' . JText::translate('VAPCONFORDISCONFIRMED') . '</div>';
369 return true;
370 }
371 else
372 {
373 // order expired, cannot confirm it
374 echo '<div class="vap-confirmpage order-error">' . JText::translate('VAPCONFORDISREMOVED') . '</div>';
375 return false;
376 }
377 }
378
379 // get reservation model
380 $model = JModelVAP::getInstance('reservation');
381
382 /**
383 * NOTE: it is possible to use the onBeforeSaveReservation hook to validate the order
384 * before saving it. The "scope" attribute will let you understand that we are
385 * going to approve one or more appointments.
386 */
387
388 // prepare save data
389 $data = array(
390 'id' => $id,
391 'status' => JHtml::fetch('vaphtml.status.confirmed', 'appointments', 'code'),
392 'status_comment' => 'VAP_STATUS_CONFIRMED_WITH_LINK',
393 'scope' => 'approve',
394 );
395
396 // update records
397 if (!$model->save($data))
398 {
399 // get last registered error
400 $error = $model->getError($index = null, $string = true);
401
402 echo '<div class="vap-confirmpage order-error">' . ($error ? $error : JText::translate('ERROR')) . '</div>';
403 return false;
404 }
405
406 // try to send e-mail notifications
407 VikAppointments::sendMailAction($order->id);
408
409 // try to send SMS notifications
410 VikAppointments::sendSmsAction($order->id);
411
412 // display successful message
413 echo '<div class="vap-confirmpage order-good">' . JText::translate('VAPCONFORDCOMPLETED') . '</div>';
414 return true;
415 }
416
417 /**
418 * Mark the specified order as cancelled.
419 *
420 * @return void
421 */
422 public function cancel()
423 {
424 $app = JFactory::getApplication();
425 $input = $app->input;
426 $config = VAPFactory::getConfig();
427
428 $id = $input->getUint('id', 0);
429 $sid = $input->getString('sid', '');
430 $id_parent = $input->getInt('parent', 0);
431
432 $itemid = $input->getUint('Itemid');
433
434 if ($id_parent > 0 && $id_parent != $id)
435 {
436 // we are cancelling an appointment that belong to a multi-order
437 $ordnum = $id_parent;
438 }
439 else
440 {
441 // go back to the specified appointment/order
442 $ordnum = $id;
443 }
444
445 // set redirection URL
446 $uri = 'index.php?option=com_vikappointments&view=order&ordnum=' . $ordnum . '&ordkey=' . $sid . ($itemid ? '&Itemid=' . $itemid : '');
447 $this->setRedirect(JRoute::rewrite($uri, false));
448
449 if (!$config->getBool('enablecanc'))
450 {
451 // cancellation disabled
452 $app->enqueueMessage(JText::translate('VAPORDERCANCDISABLEDERROR'), 'error');
453 return false;
454 }
455
456 // Get order details (filter by ID and SID).
457 // In case the order doesn't exist, an exception will be thrown.
458 VAPLoader::import('libraries.order.factory');
459 $order = VAPOrderFactory::getAppointments($id, null, array('sid' => $sid));
460
461 // all the appointments must be allowed to cancel the appointments
462 foreach ($order->appointments as $appointment)
463 {
464 if (!VikAppointments::canUserCancelOrder($appointment))
465 {
466 // make sure the appointment status is valid
467 if ($appointment->statusRole == 'APPROVED')
468 {
469 // currently unable to cancel the order
470 $error = JText::sprintf('VAPORDERCANCEXPIREDERROR', $config->getUint('canctime'));
471 $app->enqueueMessage($error, 'error');
472 }
473
474 return false;
475 }
476 }
477
478 /**
479 * Trigger event before the cancellation of the specified order.
480 *
481 * @param integer $id The order ID to cancel.
482 *
483 * @return void
484 *
485 * @since 1.6
486 * @deprecated 1.8 Use onBeforeSaveReservation instead.
487 */
488 VAPFactory::getEventDispatcher()->trigger('onBeforeCancelOrder', array($id));
489
490 // get reservation model
491 $model = JModelVAP::getInstance('reservation');
492
493 /**
494 * NOTE: it is possible to use the onBeforeSaveReservation hook to validate the order
495 * before saving it. The "scope" attribute will let you understand that we are
496 * going to cancel one or more appointments.
497 */
498
499 // prepare save data
500 $data = array(
501 'id' => $id,
502 'status' => JHtml::fetch('vaphtml.status.cancelled', 'appointments', 'code'),
503 'status_comment' => 'VAP_STATUS_ORDER_CANCELLED',
504 // auto-process the waiting list
505 'notifywl' => true,
506 'scope' => 'cancellation',
507 );
508
509 // update records
510 if (!$model->save($data))
511 {
512 // get last registered error
513 $error = $model->getError($index = null, $string = true);
514 $app->enqueueMessage($error ? $error : JText::translate('ERROR'), 'error');
515 return false;
516 }
517
518 if ($id_parent == $ordnum)
519 {
520 // we have a multi-order, make sure now all the appointments have been cancelled
521 $multiOrderModel = JModelVAP::getInstance('multiorder');
522 // load the status of all the children assigned to this order
523 $statuses = array_unique($multiOrderModel->getChildren($ordnum, 'status'));
524
525 // check if the statuses list contains only one element, meaning that all the
526 // appointments of the order has been already cancelled
527 if (count($statuses) == 1)
528 {
529 $data['id'] = $ordnum;
530
531 unset($data['notifywl']);
532 unset($data['scope']);
533
534 // change the status of the multi-order to CANCELLED once the last
535 // appointment gets cancelled
536 $model->save($data);
537 }
538 }
539
540 // try to send e-mail notifications
541 VikAppointments::sendMailAction($id);
542 return true;
543 }
544
545 /**
546 * Fetches the given order and prepares the view that is going to be printed.
547 * The template is immediately echoed and the print popup is automatically triggered.
548 *
549 * This method expects the following parameters to be sent via POST or GET.
550 *
551 * @param integer id The order number.
552 * @param string sid The order key.
553 *
554 * @return void
555 */
556 public function doprint()
557 {
558 $input = JFactory::getApplication()->input;
559
560 $oid = $input->getUint('id', 0);
561 $sid = $input->getString('sid', '');
562
563 $lang = JFactory::getLanguage()->getTag();
564
565 // Get order details (filter by ID and SID).
566 // In case the order doesn't exist, an exception will be thrown.
567 VAPLoader::import('libraries.order.factory');
568 $order = VAPOrderFactory::getAppointments($oid, $lang, array('sid' => $sid));
569
570 // load mail factory
571 VAPLoader::import('libraries.mail.factory');
572 $mail = VAPMailFactory::getInstance('customer', $order, array('lang' => $lang));
573
574 // force blank template (might be not needed)
575 $input->set('tmpl', 'component');
576
577 // get template HTML
578 $html = $mail->getHtml();
579
580 // append script to print the document
581 $html .= "<script>setTimeout(() => { window.print(); }, 500);</script>\n";
582
583 /**
584 * Use the specific blank layout to print the view
585 * and exit to avoid including internal and external assets,
586 * which may alter the default style of the template.
587 *
588 * @since 1.6
589 */
590 echo JLayoutHelper::render('document.blankpage', array('body' => $html));
591 exit;
592 }
593
594 /**
595 * AJAX end-point used to fetch the formatted countdown text.
596 *
597 * This method expects the following parameters to be sent via POST or GET.
598 *
599 * @param integer id The order number.
600 * @param integer locked_until The expiration of the order (optional).
601 *
602 * @return void
603 */
604 public function countdown()
605 {
606 $input = JFactory::getApplication()->input;
607
608 // look for a given threshold
609 $locked_until = $input->getUint('locked_until', null);
610
611 if (is_null($locked_until))
612 {
613 // load order details
614 VAPLoader::import('libraries.order.factory');
615 $order = VAPOrderFactory::getAppointments($input->getUint('order'));
616 // retrieved threshold from order details
617 $locked_until = $order->locked_until;
618 }
619
620 $result = new stdClass;
621
622 // calculate remaining seconds
623 $remaining = $locked_until - time();
624
625 if ($remaining > 0)
626 {
627 // format remaining seconds in a readable text
628 $remaining = VikAppointments::formatMinutesToTime(ceil($remaining / 60), $apply = true);
629
630 // order still active
631 $result->status = true;
632 // create countdown message
633 $result->text = JText::sprintf('VAPORDERCOUNTDOWN', $remaining);
634 }
635 else
636 {
637 // expired order
638 $result->status = false;
639 }
640
641 // send response to caller
642 $this->sendJSON($result);
643 }
644
645 /**
646 * Updates the fields of the specified order.
647 *
648 * @return bool
649 *
650 * @since 1.7.7
651 */
652 public function updatefields()
653 {
654 $app = JFactory::getApplication();
655
656 $ordnum = $app->input->getUint('ordnum');
657 $ordkey = $app->input->getAlnum('ordkey');
658 $itemid = $app->input->getUint('Itemid');
659
660 $this->setRedirect(JRoute::rewrite("index.php?option=com_vikappointments&view=order&ordnum={$ordnum}&ordkey={$ordkey}&Itemid={$itemid}", false));
661
662 if (!JSession::checkToken())
663 {
664 // invalid token, back to confirm page
665 $app->enqueueMessage(JText::translate('JINVALID_TOKEN'), 'error');
666 return false;
667 }
668
669 try
670 {
671 // get order details (filter by ID and SID)
672 VAPLoader::import('libraries.order.factory');
673 $order = VAPOrderFactory::getAppointments($ordnum, JFactory::getLanguage()->getTag(), ['sid' => $ordkey]);
674 }
675 catch (Exception $e)
676 {
677 // probably the error does not exist
678 $app->enqueueMessage($e->getMessage(), 'error');
679 return false;
680 }
681
682 // get the custom fields that can be updated for this order
683 $fields = VikAppointments::getEditableOrderFields($order)
684 ->noSeparator()
685 ->fetch();
686
687 if (!$fields)
688 {
689 // raise error in case there are no editable fields
690 $app->enqueueMessage(JText::translate('JERROR_ALERTNOAUTHOR'), 'error');
691 return false;
692 }
693
694 // import custom fields requestor
695 VAPLoader::import('libraries.customfields.requestor');
696
697 // load custom fields from request
698 $data = VAPCustomFieldsRequestor::loadForm($fields, $tmp, $strict = true);
699
700 $prevData = array_merge((array) $order->fields, (array) $order->uploads);
701
702 // merge the old data with the new one
703 $order->fields = array_merge((array) $order->fields, $data);
704 $order->uploads = array_merge((array) $order->uploads, $tmp['uploads'] ?? []);
705
706 $newData = array_merge((array) $order->fields, (array) $order->uploads);
707
708 // get reservation model
709 $model = JModelVAP::getInstance('reservation');
710
711 // attempt to save the custom fields
712 $result = $model->save([
713 'id' => $order->id,
714 'custom_f' => $order->fields,
715 'uploads' => $order->uploads,
716 'fields_data' => $tmp,
717 ]);
718
719 if ($newData != $prevData)
720 {
721 // send a notification to the administrator and to the employee only in case the fields have been actually changed
722 $model->sendEmailNotification($order->id, ['client' => 'admin', 'subject' => 'VAPMAILSUBJECTFIELDSUPDATED']);
723 $model->sendEmailNotification($order->id, ['client' => 'employee', 'subject' => 'VAPMAILSUBJECTFIELDSUPDATED']);
724 }
725
726 if (!$result)
727 {
728 // something went wrong...
729 $app->enqueueMessage($model->getError(null, true) ?: 'Error', 'error');
730 return false;
731 }
732
733 // fields updated successfully
734 return true;
735 }
736 }
737