You are here

class UnitPricingCalendar in Rooms - Drupal Booking for Hotels, B&Bs and Vacation Rentals 7

Handles querying and updating the pricing information relative to a single bookable unit.

Hierarchy

Expanded class hierarchy of UnitPricingCalendar

File

modules/rooms_pricing/includes/rooms_pricing.unit_pricing_calendar.inc, line 12
Contains UnitPricingCalendar.

View source
class UnitPricingCalendar extends RoomsCalendar implements UnitPricingCalendarInterface {

  /**
   * The actual unit relevant to this Calendar.
   */
  protected $unit;

  /**
   * The default price for the room
   *
   * @var float
   */
  protected $default_price;

  /**
   * Price modifiers - an array of operations to be performed to the price.
   * Operations are performed in the sequence they are found in the array
   *
   * @var array
   */
  protected $price_modifiers;

  /**
   * Constructs a UnitPricingCalendar instance.
   *
   * @param int $unit_id
   *   The unit ID.
   * @param array $price_modifiers
   *   The price modifiers to apply.
   */
  public function __construct($unit_id, $price_modifiers = array()) {
    $this->unit_id = $unit_id;

    // Load the booking unit.
    $this->unit = rooms_unit_load($unit_id);
    $this->default_state = $this->unit->default_state;
    $this->default_price = $this->unit->base_price;
    $this->price_modifiers = $price_modifiers;
    $this->base_table = 'rooms_pricing';
  }

  /**
   * {@inheritdoc}
   */
  public function calculatePrice(DateTime $start_date, DateTime $end_date, $persons = 0, $children = 0, $children_ages = array()) {
    if ($persons == 0) {
      $persons = $this->unit->max_sleeps;
    }
    $price = 0;
    $booking_price = 0;
    $booking_days = 0;

    // Setup pricing reply and log
    $reply = array(
      'full_price' => $price,
      'booking_price' => $booking_price,
      'log' => array(),
    );

    // Get settings to add to log
    $reply['log']['rooms_children_discount_options'] = variable_get('rooms_children_discount_options', array());
    $reply['log']['rooms_price_calculation'] = variable_get('rooms_price_calculation', ROOMS_PER_NIGHT);
    $pricing_events = $this
      ->getEvents($start_date, $end_date);
    $reply['log']['pricing_events'] = $pricing_events;
    foreach ($pricing_events as $event) {
      $days = $event
        ->diff()->days + 1;
      $booking_days += $days;
      if (variable_get('rooms_price_calculation', ROOMS_PER_NIGHT) == ROOMS_PER_PERSON) {
        $children_discount_options = variable_get('rooms_children_discount_options', array());
        $price = $price + $days * $event->amount * ($persons - $children);
        foreach ($children_ages as $age) {
          $reply['log']['children'][$age]['pre'] = $price;
          if (is_array($age)) {
            $age = $age['value'];
          }
          $discount = 0;
          foreach ($children_discount_options as $option) {
            if ($age >= $option['start'] && $age <= $option['end']) {
              $discount = $option['discount'];
              break;
            }
          }
          $price = $price + $days * $event->amount * (100 - $discount) / 100;
          $reply['log']['children'][$age]['post'] = $price;
        }
      }
      else {
        $price = $price + $days * $event->amount;
      }
    }
    $booking_info = array(
      'unit' => $this->unit,
      'start_date' => $start_date,
      'end_date' => $end_date,
      'booking_parameters' => array(
        'group_size' => $persons,
        'group_size_children' => $children,
        'childrens_age' => $children_ages,
      ),
    );
    drupal_alter('rooms_booking_amount_before_modifiers', $price, $booking_info);
    $price = $this
      ->applyPriceModifiers($price, $booking_days, $reply);
    $payment_option = variable_get('rooms_payment_options', FULL_PAYMENT);
    $reply['rooms_payment_option'] = variable_get('rooms_payment_options', FULL_PAYMENT);
    switch ($payment_option) {
      case FULL_PAYMENT:
        $booking_price = $price;
        break;
      case PERCENT_PAYMENT:
        $reply['rooms_payment_option'][PERCENT_PAYMENT] = variable_get('rooms_payment_options_percentual');
        $booking_price = $price / 100 * variable_get('rooms_payment_options_percentual');
        break;
      case FIRST_NIGHT_PAYMENT:
        $booking_price = $pricing_events[0]->amount;
        $reply['rooms_payment_option'][FIRST_NIGHT_PAYMENT] = $booking_price;
        break;
    }
    $reply['full_price'] = $price;
    $reply['booking_price'] = $booking_price;
    return $reply;
  }

