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 / models / specialrates.php
vikappointments / site / helpers / libraries / models Last commit date
conversion.php 3 days ago customer.php 3 days ago customfields.php 3 days ago index.html 3 days ago locations.php 3 days ago orderstatus.php 3 days ago restrictions.php 3 days ago specialrates.php 3 days ago statistics.php 3 days ago subscriptions.php 3 days ago
specialrates.php
599 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 defined('SPECIAL_RATES_DEBUG') or define('SPECIAL_RATES_DEBUG', 0);
15
16 /**
17 * VikAppointments special rates class handler.
18 *
19 * @since 1.6
20 */
21 abstract class VAPSpecialRates
22 {
23 /**
24 * List used to cache the base price of the services.
25 *
26 * @var array
27 */
28 protected static $baseCosts = array();
29
30 /**
31 * List used to cache the special rates found for the given service
32 * and employee relationships.
33 *
34 * @var array
35 */
36 protected static $rates = array();
37
38 /**
39 * Calculates the final rate depending on the selected service, employee,
40 * checkin timestamp and number of people.
41 *
42 * @param integer $id_service The service ID.
43 * @param integer $id_employee The employee ID.
44 * @param mixed $checkin Either a check-in timestamp or a date.
45 * @param integer $people The number of guests.
46 * @param array &$trace An array to keep track of the rates applied.
47 *
48 * @return float The final base cost.
49 *
50 * @uses getBaseCost()
51 * @uses getRatesList()
52 * @uses isCompatible()
53 */
54 public static function getRate($id_service, $id_employee = 0, $checkin = 0, $people = 1, ?array &$trace = null)
55 {
56 // obtain the base cost to use
57 $cost = static::getBaseCost($id_service, $id_employee);
58
59 if ($cost === false)
60 {
61 // the service doesn't exist, return a null cost
62 return 0;
63 }
64
65 $cost = (float) $cost;
66
67 if (!is_null($trace))
68 {
69 // add basecost to trace
70 $trace['basecost'] = $cost;
71 }
72
73 // get the full rates list compatible with the given service
74 $rates = static::getRatesList($id_service);
75
76 // iterate the rates found
77 foreach ($rates as $rate)
78 {
79 // check if the rate is compatible with the given parameters
80 if (static::isCompatible($rate, $checkin, $people, $trace))
81 {
82 // Increase the base cost with the rate charge.
83 // If the charge is negative, the base cost will be discounted.
84 $cost += $rate->charge;
85
86 if (!is_null($trace))
87 {
88 if (!isset($trace['rates']))
89 {
90 $trace['rates'] = array();
91 }
92
93 // add rate details to trace
94 $trace['rates'][] = $rate;
95 }
96 }
97 }
98
99 // avoid to return a negative value due to a bad configuration
100 return (float) max(array(0, $cost));
101 }
102
103 /**
104 * Returns the base cost of the given service-employee relationship.
105 *
106 * @param integer $id_service The service ID.
107 * @param integer $id_employee The employee ID.
108 *
109 * @return float The base cost if the service exists, otherwise false.
110 */
111 public static function getBaseCost($id_service, $id_employee = 0)
112 {
113 $sign = serialize(array($id_service, $id_employee));
114
115 // check if the base cost has been already cached
116 if (!isset(static::$baseCosts[$sign]))
117 {
118 $dbo = JFactory::getDbo();
119
120 // retrieve the base cost of the service (must be published)
121 $q = $dbo->getQuery(true)
122 ->select($dbo->qn('s.price'))
123 ->from($dbo->qn('#__vikappointments_service', 's'))
124 ->where($dbo->qn('s.published') . ' = 1')
125 ->where($dbo->qn('s.id') . ' = ' . (int) $id_service);
126
127 // if the employee is specified, we should check for a price override
128 if ($id_employee > 0)
129 {
130 $q->select($dbo->qn('a.rate'))
131 ->leftjoin($dbo->qn('#__vikappointments_ser_emp_assoc', 'a') . ' ON ' . $dbo->qn('a.id_service') . ' = ' . $dbo->qn('s.id'))
132 ->where($dbo->qn('a.id_employee') . ' = ' . (int) $id_employee);
133 }
134
135 $dbo->setQuery($q, 0, 1);
136 $service = $dbo->loadObject();
137
138 if (!$service)
139 {
140 // service not found, return false
141 return false;
142 }
143
144 // check for an override
145 if (isset($service->rate))
146 {
147 static::$baseCosts[$sign] = (float) $service->rate;
148 }
149 // otherwise use the base cost of the service
150 else
151 {
152 static::$baseCosts[$sign] = (float) $service->price;
153 }
154 }
155
156 return static::$baseCosts[$sign];
157 }
158
159 /**
160 * Returns the list of special rates compatible with the given service.
161 *
162 * @param integer $id_service The service ID.
163 *
164 * @return array The rates list.
165 */
166 public static function getRatesList($id_service)
167 {
168 // check if the rates list has been already cached
169 if (!isset(static::$rates[$id_service]))
170 {
171 $dbo = JFactory::getDbo();
172
173 /**
174 * This SELECT is able to retrieve the special rates in these 2 cases:
175 * - the given service is assigned to the rate
176 * - the rate doesn't own any service (in other words can be applied to any service)
177 *
178 *
179 * SELECT r.*
180 * FROM `#__vikappointments_special_rates` AS `r`
181 * LEFT JOIN `#__vikappointments_ser_rates_assoc` AS `a` ON `a`.`id_special_rate` = `r`.`id`
182 * WHERE
183 * (
184 * `a`.`id_service`=21 OR
185 * (
186 * SELECT COUNT(1)
187 * FROM `#__vikappointments_ser_rates_assoc` AS `ai`
188 * WHERE `a`.`id_special_rate` = `r`.`id`
189 * ) = 0
190 * ) AND `r`.published = 1
191 */
192
193 // inner query to search for rates with no service assigned
194 $no_service_query = $dbo->getQuery(true)
195 ->select('COUNT(1)')
196 ->from($dbo->qn('#__vikappointments_ser_rates_assoc', 'ai'))
197 ->where($dbo->qn('a.id_special_rate') . ' = ' . $dbo->qn('r.id'));
198
199 // main query
200 $q = $dbo->getQuery(true)
201 ->select('`r`.*')
202 ->from($dbo->qn('#__vikappointments_special_rates', 'r'))
203 ->leftjoin($dbo->qn('#__vikappointments_ser_rates_assoc', 'a') . ' ON ' . $dbo->qn('a.id_special_rate') . ' = ' . $dbo->qn('r.id'))
204 ->where($dbo->qn('a.id_service') . ' = ' . (int) $id_service)
205 ->orWhere("(" . $no_service_query . ") = 0")
206 ->andWhere($dbo->qn('r.published') . ' = 1');
207
208 $dbo->setQuery($q);
209
210 $rates = array();
211
212 foreach ($dbo->loadObjectList() as $rate)
213 {
214 // convert the week days (separated by a comma) into an array
215 $rate->weekdays = strlen($rate->weekdays) ? explode(',', $rate->weekdays) : array();
216 // convert the usergroups (separated by a comma) into an array
217 $rate->usergroups = strlen($rate->usergroups) ? explode(',', $rate->usergroups) : array();
218
219 /**
220 * Decode rate params.
221 *
222 * @since 1.6.2
223 */
224 $rate->params = $rate->params ? json_decode($rate->params) : new stdClass;
225
226 // push the rate in the list
227 $rates[] = $rate;
228 }
229
230 static::$rates[$id_service] = $rates;
231 }
232
233 return static::$rates[$id_service];
234 }
235
236 /**
237 * Checks if the parameters are compatible with the given rate.
238 *
239 * @param object $rate The rate object.
240 * @param mixed $checkin Either a check-in timestamp or a date.
241 * @param integer $people The number of guests.
242 * @param array &$trace An array to keep track of the rates applied.
243 *
244 * @return boolean True if compatible, otherwise false.
245 *
246 * @uses debugTrace()
247 */
248 public static function isCompatible($rate, $checkin, $people, ?array &$trace = null)
249 {
250 // make sure the checkin has been provided
251 if (!$checkin)
252 {
253 // keep failure in debug trace
254 static::debugTrace($trace, 'nocheckin', $rate, $checkin);
255
256 // no checkin provided
257 return false;
258 }
259
260 // compare the number of guests
261 if ($rate->people && $rate->people != $people)
262 {
263 // keep failure in debug trace
264 static::debugTrace($trace, 'people', $rate, $people);
265
266 // the people is defined in the rate but
267 // it is not equal to the given value
268 return false;
269 }
270
271 // handle check-in with JDate
272 $checkinDate = JFactory::getDate($checkin);
273 $checkin = $checkinDate->toSql();
274
275 // make sure the checkin is valid
276 if (!VAPDateHelper::isNull($rate->fromdate) && $rate->fromdate > $checkin)
277 {
278 // keep failure in debug trace
279 static::debugTrace($trace, 'fromdate', $rate, $checkin);
280
281 // the start publishing is higher than the specified checkin
282 return false;
283 }
284
285 // make sure the checkin is valid
286 if (!VAPDateHelper::isNull($rate->todate) && $rate->todate < $checkin)
287 {
288 // keep failure in debug trace
289 static::debugTrace($trace, 'todate', $rate, $checkin);
290
291 // the end publishing is lower than the specified checkin
292 return false;
293 }
294
295 // get checkin day of the week
296 $wd = $checkinDate->format('w');
297
298 // check the weekday
299 if ($rate->weekdays && !in_array($wd, $rate->weekdays))
300 {
301 // keep failure in debug trace
302 static::debugTrace($trace, 'weekdays', $rate, $wd);
303
304 // the weekdays list is not empty and doesn't
305 // contain the given day of the week
306 return false;
307 }
308
309 // make sure the checkin time is contained
310 // in the time range of the rate
311 if ($rate->fromtime < $rate->totime)
312 {
313 // Format time locally, because from time and to time
314 // provided by the special rate are fixed.
315 // Use UTC timezone to avoid altering the check-in time,
316 // since that is the offset assumed by the "date" helper.
317 $checkinTime = JHtml::fetch('date', $checkin, 'H:i', 'UTC');
318 // convert time to integer for easier comparison
319 $checkinTime = JHtml::fetch('vikappointments.time2min', $checkinTime);
320
321 // make sure the check-in stays between from time and to time
322 if ($checkinTime < $rate->fromtime || $rate->totime < $checkinTime)
323 {
324 // keep failure in debug trace
325 static::debugTrace($trace, 'time', $rate, $checkin);
326
327 // the checkin time is outside the bounds
328 return false;
329 }
330 }
331
332 // make sure the usergroup is supported by the rate
333 if (count($rate->usergroups))
334 {
335 // check if the usergroup was specified in the trace array
336 if (isset($trace['usergroup']))
337 {
338 $usergroup = array($trace['usergroup']);
339 }
340 // check if the user ID was specified in the trace
341 else if (isset($trace['id_user']))
342 {
343 $user = JUser::getInstance((int) $trace['id_user']);
344
345 $usergroup = array_values($user->groups);
346 }
347 // get usergroup from the current user
348 else
349 {
350 $user = JFactory::getUser();
351
352 $usergroup = array_values($user->groups);
353 }
354
355 // check if the intersection between the arrays returns
356 // at least an element (in common)
357 if (!array_intersect($usergroup, $rate->usergroups))
358 {
359 // keep failure in debug trace
360 static::debugTrace($trace, 'usergroup', $rate, $usergroup);
361
362 // the usergroup is not supported
363 return false;
364 }
365 }
366
367 // the rate is compatible
368 return true;
369 }
370
371 /**
372 * Returns a list of rates available for the specified service and checkin day.
373 *
374 * @param integer $id_service The service ID.
375 * @param mixed $checkin Either a check-in timestamp or a date.
376 *
377 * @return array The available rates.
378 *
379 * @uses getRatesList()
380 */
381 public static function getRatesOnDay($id_service, $checkin = 0)
382 {
383 $list = array();
384
385 // get the full rates list compatible with the given service
386 $rates = static::getRatesList($id_service);
387
388 // handle check-in with JDate
389 $checkinDate = JFactory::getDate($checkin);
390 $checkin = $checkinDate->toSql();
391
392 // get check-in day of the week
393 $wd = $checkinDate->format('w');
394
395 // iterate the rates found
396 foreach ($rates as $rate)
397 {
398 if (
399 // validate start publishing
400 (VAPDateHelper::isNull($rate->fromdate) || $rate->fromdate <= $checkin)
401 // validate end publishing
402 && (VAPDateHelper::isNull($rate->todate) || $rate->todate >= $checkin)
403 // validate day of the week
404 && (!$rate->weekdays || in_array($wd, $rate->weekdays))
405 ) {
406 // rate found, push it within the list
407 $list[] = $rate;
408 }
409
410 }
411
412 return $list;
413 }
414
415 /**
416 * Extracts the CSS class to be used from a list of special rates.
417 *
418 * @param array $rates A list of rates.
419 * @param string $caller The caller that requested the method.
420 *
421 * @return string The fetched class.
422 *
423 * @since 1.6.2
424 */
425 public static function extractClass(array $rates = array(), $caller = '')
426 {
427 $class = array();
428
429 // fetch caller
430 if (preg_match("/cal|calendar/i", $caller))
431 {
432 $caller = 'cal_';
433 }
434 else
435 {
436 $caller = '';
437 }
438
439 // iterate all rates
440 foreach ($rates as $trace)
441 {
442 if (!empty($trace->params->{$caller . 'class_sfx'}))
443 {
444 // include class suffix within the list
445 $class = array_merge($class, explode(' ', $trace->params->{$caller . 'class_sfx'}));
446 }
447
448 if (!empty($trace->params->{$caller . 'style_class'}))
449 {
450 // include style preset
451 $class[] = $trace->params->{$caller . 'style_class'};
452 }
453 }
454
455 // make array unique and join all classes
456 return $class ? ' ' . implode(' ', array_unique($class)) : '';
457 }
458
459 /**
460 * Tries to push the (failure) log within the debug attribute of the trace.
461 *
462 * @param array &$trace An array to keep track of the rates applied.
463 * @param string $id An identifier to build the log.
464 * @param object $rate The rate that is not compatible.
465 * @param mixed $target The value of the element that caused the failure.
466 *
467 * @return boolean True if added, otherwise false.
468 */
469 protected static function debugTrace(?array &$trace = null, $id = null, $rate = null, $target = null)
470 {
471 if (is_null($trace) || (!isset($trace['debug']) && !SPECIAL_RATES_DEBUG))
472 {
473 // we are here for 2 possible reasons:
474 // - trace var not provided
475 // - the trace doesn't need to register debug data
476 return false;
477 }
478
479 if (!isset($trace['debug']))
480 {
481 // the debug is forced by the global constant
482 $trace['debug'] = array();
483 }
484
485 $config = VAPFactory::getConfig();
486
487 switch ($id)
488 {
489 case 'nocheckin':
490 $log = sprintf(
491 'The checkin date and time was not provided: %d given.',
492 $target
493 );
494 break;
495
496 case 'people':
497 $log = sprintf(
498 'The number of people does not match: %d expected, %d given.',
499 $rate->people,
500 $target
501 );
502 break;
503
504 case 'fromdate':
505 $log = sprintf(
506 'The given date is previous than the start publishing: starts on %s, %s given.',
507 JHtml::fetch('date', $rate->fromdate, $config->get('dateformat') . ' ' . $config->get('timeformat')),
508 JHtml::fetch('date', $target, $config->get('dateformat') . ' ' . $config->get('timeformat'), 'UTC')
509 );
510 break;
511
512 case 'todate':
513 $log = sprintf(
514 'The given date is after the end publishing: ends on %s, %s given.',
515 JHtml::fetch('date', $rate->todate, $config->get('dateformat') . ' ' . $config->get('timeformat')),
516 JHtml::fetch('date', $target, $config->get('dateformat') . ' ' . $config->get('timeformat'), 'UTC')
517 );
518 break;
519
520 case 'weekdays':
521 $lookup = array(
522 JText::translate('SUN'),
523 JText::translate('MON'),
524 JText::translate('TUE'),
525 JText::translate('WED'),
526 JText::translate('THU'),
527 JText::translate('FRI'),
528 JText::translate('SAT'),
529 );
530
531 $allowed_days = array_map(function($day) use ($lookup)
532 {
533 return $lookup[$day];
534 }, $rate->weekdays);
535
536 $log = sprintf(
537 'The day of the week is not included: %s expected, %s given.',
538 implode(', ', $allowed_days),
539 $lookup[(int) $target]
540 );
541 break;
542
543 case 'time':
544 $flabel = JHtml::fetch('vikappointments.min2time', $rate->fromtime);
545 $tlabel = JHtml::fetch('vikappointments.min2time', $rate->totime);
546
547 $log = sprintf(
548 'The given time is not included between the range of the rate: %s expected, %s given.',
549 $flabel . '-' . $tlabel,
550 JHtml::fetch('date', $target, $config->get('timeformat'), 'UTC')
551 );
552 break;
553
554 case 'usergroup':
555 $lookup = JHelperUsergroups::getInstance()->getAll();
556
557 $allowed_groups = array_map(function($group) use ($lookup)
558 {
559 if (!isset($lookup[$group]))
560 {
561 return $group;
562 }
563
564 return $lookup[$group]->title;
565 }, $rate->usergroups);
566
567 $given_groups = array_map(function($group) use ($lookup)
568 {
569 if (!isset($lookup[$group]))
570 {
571 return $group;
572 }
573
574 return $lookup[$group]->title;
575 }, $target);
576
577 $log = sprintf(
578 'The specified usergroup is not included: %s expected, %s given.',
579 implode(', ', $allowed_groups),
580 implode(', ', $given_groups)
581 );
582 break;
583
584 default:
585 $log = 'Unknown reason.';
586 }
587
588 // push the log within the list
589 $trace['debug'][] = array(
590 'id' => $rate->id,
591 'name' => $rate->name,
592 'description' => $rate->description,
593 'error' => $log,
594 );
595
596 return true;
597 }
598 }
599