PluginProbe ʕ •ᴥ•ʔ
VikAppointments Services Booking Calendar / 1.2.21
VikAppointments Services Booking Calendar v1.2.21
1.2.21 1.2.20 trunk 1.2.17 1.2.18 1.2.19
vikappointments / site / helpers / libraries / cart / cart.php
vikappointments / site / helpers / libraries / cart Last commit date
cart.php 4 days ago discount.php 4 days ago index.html 4 days ago item.php 4 days ago option.php 4 days ago utils.php 4 days ago
cart.php
861 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.cart.item');
15 VAPLoader::import('libraries.cart.discount');
16
17 /**
18 * Class used to handle a cart to book the appointments.
19 *
20 * @since 1.6
21 */
22 class VAPCart implements JsonSerializable
23 {
24 /**
25 * The instance of the Cart.
26 * There should be only one cart instance for the whole session.
27 *
28 * @var VAPCart
29 *
30 * @since 1.6
31 */
32 protected static $instance = null;
33
34 /**
35 * The list containing the selected items.
36 *
37 * @var VAPCartItem[]
38 */
39 private $cart = array();
40
41 /**
42 * A list of applied discounts.
43 *
44 * @var VAPCartDiscount[]
45 * @since 1.7
46 */
47 private $discounts = array();
48
49 /**
50 * The configuration array.
51 *
52 * @var array
53 */
54 private $params = array(
55 'append' => true,
56 'maxsize' => self::UNLIMITED,
57 'allowsync' => true,
58 );
59
60 /**
61 * Returns the instance of the cart object, only creating it
62 * if doesn't exist yet.
63 *
64 * @param array $cart The array containing all the items to push.
65 * @param array $params The settings array.
66 *
67 * @return self A new instance.
68 *
69 * @since 1.6
70 */
71 public static function getInstance(array $cart = array(), array $params = array())
72 {
73 if (static::$instance === null)
74 {
75 // get cart from session
76 $session_cart = JFactory::getSession()->get(self::CART_SESSION_KEY, null);
77
78 if (empty($session_cart))
79 {
80 $cart = new static($cart, $params);
81 }
82 else
83 {
84 $cart = unserialize($session_cart);
85 }
86
87 static::$instance = $cart;
88 }
89
90 // always overwrite existing params
91 static::$instance->setParams($params);
92
93 return static::$instance;
94 }
95
96 /**
97 * Class constructor.
98 *
99 * @param array $cart The array containing all the items to push.
100 * @param array $params The settings array.
101 *
102 * @uses setParams()
103 */
104 public function __construct(array $cart = array(), array $params = array())
105 {
106 $this->cart = $cart;
107 $this->setParams($params);
108 }
109
110 /**
111 * Store this instance into the PHP session.
112 *
113 * @return self This object to support chaining.
114 *
115 * @since 1.6
116 */
117 public function store()
118 {
119 JFactory::getSession()->set(self::CART_SESSION_KEY, serialize($this));
120
121 return $this;
122 }
123
124 /**
125 * Sets the configuration of the cart.
126 *
127 * @param array $params The settings array.
128 *
129 * @return self This object to support chaining.
130 */
131 public function setParams(array $params = array())
132 {
133 foreach ($params as $k => $v)
134 {
135 $this->params[$k] = $v;
136 }
137
138 return $this;
139 }
140
141 /**
142 * Empties the items within the cart.
143 *
144 * @return self This object to support chaining.
145 */
146 public function emptyCart()
147 {
148 $this->cart = array();
149
150 // reset discounts too
151 $this->discounts = array();
152
153 return $this;
154 }
155
156 /**
157 * Checks if the cart is empty.
158 *
159 * @return boolean True if empty, false otherwise.
160 */
161 public function isEmpty()
162 {
163 return count($this->cart) == 0;
164 }
165
166 /**
167 * Balances the cart in order to empty the free slots
168 * created after removing one or more items.
169 *
170 * @return self This object to support chaining.
171 *
172 * @deprecated 1.8 Without replacement.
173 */
174 public function balance()
175 {
176 // do nothing, balance is automatically made every
177 // time an item gets removed
178
179 return $this;
180 }
181
182 /**
183 * Pushes a new item within the cart.
184 * This method checks if the item can be added as the cart
185 * may own an internal size limit.
186 *
187 * @param VAPCartItem $item The item to push.
188 *
189 * @return boolean True on success, false otherwise.
190 *
191 * @uses getCartLength()
192 * @uses indexOf()
193 * @uses emptyCart()
194 */
195 public function addItem(VAPCartItem $item)
196 {
197 if ($this->params['maxsize'] == -1 || $this->getCartLength() < $this->params['maxsize'] || !$this->params['append'])
198 {
199 // check whether the same appointment already exists
200 $index = $this->indexOf($item->getServiceID(), $item->getEmployeeID(), $item->getCheckinDate(), $item->getDuration());
201
202 if ($index == -1 || !$this->params['append'])
203 {
204 if (!$this->params['append'])
205 {
206 // cart system not supported, replace the existing item
207 // with the new one
208 $this->emptyCart();
209 }
210
211 $this->cart[] = $item;
212
213 return true;
214 }
215 }
216
217 return false;
218 }
219
220 /**
221 * Removes the item from the cart considering the specified arguments.
222 *
223 * @param integer $id The service ID.
224 * @param integer $id2 The employee ID.
225 * @param string $checkin The check-in date time (UTC).
226 *
227 * @return boolean True on success, false otherwise.
228 *
229 * @uses indexOf()
230 */
231 public function removeItem($id, $id2, $checkin)
232 {
233 // reset allow sync to retrieve the correct item
234 $tmp = $this->params['allowsync'];
235 $this->params['allowsync'] = true;
236
237 $index = $this->indexOf($id, $id2, $checkin);
238 $this->params['allowsync'] = $tmp;
239
240 if ($index != -1)
241 {
242 // remove item from cart
243 array_splice($this->cart, $index, 1);
244
245 return true;
246 }
247
248 return false;
249 }
250
251 /**
252 * Returns the index of the item that matches the specified arguments.
253 *
254 * @param integer $id_service The service ID.
255 * @param integer $id_employee The employee ID.
256 * @param string $checkin The check-in date time (UTC).
257 * @param integer $duration The duration used to calculate the ending delimiter
258 * to check if there is an intersection.
259 *
260 * @return integer The item index on success, -1 on failure.
261 *
262 * @uses getCartLength()
263 * @uses bounds()
264 */
265 public function indexOf($id_service, $id_employee, $checkin, $duration = 0)
266 {
267 $checkout = JFactory::getDate($checkin);
268 // re-format check-in date time for a correct comparison
269 $checkin = $checkout->format('Y-m-d H:i:s');
270 // Then create check-out date string.
271 // When not specified, use "5" as duration to avoid a failure due to "bounds" method.
272 $checkout->modify('+' . ($duration ? $duration : 5) . ' minutes');
273 $checkout = $checkout->format('Y-m-d H:i:s');
274
275 for ($i = 0; $i < $this->getCartLength(); $i++)
276 {
277 if ($this->params['allowsync'])
278 {
279 // check whether the services are matching
280 $same_service = $this->cart[$i]->getServiceID() == $id_service;
281 // check whether the employees are matching (add check to include different identifiers for employees not selected: <= 0)
282 $same_employee = $this->cart[$i]->getEmployeeID() == $id_employee || ($this->cart[$i]->getEmployeeID() <= 0 && $id_employee <= 0);
283
284 // check the exact item stored within the cart
285 if ($same_service && $same_employee && $this->cart[$i]->getCheckinDate() == $checkin)
286 {
287 return $i;
288 }
289 }
290 else
291 {
292 // look for any item that intersects the specified query
293 if ($this->bounds($this->cart[$i]->getCheckinDate(), $this->cart[$i]->getCheckoutDate(), $checkin, $checkout))
294 {
295 return $i;
296 }
297 }
298 }
299
300 return -1;
301 }
302
303 /**
304 * Checks if there is an intersection between the specified delimiters.
305 *
306 * @param string $start_a The first initial delimiter.
307 * @param string $end_a The first ending delimiter.
308 * @param string $start_b The second initial delimiter.
309 * @param string $end_b The second ending delimiter.
310 *
311 * @return boolean True if they intersect, false otherwise.
312 */
313 private function bounds($start_a, $end_a, $start_b, $end_b)
314 {
315 // IN_A <= IN_B AND IN_B < OUT_A
316 // IN_A < OUT_B AND OUT_B <= OUT_A
317 // IN_B < IN_A AND OUT_A < OUT_B
318 return ($start_a <= $start_b && $start_b < $end_a)
319 || ($start_a < $end_b && $end_b <= $end_a)
320 || ($start_b < $start_a && $end_a < $end_b);
321 }
322
323 /**
324 * Returns the total cost of the cart.
325 *
326 * @return float
327 */
328 public function getTotalCost()
329 {
330 $total = 0;
331
332 foreach ($this->cart as $i)
333 {
334 $total += $i->getTotalCost();
335 }
336
337 return $total;
338 }
339
340 /**
341 * Returns the total net of the cart.
342 *
343 * @return float
344 *
345 * @since 1.7
346 */
347 public function getTotalNet()
348 {
349 $this->prepareDiscounts();
350
351 $total = 0;
352
353 foreach ($this->cart as $i)
354 {
355 $total += $i->getTotalNet($this);
356 }
357
358 return $total;
359 }
360
361 /**
362 * Returns the total tax of the cart.
363 *
364 * @return float
365 *
366 * @since 1.7
367 */
368 public function getTotalTax()
369 {
370 $this->prepareDiscounts();
371
372 $total = 0;
373
374 foreach ($this->cart as $i)
375 {
376 $total += $i->getTotalTax($this);
377 }
378
379 return $total;
380 }
381
382 /**
383 * Returns the total gross of the cart.
384 *
385 * @return float
386 *
387 * @since 1.7
388 */
389 public function getTotalGross()
390 {
391 $this->prepareDiscounts();
392
393 $total = 0;
394
395 foreach ($this->cart as $i)
396 {
397 $total += $i->getTotalGross($this);
398 }
399
400 return $total;
401 }
402
403 /**
404 * Returns the total discount.
405 *
406 * @param array &$lookup A lookup used to track the applied discounts.
407 *
408 * @return float
409 *
410 * @since 1.7
411 */
412 public function getTotalDiscount(&$lookup = array())
413 {
414 $this->prepareDiscounts();
415
416 $total = 0;
417
418 foreach ($this->cart as $i)
419 {
420 // calculate the difference between the item full price
421 // and the discounted price, if any
422 $total += $i->getPrice() - $i->getDiscountedPrice($this, $lookup);
423
424 foreach ($i->getOptionsList() as $o)
425 {
426 $optPrice = $o->getTotalPrice();
427
428 if ($optPrice > 0)
429 {
430 // Calculate the difference between the option full price
431 // and the discounted price, if any. Ignore in case the
432 // option is a discount itself, because it is already
433 // considered by the item discounted price.
434 $total += $optPrice - $o->getDiscountedPrice($this, $lookup);
435 }
436 }
437 }
438
439 return round($total, 2);
440 }
441
442 /**
443 * Returns the totals per each registered item and option.
444 *
445 * @return array An array of discounts, matching the index
446 * of the related item.
447 *
448 * @since 1.7
449 */
450 public function getTotalsPerItem()
451 {
452 $this->prepareDiscounts();
453
454 $items = array();
455
456 $options = array();
457 // $options['id_user'] = JFactory::getUser()->id;
458
459 foreach ($this->cart as $i => $item)
460 {
461 $itemTotals = new stdClass;
462 // calculate original price
463 $itemTotals->priceBeforeDiscount = $item->getPrice();
464 // calculate final price per item and related discount
465 $itemTotals->price = $item->getDiscountedPrice($this);
466 $itemTotals->discount = $itemTotals->priceBeforeDiscount - $itemTotals->price;
467
468 $options['subject'] = 'service';
469
470 // re-calculate totals of discounted item
471 $tmp = VAPTaxFactory::calculate($item->getServiceID(), $itemTotals->price, $options);
472
473 // register new totals inside the object
474 foreach ($tmp as $k => $v)
475 {
476 $itemTotals->{$k} = $v;
477 }
478
479 $itemTotals->options = array();
480
481 $itemTotals->subdisc = $itemTotals->discount;
482 $itemTotals->subnet = $itemTotals->net;
483 $itemTotals->subtax = $itemTotals->tax;
484 $itemTotals->subgross = $itemTotals->gross;
485
486 // iterate internal options
487 foreach ($item->getOptionsList() as $option)
488 {
489 $optionTotals = new stdClass;
490 // calculate original price
491 $optionTotals->priceBeforeDiscount = $option->getTotalPrice();
492 // calculate final price per item and related discount
493 $optionTotals->price = $option->getDiscountedPrice($this);
494 $optionTotals->discount = $optionTotals->priceBeforeDiscount - $optionTotals->price;
495
496 $options['subject'] = 'option';
497
498 // re-calculate totals of discounted option
499 $tmp = VAPTaxFactory::calculate($option->getID(), $optionTotals->price, $options);
500
501 // register new totals inside the object
502 foreach ($tmp as $k => $v)
503 {
504 $optionTotals->{$k} = $v;
505 }
506
507 // do not recalculate totals in case the option is used
508 // to offer a discount, since it has been already applied
509 if ($optionTotals->gross > 0)
510 {
511 // increase item sub-totals
512 $itemTotals->subdisc += $optionTotals->discount;
513 $itemTotals->subnet += $optionTotals->net;
514 $itemTotals->subtax += $optionTotals->tax;
515 $itemTotals->subgross += $optionTotals->gross;
516 }
517
518 // register item option
519 $itemTotals->options[] = $optionTotals;
520 }
521
522 // register item
523 $items[] = $itemTotals;
524 }
525
526 return $items;
527 }
528
529 /**
530 * Returns the total discount per each registered offer.
531 *
532 * @return array A lookup of discounts, where the key is the
533 * title/ID and the value is the discount.
534 *
535 * @since 1.7
536 */
537 public function getTotalDiscountPerOffer()
538 {
539 // pass a junk variable to the method used to calculate the
540 // total discount per each offer
541 $this->getTotalDiscount($lookup);
542
543 $map = array();
544
545 // iterate all registered discounts
546 foreach ($this->getDiscounts() as $discount)
547 {
548 $id = $discount->getID();
549
550 if (!isset($lookup[$id]))
551 {
552 // discount not set, go ahead
553 continue;
554 }
555
556 // try to check whether the discount supports a readable title
557 $k = $discount->get('title');
558
559 if ($k)
560 {
561 // title given, try to translate it
562 $k = JText::translate($k);
563 }
564 else
565 {
566 // missing title, use the ID
567 $k = $id;
568 }
569
570 // register discount total
571 $map[$k] = round($lookup[$id], 2);
572 }
573
574 return $map;
575 }
576
577 /**
578 * Returns the item at the specified position.
579 *
580 * @param integer $index
581 *
582 * @return mixed The item if exists, null otherwise.
583 *
584 * @uses getCartLength()
585 */
586 public function getItemAt($index)
587 {
588 if ($index >= 0 && $index < $this->getCartLength())
589 {
590 return $this->cart[$index];
591 }
592
593 return null;
594 }
595
596 /**
597 * Returns the number of items within the cart.
598 * The list may contain also items that are no more
599 * active.
600 *
601 * @return integer
602 */
603 public function getCartLength()
604 {
605 return count($this->cart);
606 }
607
608 /**
609 * Returns the number of active items within the list.
610 *
611 * @return integer
612 *
613 * @deprecated 1.8 Use getCartLength() instead.
614 */
615 public function getCartRealLength()
616 {
617 return $this->getCartLength();
618 }
619
620 /**
621 * Returns a list containing all the active items.
622 *
623 * @return array
624 */
625 public function getItemsList()
626 {
627 return $this->cart;
628 }
629
630 /**
631 * Configures the discount objects before being used.
632 *
633 * @return self
634 *
635 * @since 1.7
636 */
637 protected function prepareDiscounts()
638 {
639 $count = 0;
640
641 // counts the total number of items that have a cost
642 foreach ($this->cart as $item)
643 {
644 if ($item->getPrice() > 0)
645 {
646 // item with cost, increase counter
647 $count++;
648 }
649
650 foreach ($item->getOptionsList() as $option)
651 {
652 if ($option->getPrice() > 0)
653 {
654 // option with cost, increase counter
655 $count++;
656 }
657 }
658 }
659
660 foreach ($this->discounts as $discount)
661 {
662 // reset internal index
663 $discount->set('count', 0);
664 // reset internal total discount
665 $discount->set('disctot', 0);
666 // set total number of items with cost
667 $discount->set('length', $count);
668 // register the total cost of the order
669 $discount->set('total', $this->getTotalCost());
670 }
671
672 return $this;
673 }
674
675 /**
676 * Registers a new discount within the cart.
677 *
678 * @param VAPCartDiscount $discount The discount to apply.
679 *
680 * @return self This object to support chaining.
681 *
682 * @since 1.7
683 */
684 public function addDiscount(VAPCartDiscount $discount)
685 {
686 // add discount element
687 $this->discounts[] = $discount;
688
689 return $this;
690 }
691
692 /**
693 * Removes a discount from the cart, if any.
694 *
695 * @param mixed $discount Either the discount ID or an object.
696 *
697 * @return mixed The deleted discount on success, false otherwise.
698 *
699 * @since 1.7
700 */
701 public function removeDiscount($discount)
702 {
703 foreach ($this->discounts as $i => $elem)
704 {
705 if ($elem === $discount || (is_scalar($discount) && $elem->getID() == $discount)
706 || ($discount instanceof VAPCartDiscount && $discount->getID() == $elem->getID()))
707 {
708 return array_splice($this->discounts, $i, 1);
709 }
710 }
711
712 return false;
713 }
714
715 /**
716 * Sets a discount within the cart. In case the same discount
717 * is already set into the cart, the old one will be replaced
718 * by the new one.
719 *
720 * @param VAPCartDiscount $discount The discount to apply.
721 *
722 * @return self This object to support chaining.
723 *
724 * @since 1.7
725 */
726 public function setDiscount(VAPCartDiscount $discount)
727 {
728 // remove discount first
729 $this->removeDiscount($discount);
730
731 // then add new discount element
732 $this->discounts[] = $discount;
733
734 return $this;
735 }
736
737 /**
738 * Returns the discount matching the specified code.
739 *
740 * @param mixed $discount Either the discount ID or an object.
741 *
742 * @return mixed The discount object on success, null otherwise.
743 *
744 * @since 1.7
745 */
746 public function getDiscount($discount)
747 {
748 foreach ($this->discounts as $i => $elem)
749 {
750 if ((is_scalar($discount) && $elem->getID() == $discount)
751 || ($discount instanceof VAPCartDiscount && $discount->getID() == $elem->getID()))
752 {
753 return $elem;
754 }
755 }
756
757 return null;
758 }
759
760 /**
761 * Returns the list containing all the discounts.
762 *
763 * @return array
764 *
765 * @since 1.7
766 */
767 public function getDiscounts()
768 {
769 return $this->discounts;
770 }
771
772 /**
773 * Returns the first available index to push a new item.
774 * Used to replace a unactive item with a new one.
775 *
776 * @return integer
777 *
778 * @uses getCartLength()
779 *
780 * @deprecated 1.8 Without replacement.
781 */
782 protected function getFirstAvailableIndex()
783 {
784 return $this->getCartLength();
785 }
786
787 /**
788 * Magic method used to return a string representation of this instance.
789 *
790 * @return string
791 */
792 public function __tostring()
793 {
794 return '<pre>' . print_r($this, true) . '</pre><br />Total Cost = ' . $this->getTotalCost();
795 }
796
797 /**
798 * Creates a standard object, containing all the supported properties,
799 * to be used when this class is passed to "json_encode()".
800 *
801 * @return object
802 *
803 * @since 1.7
804 *
805 * @see JsonSerializable
806 */
807 #[ReturnTypeWillChange]
808 public function jsonSerialize()
809 {
810 return [
811 'cart' => $this->cart,
812 'discounts' => $this->discounts,
813 'total' => $this->getTotalCost(),
814 'totalNet' => $this->getTotalNet(),
815 'totalTax' => $this->getTotalTax(),
816 'totalGross' => $this->getTotalGross(),
817 ];
818 }
819
820 /**
821 * Identifier used to make the size of the cart unlimited.
822 *
823 * @var integer
824 */
825 const UNLIMITED = -1;
826
827 /**
828 * Setting name used to check if the cart is enabled or not.
829 * In case the cart is disabled, before pushing a new item, the list
830 * will be always emptied.
831 *
832 * @var string
833 */
834 const CART_ENABLED = 'append';
835
836 /**
837 * Setting name used to retrieve the maximum number of items
838 * that can be added within the list.
839 *
840 * @var string
841 */
842 const MAX_SIZE = 'maxsize';
843
844 /**
845 * Setting name used to check if the cart can contain more than
846 * one appointment at the same date and time.
847 *
848 * @var string
849 */
850 const ALLOW_SYNC = 'allowsync';
851
852 /**
853 * CART_SESSION_KEY identifier for session key.
854 *
855 * @var string
856 *
857 * @since 1.6
858 */
859 const CART_SESSION_KEY = 'vapcartdev';
860 }
861