  /**
   * {@inheritdoc}
   */
  public function applyPriceModifiers($base_price, $days, &$reply) {
    $price = $base_price;
    if (!empty($this->price_modifiers)) {
      foreach ($this->price_modifiers as $source => $modifier) {
        if ($modifier['#type'] == ROOMS_PRICE_SINGLE_OCCUPANCY) {
          $reply['log']['modifiers'][$source][$mod_count][ROOMS_PRICE_SINGLE_OCCUPANCY]['pre'] = $price;
          $reply['log']['modifiers'][$source][$mod_count][ROOMS_PRICE_SINGLE_OCCUPANCY]['amount'] = $this->unit->data['singlediscount'];
          $reply['log']['modifiers'][$source][$mod_count][ROOMS_PRICE_SINGLE_OCCUPANCY]['modifier'] = $modifier;
          $this->unit->data['singlediscount'];
          $price -= $base_price * $this->unit->data['singlediscount'] / 100;
          $reply['log']['modifiers'][$source][ROOMS_PRICE_SINGLE_OCCUPANCY]['post'] = $price;
        }
        elseif ($modifier['#type'] == ROOMS_DYNAMIC_MODIFIER) {
          $reply['log']['modifiers'][$source][ROOMS_DYNAMIC_MODIFIER]['modifier'] = $modifier;
          $reply['log']['modifiers'][$source][ROOMS_DYNAMIC_MODIFIER]['pre'] = $price;
          switch ($modifier['#op_type']) {
            case ROOMS_ADD:
              $price += $modifier['#amount'] * $modifier['#quantity'];
              $reply['log']['modifiers'][$source][ROOMS_DYNAMIC_MODIFIER]['op'] = ROOMS_ADD;
              $reply['log']['modifiers'][$source][ROOMS_DYNAMIC_MODIFIER]['post'] = $price;
              break;
            case ROOMS_ADD_DAILY:
              $price += $modifier['#amount'] * $modifier['#quantity'] * $days;
              $reply['log']['modifiers'][$source][ROOMS_DYNAMIC_MODIFIER]['op'] = ROOMS_ADD_DAILY;
              $reply['log']['modifiers'][$source][ROOMS_DYNAMIC_MODIFIER]['post'] = $price;
              break;
            case ROOMS_SUB:
              $price -= $modifier['#amount'] * $modifier['#quantity'];
              $reply['log']['modifiers'][$source][ROOMS_DYNAMIC_MODIFIER]['op'] = ROOMS_SUB;
              $reply['log']['modifiers'][$source][ROOMS_DYNAMIC_MODIFIER]['post'] = $price;
              break;
            case ROOMS_SUB_DAILY:
              $price -= $modifier['#amount'] * $modifier['#quantity'] * $days;
              $reply['log']['modifiers'][$source][ROOMS_DYNAMIC_MODIFIER]['op'] = ROOMS_SUB_DAILY;
              $reply['log']['modifiers'][$source][ROOMS_DYNAMIC_MODIFIER]['post'] = $price;
              break;
            case ROOMS_REPLACE:
              $price = $modifier['#amount'];
              $reply['log']['modifiers'][$source][ROOMS_DYNAMIC_MODIFIER]['op'] = ROOMS_REPLACE;
              $reply['log']['modifiers'][$source][ROOMS_DYNAMIC_MODIFIER]['post'] = $price;
              break;
            case ROOMS_INCREASE:
              $price += $base_price * ($modifier['#amount'] * $modifier['#quantity']) / 100;
              $reply['log']['modifiers'][$source][ROOMS_DYNAMIC_MODIFIER]['op'] = ROOMS_INCREASE;
              $reply['log']['modifiers'][$source][ROOMS_DYNAMIC_MODIFIER]['post'] = $price;
              break;
            case ROOMS_DECREASE:
              $price -= $base_price * ($modifier['#amount'] * $modifier['#quantity']) / 100;
              $reply['log']['modifiers'][$source][ROOMS_DYNAMIC_MODIFIER]['op'] = ROOMS_DECREASE;
              $reply['log']['modifiers'][$source][ROOMS_DYNAMIC_MODIFIER]['post'] = $price;
              break;
          }
        }
      }
    }
    return $price;
  }

