PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.7.2
VikBooking Hotel Booking Engine & PMS v1.7.2
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.7.2, at admin/helpers/src/taxonomy/finance.php

687 lines 21.5 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, $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 taxes
318 'taxes' => 0,
319 // commissions (amount)
320 'cmms' => 0,
321 // net revenue before tax (otas + ibe)
322 'revenue' => 0,
323 // gross revenue after tax
324 'gross_revenue' => 0,
325 // otas total before tax (same as ota_revenue, but only if ota commissions were applied)
326 'ota_tot_net' => 0,
327 // otas commissions
328 'ota_cmms' => 0,
329 // percent value for average ota commissions amount
330 'ota_avg_cmms' => 0,
331 // commission savings amount
332 'cmm_savings' => 0,
333 // total cancelled reservations
334 'tot_cancellations' => 0,
335 // cancelled reservation IDs
336 'cancellation_ids' => [],
337 // total amount of cancelled bookings
338 'cancellations_amt' => 0,
339 ];
340
341 // get all (real/completed) bookings in the given range of dates
342 $q = $dbo->getQuery(true);
343 $q->select($dbo->qn([
344 'o.id',
345 'o.ts',
346 'o.status',
347 'o.days',
348 'o.checkin',
349 'o.checkout',
350 'o.totpaid',
351 'o.roomsnum',
352 'o.total',
353 'o.idorderota',
354 'o.channel',
355 'o.country',
356 'o.tot_taxes',
357 'o.tot_city_taxes',
358 'o.tot_fees',
359 'o.cmms',
360 'o.refund',
361 'or.idorder',
362 'or.idroom',
363 'or.optionals',
364 'or.cust_cost',
365 'or.cust_idiva',
366 'or.extracosts',
367 'or.room_cost',
368 'ob.idbusy',
369 'c.country_name',
370 ]));
371 $q->select($dbo->qn('b.checkin', 'room_checkin'));
372 $q->select($dbo->qn('b.checkout', 'room_checkout'));
373 $q->from($dbo->qn('#__vikbooking_orders', 'o'));
374 $q->leftjoin($dbo->qn('#__vikbooking_ordersrooms', 'or') . ' ON ' . $dbo->qn('or.idorder') . ' = ' . $dbo->qn('o.id'));
375 $q->leftjoin($dbo->qn('#__vikbooking_ordersbusy', 'ob') . ' ON ' . $dbo->qn('ob.idorder') . ' = ' . $dbo->qn('o.id'));
376 $q->leftjoin($dbo->qn('#__vikbooking_busy', 'b') . ' ON ' . $dbo->qn('b.id') . ' = ' . $dbo->qn('ob.idbusy'));
377 $q->leftjoin($dbo->qn('#__vikbooking_countries', 'c') . ' ON ' . $dbo->qn('c.country_3_code') . ' = ' . $dbo->qn('o.country'));
378 $q->where($dbo->qn('o.total') . ' > 0');
379 $q->where($dbo->qn('o.closure') . ' = 0');
380 // use the "andWhere" method after having set some "where" clauses
381 $q->andWhere([
382 $dbo->qn('o.status') . ' = ' . $dbo->q('confirmed'),
383 $dbo->qn('o.status') . ' = ' . $dbo->q('cancelled'),
384 ], 'OR');
385 if ($type == 'stay_dates') {
386 // regular calculation based on stay dates
387 $q->where($dbo->qn('o.checkout') . ' >= ' . $from_ts);
388 $q->where($dbo->qn('o.checkin') . ' <= ' . $to_ts);
389 } else {
390 // calculation based on booked dates
391 $q->where($dbo->qn('o.ts') . ' >= ' . $from_ts);
392 $q->where($dbo->qn('o.ts') . ' <= ' . $to_ts);
393 }
394
395 if (is_array($rooms) && $rooms) {
396 $rooms = array_map('intval', $rooms);
397 $q->where($dbo->qn('or.idroom') . ' IN (' . implode(', ', $rooms) . ')');
398 }
399
400 $q->order($dbo->qn('o.checkin') . ' ASC');
401 $q->order($dbo->qn('o.id') . ' ASC');
402
403 $dbo->setQuery($q);
404 $records = $dbo->loadAssocList();
405
406 if (!$records) {
407 // no bookings found, do not proceed
408 return $stats;
409 }
410
411 /**
412 * Immediately count cancellations and unset the records found.
413 *
414 * @since 1.16.10 (J) - 1.6.10 (WP)
415 */
416 foreach ($records as $k => $b) {
417 if (!strcasecmp($b['status'], 'cancelled')) {
418 if (!in_array($b['id'], $stats['cancellation_ids'])) {
419 // add statistics for the cancelled booking only once
420 $stats['tot_cancellations']++;
421 $stats['cancellations_amt'] += (float) $b['total'];
422 $stats['cancellation_ids'][] = $b['id'];
423 }
424
425 // get rid of this record to not alter the regular statistics
426 unset($records[$k]);
427 }
428 }
429
430 if ($stats['cancellation_ids']) {
431 // reset key values
432 $records = array_values($records);
433 }
434
435 // nest records with multiple rooms booked inside sub-arrays
436 $bookings = [];
437 foreach ($records as $b) {
438 if (!isset($bookings[$b['id']])) {
439 $bookings[$b['id']] = [];
440 }
441 // calculate the effective from and to stay timestamps for this room (by supporting split stays or early departures/late arrivals)
442 $room_checkin = !empty($b['room_checkin']) && $b['room_checkin'] != $b['checkin'] ? $b['room_checkin'] : $b['checkin'];
443 $room_checkout = !empty($b['room_checkout']) && $b['room_checkout'] != $b['checkout'] ? $b['room_checkout'] : $b['checkout'];
444 $in_info = getdate($room_checkin);
445 $out_info = getdate($room_checkout);
446 $b['stay_from_ts'] = mktime(0, 0, 0, $in_info['mon'], $in_info['mday'], $in_info['year']);
447 $b['stay_to_ts'] = mktime(23, 59, 59, $out_info['mon'], ($out_info['mday'] - 1), $out_info['year']);
448 $b['stay_nights'] = $room_checkin != $b['checkin'] || $room_checkout != $b['checkout'] ? $av_helper->countNightsOfStay($room_checkin, $room_checkout) : $b['days'];
449 $b['stay_nights'] = $b['stay_nights'] < 1 ? 1 : $b['stay_nights'];
450 // push room-booking
451 $bookings[$b['id']][] = $b;
452 }
453
454 // free memory up
455 unset($records);
456
457 // counters and pools
458 $los_counter = 0;
459 $pos_pool = [];
460 $pos_counter = [];
461 $countries = [];
462 $country_map = [];
463
464 // sum of booking window
465 $sum_booking_window = 0;
466
467 // parse all bookings
468 foreach ($bookings as $bid => $booking) {
469 // push booking ID
470 $stats['bids'][] = $bid;
471
472 // increase tot bookings and los counter
473 $stats['tot_bookings']++;
474 $los_counter += $booking[0]['days'];
475
476 // use default total values
477 $booking_rooms = count($booking);
478 $room_total = $booking[0]['total'];
479 $room_cmms = $booking[0]['cmms'];
480 $room_refund = $booking[0]['refund'];
481 $room_tot_taxes = $booking[0]['tot_taxes'];
482 $room_tot_city_taxes = $booking[0]['tot_city_taxes'];
483 $room_tot_fees = $booking[0]['tot_fees'];
484
485 // point of sale name
486 $pos_name = null;
487
488 // parse all rooms booked
489 foreach ($booking as $room_booking) {
490 // increase rooms booked
491 $stats['rooms_booked']++;
492
493 // number of nights affected
494 $los_affected = $room_booking['stay_nights'];
495
496 // check if amounts must be calculated proportionally for the range of dates requested
497 if ($type == 'stay_dates' && ($room_booking['stay_from_ts'] < $from_ts || $room_booking['stay_to_ts'] > $to_ts)) {
498 // calculate number of nights of stay affected
499 $los_affected = $this->countNightsAffected($from_ts, $to_ts, $room_booking['stay_from_ts'], $room_booking['stay_to_ts']);
500
501 // adjust the amounts proportionally
502 $room_total = $room_total * $los_affected / $room_booking['stay_nights'];
503 $room_cmms = $room_cmms * $los_affected / $room_booking['stay_nights'];
504 $room_refund = $room_refund * $los_affected / $room_booking['stay_nights'];
505 $room_tot_taxes = $room_tot_taxes * $los_affected / $room_booking['stay_nights'];
506 $room_tot_city_taxes = $room_tot_city_taxes * $los_affected / $room_booking['stay_nights'];
507 $room_tot_fees = $room_tot_fees * $los_affected / $room_booking['stay_nights'];
508 }
509
510 // apply average values per room booked
511 $room_total /= $booking_rooms;
512 $room_cmms /= $booking_rooms;
513 $room_refund /= $booking_rooms;
514 $room_tot_taxes /= $booking_rooms;
515 $room_tot_city_taxes /= $booking_rooms;
516 $room_tot_fees /= $booking_rooms;
517
518 // calculate and sum average values per room booked
519 $tot_net = $room_total - (float)$room_tot_taxes - (float)$room_tot_city_taxes - (float)$room_tot_fees - (float)$room_cmms;
520 $stats['revenue'] += $tot_net;
521 $stats['gross_revenue'] += $room_total;
522 $stats['nights_booked'] += $los_affected;
523
524 // increase booking window for this room-booking
525 $booking_window = $this->countDaysDiff($room_booking['ts'], $room_booking['stay_from_ts']);
526 $sum_booking_window += $booking_window >= 0 ? $booking_window : 0;
527
528 // increase country stats
529 $country_code = !empty($room_booking['country']) ? $room_booking['country'] : 'unknown';
530 if (!isset($countries[$country_code])) {
531 $countries[$country_code] = 0;
532 if (!empty($room_booking['country_name'])) {
533 $country_map[$country_code] = $room_booking['country_name'];
534 }
535 }
536 $countries[$country_code] += $tot_net;
537
538 if (!empty($room_booking['idorderota']) && !empty($room_booking['channel'])) {
539 $stats['ota_revenue'] += $tot_net;
540 if ($room_cmms > 0) {
541 $stats['ota_tot_net'] += $tot_net;
542 $stats['ota_cmms'] += $room_cmms;
543 }
544 // set pos name
545 $channel_parts = explode('_', $room_booking['channel']);
546 $pos_name = trim($channel_parts[0]);
547 } else {
548 $stats['ibe_revenue'] += $tot_net;
549 // set pos name
550 $pos_name = 'website';
551 }
552
553 // set pos net revenue
554 if (!isset($pos_pool[$pos_name])) {
555 $pos_pool[$pos_name] = 0;
556 }
557 $pos_pool[$pos_name] += $tot_net;
558
559 $stats['taxes'] += (float)$room_tot_taxes + (float)$room_tot_city_taxes + (float)$room_tot_fees;
560 $stats['cmms'] += (float)$room_cmms;
561 $stats['tot_refunds'] += $room_refund;
562 }
563
564 if ($pos_name) {
565 // increase number of bookings for this pos
566 if (!isset($pos_counter[$pos_name])) {
567 $pos_counter[$pos_name] = 0;
568 }
569 $pos_counter[$pos_name]++;
570 }
571 }
572
573 // count the average length of stay (no proportional data for the dates requested)
574 $stats['avg_los'] = $stats['tot_bookings'] > 0 ? round($los_counter / $stats['tot_bookings'], 1) : 0;
575
576 // calculate occupancy percent value
577 $stats['occupancy'] = round(($stats['nights_booked'] * 100 / ($total_room_units * $total_range_nights)), 2);
578
579 // count the average daily rate (ADR)
580 $stats['adr'] = $stats['rooms_booked'] > 0 ? $stats['revenue'] / $stats['rooms_booked'] / $total_range_nights : 0;
581
582 // count the revenue per available room (RevPAR)
583 $stats['revpar'] = $stats['revenue'] / $total_room_units;
584
585 // count the average booking window (ABW - number of days between the reservation date and the check-in date)
586 $stats['abw'] = $stats['rooms_booked'] > 0 ? $sum_booking_window / $stats['rooms_booked'] : 0;
587
588 // count OTAs average commission amount
589 if ($stats['ota_tot_net'] > 0 && $stats['ota_cmms'] > 0) {
590 // find the average percent value of OTA commissions (tot_net : tot_cmms = 100 : x)
591 $stats['ota_avg_cmms'] = round(($stats['ota_cmms'] * 100 / $stats['ota_tot_net']), 2);
592
593 if ($stats['ibe_revenue'] > 0) {
594 // calculate the commission savings amount
595 $stats['cmm_savings'] = $stats['ibe_revenue'] * $stats['ota_avg_cmms'] / 100;
596 }
597 }
598
599 // get channel logos helper
600 $vcm_logos = VikBooking::getVcmChannelsLogo('', true);
601
602 // sort and build pos revenues
603 if ($pos_pool && $stats['revenue'] > 0) {
604 // apply sorting descending
605 arsort($pos_pool);
606 // build readable pos values
607 foreach ($pos_pool as $pos_name => $pos_revenue) {
608 $pos_data = [
609 'name' => $pos_name,
610 'revenue' => $pos_revenue,
611 'pcent' => round(($pos_revenue * 100 / $stats['revenue']), 2),
612 'logo' => null,
613 'bookings' => isset($pos_counter[$pos_name]) ? $pos_counter[$pos_name] : 0,
614 'ibe' => false,
615 'ota' => false,
616 ];
617 if (!strcasecmp($pos_name, 'website')) {
618 // ibe revenue
619 $pos_data['name'] = JText::translate('VBORDFROMSITE');
620 $pos_data['ibe'] = true;
621 } else {
622 // ota revenue
623 $pos_data['ota'] = true;
624 if (is_object($vcm_logos)) {
625 $ota_logo_img = $vcm_logos->setProvenience($pos_name)->getSmallLogoURL();
626 if ($ota_logo_img !== false) {
627 $pos_data['logo'] = $ota_logo_img;
628 }
629 }
630 }
631 // push pos data
632 $stats['pos_revenue'][] = $pos_data;
633 }
634 }
635
636 // sort countries revenue
637 if (count($countries) && $stats['revenue'] > 0) {
638 // apply sorting descending
639 arsort($countries);
640 // build readable values
641 foreach ($countries as $country_code => $country_revenue) {
642 // push country data
643 $stats['country_ranks'][] = [
644 'code' => $country_code,
645 'name' => (isset($country_map[$country_code]) ? $country_map[$country_code] : $country_code),
646 'revenue' => $country_revenue,
647 'pcent' => round(($country_revenue * 100 / $stats['revenue']), 2),
648 ];
649 }
650 }
651
652 // return the statistics information
653 return $stats;
654 }
655
656 /**
657 * Counts the number of nights involved in a range of dates. This is
658 * useful to proportionally calculate the amounts to be used.
659 *
660 * @param int $from_ts the 00:00:00 timestamp of the range start date.
661 * @param int $to_ts the 23:59:59 timestamp of the range end date.
662 * @param int $in_ts the 00:00:00 timestamp of the check-in date.
663 * @param int $out_ts the 23:59:59 timestamp of the last night of stay (check-out -1).
664 *
665 * @return int the number of stay nights involved in the range.
666 */
667 protected function countNightsAffected($from_ts, $to_ts, $in_ts, $out_ts)
668 {
669 $nights_affected = 0;
670
671 if ($from_ts > $to_ts) {
672 return $nights_affected;
673 }
674
675 $range_from_info = getdate($from_ts);
676 while ($range_from_info[0] < $to_ts) {
677 if ($range_from_info[0] >= $in_ts && $range_from_info[0] <= $out_ts) {
678 $nights_affected++;
679 }
680 // next day iteration
681 $range_from_info = getdate(mktime(0, 0, 0, $range_from_info['mon'], ($range_from_info['mday'] + 1), $range_from_info['year']));
682 }
683
684 return $nights_affected;
685 }
686 }
687