PluginProbe ʕ •ᴥ•ʔ
VikAppointments Services Booking Calendar / trunk
VikAppointments Services Booking Calendar vtrunk
trunk 1.2.17 1.2.18 1.2.19
vikappointments / site / models / cart.php
vikappointments / site / models Last commit date
allorders.php 2 years ago calendarweek.php 1 month ago cart.php 1 month ago confirmapp.php 1 month ago empaccountstat.php 2 years ago empattachser.php 2 years ago empcoupons.php 2 years ago empcustfields.php 2 years ago empeditcoupon.php 3 years ago empeditcustfield.php 3 years ago empeditlocation.php 3 years ago empeditpay.php 3 years ago empeditprofile.php 5 months ago empeditservice.php 3 years ago empeditwdays.php 3 years ago emplocations.php 2 years ago emplocwdays.php 4 years ago emplogin.php 1 month ago employeesearch.php 1 month ago employeeslist.php 1 month ago empmanres.php 2 years ago emppaylist.php 2 years ago empserviceslist.php 2 years ago empsettingsman.php 4 years ago empsubscrcart.php 4 years ago empsubscrhistory.php 2 years ago empsubscrorder.php 5 months ago empwdays.php 2 years ago index.html 4 years ago packages.php 2 years ago packagescart.php 4 years ago packagesconfirm.php 5 months ago packorders.php 2 years ago servicesearch.php 2 years ago serviceslist.php 2 years ago subscrcart.php 2 years ago subscrhistory.php 2 years ago subscrpayment.php 5 months ago
cart.php
1173 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.model');
15 // load cart framework
16 VikAppointments::loadCartLibrary();
17
18 /**
19 * VikAppointments cart model.
20 *
21 * @since 1.7
22 */
23 class VikAppointmentsModelCart extends JModelVAP
24 {
25 /**
26 * Registers a new appointment within the cart.
27 *
28 * @param mixed $data Either an array or an object holding
29 * the appointment details.
30 *
31 * @return mixed The added item on success, false otherwise.
32 */
33 public function addItem($data)
34 {
35 $data = (array) $data;
36
37 $dispatcher = VAPFactory::getEventDispatcher();
38 $config = VAPFactory::getConfig();
39
40 // load service details according to the overrides of the specified employee (if any)
41 $service = JModelVAP::getInstance('serempassoc')->getOverrides($data['id_service'], $data['id_employee']);
42
43 if (!$service)
44 {
45 // the service (or the relation with the employee) doesn't exist
46 $this->setError(JText::translate('VAPSERNOTFOUNDERROR'));
47 return false;
48 }
49
50 $empModel = JModelVAP::getInstance('employee');
51
52 // validate selected employee
53 $data['id_employee'] = isset($data['id_employee']) ? (int) $data['id_employee'] : 0;
54
55 if ($data['id_employee'] > 0)
56 {
57 // get employee details
58 $employee = $empModel->getItem($data['id_employee']);
59
60 if (!$employee)
61 {
62 // employee not found
63 $this->setError(JText::translate('VAPEMPNOTFOUNDERROR'));
64 return false;
65 }
66 }
67
68 // check if we have to build the check-in date
69 if (isset($data['date']) && empty($data['checkin']))
70 {
71 $tz = $empModel->getTimezone($data['id_employee']);
72 // create date according to the employee timezone
73 $dt = JFactory::getDate("{$data['date']} {$data['hour']}:{$data['min']}:00", $tz);
74 // adjust date to UTC
75 $data['checkin'] = $dt->format('Y-m-d H:i:s');
76 }
77
78 $data['options'] = isset($data['options']) ? $data['options'] : array();
79
80 // validate options
81 $empty_options = $this->validateOptions($service->id, $data['options']);
82
83 if ($empty_options !== false)
84 {
85 // missing required options
86 $this->setError(JText::translate('VAPOPTIONREQUIREDERR'));
87 return false;
88 }
89
90 /**
91 * Validate service restrictions.
92 *
93 * @since 1.6.5
94 */
95 VAPLoader::import('libraries.models.restrictions');
96
97 if (!VAPSpecialRestrictions::canBookService($data['id_service'], $data['checkin'], $restr))
98 {
99 if (VikAppointments::isUserLogged())
100 {
101 // the user already reached the maximum threshold
102 $err = JText::sprintf(
103 'VAPRESTRICTIONLIMITREACHED',
104 $restr->maxapp,
105 strtolower(JText::translate('VAPMANAGERESTRINTERVAL' . strtoupper($restr->interval)))
106 );
107 }
108 else
109 {
110 // login needed before to see the available slots
111 $err = JText::translate('VAPRESTRICTIONLIMITGUEST2');
112 }
113
114 // register fetched error
115 $this->setError($err);
116 return false;
117 }
118
119 // fetch number of participants
120 $data['people'] = isset($data['people']) ? (int) $data['people'] : 1;
121 $data['people'] = max(array(1, $data['people']));
122
123 // get check-in date adjusted to the local timezone of the employee, so that we can
124 // properly calculate the special rates
125 $empCheckin = JHtml::fetch('date', $data['checkin'], 'Y-m-d H:i:s', $empModel->getTimezone($data['id_employee']));
126
127 /**
128 * The price is calculated using the special rates.
129 *
130 * @since 1.6
131 */
132 $service->price = VAPSpecialRates::getRate($service->id, $data['id_employee'], $empCheckin, $data['people']);
133
134 if ($service->priceperpeople)
135 {
136 // multiply the price by the number of participants
137 $service->price *= $data['people'];
138 }
139
140 /**
141 * In case the checkout selection is allowed, we need to extend the price
142 * and the duration by the number of selected slots.
143 *
144 * @since 1.6
145 */
146 if ($service->checkout_selection)
147 {
148 // fetch selected factor
149 $factor = isset($data['factor']) ? (int) $data['factor'] : 1;
150 }
151 else
152 {
153 $factor = 1;
154 }
155
156 // get cart instance
157 $cart = $this->getCart();
158
159 // create new cart item
160 $item = new VAPCartItem(
161 $service->id,
162 $data['id_employee'] > 0 ? $employee->id : -1,
163 $service->name,
164 $data['id_employee'] > 0 ? $employee->nickname : '',
165 $service->price,
166 $service->duration,
167 $data['checkin'],
168 $data['people']
169 );
170
171 // set factor in case of check-out selection
172 $item->setFactor($factor);
173
174 // get currently logged-in customer
175 $customer = VikAppointments::getCustomer();
176
177 // check whether the customer is logged-in and it is subscribed for the
178 // booked service and the selected check-in
179 if ($customer && $customer->isSubscribed($service->id, $data['checkin']))
180 {
181 // the customer owns a subscription, unset the service price
182 $item->setPrice(0);
183 }
184 else if ($config->getBool('subscrmandatory'))
185 {
186 // a subscription is mandatory, do not allow the purchase of this service
187 if (VikAppointments::isUserLogged())
188 {
189 // customer logged-in with an expired (or missing) subscription
190 $link = JRoute::rewrite('index.php?option=com_vikappointments&view=subscriptions');
191 $this->setError(JText::sprintf('VAPSUBSCRREQERR', $link));
192 }
193 else
194 {
195 // login needed in order to check the subscription plan
196 $this->setError(JText::translate('VAPSUBSCRREQERRGUEST'));
197 }
198
199 return false;
200 }
201
202 // get options model
203 $optModel = JModelVAP::getInstance('option');
204
205 $sharedOptionsCountLookup = [];
206
207 // validate specified options
208 foreach ($data['options'] as $opt)
209 {
210 $pk = array(
211 'id' => $opt['id'],
212 'published' => 1,
213 'id_variation' => $opt['variation'],
214 );
215
216 // get option details
217 $option = $optModel->getItem($pk);
218
219 if (!$option || !$optModel->exists($opt['id'], $service->id))
220 {
221 // option not found or not assigned to the given service
222 continue;
223 }
224
225 /**
226 * Check whether the maximum quantity varies according to the
227 * number of selected participants.
228 *
229 * @since 1.7
230 */
231 if ($option->maxqpeople && $option->single)
232 {
233 // use current number of participants as maximum amount
234 $option->maxq = $data['people'];
235
236 /**
237 * Force the quantity in case the option was configured to be
238 * equal to the selected number of participants
239 *
240 * @since 1.7.4
241 */
242 if ($option->maxqpeople == 2)
243 {
244 $opt['quantity'] = $option->maxq;
245 }
246 }
247
248 // fetch quantity
249 $option->quantity = min(array(intval($opt['quantity']), $option->maxq));
250 $option->quantity = max(array(1, $option->quantity));
251
252 $isShared = false;
253
254 /**
255 * Validate the shared options.
256 *
257 * @since 1.7.5
258 */
259 if ($option->maxqpeople == 1 && $option->shared)
260 {
261 $groupId = $option->id_group ?: 0;
262 $sharedOptionsCountLookup[$groupId] = ($sharedOptionsCountLookup[$groupId] ?? 0) + $option->quantity;
263
264 $isShared = true;
265
266 // in case the total count of selected units (for this group) exceeds the maximum capacity,
267 // abort and return an error message to the customer
268 if ($sharedOptionsCountLookup[$groupId] > $option->maxq)
269 {
270 $this->setError(JText::translate('VAP_OPTGROUP_SHARED_QTY_EXCEEDED'));
271 return false;
272 }
273 }
274
275 if ($option->variations)
276 {
277 // add variation name to option
278 $option->name .= ' - ' . $option->variations[0]->name;
279 // increase price by the variation cost
280 $option->price += $option->variations[0]->inc_price;
281 // increase duration
282 $option->duration += $option->variations[0]->inc_duration;
283 }
284
285 // create option instance
286 $option = new VAPCartOption(
287 $option->id,
288 $opt['variation'],
289 $option->name,
290 $option->price,
291 $option->maxq,
292 $option->required,
293 $option->quantity,
294 $option->duration
295 );
296
297 /**
298 * Track the configured shared option within the cart.
299 *
300 * @since 1.7.8
301 */
302 $option->setShared($isShared);
303
304 /**
305 * Trigger event before adding an option to the cart item.
306 *
307 * @param mixed $item The cart item object.
308 * @param mixed &$option The item option object.
309 *
310 * @return boolean False to avoid adding the option.
311 *
312 * @since 1.6
313 */
314 if (!$dispatcher->not('onAddOptionCart', array($item, &$option)))
315 {
316 // push option in case no plugin was triggered
317 // or in case we got only positive results
318 $item->addOption($option);
319 }
320
321 /**
322 * Make sure the remaining number of units actually available in stock
323 * is equal or lower than the specified quantity.
324 *
325 * @since 1.7.7
326 */
327 if (!$this->checkStockAvailability($option))
328 {
329 return false;
330 }
331 }
332
333 // get reservation model
334 $reservationModel = JModelVAP::getInstance('reservation');
335
336 // build availability query
337 $query = [
338 'id_service' => $item->getServiceID(),
339 'id_employee' => $item->getEmployeeID() > 0 ? $item->getEmployeeID() : 0,
340 'duration' => $item->getDuration(),
341 'sleep' => $service->sleep,
342 'people' => $item->getPeople(),
343 'checkin_ts' => $item->getCheckinDate(),
344 ];
345
346 // check availability
347 if (!$reservationModel->isAvailable($query))
348 {
349 // the selected appointment is no longer available
350 $this->setError(JText::translate('VAPCARTITEMADDERR3'));
351
352 return false;
353 }
354
355 // junk variable used by plugins to set custom errors
356 $err = '';
357
358 /**
359 * Trigger event before adding an item into the cart.
360 *
361 * @param mixed $cart The cart instance.
362 * @param mixed &$item The cart item object.
363 * @param string &$err String used to raise custom errors.
364 *
365 * @return boolean False to avoid adding the item.
366 *
367 * @since 1.6
368 */
369 if ($dispatcher->not('onAddItemCart', array($cart, &$item, &$err)))
370 {
371 // Avoid pushing the item into the cart in case at least a plugin
372 // returns a negative value. If no plugin is attached to this event,
373 // the item will be added correctly.
374 $this->setError($err ? $err : JText::translate('ERROR'));
375 return false;
376 }
377
378 // push item into the cart
379 $res = $cart->addItem($item);
380
381 if (!$res)
382 {
383 if (!VikAppointments::canAddItemToCart($cart->getCartLength()))
384 {
385 // limit reached
386 $this->setError(JText::translate('VAPCARTITEMADDERR1'));
387 }
388 else
389 {
390 // service already in cart
391 $this->setError(JText::translate('VAPCARTITEMADDERR2'));
392 }
393
394 return false;
395 }
396
397 /**
398 * Validates the "Mandatory Purchase" setting of the packages, by checking
399 * whether all the items within the cart can be redeemed.
400 *
401 * @since 1.7
402 */
403 if (VikAppointments::isCompliantWithMandatoryPackage($cart) == false)
404 {
405 if (VikAppointments::isUserLogged())
406 {
407 // not enough packages to redeem
408 $link = JRoute::rewrite('index.php?option=com_vikappointments&view=packages');
409 $this->setError(JText::sprintf('VAPPACKAGEREQERR', $link));
410 }
411 else
412 {
413 // login needed in order to count the packages
414 $this->setError(JText::translate('VAPPACKAGEREQERRGUEST'));
415 }
416
417 return false;
418 }
419
420 // revalidate coupon code
421 $this->revalidateCoupon();
422
423 // save cart data
424 $cart->store();
425
426 return $item;
427 }
428
429 /**
430 * Removes the matching appointment from the cart.
431 *
432 * @param mixed $data Either an array or an object holding
433 * the appointment details.
434 *
435 * @return boolean True on success, false otherwise.
436 */
437 public function removeItem($data)
438 {
439 $data = (array) $data;
440
441 $dispatcher = VAPFactory::getEventDispatcher();
442
443 // validate selected employee
444 $data['id_employee'] = isset($data['id_employee']) ? (int) $data['id_employee'] : 0;
445
446 // load service details according to the overrides of the specified employee (if any)
447 $service = JModelVAP::getInstance('serempassoc')->getOverrides($data['id_service'], $data['id_employee']);
448
449 if (!$service)
450 {
451 // the service (or the relation with the employee) doesn't exist
452 $this->setError(JText::translate('VAPSERNOTFOUNDERROR'));
453 return false;
454 }
455
456 // get cart handler
457 $cart = $this->getCart();
458
459 /**
460 * Trigger event before deleting an item from the cart.
461 *
462 * @param mixed $cart The cart instance.
463 * @param integer $id_service The service ID.
464 * @param integer $id_employee The employee ID.
465 * @param string $checkin The check-in date time (UTC).
466 *
467 * @return boolean False to avoid deleting the item.
468 *
469 * @since 1.6
470 */
471 if ($dispatcher->not('onRemoveItemCart', array($cart, $data['id_service'], $data['id_employee'], $data['checkin'])))
472 {
473 // Avoid deleting the item into the cart in case at least a plugin
474 // returns a negative value. If no plugin is attached to this event,
475 // the item will be removed correctly.
476
477 $this->setError(JText::translate('VAPCARTITEMDELERR'));
478 return false;
479 }
480
481 // try to delete the item
482 $res = $cart->removeItem($data['id_service'], $data['id_employee'], $data['checkin']);
483
484 // check item removed
485
486 if (!$res)
487 {
488 $this->setError(JText::translate('VAPCARTITEMDELERR'));
489 return false;
490 }
491
492 // revalidate coupon code
493 $this->revalidateCoupon();
494
495 // apply the changes
496 $cart->store();
497
498 return true;
499 }
500
501 /**
502 * Removes all the appointments from the cart.
503 *
504 * @return void
505 */
506 public function emptyCart()
507 {
508 $dispatcher = VAPFactory::getEventDispatcher();
509
510 // get cart handler
511 $cart = $this->getCart();
512
513 /**
514 * Trigger event before flushing the cart.
515 *
516 * @param mixed $cart The cart instance.
517 *
518 * @return void
519 *
520 * @since 1.6
521 */
522 $dispatcher->not('onEmptyCart', array($cart));
523
524 // flush the cart
525 $cart->emptyCart();
526
527 // revalidate coupon code
528 $this->revalidateCoupon();
529
530 // apply the changes
531 $cart->store();
532 }
533
534 /**
535 * Increases the units of the specified option.
536 *
537 * @param mixed $data Either an array or an object holding
538 * the appointment details.
539 *
540 * @return boolean True on success, false otherwise.
541 */
542 public function addOption($data)
543 {
544 $data = (array) $data;
545
546 $dispatcher = VAPFactory::getEventDispatcher();
547
548 // get cart handler
549 $cart = $this->getCart();
550
551 // validate selected employee
552 $data['id_employee'] = isset($data['id_employee']) ? (int) $data['id_employee'] : 0;
553
554 // find the item matching the appointment
555 $index = $cart->indexOf($data['id_service'], $data['id_employee'], $data['checkin']);
556 $item = $cart->getItemAt($index);
557
558 if (!$item)
559 {
560 // item not found
561 $this->setError(JText::translate('VAPCARTOPTADDERR1'));
562 return false;
563 }
564
565 // get the specified option from the item
566 $option = $item->getOptionAt($item->indexOf($data['id_option']));
567
568 if (!$option)
569 {
570 // option not found
571 $this->setError(JText::translate('VAPCARTOPTADDERR1'));
572 return false;
573 }
574
575 /**
576 * Trigger event before adding an option to the cart item.
577 *
578 * @param mixed $item The cart item object.
579 * @param mixed &$option The item option object.
580 *
581 * @return boolean False to avoid adding the option.
582 *
583 * @since 1.6
584 */
585 if ($dispatcher->not('onAddOptionCart', array($item, &$option)))
586 {
587 // Avoid adding the option into the item in case at least a plugin
588 // returns a negative value. If no plugin is attached to this event,
589 // the option will be added correctly.
590
591 $this->setError(JText::translate('VAPCARTOPTADDERR1'));
592 return false;
593 }
594
595 // get current quantity
596 $qty = $option->getQuantity();
597 // increase by the specified units (1 by default)
598 $option->add(isset($data['units']) ? $data['units'] : 1);
599
600 // check whether something has changed
601 if ($qty == $option->getQuantity())
602 {
603 // the maximum quantity was reached
604 $this->setError(JText::translate('VAPOPTIONMAXQUANTITYNOTICE'));
605 return false;
606 }
607
608 /**
609 * Make sure the remaining number of units actually available in stock
610 * is equal or lower than the specified quantity.
611 *
612 * @since 1.7.7
613 */
614 if (!$this->checkStockAvailability($option))
615 {
616 return false;
617 }
618
619 // revalidate coupon code
620 $this->revalidateCoupon();
621
622 // save changes
623 $cart->store();
624
625 return true;
626 }
627
628 /**
629 * Decreases the units of the specified option.
630 *
631 * @param mixed $data Either an array or an object holding
632 * the appointment details.
633 *
634 * @return boolean True on success, false otherwise.
635 */
636 public function removeOption($data)
637 {
638 $data = (array) $data;
639
640 $dispatcher = VAPFactory::getEventDispatcher();
641
642 // get cart handler
643 $cart = $this->getCart();
644
645 // validate selected employee
646 $data['id_employee'] = isset($data['id_employee']) ? (int) $data['id_employee'] : 0;
647
648 // find the item matching the appointment
649 $index = $cart->indexOf($data['id_service'], $data['id_employee'], $data['checkin']);
650 $item = $cart->getItemAt($index);
651
652 if (!$item)
653 {
654 // item not found
655 $this->setError(JText::translate('VAPCARTOPTADDERR1'));
656 return false;
657 }
658
659 /**
660 * Trigger event before detaching an option from the item.
661 *
662 * @param integer $id_option The option ID.
663 * @param mixed $item The cart item instance.
664 *
665 * @return boolean False to avoid detaching the option.
666 *
667 * @since 1.6
668 */
669 if ($dispatcher->not('onRemoveOptionCart', array($data['id_option'], $item)))
670 {
671 // Avoid detaching the option from the item in case at least a plugin
672 // returns a negative value. If no plugin is attached to this event,
673 // the option will be detached correctly.
674
675 $this->setError(JText::translate('VAPCARTOPTDELERR'));
676 return false;
677 }
678
679 // validate units to decrease
680 $units = isset($data['units']) ? $data['units'] : 1;
681 // try to delete the option
682 $res = $item->removeOption($data['id_option'], $units);
683
684 if (!$res)
685 {
686 // unable to delete the option
687 $this->setError(JText::translate('VAPCARTOPTDELERR'));
688 return false;
689 }
690
691 // revalidate coupon code
692 $this->revalidateCoupon();
693
694 // save changes
695 $cart->store();
696
697 return true;
698 }
699
700 /**
701 * Registers a new appointment with recurrence within the cart.
702 *
703 * @param mixed $data Either an array or an object holding the
704 * appointment details.
705 * @param array $recurrence An array containing the recurrence roles.
706 */
707 public function addRecurringItem($data, $recurrence)
708 {
709 $data = (array) $data;
710
711 // get recurrence model
712 $model = JModelVAP::getInstance('makerecurrence');
713
714 $data['id_employee'] = isset($data['id_employee']) ? $data['id_employee'] : 0;
715
716 // get employee timezone
717 $tz = JModelVAP::getInstance('employee')->getTimezone($data['id_employee']);
718
719 // check if we have to build the check-in date
720 if (isset($data['date']))
721 {
722 // create date according to the employee timezone
723 $dt = JFactory::getDate("{$data['date']} {$data['hour']}:{$data['min']}:00", $tz);
724 // adjust date to UTC
725 $data['checkin'] = $dt->format('Y-m-d H:i:s');
726 }
727
728 // adjust recurrence date to the employee/system timezone because
729 // DST might change over time
730 $dt = JFactory::getDate($data['checkin']);
731 $dt->setTimezone(new DateTimeZone($tz));
732
733 // compose dates recurrence
734 $arr = $model->getRecurrence($dt, $recurrence);
735
736 if (!$arr)
737 {
738 // invalid recurrence
739 $this->setError(JText::translate('VAPMAKERECNOROWS'));
740 return false;
741 }
742
743 // include selected check-in within the list
744 array_unshift($arr, $data['checkin']);
745
746 $results = array();
747
748 // iterate all dates found
749 foreach ($arr as $date)
750 {
751 $tmp = array();
752 // format check-in date for response
753 $tmp['date'] = JHtml::fetch('date', $date, JText::translate('DATE_FORMAT_LC2'), VikAppointments::getUserTimezone()->getName());
754 // set initial status
755 $tmp['status'] = 0;
756
757 // make sure we haven't reached the maximum size
758 if (!VikAppointments::canAddItemToCart($this->getCart()->getCartLength()))
759 {
760 // max cart length reached
761 $tmp['error'] = JText::sprintf('VAPCARTRECURITEMERR1', $tmp['date']);
762
763 $results[] = $tmp;
764
765 // go to the next date
766 continue;
767 }
768
769 $data['checkin'] = $date;
770
771 // try to add the item into the cart
772 $item = $this->addItem($data);
773
774 if (!$item)
775 {
776 // get registered error message
777 $error = $this->getError($index = null, $string = true);
778
779 if ($error == JText::translate('VAPCARTITEMADDERR2'))
780 {
781 // item already in cart
782 $tmp['error'] = JText::sprintf('VAPCARTRECURITEMERR2', $tmp['date']);
783 }
784 else if ($error == JText::translate('VAPCARTITEMADDERR3'))
785 {
786 // item no longer available
787 $tmp['error'] = JText::sprintf('VAPCARTRECURITEMERR3', $tmp['date']);
788 }
789 else
790 {
791 // use the specified error
792 $tmp['error'] = $error;
793 }
794
795 $results[] = $tmp;
796
797 // go to the next date
798 continue;
799 }
800
801 // item added successfully
802 $tmp['status'] = 1;
803 $tmp['item'] = $item->toArray();
804
805 // register result
806 $results[] = $tmp;
807 }
808
809 return $results;
810 }
811
812 /**
813 * Validates the items inside the cart and makes sure they are
814 * still available for booking, by checking whether the selected
815 * slots have been already booked by other customers or by checkin
816 * whether the selected check-in is still in the future.
817 *
818 * @param array &$errors An argument to be passed as reference,
819 * which will be filled with all the fetched
820 * error messages.
821 * @param array &$employees An array containing all the employees
822 * that have been assigned to the appointments
823 * registered into the cart.
824 *
825 * @return boolean True on success, false in case of invalid items.
826 */
827 public function checkIntegrity(&$errors = null, &$employees = array())
828 {
829 // get cart handler
830 $cart = $this->getCart();
831
832 // get reservation model
833 $model = JModelVAP::getInstance('reservation');
834
835 $errors = array();
836
837 // temporary flag used to check whether the same employee has been already assigned
838 // to a different booking at the same date and time
839 $lookup = array();
840
841 foreach ($cart->getItemsList() as $k => $item)
842 {
843 // get item data
844 $data = $item->toArray();
845
846 // replicate check-in into a different attribute that will be used
847 // by the reservation model to check the availability (UTC)
848 $data['checkin_ts'] = $data['checkin'];
849
850 if (isset($lookup[$data['checkin_ts']]))
851 {
852 /**
853 * When the cart allows the selection of concurrent check-ins, we need
854 * to make sure that the appointments at the same date time are not
855 * assigned to the same employee, causing an unexpected overbooking.
856 * For this reason, we need to track all the employees that have been
857 * currently assigned and for which dates. This way, we are able to
858 * avoid validating certain employees in case of colliding slots.
859 *
860 * Instruct the availability search to skip all the employees that have
861 * been currently assigned for the same date and time.
862 *
863 * @since 1.7
864 */
865 $data['exclude_employees'] = $lookup[$data['checkin_ts']];
866 }
867
868 // fetch overrides for the selected service-employee relation
869 $service = JModelVAP::getInstance('serempassoc')->getOverrides($data['id_service'], $data['id_employee']);
870
871 if ($service)
872 {
873 // always register the sleep time to estimate a correct availability
874 $data['sleep'] = $service->sleep;
875 }
876
877 // check whether the item is still available
878 $avail = $model->isAvailable($data);
879
880 if ($avail)
881 {
882 // assign matching employee
883 $employees[$k] = $avail === true ? $item->getEmployeeID() : (int) $avail;
884
885 if (!isset($lookup[$data['checkin_ts']]))
886 {
887 // create check-in lookup
888 $lookup[$data['checkin_ts']] = array();
889 }
890
891 // assign employee to check-in time
892 $lookup[$data['checkin_ts']][] = $employees[$k];
893 }
894 else
895 {
896 // register error
897 $errors[] = array(
898 'item' => $item,
899 'reason' => $model->getError($index = null, $string = true),
900 );
901
902 // remove item from the list
903 $cart->removeItem($item->getServiceID(), $item->getEmployeeID(), $item->getCheckinDate());
904 }
905 }
906
907 if (!$errors)
908 {
909 // no faced errors
910 return true;
911 }
912
913 // revalidate coupon code
914 $this->revalidateCoupon();
915
916 // save changes
917 $cart->store();
918
919 // return the list of invalid items
920 return false;
921 }
922
923 /**
924 * Helper method used to redeem the specified coupon code.
925 *
926 * @param mixed $coupon Either the coupon details or its code.
927 *
928 * @return boolean True on success, false otherwise.
929 */
930 public function redeemCoupon($coupon)
931 {
932 if (empty($coupon))
933 {
934 // coupon code not specified
935 $this->setError(JText::translate('VAPCOUPONNOTVALID'));
936 return false;
937 }
938
939 if (is_string($coupon))
940 {
941 // get model to load coupon details
942 $couponModel = JModelVAP::getInstance('coupon');
943 $coupon = $couponModel->getCoupon($coupon);
944
945 if (!$coupon)
946 {
947 // coupon not found in database
948 $this->setError(JText::translate('VAPCOUPONNOTVALID'));
949 return false;
950 }
951 }
952
953 // get cart instance
954 $cart = $this->getCart();
955
956 // validate the coupon code
957 if (!VikAppointments::validateCoupon($coupon, $cart))
958 {
959 // cannot apply the coupon code
960 $this->setError(JText::translate('VAPCOUPONNOTVALID'));
961 return false;
962 }
963
964 // coupon valid, create discount object
965 $discount = new VAPCartDiscount('coupon', $coupon->value, $coupon->percentot == 1);
966 // register coupon data for later use
967 $discount->set('couponData', $coupon);
968
969 // apply discount, by replacing any other coupon discount previously set
970 $cart->setDiscount($discount);
971 // commit cart changes
972 $cart->store();
973
974 return true;
975 }
976
977 /**
978 * Revalidates the internal coupon code, since the cart might be no more
979 * compliant with the coupon restrictions after some changes.
980 *
981 * @param boolean $store True to commit the changes.
982 *
983 * @return boolean True in case of valid coupon, false otherwise.
984 */
985 public function revalidateCoupon($store = false)
986 {
987 // get cart instance
988 $cart = $this->getCart();
989 // get coupon discount, if any
990 $discount = $cart->getDiscount('coupon');
991
992 if (!$discount)
993 {
994 // coupon discount not set
995 return false;
996 }
997
998 // extract coupon data
999 $coupon = $discount->get('couponData');
1000
1001 // try to redeem the coupon code
1002 $res = $this->redeemCoupon($coupon);
1003
1004 if (!$res)
1005 {
1006 // coupon no more valid, unset it
1007 $cart->removeDiscount($discount);
1008
1009 if ($store)
1010 {
1011 // commit changes
1012 $cart->store();
1013 }
1014
1015 return false;
1016 }
1017
1018 // coupon still valid
1019 return true;
1020 }
1021
1022 /**
1023 * Helper method used to obtain an instance of the cart.
1024 *
1025 * @return VAPCart
1026 */
1027 public function getCart()
1028 {
1029 static $cart = null;
1030
1031 if (!$cart)
1032 {
1033 // load cart instance
1034 $cart = VAPCart::getInstance();
1035
1036 $config = VAPFactory::getConfig();
1037
1038 // set cart configuration
1039 $cart->setParams(array(
1040 VAPCart::CART_ENABLED => $config->getBool('enablecart'),
1041 VAPCart::MAX_SIZE => $config->getInt('maxcartsize'),
1042 VAPCart::ALLOW_SYNC => $config->getBool('cartallowsync'),
1043 ));
1044 }
1045
1046 return $cart;
1047 }
1048
1049 /**
1050 * Helper method used to make sure all the required options have been selected.
1051 *
1052 * @param integer $id_ser The service ID.
1053 * @param array $options An array containing the selected options.
1054 *
1055 * @return array The list of the required options that haven't been selected, otherwise false.
1056 */
1057 protected function validateOptions($id_ser, $options)
1058 {
1059 $dbo = JFactory::getDbo();
1060
1061 $q = $dbo->getQuery(true)
1062 ->select($dbo->qn('o.id'))
1063 ->from($dbo->qn('#__vikappointments_option', 'o'))
1064 ->leftjoin($dbo->qn('#__vikappointments_ser_opt_assoc', 'ao') . ' ON ' . $dbo->qn('o.id') . ' = ' . $dbo->qn('ao.id_option'))
1065 ->where(array(
1066 $dbo->qn('ao.id_service') . ' = ' . (int) $id_ser,
1067 $dbo->qn('o.required') . ' = 1',
1068 $dbo->qn('o.published') . ' = 1',
1069 ));
1070
1071 /**
1072 * Retrieve only the options that belong to the view
1073 * access level of the current user.
1074 *
1075 * @since 1.7.3
1076 */
1077 $levels = JFactory::getUser()->getAuthorisedViewLevels();
1078
1079 if ($levels)
1080 {
1081 $q->where($dbo->qn('o.level') . ' IN (' . implode(', ', $levels) . ')');
1082 }
1083
1084 if (count($options))
1085 {
1086 $options = array_map(function($op)
1087 {
1088 return (int) $op['id'];
1089 }, $options);
1090
1091 $q->where($dbo->qn('o.id') . ' NOT IN (' . implode(',', $options) . ')');
1092 }
1093
1094 $dbo->setQuery($q);
1095
1096 // In case the query returned some rows, they are the required options that the customer
1097 // haven't specified. False indicates that all the required options have been filled in.
1098 return $dbo->loadColumn() ?: false;
1099 }
1100
1101 /**
1102 * Checks whether the specified option can be actually purchased by looking
1103 * at the number of remaining units.
1104 *
1105 * @param VAPCartOption $option The option to check.
1106 *
1107 * @return bool True if available, false otherwise.
1108 *
1109 * @since 1.7.7
1110 */
1111 public function checkStockAvailability($option)
1112 {
1113 // make sure the remaining number of units actually available in stock is equal or lower than the specified quantity
1114 $remainingUnits = JModelVAP::getInstance('option')->getStock($option->getID(), $option->getVariationID());
1115
1116 if ($remainingUnits !== false && $remainingUnits - $option->getQuantity() < 0)
1117 {
1118 // not enough remaining units
1119 $this->setError(JText::plural('VAP_OPTION_STOCK_N_ERR', $remainingUnits));
1120 return false;
1121 }
1122
1123 return true;
1124 }
1125
1126 /**
1127 * Normalizes the stock of the items already in cart to prevent overbookings.
1128 *
1129 * @param VAPCart $cart The current cart instance.
1130 *
1131 * @return bool True whether something has changed, false otherwise.
1132 *
1133 * @since 1.7.7
1134 */
1135 public function normalizeStockAvailability($cart)
1136 {
1137 $changed = false;
1138 $remaining = [];
1139
1140 $optionModel = JModelVAP::getInstance('option');
1141
1142 foreach ($cart->getItemsList() as $item)
1143 {
1144 foreach ($item->getOptionsList() as $option)
1145 {
1146 // create lookup signature
1147 $sign = $option->getID() . '-' . max(0, (int) $option->getVariationID());
1148
1149 if (!isset($remaining[$sign]))
1150 {
1151 // calculate remaining units
1152 $remaining[$sign] = $optionModel->getStock($option->getID(), $option->getVariationID());
1153 }
1154
1155 if ($remaining[$sign] !== false && $remaining[$sign] < $option->getQuantity())
1156 {
1157 // not enough units, take only the remaining ones
1158 $item->removeOption($option->getID(), $option->getQuantity() - $remaining[$sign]);
1159 $changed = true;
1160 }
1161
1162 if ($remaining[$sign] !== false)
1163 {
1164 // subtract used units from the cache
1165 $remaining[$sign] = max(0, $remaining[$sign] - $option->getQuantity());
1166 }
1167 }
1168 }
1169
1170 return $changed;
1171 }
1172 }
1173