  /**
   * {@inheritdoc}
   */
  public function getEvents(DateTime $start_date, DateTime $end_date) {

    // Get the raw day results.
    $results = $this
      ->getRawDayData($start_date, $end_date);
    $events = array();
    foreach ($results[$this->unit_id] as $year => $months) {
      foreach ($months as $mid => $month) {

        // Event array gives us the start days for each event within a month.
        $start_days = array_keys($month['states']);
        foreach ($month['states'] as $state) {

          // Create a booking event.
          $start = $state['start_day'];
          $end = $state['end_day'];
          $sd = new DateTime("{$year}-{$mid}-{$start}");
          $ed = new DateTime("{$year}-{$mid}-{$end}");
          $amount = commerce_currency_amount_to_decimal($state['state'], commerce_default_currency());
          $event = new PricingEvent($this->unit_id, $amount, $sd, $ed);
          $events[] = $event;
        }
      }
    }
    return $events;
  }

  /**
   * {@inheritdoc}
   */
  public function getRawDayData(DateTime $start_date, DateTime $end_date) {

    // To handle single-day bookings (Tours) we pretend that they are overnight
    // bookings.
    if ($end_date < $start_date) {
      $end_date
        ->add(new DateInterval('P1D'));
    }

    // Create a dummy PricingEvent to represent the range we are searching over.
    // This gives us access to handy functions that PricingEvents have.
    $s = new PricingEvent($this->unit_id, 0, $start_date, $end_date);
    $results = array();

    // If search across the same year do a single query.
    if ($s
      ->sameYear()) {
      $query = db_select($this->base_table, 'a');
      $query
        ->fields('a');
      $query
        ->condition('a.unit_id', $this->unit_id);
      $query
        ->condition('a.year', $s
        ->startYear());
      $query
        ->condition('a.month', $s
        ->startMonth(), '>=');
      $query
        ->condition('a.month', $s
        ->endMonth(), '<=');
      $months = $query
        ->execute()
        ->fetchAll(PDO::FETCH_ASSOC);
      if (count($months) > 0) {
        foreach ($months as $month) {
          $m = $month['month'];
          $y = $month['year'];
          $id = $month['unit_id'];

          // Remove the three first rows and just keep the days.
          unset($month['month']);
          unset($month['year']);
          unset($month['unit_id']);
          $results[$id][$y][$m]['days'] = $month;
        }
      }
    }
    else {
      for ($j = $s
        ->startYear(); $j <= $s
        ->endYear(); $j++) {
        $query = db_select($this->base_table, 'a');
        $query
          ->fields('a');
        $query
          ->condition('a.unit_id', $this->unit_id);
        $query
          ->condition('a.year', $j);
        if ($j == $s
          ->startYear()) {
          $query
            ->condition('a.month', $s
            ->startMonth(), '>=');
        }
        elseif ($j == $s
          ->endYear()) {
          $query
            ->condition('a.month', $s
            ->endMonth(), '<=');
        }
        $months = $query
          ->execute()
          ->fetchAll(PDO::FETCH_ASSOC);
        if (count($months) > 0) {
          foreach ($months as $month) {
            $m = $month['month'];
            $y = $month['year'];
            $id = $month['unit_id'];
            unset($month['month']);
            unset($month['year']);
            unset($month['unit_id']);
            $results[$id][$y][$m]['days'] = $month;
          }
        }
      }
    }

    // With the results from the db in place fill in any missing months
    // with the default state for the unit.
    for ($j = $s
      ->startYear(); $j <= $s
      ->endYear(); $j++) {
      $eod = rooms_end_of_month_dates($j);

      // We start by setting the expected start and end months for each year.
      if ($s
        ->sameYear()) {
        $expected_months = $s
          ->endMonth() - $s
          ->startMonth() + 1;
        $sm = $s
          ->startMonth();
        $em = $s
          ->endMonth();
      }
      elseif ($j == $s
        ->endYear()) {
        $expected_months = $s
          ->endMonth();
        $sm = 1;
        $em = $s
          ->endMonth();
      }
      elseif ($j == $s
        ->startYear()) {
        $expected_months = 12 - $s
          ->startMonth() + 1;
        $em = 12;
        $sm = $s
          ->startMonth();
      }
      else {
        $expected_months = 12;
        $sm = 1;
        $em = 12;
      }

      // We check to see if the months we have already fit our expectations.
      $actual_months = isset($result[$this->unit_id][$j]) ? count($results[$id][$j]) : 0;
      if ($expected_months > $actual_months) {

        // We have missing months so lets go fill them.
        for ($i = $sm; $i <= $em; $i++) {
          if (!isset($results[$this->unit_id][$j][$i])) {
            $last_day = $eod[$i];
            $month = $this
              ->prepareFullMonthArray(new PricingEvent($this->unit_id, $this->default_price, new DateTime("{$j}-{$i}-1"), new DateTime("{$j}-{$i}-{$last_day}")));

            // Add the month in its rightful position.
            $results[$this->unit_id][$j][$i]['days'] = $month;

            // And sort months.
            ksort($results[$this->unit_id][$j]);
          }
        }
      }
    }

    // With all the months in place we now need to clean results to set the
    // right start and end date for each month - this will save code downstream
    // from having to worry about it.
    foreach ($results[$this->unit_id] as $year => $months) {
      foreach ($months as $mid => $days) {

        // Get the end of month values again to make sure we have the right year
        // because it might change for queries spanning years.
        $eod = rooms_end_of_month_dates($year);

        // There is undoubtetly a smarter way to do this.
        if (count($days['days']) != $eod[$mid]) {
          switch ($eod[$mid]) {
            case 30:
              unset($results[$this->unit_id][$year][$mid]['days']['d31']);
              break;
            case 29:
              unset($results[$this->unit_id][$year][$mid]['days']['d31']);
              unset($results[$this->unit_id][$year][$mid]['days']['d30']);
              break;
            case 28:
              unset($results[$this->unit_id][$year][$mid]['days']['d31']);
              unset($results[$this->unit_id][$year][$mid]['days']['d30']);
              unset($results[$this->unit_id][$year][$mid]['days']['d29']);
              break;
          }
        }
        if ($year == $s
          ->startYear() && $mid == $s
          ->startMonth()) {

          // We know we have the entire months over the range so we just unset
          // all the dates from the start of the month to the actual start day.
          for ($i = 1; $i < $s
            ->startDay(); $i++) {
            unset($results[$this->unit_id][$year][$mid]['days']['d' . $i]);
          }
        }
        if ($year == $s
          ->endYear() && $mid == $s
          ->endMonth()) {

          // And from the end of the month back to the actual end day.
          for ($i = $s
            ->endDay() + 1; $i <= $eod[$mid]; $i++) {
            unset($results[$this->unit_id][$year][$mid]['days']['d' . $i]);
          }
        }
      }
    }

    // We store -1 instead of the default price in the DB so this is our chance to get the default price back
    // cycling through the data and replace -1 with the current default price of the unit.
    foreach ($results[$this->unit_id] as $year => $months) {
      foreach ($months as $mid => $days) {

        // The number of days in the month we are interested in eventing.
        $j = count($days);

        // The start date.
        $i = substr(key($days['days']), 1);
        while ($j <= count($days['days'])) {
          if ($days['days']['d' . $i] == -1) {
            $results[$this->unit_id][$year][$mid]['days']['d' . $i] = commerce_currency_decimal_to_amount($this->default_price, commerce_default_currency());
          }
          $i++;
          $j++;
        }
      }
    }

    // With the results in place we do a states array with the start and
    // end dates of each event.
    foreach ($results[$this->unit_id] as $year => $months) {
      foreach ($months as $mid => $days) {

        // The number of days in the month we are interested in eventing.
        $j = count($days);

        // The start date.
        $i = substr(key($days['days']), 1);
        $start_day = $i;
        $end_day = NULL;
        $unique_states = array();
        $old_state = $days['days']['d' . $i];
        $state = $days['days']['d' . $i];
        while ($j <= count($days['days'])) {
          $state = $days['days']['d' . $i];
          if ($state != $old_state) {
            $unique_states[] = array(
              'state' => $old_state,
              'start_day' => $start_day,
              'end_day' => $i - 1,
            );
            $end_day = $i - 1;
            $start_day = $i;
            $old_state = $state;
          }
          $i++;
          $j++;
        }

        // Get the last event in.
        $unique_states[] = array(
          'state' => $state,
          'start_day' => isset($end_day) ? $end_day + 1 : $start_day,
          'end_day' => $i - 1,
        );
        $results[$this->unit_id][$year][$mid]['states'] = $unique_states;
      }
    }
    return $results;
  }

