PluginProbe ʕ •ᴥ•ʔ
VikAppointments Services Booking Calendar / trunk
VikAppointments Services Booking Calendar vtrunk
trunk 1.2.17 1.2.18 1.2.19
vikappointments / admin / models / packorder.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
packorder.php
733 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 package order model.
18 *
19 * @since 1.7
20 */
21 class VikAppointmentsModelPackorder 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 $data = (array) $data;
33
34 // get order-package model
35 $model = JModelVAP::getInstance('packorderitem');
36
37 if (!empty($data['deletedItems']) && !isset($data['discount']))
38 {
39 // get total discount of items to remove
40 $discount = $model->getTotalDiscount($data['deletedItems']);
41
42 if ($discount > 0)
43 {
44 // load order details
45 $table = $this->getTable();
46 $table->load($data['id']);
47
48 // subtract discount of items to remove from the total one
49 $data['discount'] = max(array(0, $table->discount - $discount));
50 }
51 }
52
53 // get order statuses handler
54 $orderStatus = VAPOrderStatus::getInstance('package_order');
55
56 $prev_status = null;
57
58 if (!empty($data['status']) && !empty($data['id']))
59 {
60 // register previous order status
61 $prev_status = $orderStatus->getStatus($data['id']);
62 }
63
64 // attempt to save the order
65 $id = parent::save($data);
66
67 if (!$id)
68 {
69 // an error occurred, do not go ahead
70 return false;
71 }
72
73 // always clear order from cache after saving
74 VAPLoader::import('libraries.order.factory');
75 VAPOrderFactory::changed('package', $id);
76
77 if (!empty($data['deletedItems']))
78 {
79 // delete specified items, needed to properly apply
80 // discount calculation (if requested)
81 $model->delete($data['deletedItems']);
82 }
83
84 if (!empty($data['items']))
85 {
86 foreach ((array) $data['items'] as $item)
87 {
88 // check if we are dealing with a JSON object
89 $item = is_string($item) ? json_decode($item, true) : (array) $item;
90
91 if (!empty($item['validthru']))
92 {
93 // convert valid through date into UTC
94 $item['validthru'] = VAPDateHelper::getSqlDateLocale($item['validthru']);
95 }
96
97 // make relation with saved order
98 $item['id_order'] = $id;
99
100 // save item
101 $model->save($item);
102 }
103 }
104
105 // Check whether the status has changed.
106 // Create a new status record also for new reservations
107 if (!empty($data['status']) && $data['status'] != $prev_status)
108 {
109 if (empty($data['status_comment']))
110 {
111 // use default status comment
112 $data['status_comment'] = 'VAP_STATUS_CHANGED_ON_MANAGE';
113 }
114
115 // track status change
116 $orderStatus->keepTrack($data['status'], $id, $data['status_comment']);
117 }
118
119 // check whether we should apply or delete a discount
120 if (!empty($data['add_discount']))
121 {
122 $this->addDiscount($id, $data['add_discount']);
123 }
124 else if (!empty($data['remove_discount']))
125 {
126 $this->removeDiscount($id);
127 }
128
129 if (!empty($data['notify']))
130 {
131 // send e-mail notification to customer
132 $this->sendEmailNotification($id);
133 }
134
135 return $id;
136 }
137
138 /**
139 * Extend delete implementation to delete any related records
140 * stored within a separated table.
141 *
142 * @param mixed $ids Either the record ID or a list of records.
143 *
144 * @return boolean True on success, false otherwise.
145 */
146 public function delete($ids)
147 {
148 // only int values are accepted
149 $ids = array_map('intval', (array) $ids);
150
151 // invoke parent first
152 if (!parent::delete($ids))
153 {
154 // nothing to delete
155 return false;
156 }
157
158 $dbo = JFactory::getDbo();
159
160 // load any children
161 $q = $dbo->getQuery(true)
162 ->select($dbo->qn('id'))
163 ->from($dbo->qn('#__vikappointments_package_order_item'))
164 ->where($dbo->qn('id_order') . ' IN (' . implode(',', $ids) . ')' );
165
166 $dbo->setQuery($q);
167
168 if ($child_id = $dbo->loadColumn())
169 {
170 // get item model
171 $model = JModelVAP::getInstance('packorderitem');
172 // delete children
173 $model->delete($child_id);
174 }
175
176 // load any assigned order statuses
177 $q = $dbo->getQuery(true)
178 ->select($dbo->qn('id'))
179 ->from($dbo->qn('#__vikappointments_order_status'))
180 ->where($dbo->qn('type') . ' = ' . $dbo->q('package_order'))
181 ->where($dbo->qn('id_order') . ' IN (' . implode(',', $ids) . ')' );
182
183 $dbo->setQuery($q);
184
185 if ($assoc_ids = $dbo->loadColumn())
186 {
187 // get order status model
188 $model = JModelVAP::getInstance('orderstatus');
189 // delete relations
190 $model->delete($assoc_ids);
191 }
192
193 return true;
194 }
195
196 /**
197 * Adds a discount to the specified package order.
198 *
199 * @param integer $id The order ID.
200 * @param mixed $coupon Either a coupon code or an array/object
201 * containing its details.
202 *
203 * @return boolean True on success, false otherwise.
204 */
205 public function addDiscount($id, $coupon)
206 {
207 // get coupon model
208 $couponModel = JModelVAP::getInstance('coupon');
209
210 if (is_string($coupon))
211 {
212 // get coupon code details
213 $coupon = $couponModel->getCoupon($coupon);
214 }
215 else
216 {
217 // treat as object
218 $coupon = (object) $coupon;
219 }
220
221 // make sure we have a valid coupon code
222 if (!$coupon || empty($coupon->value))
223 {
224 // invalid/missing coupon
225 $this->setError('Missing coupon code');
226
227 return false;
228 }
229
230 $dbo = JFactory::getDbo();
231
232 // load any children
233 $q = $dbo->getQuery(true)
234 ->select($dbo->qn(array('id', 'id_package', 'price', 'quantity')))
235 ->from($dbo->qn('#__vikappointments_package_order_item'))
236 ->where($dbo->qn('id_order') . ' = ' . (int) $id)
237 ->where($dbo->qn('price') . ' > 0');
238
239 $dbo->setQuery($q);
240 $items = $dbo->loadObjectList();
241
242 if (!$items)
243 {
244 // no assigned items
245 $this->setError(JText::translate('JGLOBAL_NO_MATCHING_RESULTS'));
246
247 return false;
248 }
249
250 // load package order details
251 $table = $this->getTable();
252 $table->load((int) $id);
253
254 // define options for tax calculation
255 $options = array(
256 'subject' => 'package',
257 'lang' => $table->langtag,
258 'id_user' => $table->id_user,
259 );
260
261 $total_q = 0;
262
263 // calculate total number of quantity
264 foreach ($items as $item)
265 {
266 $total_q += (int) $item->quantity;
267 }
268
269 // prepare order data
270 $orderData = array(
271 'id' => $table->id,
272 'total_cost' => $table->payment_charge + $table->payment_tax,
273 'total_net' => 0,
274 'total_tax' => $table->payment_tax,
275 'discount' => 0,
276 'coupon' => '',
277 'items' => array(),
278 );
279
280 VAPLoader::import('libraries.tax.factory');
281
282 foreach ($items as $i => $item)
283 {
284 $cost_with_disc = $item->price * $item->quantity;
285
286 if (empty($coupon->percentot) || $coupon->percentot == 1)
287 {
288 // percentage discount
289 $disc_val = round($cost_with_disc * $coupon->value / 100, 2);
290 }
291 else
292 {
293 if ($i < count($items) - 1)
294 {
295 // fixed discount, apply proportionally according to
296 // the total number of quantities
297 $disc_val = round($coupon->value * $item->quantity / $total_q, 2);
298 }
299 else
300 {
301 // We are fetching the last element of the list, instead of calculating the
302 // proportional discount, we should subtract the total discount from the coupon
303 // value, in order to avoid rounding issues. Let's take as example a coupon of
304 // EUR 10 applied on 3 packages. The final result would be 3.33 + 3.33 + 3.33,
305 // which won't match the initial discount value of the coupon. With this
306 // alternative way, the result would be: 10 - 3.33 - 3.33 = 3.34.
307 $disc_val = $coupon->value - $orderData['discount'];
308 }
309 }
310
311 // increase total discount
312 $orderData['discount'] += $disc_val;
313
314 // subtract discount from item cost
315 $cost_with_disc -= $disc_val;
316
317 // recalculate totals
318 $totals = VAPTaxFactory::calculate($item->id_package, $cost_with_disc, $options);
319
320 // prepare item to save
321 $itemData = array(
322 'id' => $item->id,
323 'net' => $totals->net,
324 'tax' => $totals->tax,
325 'gross' => $totals->gross,
326 'discount' => $disc_val,
327 'tax_breakdown' => $totals->breakdown,
328 );
329
330 // update order totals
331 $orderData['total_net'] += $itemData['net'];
332 $orderData['total_tax'] += $itemData['tax'];
333 $orderData['total_cost'] += $itemData['gross'];
334
335 // append to items list
336 $orderData['items'][] = $itemData;
337 }
338
339 if (!empty($coupon->code))
340 {
341 // save coupon data
342 $orderData['coupon'] = $coupon;
343
344 // redeem coupon usage
345 $couponModel->redeem($coupon);
346 }
347
348 // update order details
349 return $this->save($orderData);
350 }
351
352 /**
353 * Removes discount from the specified package order.
354 *
355 * @param integer $id The order ID.
356 *
357 * @return boolean True on success, false otherwise.
358 */
359 public function removeDiscount($id)
360 {
361 $dbo = JFactory::getDbo();
362
363 // load any children
364 $q = $dbo->getQuery(true)
365 ->select($dbo->qn(array('id', 'id_package', 'price', 'quantity')))
366 ->from($dbo->qn('#__vikappointments_package_order_item'))
367 ->where($dbo->qn('id_order') . ' = ' . (int) $id)
368 ->where($dbo->qn('price') . ' > 0');
369
370 $dbo->setQuery($q);
371 $items = $dbo->loadObjectList();
372
373 if (!$items)
374 {
375 // no assigned items
376 $this->setError(JText::translate('JGLOBAL_NO_MATCHING_RESULTS'));
377
378 return false;
379 }
380
381 // load package order details
382 $table = $this->getTable();
383 $table->load((int) $id);
384
385 if ($table->coupon)
386 {
387 // decode coupon string
388 $coupon = explode(';;', $table->coupon);
389
390 // unredeem coupon usage
391 JModelVAP::getInstance('coupon')->unredeem($coupon[0]);
392 }
393
394 // define options for tax calculation
395 $options = array(
396 'subject' => 'package',
397 'lang' => $table->langtag,
398 'id_user' => $table->id_user,
399 );
400
401 // prepare order data
402 $orderData = array(
403 'id' => $table->id,
404 'total_cost' => $table->payment_charge + $table->payment_tax,
405 'total_net' => 0,
406 'total_tax' => $table->payment_tax,
407 'discount' => 0,
408 'coupon' => '',
409 'items' => array(),
410 );
411
412 VAPLoader::import('libraries.tax.factory');
413
414 foreach ($items as $i => $item)
415 {
416 $cost_no_disc = $item->price * $item->quantity;
417
418 // recalculate totals
419 $totals = VAPTaxFactory::calculate($item->id_package, $cost_no_disc, $options);
420
421 // prepare item to save
422 $itemData = array(
423 'id' => $item->id,
424 'net' => $totals->net,
425 'tax' => $totals->tax,
426 'gross' => $totals->gross,
427 'discount' => 0,
428 'tax_breakdown' => $totals->breakdown,
429 );
430
431 // update order totals
432 $orderData['total_net'] += $itemData['net'];
433 $orderData['total_tax'] += $itemData['tax'];
434 $orderData['total_cost'] += $itemData['gross'];
435
436 // append to items list
437 $orderData['items'][] = $itemData;
438 }
439
440 // update order details
441 return $this->save($orderData);
442 }
443
444 /**
445 * Sends an e-mail notification to the customer of the
446 * specified order.
447 *
448 * @param integer $id The order ID.
449 * @param array $options An array of options.
450 *
451 * @return boolean True on success, false otherwise.
452 */
453 public function sendEmailNotification($id, array $options = array())
454 {
455 VAPLoader::import('libraries.mail.factory');
456
457 // fetch receiver alias
458 $client = isset($options['client']) ? $options['client'] : 'package';
459
460 try
461 {
462 // instantiate mail
463 $mail = VAPMailFactory::getInstance($client, $id, $options);
464 }
465 catch (Exception $e)
466 {
467 // probably order not found, register error message
468 $this->setError($e->getMessage());
469
470 return false;
471 }
472
473 // in case the "check" attribute is set, we need to make
474 // sure whether the specified client should receive the
475 // e-mail according to the configuration rules
476 if (!empty($options['check']) && !$mail->shouldSend())
477 {
478 // configured to avoid receiving this kind of e-mails
479 return false;
480 }
481
482 // send notification
483 return $mail->send();
484 }
485
486 /**
487 * Counts the number of services within the purchased packages that the
488 * specified customer/user is still able to use.
489 *
490 * @param integer $id_service The service ID.
491 * @param integer $id_user The user ID. If not provided,
492 * the current user will be retrieved.
493 *
494 * @return integer The remaining number of services.
495 */
496 public function countRemaining($id_service = null, $id_user = null)
497 {
498 $dbo = JFactory::getDbo();
499
500 if (!$id_user || $id_user == -1)
501 {
502 $user = JFactory::getUser();
503
504 if ($user->guest)
505 {
506 return 0;
507 }
508
509 $id_user = $user->id;
510
511 // make relation with Joomla ID
512 $user_column = 'u.jid';
513 }
514 else
515 {
516 // make relation using customer ID
517 $user_column = 'o.id_user';
518 }
519
520 // get any approved codes
521 $approved = JHtml::fetch('vaphtml.status.find', 'code', array('packages' => 1, 'approved' => 1));
522
523 $q = $dbo->getQuery(true)
524 ->select(sprintf('SUM(%s - %s) AS %s', $dbo->qn('i.num_app'), $dbo->qn('i.used_app'), $dbo->qn('count')))
525 ->from($dbo->qn('#__vikappointments_package_order', 'o'))
526 ->innerjoin($dbo->qn('#__vikappointments_package_order_item', 'i') . ' ON ' . $dbo->qn('i.id_order') . ' = ' . $dbo->qn('o.id'))
527 ->leftjoin($dbo->qn('#__vikappointments_users', 'u') . ' ON ' . $dbo->qn('o.id_user') . ' = ' . $dbo->qn('u.id'))
528 ->where($dbo->qn($user_column) . ' = ' . $id_user);
529
530 if ($id_service)
531 {
532 // filter by service
533 $q->leftjoin($dbo->qn('#__vikappointments_package_service', 'a') . ' ON ' . $dbo->qn('a.id_package') . ' = ' . $dbo->qn('i.id_package'));
534 $q->where($dbo->qn('a.id_service') . ' = ' . (int) $id_service);
535 }
536
537 if ($approved)
538 {
539 // filter by approved status
540 $q->where($dbo->qn('o.status') . ' IN (' . implode(',', array_map(array($dbo, 'q'), $approved)) . ')');
541 }
542
543 /**
544 * Ignore the expired packages.
545 *
546 * @since 1.7.4
547 */
548 $q->andWhere([
549 $dbo->qn('i.validthru') . ' IS NULL',
550 $dbo->qn('i.validthru') . ' = ' . $dbo->q($dbo->getNullDate()),
551 $dbo->qn('i.validthru') . ' >= ' . $dbo->q(JFactory::getDate()->toSql()),
552 ], 'OR');
553
554 /**
555 * Applies additional restrictions while counting the remaining number of packages
556 * that can be redeemed for the specified service.
557 *
558 * @param string &$query The database query used to count the remaining packages.
559 * @param int $id_service The ID of the service that should be redeemed.
560 *
561 * @return void
562 *
563 * @since 1.7.4
564 */
565 VAPFactory::getEventDispatcher()->trigger('onCountRemainingPackages', [&$q, $id_service]);
566
567 $dbo->setQuery($q);
568
569 // return the remaining number of packages (cannot be lower than 0)
570 return max(array(0, (int) $dbo->loadResult()));
571 }
572
573 /**
574 * Registers all the packages that have been used to purchase a service.
575 *
576 * @param mixed $order Either the order details instance or an ID.
577 * @param boolean $increase True to increase the number of used packages,
578 * false to free them.
579 *
580 * @return integer The number of packages redeemed/unreedemed.
581 */
582 public function usePackages($order, $increase = true)
583 {
584 $dbo = JFactory::getDbo();
585
586 if (is_numeric($order))
587 {
588 try
589 {
590 // get order details
591 VAPLoader::import('libraries.order.factory');
592 $order = VAPOrderFactory::getAppointments($order);
593 }
594 catch (Exception $e)
595 {
596 // order not found
597 return 0;
598 }
599 }
600
601 if ($order->id_user <= 0)
602 {
603 // the owner of the order is not registered
604 return 0;
605 }
606
607 // count the total number of guests for each service in the list
608 $count_map = array();
609
610 foreach ($order->appointments as $app)
611 {
612 $id_ser = $app->service->id;
613
614 if (!array_key_exists($id_ser, $count_map))
615 {
616 $count_map[$id_ser] = 0;
617 }
618
619 $count_map[$id_ser] += $app->people;
620 }
621
622 $reedemed = 0;
623
624 // get any approved codes
625 $approved = JHtml::fetch('vaphtml.status.find', 'code', array('packages' => 1, 'approved' => 1));
626
627 // get package order item model
628 $itemModel = JModelVAP::getInstance('packorderitem');
629
630 $dispatcher = VAPFactory::getEventDispatcher();
631
632 // iterate the map
633 foreach ($count_map as $id_ser => $count)
634 {
635 // get all the packages that can be redeemed for the service/user pair
636 $q = $dbo->getQuery(true)
637 ->select($dbo->qn(array('i.id', 'i.num_app', 'i.used_app')))
638 ->from($dbo->qn('#__vikappointments_package_order', 'o'))
639 ->innerjoin($dbo->qn('#__vikappointments_package_order_item', 'i') . ' ON ' . $dbo->qn('i.id_order') . ' = ' . $dbo->qn('o.id'))
640 ->leftjoin($dbo->qn('#__vikappointments_package_service', 'a') . ' ON ' . $dbo->qn('a.id_package') . ' = ' . $dbo->qn('i.id_package'))
641 ->where(array(
642 $dbo->qn('o.id_user') . ' = ' . (int) $order->id_user,
643 $dbo->qn('a.id_service') . ' = ' . (int) $id_ser,
644 ));
645
646 if ($approved)
647 {
648 // filter by approved status
649 $q->where($dbo->qn('o.status') . ' IN (' . implode(',', array_map(array($dbo, 'q'), $approved)) . ')');
650 }
651
652 if ($increase)
653 {
654 // restrict number of used apps only if we are increasing
655 $q->where($dbo->qn('i.used_app') . ' < ' . $dbo->qn('i.num_app'));
656 }
657
658 /**
659 * Ignore the expired packages.
660 *
661 * @since 1.7.4
662 */
663 $q->andWhere([
664 $dbo->qn('i.validthru') . ' IS NULL',
665 $dbo->qn('i.validthru') . ' = ' . $dbo->q($dbo->getNullDate()),
666 $dbo->qn('i.validthru') . ' >= ' . $dbo->q(JFactory::getDate()->toSql()),
667 ], 'OR');
668
669 /**
670 * Applies additional restrictions while counting the remaining number of packages
671 * that can be redeemed for the specified service.
672 *
673 * @param string &$query The database query used to count the remaining packages.
674 * @param int $id_service The ID of the service that should be redeemed.
675 *
676 * @return void
677 *
678 * @since 1.7.4
679 */
680 $dispatcher->trigger('onCountRemainingPackages', [&$q, $id_ser]);
681
682 $dbo->setQuery($q);
683
684 $rows = $dbo->loadObjectList();
685
686 $i = 0;
687 // iterate until the total number of services is redeemed,
688 // or at least until we reach the end of the array
689 while ($count > 0 && $i < count($rows))
690 {
691 $r = $rows[$i];
692
693 /**
694 * Evaluates if we have to increase or decrease the number
695 * of used packages.
696 *
697 * @since 1.6.3
698 */
699 if ($increase)
700 {
701 // Get the number of packages to redeem.
702 // Obtain the minimum value between the total services and the remaining packages.
703 $used = min(array($count, $r->num_app - $r->used_app));
704
705 // increase used packages
706 $r->used_app += $used;
707 }
708 else
709 {
710 // Get the number of packages to redeem.
711 // Obtain the minimum value between the total services and the number of used packages.
712 $used = min(array($count, $r->used_app));
713
714 // decrease used packages
715 $r->used_app -= $used;
716 }
717
718 // update the record by changing the total number of units used
719 $itemModel->save($r);
720
721 // decrease the services count by the number of used packages
722 $count -= $used;
723 $i++;
724
725 // increase the total number of redeemed packages
726 $reedemed += $used;
727 }
728 }
729
730 return $reedemed;
731 }
732 }
733