PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.8
VikBooking Hotel Booking Engine & PMS v1.8.8
1.8.16 1.8.15 1.8.14 1.8.13 1.8.12 1.8.11 1.8.10 1.8.9 1.8.6 1.8.7 1.8.8 trunk 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 All 37 releases
vikbooking / admin / helpers / src / taxonomy / finance.php

finance.php in VikBooking Hotel Booking Engine & PMS 1.8.8, at admin/helpers/src/taxonomy/finance.php

809 lines 25.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package VikBooking
4 * @subpackage core
5 * @author E4J s.r.l.
6 * @copyright Copyright (C) 2022 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 /**
15 * Taxonomy finance helper class.
16 *
17 * @since 1.16.0 (J) - 1.6.0 (WP)
18 */
19 class VBOTaxonomyFinance
20 {
21 /**
22 * The singleton instance of the class.
23 *
24 * @var VBOTaxonomyFinance
25 */
26 protected static $instance = null;
27
28 /**
29 * Class constructor is protected.
30 *
31 * @see getInstance()
32 */
33 protected function __construct()
34 {
35 // do nothing
36 }
37
38 /**
39 * Returns the global object, either
40 * a new instance or the existing instance
41 * if the class was already instantiated.
42 *
43 * @return self A new instance of the class.
44 */
45 public static function getInstance()
46 {
47 if (is_null(static::$instance)) {
48 static::$instance = new static();
49 }
50
51 return static::$instance;
52 }
53
54 /**
55 * Returns the number of total units for all rooms, or for a specific room.
56 * By default, the rooms unpublished are skipped, and all rooms are used.
57 *
58 * @param mixed $idroom int or array.
59 * @param bool $published true or false.
60 *
61 * @return int
62 */
63 public function countRooms($idroom = 0, $published = true)
64 {
65 $dbo = JFactory::getDbo();
66
67 $totrooms = 0;
68 $clauses = [];
69
70 if (is_int($idroom) && $idroom > 0) {
71 $clauses[] = "`id`=" . (int)$idroom;
72 } elseif (is_array($idroom) && count($idroom)) {
73 $idroom = array_map('intval', $idroom);
74 $clauses[] = "`id` IN (" . implode(', ', $idroom) . ")";
75 }
76
77 if ($published) {
78 $clauses[] = "`avail`=1";
79 }
80
81 $q = "SELECT SUM(`units`) FROM `#__vikbooking_rooms`" . (count($clauses) ? " WHERE " . implode(' AND ', $clauses) : "");
82 $dbo->setQuery($q);
83 $totrooms = (int)$dbo->loadResult();
84
85 return $totrooms;
86 }
87
88 /**
89 * Counts the number of nights of difference between two timestamps.
90 *
91 * @param int $to_ts the target end date timestamp.
92 * @param int $from_ts the starting date timestamp.
93 *
94 * @return int the nights of difference between from and to timestamps.
95 */
96 public function countNightsTo($to_ts, $from_ts = 0)
97 {
98 if (empty($from_ts)) {
99 $from_ts = time();
100 }
101
102 $from_ymd = date('Y-m-d', $from_ts);
103 $to_ymd = date('Y-m-d', $to_ts);
104
105 if ($from_ymd == $to_ymd) {
106 return 1;
107 }
108
109 $from_date = new DateTime($from_ymd);
110 $to_date = new DateTime($to_ymd);
111 $daysdiff = (int)$from_date->diff($to_date)->format('%a');
112
113 if ($to_ts < $from_ts) {
114 // we need a negative integer number in this case
115 $daysdiff = $daysdiff - ($daysdiff * 2);
116 }
117
118 if ($from_ymd != $to_ymd && $daysdiff > 0) {
119 // the to date is actually another night of stay
120 $daysdiff += 1;
121 }
122
123 return $daysdiff;
124 }
125
126 /**
127 * Counts the number of days of difference between two timestamps.
128 *
129 * @param int $from_ts the starting date timestamp.
130 * @param int $to_ts the target end date timestamp.
131 *
132 * @return int the days of difference from the given dates.
133 *
134 * @since 1.16.10 (J) - 1.6.10 (WP)
135 */
136 public function countDaysDiff($from_ts, $to_ts)
137 {
138 if (empty($from_ts) || empty($to_ts)) {
139 return 0;
140 }
141
142 $from_ymd = date('Y-m-d', $from_ts);
143 $to_ymd = date('Y-m-d', $to_ts);
144
145 if ($from_ymd == $to_ymd) {
146 return 0;
147 }
148
149 $from_date = new DateTime($from_ymd);
150 $to_date = new DateTime($to_ymd);
151 $daysdiff = (int)$from_date->diff($to_date)->format('%a');
152
153 if ($to_ts < $from_ts) {
154 // we need a negative integer number in this case
155 $daysdiff = $daysdiff - ($daysdiff * 2);
156 }
157
158 return $daysdiff;
159 }
160
161
162 /**
163 * Helper method to format long currency values into short numbers.
164 * I.e. 2.600.000 = 2.6M (empty decimals are always removed to keep the string short).
165 *
166 * @param int|float $num the amount to format.
167 * @param int $decimals the precision to use.
168 *
169 * @return string the formatted amount string.
170 */
171 public function numberFormatShort($num, $decimals = 1)
172 {
173 // get global formatting values
174 $formatvals = VikBooking::getNumberFormatData();
175 $formatparts = explode(':', $formatvals);
176
177 if ($num < 951) {
178 // 0 - 950
179 $short_amount = number_format($num, $decimals, $formatparts[1], $formatparts[2]);
180 $type_amount = '';
181 } elseif ($num < 900000) {
182 // 1k - 950k
183 $short_amount = number_format($num / 1000, $decimals, $formatparts[1], $formatparts[2]);
184 $type_amount = 'k';
185 } elseif ($num < 900000000) {
186 // 0.9m - 950m
187 $short_amount = number_format($num / 1000000, $decimals, $formatparts[1], $formatparts[2]);
188 $type_amount = 'm';
189 } elseif ($num < 900000000000) {
190 // 0.9b - 950b
191 $short_amount = number_format($num / 1000000000, $decimals, $formatparts[1], $formatparts[2]);
192 $type_amount = 'b';
193 } else {
194 // >= 0.9t
195 $short_amount = number_format($num / 1000000000000, $decimals, $formatparts[1], $formatparts[2]);
196 $type_amount = 't';
197 }
198
199 // unpad zeroes from the right
200 if ($decimals > 0) {
201 while (substr($short_amount, -1, 1) == '0') {
202 $short_amount = substr($short_amount, 0, strlen($short_amount) - 1);
203 }
204
205 if (substr($short_amount, -1, 1) == $formatparts[1]) {
206 // remove also the decimals separator if no more decimals
207 $short_amount = substr($short_amount, 0, strlen($short_amount) - 1);
208 }
209 }
210
211 // return the formatted amount string
212 return $short_amount . $type_amount;
213 }
214
215 /**
216 * Calculate the absolute percent amount between the current and previous financial stat.
217 * This method will use the following calculation: ((A1 - A2) / A2) * 100.
218 *
219 * @param float $stat the current (previously calculated) financial amount.
220 * @param float $compare the previous period financial amount.
221 * @param int $precision the precision for apply rounding.
222 *
223 * @return int|float the absolute percent amount calculated.
224 */
225 public function calcAbsPercent($current, $compare, $precision = 1)
226 {
227 if ($current == $compare) {
228 return 0;
229 }
230
231 if ($current > $compare && $compare < 1) {
232 return 100;
233 }
234
235 if ($current < $compare && $current < 1) {
236 return 100;
237 }
238
239 return round(abs(($current - $compare) / $compare * 100), $precision);
240 }
241
242 /**
243 * Obtain booking statistics from a range of dates and an
244 * optional list of room types. The data obtained will be
245 * based on the effective nights of stay within the range,
246 * unless type "booking_dates" to obtain different data.
247 *
248 * @param string $from the Y-m-d (or website) date from.
249 * @param string $to the Y-m-d (or website) date to.
250 * @param array $rooms list of room IDs to filter.
251 * @param string $type either "stay_dates" or "booking_dates".
252 *
253 * @return array associative list of information.
254 *
255 * @throws Exception
256 */
257 public function getStats($from, $to, array $rooms = [], $type = 'stay_dates')
258 {
259 $dbo = JFactory::getDbo();
260
261 // access the availability helper
262 $av_helper = VikBooking::getAvailabilityInstance();
263
264 $from_ts = VikBooking::getDateTimestamp($from, 0, 0);
265 $to_ts = VikBooking::getDateTimestamp($to, 23, 59, 59);
266 if (empty($from) || empty($from_ts) || empty($to_ts) || $to_ts < $from_ts) {
267 throw new Exception('Invalid dates provided', 500);
268 }
269
270 // total number of days (nights) in the range
271 $total_range_nights = $this->countNightsTo($to_ts, $from_ts);
272 if ($total_range_nights < 1) {
273 throw new Exception('Invalid number of days provided', 500);
274 }
275
276 // total number of room units
277 $total_room_units = $this->countRooms($rooms);
278
279 if ($total_room_units < 1) {
280 throw new Exception('Having no rooms published may lead to divisions by zero, hence errors.', 500);
281 }
282
283 // the associative array of statistics to collect and return
284 $stats = [
285 // booking ids involved
286 'bids' => [],
287 // total number of room units counted for stats
288 'room_units' => $total_room_units,
289 // total number of room units times the number of nights in the range
290 'tot_inventory' => ($total_room_units * $total_range_nights),
291 // number of rooms booked
292 'rooms_booked' => 0,
293 // number of bookings found
294 'tot_bookings' => 0,
295 // total number of nights booked (proportionally adjusted according to affected dates)
296 'nights_booked' => 0,
297 // percent value
298 'occupancy' => 0,
299 // average length of stay
300 'avg_los' => 0,
301 // points of sale revenue (revenue divided by each individual ota and ibe)
302 'pos_revenue' => [],
303 // list of countries with ranking
304 'country_ranks' => [],
305 // ibe revenue net
306 'ibe_revenue' => 0,
307 // otas revenue net (no matter if commissions were applied)
308 'ota_revenue' => 0,
309 // total refunded amounts (never deducted)
310 'tot_refunds' => 0,
311 // average daily rate
312 'adr' => 0,
313 // revenue per available room
314 'revpar' => 0,
315 // average booking window
316 'abw' => 0,
317 // amount of all taxes
318 'taxes' => 0,
319 // amount of VAT
320 'tot_vat' => 0,
321 // amount of city taxes
322 'city_taxes' => 0,
323 // amount of damage deposits
324 'damage_deposits' => 0,
325 // commissions (amount)
326 'cmms' => 0,
327 // net revenue before tax (otas + ibe)
328 'revenue' => 0,
329 // gross revenue after tax
330 'gross_revenue' => 0,
331 // otas total before tax (same as ota_revenue, but only if ota commissions were applied)
332 'ota_tot_net' => 0,
333 // otas commissions
334 'ota_cmms' => 0,
335 // percent value for average ota commissions amount
336 'ota_avg_cmms' => 0,
337 // commission savings amount
338 'cmm_savings' => 0,
339 // total cancelled reservations
340 'tot_cancellations' => 0,
341 // cancelled reservation IDs
342 'cancellation_ids' => [],
343 // total amount of cancelled bookings
344 'cancellations_amt' => 0,
345 ];
346
347 // get all (real/completed) bookings in the given range of dates
348 $q = $dbo->getQuery(true);
349 $q->select($dbo->qn([
350 'o.id',
351 'o.ts',
352 'o.status',
353 'o.days',
354 'o.checkin',
355 'o.checkout',
356 'o.totpaid',
357 'o.coupon',
358 'o.roomsnum',
359 'o.total',
360 'o.idorderota',
361 'o.channel',
362 'o.country',
363 'o.tot_taxes',
364 'o.tot_city_taxes',
365 'o.tot_fees',
366 'o.tot_damage_dep',
367 'o.cmms',
368 'o.refund',
369 'or.idorder',
370 'or.idroom',
371 'or.optionals',
372 'or.cust_cost',
373 'or.cust_idiva',
374 'or.extracosts',
375 'or.room_cost',
376 'c.country_name',
377 ]));
378 $q->from($dbo->qn('#__vikbooking_orders', 'o'));
379 $q->leftjoin($dbo->qn('#__vikbooking_ordersrooms', 'or') . ' ON ' . $dbo->qn('or.idorder') . ' = ' . $dbo->qn('o.id'));
380 $q->leftjoin($dbo->qn('#__vikbooking_countries', 'c') . ' ON ' . $dbo->qn('c.country_3_code') . ' = ' . $dbo->qn('o.country'));
381 $q->where($dbo->qn('o.total') . ' > 0');
382 $q->where($dbo->qn('o.closure') . ' = 0');
383 // use the "andWhere" method after having set some "where" clauses
384 $q->andWhere([
385 $dbo->qn('o.status') . ' = ' . $dbo->q('confirmed'),
386 $dbo->qn('o.status') . ' = ' . $dbo->q('cancelled'),
387 ], 'OR');
388 if ($type == 'stay_dates') {
389 // regular calculation based on stay dates
390 $q->where($dbo->qn('o.checkout') . ' >= ' . $from_ts);
391 $q->where($dbo->qn('o.checkin') . ' <= ' . $to_ts);
392 } else {
393 // calculation based on booked dates
394 $q->where($dbo->qn('o.ts') . ' >= ' . $from_ts);
395 $q->where($dbo->qn('o.ts') . ' <= ' . $to_ts);
396 }
397
398 /**
399 * Do not filter the room booking records by room ID, but always join them, to improve accuracy with calculations.
400 *
401 * @since 1.18.0 (J) - 1.8.0 (WP)
402 */
403 if ($rooms) {
404 // $q->where($dbo->qn('or.idroom') . ' IN (' . implode(', ', array_map('intval', $rooms)) . ')');
405 }
406
407 $q->order($dbo->qn('o.checkin') . ' ASC');
408 $q->order($dbo->qn('o.id') . ' ASC');
409 $q->order($dbo->qn('or.id') . ' ASC');
410
411 $dbo->setQuery($q);
412 $records = $dbo->loadAssocList();
413
414 if (!$records) {
415 // no bookings found, do not proceed
416 return $stats;
417 }
418
419 /**
420 * Join the busy records afterwards to support accurate calculations for split stays or early departures/late arrivals.
421 * This is also needed to support multi-room reservations without joining the busy records together with the room records.
422 *
423 * @since 1.18.0 (J) - 1.8.0 (WP)
424 */
425 $busy_joined = [];
426 foreach ($records as &$booking) {
427 if (strcasecmp($booking['status'], 'confirmed')) {
428 // we only want to target confirmed bookings
429 continue;
430 }
431
432 // build cache signature for busy records joined
433 $booking_room_signature = $booking['id'] . '-' . $booking['idroom'];
434
435 // build the db query
436 $dbo->setQuery(
437 $dbo->getQuery(true)
438 ->select($dbo->qn('ob.idbusy'))
439 ->select($dbo->qn('b.checkin', 'room_checkin'))
440 ->select($dbo->qn('b.checkout', 'room_checkout'))
441 ->from($dbo->qn('#__vikbooking_ordersbusy', 'ob'))
442 ->leftjoin($dbo->qn('#__vikbooking_busy', 'b') . ' ON ' . $dbo->qn('b.id') . ' = ' . $dbo->qn('ob.idbusy'))
443 ->where($dbo->qn('ob.idorder') . ' = ' . (int) $booking['id'])
444 ->where($dbo->qn('b.idroom') . ' = ' . (int) $booking['idroom'])
445 ->order($dbo->qn('b.id') . ' ASC')
446 );
447
448 // either fetch the records or use the previously cached ones to support multi-room bookings with equal room IDs
449 $busy_joined[$booking_room_signature] = $busy_joined[$booking_room_signature] ?? $dbo->loadAssocList();
450
451 // get the busy record to process for the current booking-room by shifting the list
452 $process_busy = array_shift($busy_joined[$booking_room_signature]);
453
454 if (!$process_busy) {
455 continue;
456 }
457
458 // inject busy details
459 $booking['idbusy'] = $process_busy['idbusy'];
460 $booking['room_checkin'] = $process_busy['room_checkin'];
461 $booking['room_checkout'] = $process_busy['room_checkout'];
462 }
463
464 // unset last reference and cached values
465 unset($booking, $busy_joined);
466
467 /**
468 * Immediately count cancellations and unset the records found.
469 *
470 * @since 1.16.10 (J) - 1.6.10 (WP)
471 */
472 foreach ($records as $k => $b) {
473 if (!strcasecmp($b['status'], 'cancelled')) {
474 if (!in_array($b['id'], $stats['cancellation_ids'])) {
475 // add statistics for the cancelled booking only once
476 $stats['tot_cancellations']++;
477 $stats['cancellations_amt'] += (float) $b['total'];
478 $stats['cancellation_ids'][] = $b['id'];
479 }
480
481 // get rid of this record to not alter the regular statistics
482 unset($records[$k]);
483 }
484 }
485
486 if ($stats['cancellation_ids']) {
487 // reset key values
488 $records = array_values($records);
489 }
490
491 // nest records with multiple rooms booked inside sub-arrays
492 $bookings = [];
493 foreach ($records as $b) {
494 if (!isset($bookings[$b['id']])) {
495 $bookings[$b['id']] = [];
496 }
497
498 // calculate the effective from and to stay timestamps for this room (by supporting split stays or early departures/late arrivals)
499 $room_checkin = !empty($b['room_checkin']) && $b['room_checkin'] != $b['checkin'] ? $b['room_checkin'] : $b['checkin'];
500 $room_checkout = !empty($b['room_checkout']) && $b['room_checkout'] != $b['checkout'] ? $b['room_checkout'] : $b['checkout'];
501 $in_info = getdate($room_checkin);
502 $out_info = getdate($room_checkout);
503 $b['stay_from_ts'] = mktime(0, 0, 0, $in_info['mon'], $in_info['mday'], $in_info['year']);
504 $b['stay_to_ts'] = mktime(23, 59, 59, $out_info['mon'], ($out_info['mday'] - 1), $out_info['year']);
505 $b['stay_nights'] = $room_checkin != $b['checkin'] || $room_checkout != $b['checkout'] ? $av_helper->countNightsOfStay($room_checkin, $room_checkout) : $b['days'];
506 $b['stay_nights'] = $b['stay_nights'] < 1 ? 1 : $b['stay_nights'];
507
508 // push room-booking
509 $bookings[$b['id']][] = $b;
510 }
511
512 // free memory up
513 unset($records);
514
515 // counters and pools
516 $los_counter = 0;
517 $pos_pool = [];
518 $pos_counter = [];
519 $countries = [];
520 $country_map = [];
521
522 // sum of booking window
523 $sum_booking_window = 0;
524
525 // parse all bookings
526 foreach ($bookings as $bid => $booking) {
527 // push booking ID
528 $stats['bids'][] = $bid;
529
530 // increase tot bookings and los counter
531 if (!$rooms || ($rooms && array_intersect(array_column($booking, 'idroom'), $rooms))) {
532 // make sure to increase counters only if filtered listing(s) involved or if no filters set
533 $stats['tot_bookings']++;
534 $los_counter += $booking[0]['days'];
535 }
536
537 // count rooms booked within the reservation
538 $booking_rooms = count($booking);
539
540 // define the total booking amount for multi-room reservations
541 $multi_room_total = 0;
542
543 if ($rooms && $booking[0]['roomsnum'] > 1) {
544 // when filters applied to multi-room bookings, count the effective number of rooms involved
545 $booking_rooms = count(array_intersect(array_column($booking, 'idroom'), $rooms));
546
547 // overwrite room total amount when filtering by listing(s)
548 foreach ($booking as $room_booking) {
549 if (!in_array($room_booking['idroom'], $rooms)) {
550 continue;
551 }
552 if ($room_booking['cust_cost'] > 0) {
553 $multi_room_total += $room_booking['cust_cost'];
554 } elseif ($room_booking['room_cost'] > 0) {
555 $multi_room_total += $room_booking['room_cost'];
556 }
557 }
558 }
559
560 // point of sale name
561 $pos_name = null;
562
563 // parse all rooms booked
564 foreach ($booking as $room_booking) {
565 if ($rooms && !in_array($room_booking['idroom'], $rooms)) {
566 // room booked is excluded from filters
567 continue;
568 }
569
570 // increase rooms booked
571 $stats['rooms_booked']++;
572
573 // number of nights affected
574 $los_affected = $room_booking['stay_nights'];
575
576 // use default total values
577 $room_total = $multi_room_total ?: $room_booking['total'];
578 $room_cmms = $room_booking['cmms'];
579 $room_refund = $room_booking['refund'];
580 $room_tot_taxes = $room_booking['tot_taxes'];
581 $room_tot_city_taxes = $room_booking['tot_city_taxes'];
582 $room_tot_damage_dep = $room_booking['tot_damage_dep'];
583 $room_tot_fees = $room_booking['tot_fees'];
584
585 // check if amounts must be calculated proportionally for the range of dates requested
586 if ($type == 'stay_dates' && ($room_booking['stay_from_ts'] < $from_ts || $room_booking['stay_to_ts'] > $to_ts)) {
587 // calculate number of nights of stay affected
588 $los_affected = $this->countNightsAffected($from_ts, $to_ts, $room_booking['stay_from_ts'], $room_booking['stay_to_ts']);
589
590 // adjust the amounts proportionally
591 $room_total = $room_total * $los_affected / $room_booking['stay_nights'];
592 $room_cmms = $room_cmms * $los_affected / $room_booking['stay_nights'];
593 $room_refund = $room_refund * $los_affected / $room_booking['stay_nights'];
594 $room_tot_taxes = $room_tot_taxes * $los_affected / $room_booking['stay_nights'];
595 $room_tot_city_taxes = $room_tot_city_taxes * $los_affected / $room_booking['stay_nights'];
596 $room_tot_damage_dep = $room_tot_damage_dep * $los_affected / $room_booking['stay_nights'];
597 $room_tot_fees = $room_tot_fees * $los_affected / $room_booking['stay_nights'];
598 }
599
600 // apply average values per room booked (with filters or booked in total)
601 $room_total /= $booking_rooms;
602 $room_cmms /= $booking_rooms;
603 $room_refund /= $booking_rooms;
604 $room_tot_taxes /= $booking_rooms;
605 $room_tot_city_taxes /= $booking_rooms;
606 $room_tot_damage_dep /= $room_booking['roomsnum'];
607 $room_tot_fees /= $booking_rooms;
608
609 // calculate and sum average values per room booked
610 $tot_net = $multi_room_total ?: ($room_total - (float) $room_tot_taxes - (float) $room_tot_city_taxes - (float) $room_tot_fees - (float) $room_tot_damage_dep - (float) $room_cmms);
611 $tot_revenue = $multi_room_total ? ($tot_net / $booking_rooms) : $tot_net;
612 $stats['revenue'] += $tot_revenue;
613 $stats['gross_revenue'] += $room_total;
614 $stats['nights_booked'] += $los_affected;
615
616 // increase booking window for this room-booking
617 $booking_window = $this->countDaysDiff($room_booking['ts'], $room_booking['stay_from_ts']);
618 $sum_booking_window += $booking_window >= 0 ? $booking_window : 0;
619
620 // increase country stats
621 $country_code = !empty($room_booking['country']) ? $room_booking['country'] : 'unknown';
622 if (!isset($countries[$country_code])) {
623 $countries[$country_code] = 0;
624 if (!empty($room_booking['country_name'])) {
625 $country_map[$country_code] = $room_booking['country_name'];
626 }
627 }
628 $countries[$country_code] += $tot_revenue;
629
630 if (!empty($room_booking['idorderota']) && !empty($room_booking['channel'])) {
631 $stats['ota_revenue'] += $tot_revenue;
632 if ($room_cmms > 0) {
633 $stats['ota_tot_net'] += $tot_revenue;
634 $stats['ota_cmms'] += $room_cmms;
635 }
636 // set pos name
637 $channel_parts = explode('_', $room_booking['channel']);
638 $pos_name = trim($channel_parts[0]);
639 } else {
640 $stats['ibe_revenue'] += $tot_revenue;
641 // set pos name
642 $pos_name = 'website';
643 }
644
645 // set pos net revenue
646 if (!isset($pos_pool[$pos_name])) {
647 $pos_pool[$pos_name] = 0;
648 }
649 $pos_pool[$pos_name] += $tot_revenue;
650
651 $stats['taxes'] += (float) $room_tot_taxes + (float) $room_tot_city_taxes + (float) $room_tot_fees;
652 $stats['tot_vat'] += (float) $room_tot_taxes;
653 $stats['city_taxes'] += (float) $room_tot_city_taxes;
654 $stats['damage_deposits'] += (float) $room_tot_damage_dep;
655 $stats['cmms'] += (float) $room_cmms;
656 $stats['tot_refunds'] += $room_refund;
657 }
658
659 if ($pos_name) {
660 // increase number of bookings for this pos
661 if (!isset($pos_counter[$pos_name])) {
662 $pos_counter[$pos_name] = 0;
663 }
664 $pos_counter[$pos_name]++;
665 }
666 }
667
668 // count the average length of stay (no proportional data for the dates requested)
669 $stats['avg_los'] = $stats['tot_bookings'] > 0 ? round($los_counter / $stats['tot_bookings'], 1) : 0;
670
671 // calculate occupancy percent value
672 $stats['occupancy'] = round(($stats['nights_booked'] * 100 / ($total_room_units * $total_range_nights)), 2);
673
674 // count the average daily rate (ADR)
675 $stats['adr'] = $stats['rooms_booked'] > 0 ? $stats['revenue'] / $stats['rooms_booked'] / $total_range_nights : 0;
676
677 // count the revenue per available room (RevPAR)
678 $stats['revpar'] = $stats['revenue'] / $total_room_units;
679
680 // count the average booking window (ABW - number of days between the reservation date and the check-in date)
681 $stats['abw'] = $stats['rooms_booked'] > 0 ? $sum_booking_window / $stats['rooms_booked'] : 0;
682
683 // count OTAs average commission amount
684 if ($stats['ota_tot_net'] > 0 && $stats['ota_cmms'] > 0) {
685 // find the average percent value of OTA commissions (tot_net : tot_cmms = 100 : x)
686 $stats['ota_avg_cmms'] = round(($stats['ota_cmms'] * 100 / $stats['ota_tot_net']), 2);
687
688 if ($stats['ibe_revenue'] > 0) {
689 // calculate the commission savings amount
690 $stats['cmm_savings'] = $stats['ibe_revenue'] * $stats['ota_avg_cmms'] / 100;
691 }
692 }
693
694 // get channel logos helper
695 $vcm_logos = VikBooking::getVcmChannelsLogo('', true);
696
697 // sort and build pos revenues
698 if ($pos_pool && $stats['revenue'] > 0) {
699 // apply sorting descending
700 arsort($pos_pool);
701 // build readable pos values
702 foreach ($pos_pool as $pos_name => $pos_revenue) {
703 $pos_data = [
704 'name' => $pos_name,
705 'revenue' => $pos_revenue,
706 'pcent' => round(($pos_revenue * 100 / $stats['revenue']), 2),
707 'logo' => null,
708 'bookings' => isset($pos_counter[$pos_name]) ? $pos_counter[$pos_name] : 0,
709 'ibe' => false,
710 'ota' => false,
711 ];
712 if (!strcasecmp($pos_name, 'website')) {
713 // ibe revenue
714 $pos_data['name'] = JText::translate('VBORDFROMSITE');
715 $pos_data['ibe'] = true;
716 } else {
717 // ota revenue
718 $pos_data['ota'] = true;
719 if (is_object($vcm_logos)) {
720 $ota_logo_img = $vcm_logos->setProvenience($pos_name)->getSmallLogoURL();
721 if ($ota_logo_img !== false) {
722 $pos_data['logo'] = $ota_logo_img;
723 }
724 }
725 }
726 // push pos data
727 $stats['pos_revenue'][] = $pos_data;
728 }
729 }
730
731 // sort countries revenue
732 if ($countries && $stats['revenue'] > 0) {
733 // apply sorting descending
734 arsort($countries);
735 // build readable values
736 foreach ($countries as $country_code => $country_revenue) {
737 // push country data
738 $stats['country_ranks'][] = [
739 'code' => $country_code,
740 'name' => (isset($country_map[$country_code]) ? $country_map[$country_code] : $country_code),
741 'revenue' => $country_revenue,
742 'pcent' => round(($country_revenue * 100 / $stats['revenue']), 2),
743 ];
744 }
745 }
746
747 // return the statistics information
748 return $stats;
749 }
750
751 /**
752 * Returns the information of the given rate plan ID.
753 *
754 * @param int $rplan_id The rate plan ID.
755 *
756 * @return ?array
757 *
758 * @since 1.18.2 (J) - 1.8.2 (WP)
759 */
760 public function getRatePlanData(int $rplan_id)
761 {
762 if (empty($rplan_id)) {
763 return null;
764 }
765
766 $dbo = JFactory::getDbo();
767
768 $dbo->setQuery(
769 $dbo->getQuery(true)
770 ->select('*')
771 ->from($dbo->qn('#__vikbooking_prices'))
772 ->where($dbo->qn('id') . ' = ' . $rplan_id)
773 );
774
775 return $dbo->loadAssoc();
776 }
777
778 /**
779 * Counts the number of nights involved in a range of dates. This is
780 * useful to proportionally calculate the amounts to be used.
781 *
782 * @param int $from_ts the 00:00:00 timestamp of the range start date.
783 * @param int $to_ts the 23:59:59 timestamp of the range end date.
784 * @param int $in_ts the 00:00:00 timestamp of the check-in date.
785 * @param int $out_ts the 23:59:59 timestamp of the last night of stay (check-out -1).
786 *
787 * @return int the number of stay nights involved in the range.
788 */
789 protected function countNightsAffected($from_ts, $to_ts, $in_ts, $out_ts)
790 {
791 $nights_affected = 0;
792
793 if ($from_ts > $to_ts) {
794 return $nights_affected;
795 }
796
797 $range_from_info = getdate($from_ts);
798 while ($range_from_info[0] < $to_ts) {
799 if ($range_from_info[0] >= $in_ts && $range_from_info[0] <= $out_ts) {
800 $nights_affected++;
801 }
802 // next day iteration
803 $range_from_info = getdate(mktime(0, 0, 0, $range_from_info['mon'], ($range_from_info['mday'] + 1), $range_from_info['year']));
804 }
805
806 return $nights_affected;
807 }
808 }
809