  /**
   * {@inheritdoc}
   */
  public function updateCalendar($events) {
    foreach ($events as $event) {

      // Make sure event refers to the unit for this calendar.
      if ($event->unit_id == $this->unit_id) {

        // Get all the pricing events that fit within this event.
        $affected_events = $this
          ->getEvents($event->start_date, $event->end_date);
        $monthly_events = array();
        foreach ($affected_events as $a_event) {

          /** @var PricingEventInterface $a_event */

          // Apply the operation.
          $a_event
            ->applyOperation($event->amount, $event->operation);

          // If the event is in the same month span just queue to be added.
          if ($a_event
            ->sameMonth()) {
            $monthly_events[] = $a_event;
          }
          else {

            // Check if multi-year - if not just create monthly events.
            if ($a_event
              ->sameYear()) {
              $monthly_events_tmp = $a_event
                ->transformToMonthlyEvents();
              $monthly_events = array_merge($monthly_events, $monthly_events_tmp);
            }
            else {

              // Else transform to single years and then to monthly.
              $yearly_events = $a_event
                ->transformToYearlyEvents();
              foreach ($yearly_events as $ye) {
                $monthly_events_tmp = $ye
                  ->transformToMonthlyEvents();
                $monthly_events = array_merge($monthly_events, $monthly_events_tmp);
              }
            }
          }
        }
        foreach ($monthly_events as $event) {
          $this
            ->addMonthEvent($event);
        }
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  protected function prepareFullMonthArray(RoomsEventInterface $event) {
    $days = array();
    $eod = rooms_end_of_month_dates($event
      ->startYear());
    $last_day = $eod[$event
      ->startMonth()];
    for ($i = 1; $i <= $last_day; $i++) {
      if ($i >= $event
        ->startDay() && $i <= $event
        ->endDay()) {
        $days['d' . $i] = commerce_currency_decimal_to_amount($event->amount, commerce_default_currency());
      }
      else {

        // When we are writing a new month to the DB make sure to have the placeholder value -1 for the days where the
        // default price is in effect. This means as a user changes the default price we will take it into account even
        // though the price data is now in a DB row.
        $days['d' . $i] = -1;
      }
    }
    return $days;
  }

  /**
   * {@inheritdoc}
   */
  protected function preparePartialMonthArray(RoomsEventInterface $event) {
    $days = array();
    for ($i = $event
      ->startDay(); $i <= $event
      ->endDay(); $i++) {
      $days['d' . $i] = commerce_currency_decimal_to_amount($event->amount, commerce_default_currency());
    }
    return $days;
  }

  /**
   * {@inheritdoc}
   */
  public function calculatePricingEvents($unit_id, $amount, DateTime $start_date, DateTime $end_date, $operation, $days) {
    $s_timestamp = $start_date
      ->getTimestamp();
    $e_timestamp = $end_date
      ->getTimestamp();
    $events = array();
    do {
      $s_date = getdate($s_timestamp);
      $wday_start = $s_date['wday'];
      if (in_array($wday_start + 1, $days)) {
        $events[] = new PricingEvent($unit_id, $amount, new DateTime(date('Y-m-d', $s_timestamp)), new DateTime(date('Y-m-d', $s_timestamp)), $operation, $days);
      }
      $s_timestamp = strtotime('+1 days', $s_timestamp);
    } while ($s_timestamp <= $e_timestamp);
    return $events;
  }

}

Members

Namesort descending Modifiers Type Description Overrides
RoomsCalendar::$base_table protected property The base table where calendar data is stored.
RoomsCalendar::$default_state protected property The default state for the room if it has no specific booking. 1
RoomsCalendar::$unit_id protected property The default state for the room if it has no specific booking.
RoomsCalendar::addMonthEvent public function Adds an event to the calendar Overrides RoomsCalendarInterface::addMonthEvent
RoomsCalendar::monthDefined public function Checks if a month exists. Overrides RoomsCalendarInterface::monthDefined
UnitPricingCalendar::$default_price protected property The default price for the room
UnitPricingCalendar::$price_modifiers protected property Price modifiers - an array of operations to be performed to the price. Operations are performed in the sequence they are found in the array
UnitPricingCalendar::$unit protected property The actual unit relevant to this Calendar.
UnitPricingCalendar::applyPriceModifiers public function Apply price modifiers to base price. Overrides UnitPricingCalendarInterface::applyPriceModifiers
UnitPricingCalendar::calculatePrice public function Given a date range determine the cost of the room over that period. Overrides UnitPricingCalendarInterface::calculatePrice
UnitPricingCalendar::calculatePricingEvents public function Get a set of PricingEvent between start_date and end_date filtered by days. Overrides UnitPricingCalendarInterface::calculatePricingEvents
UnitPricingCalendar::getEvents public function Given a date range returns an array of RoomEvents. The heavy lifting really takes place in the getRawDayData function - here we are simply acting as a factory for event objects Overrides RoomsCalendar::getEvents
UnitPricingCalendar::getRawDayData public function Given a date range it returns all data within that range including the start and end dates of states. The MySQL queries are kept simple and then the data is cleared up. Overrides RoomsCalendar::getRawDayData
UnitPricingCalendar::prepareFullMonthArray protected function Given an event it prepares the entire month array for it assuming no other events in the month and days where there is no event get set to the default state. Overrides RoomsCalendar::prepareFullMonthArray
UnitPricingCalendar::preparePartialMonthArray protected function Given an event it prepares a partial array covering just the days for which the event is involved Overrides RoomsCalendar::preparePartialMonthArray
UnitPricingCalendar::updateCalendar public function Given an array of RoomEvents the calendar is updated with regards to the events that are relevant to the Unit this calendar refers to Overrides RoomsCalendar::updateCalendar
UnitPricingCalendar::__construct public function Constructs a UnitPricingCalendar instance.