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 |