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 |