# vikbooking/1.8.15/admin/helpers/src/taxonomy/finance.php

VikBooking Hotel Booking Engine &amp; PMS, version 1.8.15. 885 lines.

- Page: https://pluginprobe.com/plugins/vikbooking/1.8.15/code/admin/helpers/src/taxonomy/finance.php
- Raw: https://pluginprobe.com/plugins/vikbooking/1.8.15/raw/admin/helpers/src/taxonomy/finance.php
- Modified: 2026-09-14T11:37:40+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/vikbooking/1.8.15/code/admin/helpers/src/taxonomy/finance.php#L10-L20`.

```php
<?php
/** 
 * @package     VikBooking
 * @subpackage  core
 * @author      E4J s.r.l.
 * @copyright   Copyright (C) 2022 E4J s.r.l. All Rights Reserved.
 * @license     http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL
 * @link        https://vikwp.com
 */

// No direct access
defined('ABSPATH') or die('No script kiddies please!');

/**
 * Taxonomy finance helper class.
 * 
 * @since   1.16.0 (J) - 1.6.0 (WP)
 */
class VBOTaxonomyFinance
{
    /**
     * The singleton instance of the class.
     *
     * @var VBOTaxonomyFinance
     */
    protected static $instance = null;

    /**
     * Finance calculation options.
     *
     * @since  1.18.13 (J) - 1.8.13 (WP)
     */
    protected array $options = [];

    /**
     * Class constructor is protected.
     * 
     * @param   array   $options    Options to bind.
     *
     * @see     getInstance()
     */
    protected function __construct(array $options)
    {
        // bind options
        $this->options = $options;
    }

    /**
     * Returns the global object, either a new instance,
     * or the previously instantiated instance.
     * 
     * @param   ?array   $options    Optional options to bind.
     *
     * @return  VBOTaxonomyFinance
     */
    public static function getInstance(?array $options = null)
    {
        if (is_null(static::$instance)) {
            static::$instance = new static((array) $options);
        }

        return static::$instance;
    }

    /**
     * Returns the current calculation options.
     * 
     * @return  array
     * 
     * @since   1.18.13 (J) - 1.8.13 (WP)
     */
    public function getOptions()
    {
        return $this->options;
    }

    /**
     * Binds calculation options.
     * 
     * @param   array   $options    Options to bind.
     * 
     * @return  VBOTaxonomyFinance
     * 
     * @since   1.18.13 (J) - 1.8.13 (WP)
     */
    public function setOptions(array $options)
    {
        // bind options
        $this->options = $options;

        return $this;
    }

    /**
     * Returns the number of total units for all rooms, or for a specific room.
     * By default, the rooms unpublished are skipped, and all rooms are used.
     * 
     * @param   mixed   $idroom     int or array.
     * @param   bool    $published  true or false.
     *
     * @return  int
     */
    public function countRooms($idroom = 0, $published = true)
    {
        $dbo = JFactory::getDbo();

        $totrooms = 0;
        $clauses = [];

        if (is_int($idroom) && $idroom > 0) {
            $clauses[] = "`id`=" . (int)$idroom;
        } elseif (is_array($idroom) && count($idroom)) {
            $idroom = array_map('intval', $idroom);
            $clauses[] = "`id` IN (" . implode(', ', $idroom) . ")";
        }

        if ($published) {
            $clauses[] = "`avail`=1";
        }

        $q = "SELECT SUM(`units`) FROM `#__vikbooking_rooms`" . (count($clauses) ? " WHERE " . implode(' AND ', $clauses) : "");
        $dbo->setQuery($q);
        $totrooms = (int)$dbo->loadResult();

        return $totrooms;
    }

    /**
     * Counts the number of nights of difference between two timestamps.
     * 
     * @param   int     $to_ts      the target end date timestamp.
     * @param   int     $from_ts    the starting date timestamp.
     * 
     * @return  int     the nights of difference between from and to timestamps.
     */
    public function countNightsTo($to_ts, $from_ts = 0)
    {
        if (empty($from_ts)) {
            $from_ts = time();
        }

        $from_ymd = date('Y-m-d', $from_ts);
        $to_ymd = date('Y-m-d', $to_ts);

        if ($from_ymd == $to_ymd) {
            return 1;
        }

        $from_date = new DateTime($from_ymd);
        $to_date   = new DateTime($to_ymd);
        $daysdiff  = (int)$from_date->diff($to_date)->format('%a');

        if ($to_ts < $from_ts) {
            // we need a negative integer number in this case
            $daysdiff = $daysdiff - ($daysdiff * 2);
        }

        if ($from_ymd != $to_ymd && $daysdiff > 0) {
            // the to date is actually another night of stay
            $daysdiff += 1;
        }

        return $daysdiff;
    }

    /**
     * Counts the number of days of difference between two timestamps.
     * 
     * @param   int     $from_ts    the starting date timestamp.
     * @param   int     $to_ts      the target end date timestamp.
     * 
     * @return  int     the days of difference from the given dates.
     * 
     * @since   1.16.10 (J) - 1.6.10 (WP)
     */
    public function countDaysDiff($from_ts, $to_ts)
    {
        if (empty($from_ts) || empty($to_ts)) {
            return 0;
        }

        $from_ymd = date('Y-m-d', $from_ts);
        $to_ymd = date('Y-m-d', $to_ts);

        if ($from_ymd == $to_ymd) {
            return 0;
        }

        $from_date = new DateTime($from_ymd);
        $to_date   = new DateTime($to_ymd);
        $daysdiff  = (int)$from_date->diff($to_date)->format('%a');

        if ($to_ts < $from_ts) {
            // we need a negative integer number in this case
            $daysdiff = $daysdiff - ($daysdiff * 2);
        }

        return $daysdiff;
    }


    /**
     * Helper method to format long currency values into short numbers.
     * I.e. 2.600.000 = 2.6M (empty decimals are always removed to keep the string short).
     * 
     * @param   int|float   $num        the amount to format.
     * @param   int         $decimals   the precision to use.
     * 
     * @return  string                  the formatted amount string.
     */
    public function numberFormatShort($num, $decimals = 1)
    {
        // get global formatting values
        $formatvals  = VikBooking::getNumberFormatData();
        $formatparts = explode(':', $formatvals);

        if ($num < 951) {
            // 0 - 950
            $short_amount = number_format($num, $decimals, $formatparts[1], $formatparts[2]);
            $type_amount  = '';
        } elseif ($num < 900000) {
            // 1k - 950k
            $short_amount = number_format($num / 1000, $decimals, $formatparts[1], $formatparts[2]);
            $type_amount  = 'k';
        } elseif ($num < 900000000) {
            // 0.9m - 950m
            $short_amount = number_format($num / 1000000, $decimals, $formatparts[1], $formatparts[2]);
            $type_amount  = 'm';
        } elseif ($num < 900000000000) {
            // 0.9b - 950b
            $short_amount = number_format($num / 1000000000, $decimals, $formatparts[1], $formatparts[2]);
            $type_amount  = 'b';
        } else {
            // >= 0.9t
            $short_amount = number_format($num / 1000000000000, $decimals, $formatparts[1], $formatparts[2]);
            $type_amount  = 't';
        }

        // unpad zeroes from the right
        if ($decimals > 0) {
            while (substr($short_amount, -1, 1) == '0') {
                $short_amount = substr($short_amount, 0, strlen($short_amount) - 1);
            }

            if (substr($short_amount, -1, 1) == $formatparts[1]) {
                // remove also the decimals separator if no more decimals
                $short_amount = substr($short_amount, 0, strlen($short_amount) - 1);
            }
        }
        
        // return the formatted amount string
        return $short_amount . $type_amount;
    }

    /**
     * Calculate the absolute percent amount between the current and previous financial stat.
     * This method will use the following calculation: ((A1 - A2) / A2) * 100.
     * 
     * @param   float   $stat       the current (previously calculated) financial amount.
     * @param   float   $compare    the previous period financial amount.
     * @param   int     $precision  the precision for apply rounding.
     * 
     * @return  int|float           the absolute percent amount calculated.
     */
    public function calcAbsPercent($current, $compare, $precision = 1)
    {
        if ($current == $compare) {
            return 0;
        }

        if ($current > $compare && $compare < 1) {
            return 100;
        }

        if ($current < $compare && $current < 1) {
            return 100;
        }

        return round(abs(($current - $compare) / $compare * 100), $precision);
    }

    /**
     * Obtain booking statistics from a range of dates and an
     * optional list of room types. The data obtained will be
     * based on the effective nights of stay within the range,
     * unless type "booking_dates" to obtain different data.
     * 
     * @param   string  $from   the Y-m-d (or website) date from.
     * @param   string  $to     the Y-m-d (or website) date to.
     * @param   array   $rooms  list of room IDs to filter.
     * @param   string  $type   one among "stay_dates", "booking_dates" or "checkin".
     * 
     * @return  array           associative list of information.
     * 
     * @throws  Exception
     * 
     * @since   1.18.13 (J) - 1.8.13 (WP) added support to $type = "checkin".
     */
    public function getStats($from, $to, array $rooms = [], $type = 'stay_dates')
    {
        $dbo = JFactory::getDbo();

        // access the availability helper
        $av_helper = VikBooking::getAvailabilityInstance();

        $from_ts = VikBooking::getDateTimestamp($from, 0, 0);
        $to_ts = VikBooking::getDateTimestamp($to, 23, 59, 59);
        if (empty($from) || empty($from_ts) || empty($to_ts) || $to_ts < $from_ts) {
            throw new Exception('Invalid dates provided', 500);
        }

        // total number of days (nights) in the range
        $total_range_nights = $this->countNightsTo($to_ts, $from_ts);
        if ($total_range_nights < 1) {
            throw new Exception('Invalid number of days provided', 500);
        }

        // total number of room units
        $total_room_units = $this->countRooms($rooms);

        if ($total_room_units < 1) {
            throw new Exception('Having no rooms published may lead to divisions by zero, hence errors.', 500);
        }

        // the associative array of statistics to collect and return
        $stats = [
            // booking ids involved
            'bids'            => [],
            // total number of room units counted for stats
            'room_units'      => $total_room_units,
            // total number of room units times the number of nights in the range
            'tot_inventory'   => ($total_room_units * $total_range_nights),
            // number of rooms booked
            'rooms_booked'    => 0,
            // number of bookings found
            'tot_bookings'    => 0,
            // total number of nights booked (proportionally adjusted according to affected dates)
            'nights_booked'   => 0,
            // percent value
            'occupancy'       => 0,
            // average length of stay
            'avg_los'         => 0,
            // points of sale revenue (revenue divided by each individual ota and ibe)
            'pos_revenue'     => [],
            // list of countries with ranking
            'country_ranks'   => [],
            // ibe revenue net
            'ibe_revenue'     => 0,
            // otas revenue net (no matter if commissions were applied)
            'ota_revenue'     => 0,
            // total refunded amounts (never deducted)
            'tot_refunds'     => 0,
            // average daily rate
            'adr'             => 0,
            // revenue per available room
            'revpar'          => 0,
            // average booking window
            'abw'             => 0,
            // amount of all taxes
            'taxes'           => 0,
            // amount of VAT
            'tot_vat'         => 0,
            // amount of city taxes
            'city_taxes'      => 0,
            // amount of damage deposits
            'damage_deposits' => 0,
            // commissions (amount)
            'cmms'            => 0,
            // net revenue before tax (otas + ibe)
            'revenue'         => 0,
            // gross revenue after tax
            'gross_revenue'   => 0,
            // otas total before tax (same as ota_revenue, but only if ota commissions were applied)
            'ota_tot_net'     => 0,
            // otas commissions
            'ota_cmms'        => 0,
            // percent value for average ota commissions amount
            'ota_avg_cmms'    => 0,
            // commission savings amount
            'cmm_savings'     => 0,
            // total cancelled reservations
            'tot_cancellations' => 0,
            // cancelled reservation IDs
            'cancellation_ids'  => [],
            // total amount of cancelled bookings
            'cancellations_amt' => 0,
        ];

        if ($this->options['booking_level'] ?? null) {
            // include booking-level stats
            $stats['_bid_stats'] = [];
        }

        // get all (real/completed) bookings in the given range of dates
        $q = $dbo->getQuery(true);
        $q->select($dbo->qn([
            'o.id',
            'o.ts',
            'o.status',
            'o.days',
            'o.checkin',
            'o.checkout',
            'o.totpaid',
            'o.coupon',
            'o.roomsnum',
            'o.total',
            'o.idorderota',
            'o.channel',
            'o.country',
            'o.tot_taxes',
            'o.tot_city_taxes',
            'o.tot_fees',
            'o.tot_damage_dep',
            'o.cmms',
            'o.refund',
            'or.idorder',
            'or.idroom',
            'or.optionals',
            'or.cust_cost',
            'or.cust_idiva',
            'or.extracosts',
            'or.room_cost',
            'c.country_name',
        ]));
        $q->from($dbo->qn('#__vikbooking_orders', 'o'));
        $q->leftjoin($dbo->qn('#__vikbooking_ordersrooms', 'or') . ' ON ' . $dbo->qn('or.idorder') . ' = ' . $dbo->qn('o.id'));
        $q->leftjoin($dbo->qn('#__vikbooking_countries', 'c') . ' ON ' . $dbo->qn('c.country_3_code') . ' = ' . $dbo->qn('o.country'));
        $q->where($dbo->qn('o.total') . ' > 0');
        $q->where($dbo->qn('o.closure') . ' = 0');
        // use the "andWhere" method after having set some "where" clauses
        $q->andWhere([
            $dbo->qn('o.status') . ' = ' . $dbo->q('confirmed'),
            $dbo->qn('o.status') . ' = ' . $dbo->q('cancelled'),
        ], 'OR');
        if ($type == 'stay_dates') {
            // regular calculation based on stay dates
            $q->where($dbo->qn('o.checkout') . ' >= ' . $from_ts);
            $q->where($dbo->qn('o.checkin') . ' <= ' . $to_ts);
        } elseif ($type == 'checkin') {
            // calculation based on check-in date
            $q->where($dbo->qn('o.checkin') . ' >= ' . $from_ts);
            $q->where($dbo->qn('o.checkin') . ' <= ' . $to_ts);
        } else {
            // calculation based on booked dates
            $q->where($dbo->qn('o.ts') . ' >= ' . $from_ts);
            $q->where($dbo->qn('o.ts') . ' <= ' . $to_ts);
        }

        /**
         * Do not filter the room booking records by room ID, but always join them, to improve accuracy with calculations.
         * 
         * @since   1.18.0 (J) - 1.8.0 (WP)
         */
        if ($rooms) {
            // $q->where($dbo->qn('or.idroom') . ' IN (' . implode(', ', array_map('intval', $rooms)) . ')');
        }

        $q->order($dbo->qn('o.checkin') . ' ASC');
        $q->order($dbo->qn('o.id') . ' ASC');
        $q->order($dbo->qn('or.id') . ' ASC');

        $dbo->setQuery($q);
        $records = $dbo->loadAssocList();

        if (!$records) {
            // no bookings found, do not proceed
            return $stats;
        }

        /**
         * Join the busy records afterwards to support accurate calculations for split stays or early departures/late arrivals.
         * This is also needed to support multi-room reservations without joining the busy records together with the room records.
         * 
         * @since   1.18.0 (J) - 1.8.0 (WP)
         */
        $busy_joined = [];
        foreach ($records as &$booking) {
            if (strcasecmp($booking['status'], 'confirmed')) {
                // we only want to target confirmed bookings
                continue;
            }

            // build cache signature for busy records joined
            $booking_room_signature = $booking['id'] . '-' . $booking['idroom'];

            // build the db query
            $dbo->setQuery(
                $dbo->getQuery(true)
                    ->select($dbo->qn('ob.idbusy'))
                    ->select($dbo->qn('b.checkin', 'room_checkin'))
                    ->select($dbo->qn('b.checkout', 'room_checkout'))
                    ->from($dbo->qn('#__vikbooking_ordersbusy', 'ob'))
                    ->leftjoin($dbo->qn('#__vikbooking_busy', 'b') . ' ON ' . $dbo->qn('b.id') . ' = ' . $dbo->qn('ob.idbusy'))
                    ->where($dbo->qn('ob.idorder') . ' = ' . (int) $booking['id'])
                    ->where($dbo->qn('b.idroom') . ' = ' . (int) $booking['idroom'])
                    ->order($dbo->qn('b.id') . ' ASC')
            );

            // either fetch the records or use the previously cached ones to support multi-room bookings with equal room IDs
            $busy_joined[$booking_room_signature] = $busy_joined[$booking_room_signature] ?? $dbo->loadAssocList();

            // get the busy record to process for the current booking-room by shifting the list
            $process_busy = array_shift($busy_joined[$booking_room_signature]);

            if (!$process_busy) {
                continue;
            }

            // inject busy details
            $booking['idbusy'] = $process_busy['idbusy'];
            $booking['room_checkin'] = $process_busy['room_checkin'];
            $booking['room_checkout'] = $process_busy['room_checkout'];
        }

        // unset last reference and cached values
        unset($booking, $busy_joined);

        /**
         * Immediately count cancellations and unset the records found.
         * 
         * @since   1.16.10 (J) - 1.6.10 (WP)
         */
        foreach ($records as $k => $b) {
            if (!strcasecmp($b['status'], 'cancelled')) {
                if (!in_array($b['id'], $stats['cancellation_ids'])) {
                    // add statistics for the cancelled booking only once
                    $stats['tot_cancellations']++;
                    $stats['cancellations_amt'] += (float) $b['total'];
                    $stats['cancellation_ids'][] = $b['id'];
                }

                // get rid of this record to not alter the regular statistics
                unset($records[$k]);
            }
        }

        if ($stats['cancellation_ids']) {
            // reset key values
            $records = array_values($records);
        }

        // nest records with multiple rooms booked inside sub-arrays
        $bookings = [];
        foreach ($records as $b) {
            if (!isset($bookings[$b['id']])) {
                $bookings[$b['id']] = [];
            }

            // calculate the effective from and to stay timestamps for this room (by supporting split stays or early departures/late arrivals)
            $room_checkin  = !empty($b['room_checkin']) && $b['room_checkin'] != $b['checkin'] ? $b['room_checkin'] : $b['checkin'];
            $room_checkout = !empty($b['room_checkout']) && $b['room_checkout'] != $b['checkout'] ? $b['room_checkout'] : $b['checkout'];
            $in_info  = getdate($room_checkin);
            $out_info = getdate($room_checkout);
            $b['stay_from_ts'] = mktime(0, 0, 0, $in_info['mon'], $in_info['mday'], $in_info['year']);
            $b['stay_to_ts']   = mktime(23, 59, 59, $out_info['mon'], ($out_info['mday'] - 1), $out_info['year']);
            $b['stay_nights']  = $room_checkin != $b['checkin'] || $room_checkout != $b['checkout'] ? $av_helper->countNightsOfStay($room_checkin, $room_checkout) : $b['days'];
            $b['stay_nights']  = $b['stay_nights'] < 1 ? 1 : $b['stay_nights'];

            // push room-booking
            $bookings[$b['id']][] = $b;
        }

        // free memory up
        unset($records);

        // counters and pools
        $los_counter = 0;
        $pos_pool    = [];
        $pos_counter = [];
        $countries   = [];
        $country_map = [];

        // sum of booking window
        $sum_booking_window = 0;

        // parse all bookings
        foreach ($bookings as $bid => $booking) {
            // increase tot bookings and los counter
            if (!$rooms || ($rooms && array_intersect(array_column($booking, 'idroom'), $rooms))) {
                // make sure to increase counters only if filtered listing(s) involved or if no filters set
                $stats['tot_bookings']++;
                $los_counter += $booking[0]['days'];
                // push booking ID involved
                $stats['bids'][] = $bid;
            }

            // count rooms booked within the reservation
            $booking_rooms = count($booking);

            // define the total booking amount for multi-room reservations
            $multi_room_total = 0;

            if ($rooms && $booking[0]['roomsnum'] > 1) {
                // when filters applied to multi-room bookings, count the effective number of rooms involved
                $booking_rooms = count(array_intersect(array_column($booking, 'idroom'), $rooms));

                // overwrite room total amount when filtering by listing(s)
                foreach ($booking as $room_booking) {
                    if (!in_array($room_booking['idroom'], $rooms)) {
                        continue;
                    }
                    if ($room_booking['cust_cost'] > 0) {
                        $multi_room_total += $room_booking['cust_cost'];
                    } elseif ($room_booking['room_cost'] > 0) {
                        $multi_room_total += $room_booking['room_cost'];
                    }
                }
            }

            // point of sale name
            $pos_name = null;

            // parse all rooms booked
            foreach ($booking as $room_booking) {
                if ($rooms && !in_array($room_booking['idroom'], $rooms)) {
                    // room booked is excluded from filters
                    continue;
                }

                // increase rooms booked
                $stats['rooms_booked']++;

                // number of nights affected
                $los_affected = $room_booking['stay_nights'];

                // use default total values
                $room_total          = $multi_room_total ?: $room_booking['total'];
                $room_cmms           = $room_booking['cmms'];
                $room_refund         = $room_booking['refund'];
                $room_tot_taxes      = $room_booking['tot_taxes'];
                $room_tot_city_taxes = $room_booking['tot_city_taxes'];
                $room_tot_damage_dep = $room_booking['tot_damage_dep'];
                $room_tot_fees       = $room_booking['tot_fees'];

                // check if amounts must be calculated proportionally for the range of dates requested
                if ($type == 'stay_dates' && ($room_booking['stay_from_ts'] < $from_ts || $room_booking['stay_to_ts'] > $to_ts)) {
                    // calculate number of nights of stay affected
                    $los_affected = $this->countNightsAffected($from_ts, $to_ts, $room_booking['stay_from_ts'], $room_booking['stay_to_ts']);

                    // adjust the amounts proportionally
                    $room_total          = $room_total * $los_affected / $room_booking['stay_nights'];
                    $room_cmms           = $room_cmms * $los_affected / $room_booking['stay_nights'];
                    $room_refund         = $room_refund * $los_affected / $room_booking['stay_nights'];
                    $room_tot_taxes      = $room_tot_taxes * $los_affected / $room_booking['stay_nights'];
                    $room_tot_city_taxes = $room_tot_city_taxes * $los_affected / $room_booking['stay_nights'];
                    $room_tot_damage_dep = $room_tot_damage_dep * $los_affected / $room_booking['stay_nights'];
                    $room_tot_fees       = $room_tot_fees * $los_affected / $room_booking['stay_nights'];
                }

                // apply average values per room booked (with filters or booked in total)
                $room_total          /= $booking_rooms;
                $room_cmms           /= $booking_rooms;
                $room_refund         /= $booking_rooms;
                $room_tot_taxes      /= $booking_rooms;
                $room_tot_city_taxes /= $booking_rooms;
                $room_tot_damage_dep /= $room_booking['roomsnum'];
                $room_tot_fees       /= $booking_rooms;

                // calculate and sum average values per room booked
                $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);
                $tot_revenue = $multi_room_total ? ($tot_net / $booking_rooms) : $tot_net;
                $stats['revenue'] += $tot_revenue;
                $stats['gross_revenue'] += $room_total;
                $stats['nights_booked'] += $los_affected;

                // increase booking window for this room-booking
                $booking_window = $this->countDaysDiff($room_booking['ts'], $room_booking['stay_from_ts']);
                $sum_booking_window += $booking_window >= 0 ? $booking_window : 0;

                // increase country stats
                $country_code = !empty($room_booking['country']) ? $room_booking['country'] : 'unknown';
                if (!isset($countries[$country_code])) {
                    $countries[$country_code] = 0;
                    if (!empty($room_booking['country_name'])) {
                        $country_map[$country_code] = $room_booking['country_name'];
                    }
                }
                $countries[$country_code] += $tot_revenue;

                if (!empty($room_booking['idorderota']) && !empty($room_booking['channel'])) {
                    $stats['ota_revenue'] += $tot_revenue;
                    if ($room_cmms > 0) {
                        $stats['ota_tot_net'] += $tot_revenue;
                        $stats['ota_cmms'] += $room_cmms;
                    }
                    // set pos name
                    $channel_parts = explode('_', $room_booking['channel']);
                    $pos_name = trim($channel_parts[0]);
                } else {
                    $stats['ibe_revenue'] += $tot_revenue;
                    // set pos name
                    $pos_name = 'website';
                }

                // set pos net revenue
                if (!isset($pos_pool[$pos_name])) {
                    $pos_pool[$pos_name] = 0;
                }
                $pos_pool[$pos_name] += $tot_revenue;

                $stats['taxes'] += (float) $room_tot_taxes + (float) $room_tot_city_taxes + (float) $room_tot_fees;
                $stats['tot_vat'] += (float) $room_tot_taxes;
                $stats['city_taxes'] += (float) $room_tot_city_taxes;
                $stats['damage_deposits'] += (float) $room_tot_damage_dep;
                $stats['cmms'] += (float) $room_cmms;
                $stats['tot_refunds'] += $room_refund;

                /**
                 * Set booking-level stats.
                 * 
                 * @since   1.18.13 (J) - 1.8.13 (WP)
                 */
                if ($this->options['booking_level'] ?? null) {
                    $stats['_bid_stats'][$bid] = $stats['_bid_stats'][$bid] ?? [
                        'revenue'         => 0,
                        'gross_revenue'   => 0,
                        'taxes'           => 0,
                        'tot_vat'         => 0,
                        'city_taxes'      => 0,
                        'damage_deposits' => 0,
                        'cmms'            => 0,
                        'tot_refunds'     => 0,
                    ];
                    $stats['_bid_stats'][$bid]['revenue'] += $tot_revenue;
                    $stats['_bid_stats'][$bid]['gross_revenue'] += $room_total;
                    $stats['_bid_stats'][$bid]['taxes'] += (float) $room_tot_taxes + (float) $room_tot_city_taxes + (float) $room_tot_fees;
                    $stats['_bid_stats'][$bid]['tot_vat'] += (float) $room_tot_taxes;
                    $stats['_bid_stats'][$bid]['city_taxes'] += (float) $room_tot_city_taxes;
                    $stats['_bid_stats'][$bid]['damage_deposits'] += (float) $room_tot_damage_dep;
                    $stats['_bid_stats'][$bid]['cmms'] += (float) $room_cmms;
                    $stats['_bid_stats'][$bid]['tot_refunds'] += $room_refund;
                }
            }

            if ($pos_name) {
                // increase number of bookings for this pos
                if (!isset($pos_counter[$pos_name])) {
                    $pos_counter[$pos_name] = 0;
                }
                $pos_counter[$pos_name]++;
            }
        }

        // count the average length of stay (no proportional data for the dates requested)
        $stats['avg_los'] = $stats['tot_bookings'] > 0 ? round($los_counter / $stats['tot_bookings'], 1) : 0;

        // calculate occupancy percent value
        $stats['occupancy'] = round(($stats['nights_booked'] * 100 / ($total_room_units * $total_range_nights)), 2);

        // count the average daily rate (ADR)
        $stats['adr'] = $stats['rooms_booked'] > 0 ? $stats['revenue'] / $stats['rooms_booked'] / $total_range_nights : 0;

        // count the revenue per available room (RevPAR)
        $stats['revpar'] = $stats['revenue'] / $total_room_units;

        // count the average booking window (ABW - number of days between the reservation date and the check-in date)
        $stats['abw'] = $stats['rooms_booked'] > 0 ? $sum_booking_window / $stats['rooms_booked'] : 0;

        // count OTAs average commission amount
        if ($stats['ota_tot_net'] > 0 && $stats['ota_cmms'] > 0) {
            // find the average percent value of OTA commissions (tot_net : tot_cmms = 100 : x)
            $stats['ota_avg_cmms'] = round(($stats['ota_cmms'] * 100 / $stats['ota_tot_net']), 2);

            if ($stats['ibe_revenue'] > 0) {
                // calculate the commission savings amount
                $stats['cmm_savings'] = $stats['ibe_revenue'] * $stats['ota_avg_cmms'] / 100;
            }
        }

        // get channel logos helper
        $vcm_logos = VikBooking::getVcmChannelsLogo('', true);

        // sort and build pos revenues
        if ($pos_pool && $stats['revenue'] > 0) {
            // apply sorting descending
            arsort($pos_pool);
            // build readable pos values
            foreach ($pos_pool as $pos_name => $pos_revenue) {
                $pos_data = [
                    'name'     => $pos_name,
                    'revenue'  => $pos_revenue,
                    'pcent'    => round(($pos_revenue * 100 / $stats['revenue']), 2),
                    'logo'     => null,
                    'bookings' => isset($pos_counter[$pos_name]) ? $pos_counter[$pos_name] : 0,
                    'ibe'      => false,
                    'ota'      => false,
                ];
                if (!strcasecmp($pos_name, 'website')) {
                    // ibe revenue
                    $pos_data['name'] = JText::translate('VBORDFROMSITE');
                    $pos_data['ibe']  = true;
                } else {
                    // ota revenue
                    $pos_data['ota'] = true;
                    if (is_object($vcm_logos)) {
                        $ota_logo_img = $vcm_logos->setProvenience($pos_name)->getSmallLogoURL();
                        if ($ota_logo_img !== false) {
                            $pos_data['logo'] = $ota_logo_img;
                        }
                    }
                }
                // push pos data
                $stats['pos_revenue'][] = $pos_data;
            }
        }

        // sort countries revenue
        if ($countries && $stats['revenue'] > 0) {
            // apply sorting descending
            arsort($countries);
            // build readable values
            foreach ($countries as $country_code => $country_revenue) {
                // push country data
                $stats['country_ranks'][] = [
                    'code'    => $country_code,
                    'name'    => (isset($country_map[$country_code]) ? $country_map[$country_code] : $country_code),
                    'revenue' => $country_revenue,
                    'pcent'   => round(($country_revenue * 100 / $stats['revenue']), 2),
                ];
            }
        }

        // return the statistics information
        return $stats;
    }

    /**
     * Returns the information of the given rate plan ID.
     * 
     * @param   int     $rplan_id   The rate plan ID.
     * 
     * @return  ?array
     * 
     * @since   1.18.2 (J) - 1.8.2 (WP)
     */
    public function getRatePlanData(int $rplan_id)
    {
        if (empty($rplan_id)) {
            return null;
        }

        $dbo = JFactory::getDbo();

        $dbo->setQuery(
            $dbo->getQuery(true)
                ->select('*')
                ->from($dbo->qn('#__vikbooking_prices'))
                ->where($dbo->qn('id') . ' = ' . $rplan_id)
        );

        return $dbo->loadAssoc();
    }

    /**
     * Counts the number of nights involved in a range of dates. This is
     * useful to proportionally calculate the amounts to be used.
     * 
     * @param   int     $from_ts    the 00:00:00 timestamp of the range start date.
     * @param   int     $to_ts      the 23:59:59 timestamp of the range end date.
     * @param   int     $in_ts      the 00:00:00 timestamp of the check-in date.
     * @param   int     $out_ts     the 23:59:59 timestamp of the last night of stay (check-out -1).
     * 
     * @return  int                 the number of stay nights involved in the range.
     */
    protected function countNightsAffected($from_ts, $to_ts, $in_ts, $out_ts)
    {
        $nights_affected = 0;

        if ($from_ts > $to_ts) {
            return $nights_affected;
        }

        $range_from_info = getdate($from_ts);
        while ($range_from_info[0] < $to_ts) {
            if ($range_from_info[0] >= $in_ts && $range_from_info[0] <= $out_ts) {
                $nights_affected++;
            }
            // next day iteration
            $range_from_info = getdate(mktime(0, 0, 0, $range_from_info['mon'], ($range_from_info['mday'] + 1), $range_from_info['year']));
        }

        return $nights_affected;
    }
}

```
