PluginProbe ʕ •ᴥ•ʔ
VikAppointments Services Booking Calendar / trunk
VikAppointments Services Booking Calendar vtrunk
trunk 1.2.17 1.2.18 1.2.19
vikappointments / admin / models / reservation.php
vikappointments / admin / models Last commit date
apiban.php 4 years ago apilog.php 2 years ago apiplugin.php 4 years ago apiuser.php 2 years ago apiuseroptions.php 2 years ago backup.php 4 months ago caldays.php 1 month ago calendar.php 1 month ago city.php 4 years ago closure.php 1 month ago configapp.php 4 years ago configcldays.php 4 years ago configcron.php 4 years ago configemp.php 4 years ago configsmsapi.php 4 years ago configuration.php 4 months ago conversion.php 4 years ago country.php 2 years ago coupon.php 2 years ago couponemployee.php 4 years ago coupongroup.php 2 years ago couponservice.php 4 years ago cronjob.php 2 years ago cronjoblog.php 4 years ago customer.php 4 months ago customf.php 2 years ago customfservice.php 4 years ago customizer.php 4 years ago empgroup.php 2 years ago employee.php 2 years ago empsettings.php 4 years ago file.php 4 years ago findreservation.php 1 month ago group.php 2 years ago import.php 4 years ago index.html 4 years ago invoice.php 1 month ago langcustomf.php 4 years ago langempgroup.php 4 years ago langemployee.php 4 years ago langgroup.php 4 years ago langmedia.php 4 years ago langoption.php 2 years ago langoptiongroup.php 4 years ago langoptionvar.php 4 years ago langpackage.php 4 years ago langpackgroup.php 4 years ago langpayment.php 4 years ago langservice.php 4 years ago langstatuscode.php 4 years ago langsubscr.php 4 years ago langtax.php 2 years ago langtaxrule.php 4 years ago location.php 2 years ago mailtext.php 2 years ago makerecurrence.php 1 month ago media.php 2 years ago multiorder.php 1 month ago option.php 1 year ago optiongroup.php 2 years ago optionvar.php 1 year ago orderstatus.php 2 years ago package.php 2 years ago packageservice.php 4 years ago packgroup.php 2 years ago packorder.php 2 years ago packorderitem.php 1 month ago payment.php 2 years ago rate.php 2 years ago reportsemp.php 4 months ago reportsser.php 4 months ago reservation.php 1 month ago resoptassoc.php 2 years ago restriction.php 2 years ago review.php 3 years ago serempassoc.php 1 year ago seroptassoc.php 4 years ago serrateassoc.php 4 years ago serrestrassoc.php 4 years ago service.php 2 years ago state.php 2 years ago statswidget.php 2 years ago statuscode.php 2 years ago subscription.php 1 month ago subscrorder.php 1 month ago tag.php 4 years ago tax.php 2 years ago taxrule.php 2 years ago updateprogram.php 4 years ago usernote.php 2 years ago waitinglist.php 1 month ago webhook.php 1 year ago worktime.php 1 month ago
reservation.php
1615 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
16 /**
17 * VikAppointments appointment model.
18 *
19 * @since 1.7
20 */
21 class VikAppointmentsModelReservation extends JModelVAP
22 {
23 /**
24 * Basic save implementation.
25 *
26 * @param mixed $data Either an array or an object of data to save.
27 *
28 * @return mixed The ID of the record on success, false otherwise.
29 */
30 public function save($data)
31 {
32 $dbo = JFactory::getDbo();
33 $app = JFactory::getApplication();
34 $table = $this->getTable();
35
36 $data = (array) $data;
37
38 if (empty($data['id']) && !empty($data['icaluid']))
39 {
40 // search reservation by iCal UID
41 $tmp = $this->getItem(array('icaluid' => $data['icaluid']));
42
43 if ($tmp)
44 {
45 // reservation found, do update
46 $data['id'] = $tmp->id;
47 }
48 }
49
50 if (!empty($data['validate_availability']))
51 {
52 // validate reservation availability
53 if (!$this->isAvailable($data))
54 {
55 // The selected slot doesn't seem to be available...
56 // Register data within the user state before aborting.
57 $app->setUserState('vap.reservation.data', $data);
58
59 return false;
60 }
61 }
62
63 if (!empty($data['id']))
64 {
65 // register current datetime as modified date, if not specified
66 if (!isset($data['modifiedon']))
67 {
68 $data['modifiedon'] = JFactory::getDate()->toSql();
69 }
70
71 // load order details
72 $table->load($data['id']);
73
74 // increase number of updates made
75 $data['sequence'] = $table->sequence + 1;
76 }
77
78 /**
79 * When fetching the statistics, we may have to convert the check-in time to
80 * the offset of the assigned employee. Since SQL engines do not provide default
81 * tools, we need to always keep up-to-date the offset of the check-in in order
82 * to adjust the dates at runtime without having to care of DST issues.
83 *
84 * For this reason, every time the check-in and the employee are provided, we
85 * have to refresh the timezone offset of the check-in.
86 */
87 if (!empty($data['id_employee']) && !empty($data['checkin_ts']) && !VAPDateHelper::isNull($data['checkin_ts']))
88 {
89 // get employee timezone
90 $tz = JModelVAP::getInstance('employee')->getTimezone($data['id_employee']);
91 // create check-in date
92 $checkin = JFactory::getDate($data['checkin_ts'], $tz);
93 // get timezone offset for the selected check-in date time
94 $data['tz_offset'] = $checkin->format('P', $local = true);
95 }
96
97 // get reservation-option model
98 $model = JModelVAP::getInstance('resoptassoc');
99
100 if (!empty($data['deletedOptions']) && !isset($data['discount']))
101 {
102 // get total discount of items to remove
103 $discount = $model->getTotalDiscount($data['deletedOptions']);
104
105 if ($discount > 0)
106 {
107 // subtract discount of items to remove from the total one
108 $data['discount'] = max(array(0, $table->discount - $discount));
109 }
110 }
111
112 if (empty($data['id']) && !isset($data['view_emp']) && !empty($data['id_service']))
113 {
114 // use only positive values, so that we can avoid a query in case of ID equals to -1
115 $id_service = $data['id_service'] > 0 ? $data['id_service'] : 0;
116
117 // while creating a new record check whether we should display
118 // the assigned employee to the customer
119 $service = JModelVAP::getInstance('service')->getItem($id_service);
120
121 if ($service)
122 {
123 // display employee in case the service allows its selection
124 $data['view_emp'] = (int) $service->choose_emp;
125 }
126 }
127
128 // always recover the service sleep time (if not specified) while creating a new appointment
129 if (!isset($data['sleep']) && empty($data['id']) && !empty($data['id_service']) && !empty($data['id_employee']))
130 {
131 // load employee overrides
132 $service = JModelVAP::getInstance('serempassoc')->getOverrides($data['id_service'], $data['id_employee']);
133
134 if ($service)
135 {
136 // use default service sleep time
137 $data['sleep'] = $service->sleep;
138 }
139 }
140
141 if (empty($data['id']) && empty($data['status']))
142 {
143 // status not specified, use the default confirmed one
144 $data['status'] = JHtml::fetch('vaphtml.status.confirmed', 'appointments', 'code');
145 }
146
147 // get order statuses handler
148 $orderStatus = VAPOrderStatus::getInstance();
149
150 $prev_status = null;
151
152 if (!empty($data['status']) && !empty($data['id']))
153 {
154 // register previous order status
155 $prev_status = $orderStatus->getStatus($data['id']);
156 }
157
158 // attempt to save the reservation
159 $id = parent::save($data);
160
161 if (!$id)
162 {
163 // an error occurred, do not go ahead
164 return false;
165 }
166
167 // always clear order from cache after saving
168 VAPLoader::import('libraries.order.factory');
169 VAPOrderFactory::changed('appointment', $id);
170
171 if (empty($data['id']) && !isset($data['id_parent']))
172 {
173 // we are creating a new single reservation, so we need
174 // to update the record to link the parent id with the PK
175 $tmp = new stdClass;
176 $tmp->id = $id;
177 $tmp->id_parent = $id;
178
179 $dbo->updateObject('#__vikappointments_reservation', $tmp, 'id');
180 }
181
182 if (!empty($data['deletedOptions']))
183 {
184 // delete specified options, needed to properly
185 // apply discount calculation (if requested)
186 $model->delete($data['deletedOptions']);
187 }
188
189 if (!empty($data['options']))
190 {
191 foreach ((array) $data['options'] as $item)
192 {
193 // check if we are dealing with a JSON object
194 $item = is_string($item) ? json_decode($item, true) : (array) $item;
195 // make relation with saved order
196 $item['id_reservation'] = $id;
197
198 // save item
199 $model->save($item);
200 }
201 }
202
203 // Check whether the status has changed.
204 // Create a new status record also for new reservations
205 if (!empty($data['status']) && $data['status'] != $prev_status)
206 {
207 if (empty($data['status_comment']))
208 {
209 // use default status comment
210 $data['status_comment'] = 'VAP_STATUS_CHANGED_ON_MANAGE';
211 }
212
213 // track status change
214 $orderStatus->keepTrack($data['status'], $id, $data['status_comment']);
215
216 // check if we have an existing parent order
217 if ($table->id && $table->id_parent <= 0)
218 {
219 // get multi-order model
220 $multiOrderModel = JModelVAP::getInstance('multiorder');
221
222 // iterate orders found
223 foreach ($multiOrderModel->getChildren($table->id, 'id') as $order_id)
224 {
225 // prepare data to save for child record
226 $childData = array(
227 'id' => (int) $order_id,
228 'status' => $data['status'],
229 'status_comment' => $data['status_comment'],
230 );
231
232 // save on cascade
233 $this->save($childData);
234 }
235 }
236 else
237 {
238 // check whether the order was previously approved
239 $was_approved = JHtml::fetch('vaphtml.status.isapproved', 'appointments', $prev_status);
240 // check whether the order has been cancelled
241 $is_now_cancelled = JHtml::fetch('vaphtml.status.iscancelled', 'appointments', $data['status']);
242
243 if ($is_now_cancelled && $was_approved)
244 {
245 /**
246 * Try to unredeem a package if the order has been cancelled.
247 * The service cost must be zero too in order to prove that a package was redeemed.
248 *
249 * @since 1.6.3
250 */
251 if ($table->service_price == 0)
252 {
253 // get package model
254 $package = JModelVAP::getInstance('packorder');
255
256 // unredeem packages
257 $unredeemed = $package->usePackages($data['id'], $increase = false);
258
259 if ($unredeemed)
260 {
261 if ($app->isClient('administrator'))
262 {
263 // message for back-end
264 $app->enqueueMessage(JText::sprintf('VAPORDERUNREDEEMEDPACKS', $unredeemed));
265 }
266 else
267 {
268 // message for front-end
269 $app->enqueueMessage(JText::translate('VAPRESTOREPACKSONCANCEL'), 'notice');
270 }
271 }
272 }
273
274 // Check whether the appointment was paid and it is now cancelled.
275 // In case of a multi-order, the total amount will be summed recursively by each
276 // appointment assigned to the order, since the prices totals are proportional.
277 if ($table->id_user > 0 && $table->total_cost && $is_now_cancelled && JHtml::fetch('vaphtml.status.ispaid', 'appointments', $prev_status))
278 {
279 // Remove the payment charge from the total paid.
280 // Ignore the charge if it is a discount.
281 $credit = $table->total_cost - max(array($table->payment_charge + $table->payment_tax, 0));
282
283 // increase user credit by the amount paid, if any
284 JModelVAP::getInstance('customer')->addCredit($table->id_user, $credit);
285 }
286 }
287 }
288 }
289
290 // check whether we should apply or delete a discount
291 if (!empty($data['add_discount']))
292 {
293 $this->addDiscount($id, $data['add_discount']);
294 }
295 else if (!empty($data['remove_discount']))
296 {
297 $this->removeDiscount($id);
298 }
299
300 if (!empty($data['notifycust']))
301 {
302 // define options
303 $options = array(
304 'id' => isset($data['mail_custom_text']) ? $data['mail_custom_text'] : null,
305 'default' => isset($data['exclude_default_mail_texts']) ? !$data['exclude_default_mail_texts'] : null,
306 );
307
308 // send e-mail notification to customer
309 $this->sendEmailNotification($id, $options);
310 }
311
312 if (!empty($data['notifyemp']))
313 {
314 // send e-mail notification to employee
315 $this->sendEmailNotification($id, array('client' => 'employee'));
316 }
317
318 if (!empty($data['notifywl']) && !empty($data['status']))
319 {
320 // check whether we have a cancelled status
321 if (JHtml::fetch('vaphtml.status.iscancelled', 'appointments', $data['status']))
322 {
323 // process waiting list queue
324 JModelVAP::getInstance('waitinglist')->notify($id);
325 }
326 }
327
328 if (!empty($data['notes']))
329 {
330 // create new user notes for this appointment
331 JModelVAP::getInstance('usernote')->save(array(
332 'group' => 'appointments',
333 'id_parent' => $id,
334 'content' => $data['notes'],
335 'id' => isset($data['id_notes']) ? (int) $data['id_notes'] : 0,
336 ));
337 }
338
339 // check if we have an existing child appointment
340 if ($table->id && $this->isChildAppointment($table))
341 {
342 // check whether the total cost has changed
343 if (isset($data['total_cost']) && $data['total_cost'] != $table->total_cost)
344 {
345 // get multi-order model
346 $multiOrderModel = JModelVAP::getInstance('multiorder');
347 // recalculate totals of parent order
348 $multiOrderModel->recalculateTotals($table->id_parent);
349
350 // This may not the best solution because it creates a structure
351 // similar to the circular dependency pattern by creating a link
352 // between the reservation model and the multi-order model.
353 // However, I have to say that this seems to be extremely effective.
354 // The workflow will be observed carefully.
355 }
356 }
357
358 // prepare event data
359 $is_new = empty($data['id']);
360 $data['id'] = $id;
361
362 /**
363 * Trigger event to allow the plugins to make something after saving
364 * an appointment into the database. Fires once all the details of
365 * the appointment has been saved.
366 *
367 * @param array $args The saved record.
368 * @param boolean $is_new True if the record was inserted.
369 * @param JModel $model The model instance.
370 *
371 * @return void
372 *
373 * @since 1.7
374 */
375 VAPFactory::getEventDispatcher()->trigger('onAfterSaveReservationLate', array($data, $is_new, $this));
376
377 return $id;
378 }
379
380 /**
381 * Extend duplicate implementation to clone any related records
382 * stored within a separated table.
383 *
384 * @param mixed $ids Either the record ID or a list of records.
385 * @param mixed $src Specifies some values to be used while duplicating.
386 * @param array $ignore A list of columns to skip.
387 *
388 * @return mixed The ID of the records on success, false otherwise.
389 */
390 public function duplicate($ids, $src = array(), $ignore = array())
391 {
392 $new_ids = array();
393
394 // defined default columns that should never be copied
395 $ignore[] = 'sid';
396 $ignore[] = 'conf_key';
397 $ignore[] = 'id_parent';
398 $ignore[] = 'createdon';
399 $ignore[] = 'createdby';
400 $ignore[] = 'log';
401 $ignore[] = 'cc_data';
402 $ignore[] = 'payment_attempt';
403 $ignore[] = 'conversion';
404
405 $dbo = JFactory::getDbo();
406
407 // get reservation options model
408 $optModel = JModelVAP::getInstance('resoptassoc');
409
410 foreach ($ids as $id_reservation)
411 {
412 // start by duplicating the whole record
413 $new_id = parent::duplicate($id_reservation, $src, $ignore);
414
415 if ($new_id)
416 {
417 $new_id = array_shift($new_id);
418
419 // register copied
420 $new_ids[] = $new_id;
421
422 // load any assigned option
423 $q = $dbo->getQuery(true)
424 ->select($dbo->qn('id'))
425 ->from($dbo->qn('#__vikappointments_res_opt_assoc'))
426 ->where($dbo->qn('id_reservation') . ' = ' . (int) $id_reservation);
427
428 $dbo->setQuery($q);
429
430 if ($duplicate = $dbo->loadColumn())
431 {
432 $opt_data = array();
433 $opt_data['id_reservation'] = $new_id;
434
435 // duplicate options by using the new reservation ID
436 $optModel->duplicate($duplicate, $opt_data);
437 }
438 }
439 }
440
441 return $new_ids;
442 }
443
444 /**
445 * Extend delete implementation to delete any related records
446 * stored within a separated table.
447 *
448 * @param mixed $ids Either the record ID or a list of records.
449 *
450 * @return boolean True on success, false otherwise.
451 */
452 public function delete($ids)
453 {
454 // only int values are accepted
455 $ids = array_map('intval', (array) $ids);
456
457 $dbo = JFactory::getDbo();
458
459 $q = $dbo->getQuery(true)
460 ->select($dbo->qn('id'))
461 ->from($dbo->qn('#__vikappointments_reservation'))
462 ->where(array(
463 $dbo->qn('id_parent') . ' IN (' . implode(',', $ids) . ')',
464 $dbo->qn('id') . ' <> ' . $dbo->qn('id_parent'),
465 ), 'AND');
466
467 $dbo->setQuery($q);
468
469 // merge children with specified IDS list
470 $ids = array_merge($ids, array_map('intval', $dbo->loadColumn()));
471
472 // invoke parent first
473 if (!parent::delete($ids))
474 {
475 // nothing to delete
476 return false;
477 }
478
479 // load any reservation-option relation
480 $q = $dbo->getQuery(true)
481 ->select($dbo->qn('id'))
482 ->from($dbo->qn('#__vikappointments_res_opt_assoc'))
483 ->where($dbo->qn('id_reservation') . ' IN (' . implode(',', $ids) . ')' );
484
485 $dbo->setQuery($q);
486
487 if ($assoc_ids = $dbo->loadColumn())
488 {
489 // get reservation-option model
490 $model = JModelVAP::getInstance('resoptassoc');
491 // delete relations
492 $model->delete($assoc_ids);
493 }
494
495 // load any assigned order statuses
496 $q = $dbo->getQuery(true)
497 ->select($dbo->qn('id'))
498 ->from($dbo->qn('#__vikappointments_order_status'))
499 ->where($dbo->qn('type') . ' = ' . $dbo->q('reservation'))
500 ->where($dbo->qn('id_order') . ' IN (' . implode(',', $ids) . ')' );
501
502 $dbo->setQuery($q);
503
504 if ($assoc_ids = $dbo->loadColumn())
505 {
506 // get order status model
507 $model = JModelVAP::getInstance('orderstatus');
508 // delete relations
509 $model->delete($assoc_ids);
510 }
511
512 // load any assigned notes
513 $q = $dbo->getQuery(true)
514 ->select($dbo->qn('id'))
515 ->from($dbo->qn('#__vikappointments_user_notes'))
516 ->where($dbo->qn('group') . ' = ' . $dbo->q('appointments'))
517 ->where($dbo->qn('id_parent') . ' IN (' . implode(',', $ids) . ')' );
518
519 $dbo->setQuery($q);
520
521 if ($note_ids = $dbo->loadColumn())
522 {
523 // get user notes model
524 $model = JModelVAP::getInstance('usernote');
525 // delete records
526 $model->delete($note_ids);
527 }
528
529 return true;
530 }
531
532 /**
533 * Returns a list of appointments that intersects the specified date time.
534 *
535 * @param string $datetime The date time to look for.
536 * @param integer $id_emp An optional employee ID.
537 *
538 * @return array A list of appointments.
539 */
540 public function getAppointmentsAt($datetime, $id_emp = 0)
541 {
542 $dbo = JFactory::getDbo();
543
544 // get employee timezone
545 $employee_tz = JModelVAP::getInstance('employee')->getTimezone($id_emp);
546
547 // create date instance and assume it refers to the
548 // timezone of the selected employee (or global one)
549 $date = JFactory::getDate($datetime, $employee_tz);
550
551 $q = $dbo->getQuery(true);
552
553 // select all reservation columns
554 $q->select('r.*');
555 $q->from($dbo->qn('#__vikappointments_reservation', 'r'));
556
557 // select service name
558 $q->select($dbo->qn('s.name', 'service_name'));
559 $q->leftjoin($dbo->qn('#__vikappointments_service', 's') . ' ON ' . $dbo->qn('s.id') . ' = ' . $dbo->qn('r.id_service'));
560
561 if ($id_emp)
562 {
563 // filter by employee
564 $q->where($dbo->qn('r.id_employee') . ' = ' . (int) $id_emp);
565 }
566 else
567 {
568 $q->select($dbo->qn('e.nickname', 'employee_name'));
569 $q->leftjoin($dbo->qn('#__vikappointments_employee', 'e') . ' ON ' . $dbo->qn('e.id') . ' = ' . $dbo->qn('r.id_employee'));
570 }
571
572 // get any reserved codes
573 $reserved = JHtml::fetch('vaphtml.status.find', 'code', array('appointments' => 1, 'reserved' => 1));
574
575 if ($reserved)
576 {
577 // filter by reserved status
578 $q->where($dbo->qn('r.status') . ' IN (' . implode(',', array_map(array($dbo, 'q'), $reserved)) . ')');
579 }
580
581 // make sure the specified date stays between the reservation check-in and check-out
582 $q->where($dbo->qn('r.checkin_ts') . ' <= ' . $dbo->q($date->toSql()));
583 $q->where(sprintf(
584 'DATE_ADD(%s, INTERVAL (%s + %s) MINUTE) > %s',
585 $dbo->qn('r.checkin_ts'),
586 $dbo->qn('r.duration'),
587 $dbo->qn('r.sleep'),
588 $dbo->q($date->toSql())
589 ));
590
591 $dbo->setQuery($q);
592 return $dbo->loadObjectList();
593 }
594
595 /**
596 * Returns a list of appointments with check-in on the specified date.
597 *
598 * @param string $date The date to look for.
599 * @param integer $id_emp An optional employee ID.
600 *
601 * @return array A list of appointments.
602 */
603 public function getAppointmentsOn($date, $id_emp = 0)
604 {
605 $dbo = JFactory::getDbo();
606
607 // get employee timezone
608 $employee_tz = JModelVAP::getInstance('employee')->getTimezone($id_emp);
609
610 // create dates range and assume they refer to the
611 // timezone of the selected employee (or global one)
612 $start = JFactory::getDate($date, $employee_tz);
613 $start->modify('00:00:00');
614
615 $end = JFactory::getDate($date, $employee_tz);
616 $end->modify('23:59:59');
617
618 $q = $dbo->getQuery(true);
619
620 // select all reservation columns
621 $q->select('r.*');
622 $q->from($dbo->qn('#__vikappointments_reservation', 'r'));
623
624 // select service name
625 $q->select($dbo->qn('s.name', 'service_name'));
626 $q->leftjoin($dbo->qn('#__vikappointments_service', 's') . ' ON ' . $dbo->qn('s.id') . ' = ' . $dbo->qn('r.id_service'));
627
628 if ($id_emp)
629 {
630 // filter by employee
631 $q->where($dbo->qn('r.id_employee') . ' = ' . (int) $id_emp);
632 }
633 else
634 {
635 $q->select($dbo->qn('e.nickname', 'employee_name'));
636 $q->leftjoin($dbo->qn('#__vikappointments_employee', 'e') . ' ON ' . $dbo->qn('e.id') . ' = ' . $dbo->qn('r.id_employee'));
637 }
638
639 // get any reserved codes
640 $reserved = JHtml::fetch('vaphtml.status.find', 'code', array('appointments' => 1, 'reserved' => 1));
641
642 if ($reserved)
643 {
644 // filter by reserved status
645 $q->where($dbo->qn('r.status') . ' IN (' . implode(',', array_map(array($dbo, 'q'), $reserved)) . ')');
646 }
647
648 // make sure the specified date stays between the reservation check-in and check-out
649 $q->where(sprintf('%s BETWEEN %s AND %s',
650 $dbo->qn('r.checkin_ts'),
651 $dbo->q($start->toSql()),
652 $dbo->q($end->toSql())
653 ));
654
655 // sort by check-in
656 $q->order($dbo->qn('r.checkin_ts') . ' ASC');
657
658 $dbo->setQuery($q);
659 return $dbo->loadObjectList();
660 }
661
662 /**
663 * Checks whether the specified appointment is a child.
664 *
665 * @param mixed $reservation Either a reservation ID or a table object.
666 *
667 * @return boolean True if a child, false otherwise.
668 */
669 public function isChildAppointment($reservation)
670 {
671 if (is_numeric($reservation))
672 {
673 $table = $this->getTable();
674 $table->load($reservation);
675 $reservation = $table;
676 }
677
678 // check if we have a child appointment assigned to a parent order
679 return $reservation->id_parent > 0 && $reservation->id_parent != $reservation->id;
680 }
681
682 /**
683 * Recalculates the totals of the specified reservation.
684 *
685 * @param object &$reservation The reservation details.
686 * @param mixed $service The service details. If not specified, it will
687 * be automatically loaded. It is also possible to
688 * pass a number to force the service price.
689 *
690 * @return void
691 */
692 public function recalculateTotals(&$reservation, $service = null)
693 {
694 $wasArray = false;
695
696 if (is_array($reservation))
697 {
698 // cast array to object and register reminder
699 $reservation = (object) $reservation;
700 $wasArray = true;
701 }
702
703 if (is_null($service))
704 {
705 // get service details
706 $service = JModelVAP::getInstance('serempassoc')->getOverrides($reservation->id_service, $reservation->id_employee);
707
708 if (!$service)
709 {
710 if ($wasArray)
711 {
712 // back to array
713 $reservation = (array) $reservation;
714 }
715
716 throw new Exception('Employee/service relation not found.', 404);
717 }
718 }
719
720 if (!isset($reservation->jid))
721 {
722 // use guest user group if not specified
723 $reservation->jid = 0;
724 }
725
726 // in case of a service, calculate the resulting price
727 if (is_object($service))
728 {
729 $checkin = JFactory::getDate($reservation->checkin_ts);
730
731 if (!empty($reservation->timezone))
732 {
733 // adjust to the specified timezone
734 $checkin->setTimezone(new DateTimeZone($reservation->timezone));
735 }
736 else
737 {
738 // adjust to the system timezone
739 $checkin->setTimezone(new DateTimeZone(JFactory::getApplication()->get('offset', 'UTC')));
740 }
741
742 /**
743 * Calculate the reservation cost by using the special rates.
744 *
745 * @since 1.6
746 */
747 $trace = array('id_user' => (int) $reservation->jid);
748
749 $service_price = $price = VAPSpecialRates::getRate($reservation->id_service, $reservation->id_employee, $checkin, $reservation->people, $trace);
750
751 if ($service->priceperpeople)
752 {
753 // multiply by the number of participants
754 $price *= $reservation->people;
755 }
756 }
757 else
758 {
759 // use the specified price
760 $price = (float) $service;
761 }
762
763 if (!empty($reservation->id_user))
764 {
765 // get details of the customer assigned to this reservation
766 $customer = VikAppointments::getCustomer($reservation->id_user);
767 // fetch check-in date time
768 $checkin = isset($reservation->checkin_ts) ? $reservation->checkin_ts : null;
769
770 if ($customer && $customer->isSubscribed($service->id, $checkin))
771 {
772 // subscribed customer, unset price
773 $price = 0;
774 }
775 }
776
777 // define default values
778 $reservation->service_gross = isset($reservation->service_gross) ? (float) $reservation->service_gross : 0;
779 $reservation->service_net = isset($reservation->service_net) ? (float) $reservation->service_net : 0;
780 $reservation->service_tax = isset($reservation->service_tax) ? (float) $reservation->service_tax : 0;
781 $reservation->total_cost = isset($reservation->total_cost) ? (float) $reservation->total_cost : 0;
782 $reservation->total_net = isset($reservation->total_net) ? (float) $reservation->total_net : 0;
783 $reservation->total_tax = isset($reservation->total_tax) ? (float) $reservation->total_tax : 0;
784
785 // subtract existing service totals (subtract at most a self unit to prevent negative values)
786 $reservation->total_cost -= min(array($reservation->service_gross, $reservation->total_cost));
787 $reservation->total_net -= min(array($reservation->service_net, $reservation->total_net));
788 $reservation->total_tax -= min(array($reservation->service_tax, $reservation->total_tax));
789
790 VAPLoader::import('libraries.tax.factory');
791
792 // prepare options for tax
793 $options = array();
794 $options['lang'] = isset($reservation->langtag) ? $reservation->langtag : null;
795 $options['subject'] = 'service';
796 $options['id_user'] = isset($reservation->id_user) ? (int) $reservation->id_user : 0;
797
798 // calculate taxes
799 $result = VAPTaxFactory::calculate($reservation->id_service, $price, $options);
800
801 // update totals with new calculated price
802 $reservation->service_price = $service_price;
803 $reservation->service_net = $result->net;
804 $reservation->service_tax = $result->tax;
805 $reservation->service_gross = $result->gross;
806 $reservation->tax_breakdown = json_encode($result->breakdown);
807
808 // sum new sub-totals to order totals
809 $reservation->total_cost += $reservation->service_gross;
810 $reservation->total_net += $reservation->service_net;
811 $reservation->total_tax += $reservation->service_tax;
812
813 if ($wasArray)
814 {
815 // back to array
816 $reservation = (array) $reservation;
817 }
818 }
819
820 /**
821 * Adds a discount to the specified reservation.
822 *
823 * @param integer $id The order ID.
824 * @param mixed $coupon Either a coupon code or an array/object
825 * containing its details.
826 *
827 * @return boolean True on success, false otherwise.
828 */
829 public function addDiscount($id, $coupon)
830 {
831 // get coupon model
832 $couponModel = JModelVAP::getInstance('coupon');
833
834 if (is_string($coupon))
835 {
836 // get coupon code details
837 $coupon = $couponModel->getCoupon($coupon);
838 }
839 else
840 {
841 // treat as object
842 $coupon = (object) $coupon;
843 }
844
845 // make sure we have a valid coupon code
846 if (!$coupon || !isset($coupon->value))
847 {
848 // invalid/missing coupon
849 $this->setError('Missing coupon code');
850
851 return false;
852 }
853
854 $dbo = JFactory::getDbo();
855
856 // load any children (options)
857 $q = $dbo->getQuery(true)
858 ->select($dbo->qn(array('id', 'id_option', 'inc_price')))
859 ->from($dbo->qn('#__vikappointments_res_opt_assoc'))
860 ->where($dbo->qn('id_reservation') . ' = ' . (int) $id)
861 ->where($dbo->qn('inc_price') . ' > 0');
862
863 $dbo->setQuery($q);
864 $items = $dbo->loadObjectList();
865
866 // load reservation details
867 $table = $this->getTable();
868 $table->load((int) $id);
869
870 // define options for tax calculation
871 $options = array(
872 'subject' => 'service',
873 'lang' => $table->langtag,
874 'id_user' => $table->id_user,
875 );
876
877 $total_c = 0;
878
879 // calculate total cost
880 foreach ($items as $item)
881 {
882 $total_c += (float) $item->inc_price;
883 }
884
885 // prepare order data
886 $orderData = array(
887 'id' => $table->id,
888 'total_cost' => $table->payment_charge + $table->payment_tax,
889 'total_net' => 0,
890 'total_tax' => $table->payment_tax,
891 'discount' => 0,
892 'coupon' => '',
893 'options' => array(),
894 );
895
896 VAPLoader::import('libraries.tax.factory');
897
898 if ($table->service_price > 0)
899 {
900 /**
901 * Multiply the service price by the number of selected attendees.
902 *
903 * @since 1.7.4 Do not multiply in case the service has the "Price per Person"
904 * setting turned off.
905 */
906 if ((bool) JModelVAP::getInstance('service')->getItem($table->id_service, $blank = true)->priceperpeople)
907 {
908 $table->service_price *= $table->people;
909 }
910
911 // include service within the total number
912 // of items that can be discounted
913 $total_c += $table->service_price;
914
915 // recalculate service
916 $cost_with_disc = $table->service_price;
917
918 if (empty($coupon->percentot) || $coupon->percentot == 1)
919 {
920 // percentage discount
921 $disc_val = round($cost_with_disc * $coupon->value / 100, 2);
922 }
923 else
924 {
925 // fixed discount, apply proportionally according to
926 // the total cost of all the items
927 $percentage = $cost_with_disc * 100 / $total_c;
928 $disc_val = round($coupon->value * $percentage / 100, 2);
929
930 // the discount cannot exceed the total price
931 $disc_val = min(array($table->service_price, $disc_val));
932 }
933
934 // save service discount
935 $orderData['service_discount'] = $disc_val;
936 // increase total discount
937 $orderData['discount'] += $orderData['service_discount'];
938
939 // subtract discount from service cost
940 $cost_with_disc -= $disc_val;
941
942 // recalculate totals
943 $totals = VAPTaxFactory::calculate($table->id_service, $cost_with_disc, $options);
944
945 // update service totals
946 $orderData['service_net'] = $totals->net;
947 $orderData['service_tax'] = $totals->tax;
948 $orderData['service_gross'] = $totals->gross;
949 $orderData['tax_breakdown'] = $totals->breakdown;
950
951 // update order totals
952 $orderData['total_net'] += $orderData['service_net'];
953 $orderData['total_tax'] += $orderData['service_tax'];
954 $orderData['total_cost'] += $orderData['service_gross'];
955 }
956
957 $options['subject'] = 'option';
958
959 // recalculate options
960 foreach ($items as $i => $item)
961 {
962 $cost_with_disc = $item->inc_price;
963
964 if (empty($coupon->percentot) || $coupon->percentot == 1)
965 {
966 // percentage discount
967 $disc_val = round($cost_with_disc * $coupon->value / 100, 2);
968 }
969 else
970 {
971 if ($i < count($items) - 1)
972 {
973 // fixed discount, apply proportionally according to
974 // the total cost of all the items
975 $percentage = $cost_with_disc * 100 / $total_c;
976 $disc_val = round($coupon->value * $percentage / 100, 2);
977 }
978 else
979 {
980 // We are fetching the last element of the list, instead of calculating the
981 // proportional discount, we should subtract the total discount from the coupon
982 // value, in order to avoid rounding issues. Let's take as example a coupon of
983 // EUR 10 applied on 3 options. The final result would be 3.33 + 3.33 + 3.33,
984 // which won't match the initial discount value of the coupon. With this
985 // alternative way, the result would be: 10 - 3.33 - 3.33 = 3.34.
986 $disc_val = $coupon->value - $orderData['discount'];
987 }
988
989 // the discount cannot exceed the total price
990 $disc_val = min(array($item->inc_price, $disc_val));
991 }
992
993 // increase total discount
994 $orderData['discount'] += $disc_val;
995
996 // subtract discount from item cost
997 $cost_with_disc -= $disc_val;
998
999 // recalculate totals
1000 $totals = VAPTaxFactory::calculate($item->id_option, $cost_with_disc, $options);
1001
1002 // prepare item to save
1003 $itemData = array(
1004 'id' => $item->id,
1005 'net' => $totals->net,
1006 'tax' => $totals->tax,
1007 'gross' => $totals->gross,
1008 'discount' => $disc_val,
1009 'tax_breakdown' => $totals->breakdown,
1010 );
1011
1012 // update order totals
1013 $orderData['total_net'] += $itemData['net'];
1014 $orderData['total_tax'] += $itemData['tax'];
1015 $orderData['total_cost'] += $itemData['gross'];
1016
1017 // append to options list
1018 $orderData['options'][] = $itemData;
1019 }
1020
1021 if (!empty($coupon->code))
1022 {
1023 // save coupon data
1024 $orderData['coupon'] = $coupon;
1025
1026 // redeem coupon usage
1027 $couponModel->redeem($coupon);
1028 }
1029
1030 // update order details
1031 return $this->save($orderData);
1032 }
1033
1034 /**
1035 * Removes discount from the specified reservation.
1036 *
1037 * @param integer $id The order ID.
1038 *
1039 * @return boolean True on success, false otherwise.
1040 */
1041 public function removeDiscount($id)
1042 {
1043 $dbo = JFactory::getDbo();
1044
1045 // load any children
1046 $q = $dbo->getQuery(true)
1047 ->select($dbo->qn(array('id', 'id_option', 'inc_price')))
1048 ->from($dbo->qn('#__vikappointments_res_opt_assoc'))
1049 ->where($dbo->qn('id_reservation') . ' = ' . (int) $id)
1050 ->where($dbo->qn('inc_price') . ' > 0');
1051
1052 $dbo->setQuery($q);
1053 $items = $dbo->loadObjectList();
1054
1055 // load reservation details
1056 $table = $this->getTable();
1057 $table->load((int) $id);
1058
1059 if ($table->coupon_str)
1060 {
1061 // decode coupon string
1062 $coupon = explode(';;', $table->coupon_str);
1063
1064 // unredeem coupon usage
1065 JModelVAP::getInstance('coupon')->unredeem($coupon[0]);
1066 }
1067
1068 // define options for tax calculation
1069 $options = array(
1070 'subject' => 'service',
1071 'lang' => $table->langtag,
1072 'id_user' => $table->id_user,
1073 );
1074
1075 // prepare order data
1076 $orderData = array(
1077 'id' => $table->id,
1078 'total_cost' => $table->payment_charge + $table->payment_tax,
1079 'total_net' => 0,
1080 'total_tax' => $table->payment_tax,
1081 'discount' => 0,
1082 'coupon_str' => '',
1083 'options' => array(),
1084 );
1085
1086 VAPLoader::import('libraries.tax.factory');
1087
1088 if ($table->service_price > 0)
1089 {
1090 // multiply the service price by the number of selected attendees
1091 $table->service_price *= $table->people;
1092
1093 $cost_no_disc = $table->service_price;
1094
1095 // recalculate totals
1096 $totals = VAPTaxFactory::calculate($table->id_service, $cost_no_disc, $options);
1097
1098 // update service totals
1099 $orderData['service_net'] = $totals->net;
1100 $orderData['service_tax'] = $totals->tax;
1101 $orderData['service_gross'] = $totals->gross;
1102 $orderData['service_discount'] = 0;
1103 $orderData['tax_breakdown'] = $totals->breakdown;
1104
1105 // update order totals
1106 $orderData['total_net'] += $orderData['service_net'];
1107 $orderData['total_tax'] += $orderData['service_tax'];
1108 $orderData['total_cost'] += $orderData['service_gross'];
1109 }
1110
1111 $options['subject'] = 'option';
1112
1113 foreach ($items as $i => $item)
1114 {
1115 $cost_no_disc = $item->inc_price;
1116
1117 // recalculate totals
1118 $totals = VAPTaxFactory::calculate($item->id_option, $cost_no_disc, $options);
1119
1120 // prepare item to save
1121 $itemData = array(
1122 'id' => $item->id,
1123 'net' => $totals->net,
1124 'tax' => $totals->tax,
1125 'gross' => $totals->gross,
1126 'discount' => 0,
1127 'tax_breakdown' => $totals->breakdown,
1128 );
1129
1130 // update order totals
1131 $orderData['total_net'] += $itemData['net'];
1132 $orderData['total_tax'] += $itemData['tax'];
1133 $orderData['total_cost'] += $itemData['gross'];
1134
1135 // append to items list
1136 $orderData['options'][] = $itemData;
1137 }
1138
1139 // update order details
1140 return $this->save($orderData);
1141 }
1142
1143 /**
1144 * Sends an e-mail notification to the customer of the
1145 * specified reservation.
1146 *
1147 * @param integer $id The reservation ID.
1148 * @param array $options An array of options.
1149 *
1150 * @return boolean True on success, false otherwise.
1151 */
1152 public function sendEmailNotification($id, array $options = array())
1153 {
1154 VAPLoader::import('libraries.mail.factory');
1155
1156 // fetch receiver alias
1157 $client = isset($options['client']) ? $options['client'] : 'customer';
1158
1159 try
1160 {
1161 // instantiate mail
1162 $mail = VAPMailFactory::getInstance($client, $id, $options);
1163 }
1164 catch (Exception $e)
1165 {
1166 // probably order not found, register error message
1167 $this->setError($e->getMessage());
1168
1169 return false;
1170 }
1171
1172 // in case the "check" attribute is set, we need to make
1173 // sure whether the specified client should receive the
1174 // e-mail according to the configuration rules
1175 if (!empty($options['check']) && !$mail->shouldSend())
1176 {
1177 // configured to avoid receiving this kind of e-mails
1178 return false;
1179 }
1180
1181 // send notification
1182 return $mail->send();
1183 }
1184
1185 /**
1186 * Sends a SMS notification to the customer of the
1187 * specified reservation.
1188 *
1189 * @param integer $id The reservation ID.
1190 *
1191 * @return boolean True on success, false otherwise.
1192 */
1193 public function sendSmsNotification($id)
1194 {
1195 try
1196 {
1197 // get current SMS instance
1198 $smsapi = VAPApplication::getInstance()->getSmsInstance();
1199 }
1200 catch (Exception $e)
1201 {
1202 // SMS API not configured
1203 $this->setError(JText::translate('VAPSMSESTIMATEERR1'));
1204
1205 return false;
1206 }
1207
1208 VAPLoader::import('libraries.order.factory');
1209
1210 try
1211 {
1212 // load appointment details
1213 $order = VAPOrderFactory::getAppointments($id);
1214 }
1215 catch (Exception $e)
1216 {
1217 // order not found
1218 $this->setError($e->getMessage());
1219
1220 return false;
1221 }
1222
1223 // make sure we have a phone number
1224 if (!$order->purchaser_phone)
1225 {
1226 // register error
1227 $this->setError('Missing phone number.');
1228
1229 return false;
1230 }
1231
1232 // make sure the phone number reports a dial code
1233 if ($order->purchaser_prefix && !preg_match("/^\+/", $order->purchaser_phone))
1234 {
1235 // nope, add the specified one (backward compatibility)
1236 $order->purchaser_phone = $order->purchaser_prefix . $order->purchaser_phone;
1237 }
1238
1239 /**
1240 * Inject tags within the API provider as well.
1241 *
1242 * @since 1.7.8
1243 */
1244 if (method_exists($smsapi, 'setOrder'))
1245 {
1246 $smsapi->setOrder(VikAppointments::getTagsSms($order));
1247 }
1248
1249 // fetch sms message
1250 $text = VikAppointments::getSmsCustomerTextMessage($order);
1251
1252 // send message
1253 $response = $smsapi->sendMessage($order->purchaser_phone, $text);
1254
1255 // validate response
1256 if (!$smsapi->validateResponse($response))
1257 {
1258 // unable to send the notification, register error message
1259 $log = $smsapi->getLog();
1260
1261 if ($log)
1262 {
1263 $this->setError($log);
1264 }
1265
1266 return false;
1267 }
1268
1269 return true;
1270 }
1271
1272 /**
1273 * Returns a list of available times for the specified data.
1274 *
1275 * @param array $data An array of search data.
1276 *
1277 * @return mixed An array of available times. False in case of error.
1278 */
1279 public function getAvailableTimes($data)
1280 {
1281 // prepare search options
1282 $options = array();
1283 $options['people'] = !empty($data['people']) ? (int) $data['people'] : 1;
1284 $options['id_res'] = !empty($data['id']) ? (int) $data['id'] : 0;
1285
1286 // number of people cannot be lower than 1
1287 $options['people'] = max(array(1, (int) $options['people']));
1288
1289 if (JFactory::getApplication()->isClient('administrator') || (!empty($data['validate_availability']) && $data['validate_availability'] == 'admin'))
1290 {
1291 // grant administrator access
1292 $options['admin'] = true;
1293 }
1294
1295 VAPLoader::import('libraries.availability.manager');
1296 // create availability search instance
1297 $search = VAPAvailabilityManager::getInstance($data['id_service'], $data['id_employee'], $options);
1298
1299 try
1300 {
1301 // create timeline parser instance
1302 VAPLoader::import('libraries.availability.timeline.factory');
1303 $parser = VAPAvailabilityTimelineFactory::getParser($search);
1304 }
1305 catch (Exception $e)
1306 {
1307 // register exception as error
1308 $this->setError($e);
1309
1310 return false;
1311 }
1312
1313 // get employee timezone
1314 $tz = JModelVAP::getInstance('employee')->getTimezone($search->get('id_employee'));
1315
1316 // create check-in date and adjust it to the employee timezone
1317 $checkin = JFactory::getDate($data['checkin_ts']);
1318 $checkin->setTimezone(new DateTimeZone($tz));
1319
1320 // elaborate timeline
1321 $timeline = $parser->getTimeline($checkin->format('Y-m-d', true), $options['people'], $options['id_res']);
1322
1323 if (!$timeline)
1324 {
1325 // propagate error message
1326 $this->setError($parser->getError());
1327
1328 return false;
1329 }
1330
1331 return $timeline;
1332 }
1333
1334 /**
1335 * Checks the availability within the system according to the specified
1336 * search details.
1337 *
1338 * @param array $data An array of search data.
1339 *
1340 * @return mixed True if available, false otherwise. In case the employee
1341 * was not specified, the ID of the available employee will
1342 * be returned instead.
1343 */
1344 public function isAvailable($data)
1345 {
1346 // get availability timeline
1347 $timeline = $this->getAvailableTimes($data);
1348
1349 if (!$timeline)
1350 {
1351 // not available for the current day
1352 return false;
1353 }
1354
1355 // convert times into an array
1356 $times = $timeline->toArray($flatten = true);
1357
1358 // get employee timezone
1359 $tz = JModelVAP::getInstance('employee')->getTimezone($timeline->getSearch()->get('id_employee'));
1360
1361 // create check-in date and adjust it to the employee timezone
1362 $checkin = JFactory::getDate($data['checkin_ts']);
1363 $checkin->setTimezone(new DateTimeZone($tz));
1364
1365 // extract time
1366 $hm = $checkin->format('H:i', $local = true);
1367 // convert time to minutes
1368 $hm = JHtml::fetch('vikappointments.time2min', $hm);
1369
1370 // make sure the time is available
1371 if (!isset($times[$hm]) || $times[$hm] != 1)
1372 {
1373 // the time is not available/supported
1374 $this->setError(JText::translate('VAPRESDATETIMENOTAVERR'));
1375
1376 return false;
1377 }
1378
1379 $options = array();
1380
1381 if (isset($data['exclude_employees']))
1382 {
1383 // check if we should exclude certain employees from the availability check
1384 $options['exclude_employees'] = $data['exclude_employees'];
1385 }
1386
1387 // create availability search instance
1388 $search = VAPAvailabilityManager::getInstance($data['id_service'], $data['id_employee'], $options);
1389
1390 $duration = 0;
1391
1392 if (!empty($data['duration']))
1393 {
1394 $duration += $data['duration'];
1395
1396 if (!empty($data['sleep']))
1397 {
1398 $duration += $data['sleep'];
1399 }
1400 }
1401
1402 // fetch number of participants
1403 $people = !empty($data['people']) ? max(array(1, (int) $data['people'])) : 1;
1404 // check if we are editing a reservation
1405 $id = !empty($data['id']) ? (int) $data['id'] : 0;
1406
1407 // exmployee was specified, validate its availability
1408 if ($data['id_employee'] > 0)
1409 {
1410 // check if the employee is able to host the appointment
1411 $is = $search->isEmployeeAvailable($data['checkin_ts'], $duration, $people, $id);
1412 }
1413 else
1414 {
1415 // Employee not specified, we need to load all the employees assigned
1416 // to this service and take the first available one. In case of available
1417 // employee, its ID will be returned here.
1418 $is = $search->isServiceAvailable($data['checkin_ts'], $duration, $people, $id);
1419 }
1420
1421 if (!$is)
1422 {
1423 // the time is not available/supported
1424 $this->setError(JText::translate('VAPRESDATETIMENOTAVERR'));
1425
1426 return false;
1427 }
1428
1429 // employee available (true or its ID)
1430 return $is;
1431 }
1432
1433 /**
1434 * Updates the status of all the appointments out of time to REMOVED.
1435 * This method is used to free the slots occupied by pending orders
1436 * that haven't been confirmed within the specified range of time.
1437 *
1438 * Affects only the reservations that match the specified employee ID.
1439 *
1440 * @param array $options An array of options to filter the records.
1441 *
1442 * @return void
1443 */
1444 public function checkExpired(array $options = array())
1445 {
1446 $dbo = JFactory::getDbo();
1447
1448 // get any pending codes
1449 $pending = JHtml::fetch('vaphtml.status.find', 'code', array('appointments' => 1, 'reserved' => 1, 'approved' => 0));
1450
1451 /**
1452 * Do not proceed in case the PENDING status is not available, otherwise we would end to
1453 * auto-remove all the existing reservations.
1454 *
1455 * @since 1.7.7
1456 */
1457 if (!$pending)
1458 {
1459 // pending status not found, abort immediately
1460 return;
1461 }
1462
1463 // select all the expired appointments
1464 $q = $dbo->getQuery(true);
1465 $q->select($dbo->qn('id'));
1466 $q->from($dbo->qn('#__vikappointments_reservation'));
1467
1468 // filter by pending status
1469 $q->where($dbo->qn('status') . ' IN (' . implode(',', array_map(array($dbo, 'q'), $pending)) . ')');
1470 // take the expired appointments
1471 $q->where($dbo->qn('locked_until') . ' < ' . time());
1472
1473 /**
1474 * Take only unpaid appointments, as the administrator might have switched the status of a confirmed appointment
1475 * to let the customer completes the payment of an additional charge.
1476 *
1477 * @since 1.7.8
1478 */
1479 $q->where($dbo->qn('tot_paid') . ' = 0');
1480
1481 if (!empty($options['id_service']))
1482 {
1483 // get service model
1484 $serviceModel = JModelVAP::getInstance('service');
1485
1486 // check if we have a service with private calendar
1487 if ($serviceModel->hasOwnCalendar($options['id_service']))
1488 {
1489 // we can directly filter the reservations by service, because the
1490 // availability is related to the appointments assigned to the latter
1491 $q->where($dbo->qn('id_service') . ' = ' . (int) $options['id_service']);
1492 // unset employees filter
1493 $options['id_employee'] = 0;
1494 }
1495 else
1496 {
1497 /**
1498 * Service set, recover all the employees assigned to this service
1499 * in order to properly refresh the availability. It is not enough
1500 * to remove only the appointments assigned to the specified service,
1501 * because a time-slot of the employees might have been locked by
1502 * an appointment assigned to a different service.
1503 *
1504 * @since 1.7
1505 */
1506 $employees = $dbo->getQuery(true)
1507 ->select($dbo->qn('id_employee'))
1508 ->from($dbo->qn('#__vikappointments_ser_emp_assoc'))
1509 ->where($dbo->qn('id_service') . ' = ' . (int) $options['id_service']);
1510
1511 $dbo->setQuery($employees);
1512
1513 // filter by the specified employees
1514 $options['employees'] = $dbo->loadColumn();
1515 }
1516 }
1517
1518 if (!empty($options['id_employee']))
1519 {
1520 // affects only the reservations that match the specified employee ID
1521 if (is_array($options['id_employee']))
1522 {
1523 // sanitize employees array
1524 $ids = implode(',', array_map('intval', $options['id_employee']));
1525 $q->where($dbo->qn('id_employee') . ' IN (' . $ids . ')');
1526 }
1527 else
1528 {
1529 $q->where($dbo->qn('id_employee') . ' = ' . (int) $options['id_employee']);
1530 }
1531 }
1532
1533 if (!empty($options['id']))
1534 {
1535 // take only the specified reservation
1536 $q->andWhere(array(
1537 $dbo->qn('id') . ' = ' . (int) $options['id'],
1538 $dbo->qn('id_parent') . ' = ' . (int) $options['id'],
1539 ), 'OR');
1540 }
1541
1542 $dbo->setQuery($q);
1543 $rows = $dbo->loadColumn();
1544
1545 $handler = VAPOrderStatus::getInstance();
1546
1547 foreach ($rows as $id)
1548 {
1549 // Remove and track the status change (REMOVED).
1550 // Do not use the model to save the status change
1551 // to speed up the whole process. It is still possible
1552 // to track the status change by using the hook
1553 // provided by VAPOrderStatus class.
1554 $handler->remove($id, 'VAP_STATUS_ORDER_REMOVED');
1555 }
1556 }
1557
1558 /**
1559 * Counts the actual number of options already sold.
1560 *
1561 * @param int $idOption The option to count.
1562 * @param int $idVariation The variation to count, if any.
1563 * @param int $idIndex The reservation item to exlcude, if any.
1564 *
1565 * @return int The number of sold units.
1566 *
1567 * @since 1.7.7
1568 */
1569 public function countSoldOptions($idOption, $idVariation = null, $idIndex = null)
1570 {
1571 $db = JFactory::getDbo();
1572
1573 $query = $db->getQuery(true)
1574 ->select('SUM(i.quantity)')
1575 ->from($db->qn('#__vikappointments_reservation', 'r'))
1576 ->innerjoin($db->qn('#__vikappointments_res_opt_assoc', 'i') . ' ON ' . $db->qn('i.id_reservation') . ' = ' . $db->qn('r.id'));
1577
1578 if ($idIndex)
1579 {
1580 // exclude the specified reservation item
1581 $query->where($db->qn('i.id') . ' <> ' . (int) $idIndex);
1582 }
1583
1584 // filter by option
1585 $query->where($db->qn('i.id_option') . ' = ' . (int) $idOption);
1586
1587 // get any reserved status codes
1588 $reserved = JHtml::fetch('vaphtml.status.find', 'code', ['appointments' => 1, 'reserved' => 1]);
1589
1590 if ($reserved)
1591 {
1592 // filter by reserved status
1593 $query->where($db->qn('r.status') . ' IN (' . implode(',', array_map(array($db, 'q'), $reserved)) . ')');
1594 }
1595
1596 if ($idVariation)
1597 {
1598 // take only the specified variation
1599 $query->where($db->qn('i.id_variation') . ' = ' . (int) $idVariation);
1600 }
1601 else
1602 {
1603 // take all the variations of the option without self stock
1604 $query->leftjoin($db->qn('#__vikappointments_option_value', 'v') . ' ON ' . $db->qn('i.id_variation') . ' = ' . $db->qn('v.id'));
1605 $query->andWhere([
1606 $db->qn('v.id') . ' IS NULL',
1607 $db->qn('v.stock') . ' = 0',
1608 ], 'OR');
1609 }
1610
1611 $db->setQuery($query, 0, 1);
1612 return (int) $db->loadResult();
1613 }
1614 }
1615