/** * @param Event $event * @return Entry[] * * @throws Exception\RuntimeException * @throws Zend\Di\Exception\ClassNotFoundException */ public function process(Event $event) { $entries = new ArrayCollection(); $eventRange = new DateRange($event->start, $event->end); if ($eventRange->isEmpty()) { return $entries; } $rule = $event->rule; if ($rule === null) { throw new Exception\RuntimeException(sprintf('The event %s has no associated rule.', $event->id)); } $config = $this->getMatchingConfiguration($rule, $eventRange->getStart()); if ($config->end < $eventRange->getEnd()) { throw new Exception\RuntimeException(sprintf('The rule configuration %s from %s to %s does not' . ' cover the desired range %s to %s.', $config->id, $config->start->format('Y-m-d'), $config->end->format('Y-m-d'), $eventRange->getStart()->format('Y-m-d'), $eventRange->getEnd()->format('Y-m-d'))); } $strategy = $this->di->get($config->strategy); if (!$strategy instanceof StrategyInterface) { throw new Exception\RuntimeException(sprintf('The class %s does not implement %s.', get_class($strategy), 'Tillikum\\Billing\\Event\\Strategy\\StrategyInterface')); } $strategyEntries = $strategy->process($event, $config); foreach ($strategyEntries as $entry) { if ($event->is_credit) { $entry->amount *= -1; } $entries->add($entry); } return $entries; }
public function process(Event $event, RuleConfig $config) { $entries = new ArrayCollection(); $configRange = new DateRange($config->start, $config->end); $eventRange = new DateRange($event->start, $event->end); if ($configRange->isEmpty()) { return $entries; } $amountPerRange = new Money($config->amount, $config->currency); $configDays = (int) $configRange->getStart()->diff($configRange->getEnd())->format('%R%a') + 1; $totalDays = (int) $eventRange->getStart()->diff($eventRange->getEnd())->format('%R%a') + 1; // If we match up nicely, just bill at the fixed value and we're done if ($eventRange->equals($configRange)) { $rangeTotal = $amountPerRange; $entry = new Entry(); $entry->amount = $rangeTotal->round(2); $entry->currency = $rangeTotal->getCurrency(); $entry->code = $config->code; $entry->description = sprintf('%s to %s (%s %s) @ %s for the entire range', $event->start->format('Y-m-d'), $event->end->format('Y-m-d'), $totalDays, $totalDays == 1 ? 'day' : 'days', $amountPerRange->format()); $entries->add($entry); } else { // (amount per fixed range / days in fixed range) * days billed $proratedTotal = $amountPerRange->div($configDays)->mul($totalDays); $entry = new Entry(); $entry->amount = $proratedTotal->round(2); $entry->currency = $proratedTotal->getCurrency(); $entry->code = $config->code; $entry->description = sprintf('%s to %s (%s %s) @ %s for part of %s to %s (%s %s) (prorated daily)', $event->start->format('Y-m-d'), $event->end->format('Y-m-d'), $totalDays, $totalDays == 1 ? 'day' : 'days', $amountPerRange->format(), $configRange->getStart()->format('Y-m-d'), $configRange->getEnd()->format('Y-m-d'), $configDays, $configDays == 1 ? 'day' : 'days'); $entries->add($entry); } return $entries; }
public function process(Event $event, RuleConfig $config) { $entries = new ArrayCollection(); $eventRange = new DateRange($event->start, $event->end); // Days: Day difference + 1 $totalDays = (int) $eventRange->getStart()->diff($eventRange->getEnd())->format('%R%a') + 1; // The total number of full weeks $weeks = (int) floor($totalDays / 7); // The number of leftover days after full weeks are counted $leftoverDays = (int) $totalDays % 7; $amountPerWeek = new Money($config->amount, $config->currency); // amount per week * number of weeks $weekTotal = $amountPerWeek->mul($weeks); // (amount per week / 7) * leftover days (prorate) $leftoverDayTotal = $amountPerWeek->div(7)->mul($leftoverDays); // If we have solid weeks, make an entry for that total if ($weeks > 0) { $fullWeekEndDate = clone $eventRange->getEnd(); $fullWeekEndDate->modify("-{$leftoverDays} day"); $entry = new entry(); $entry->amount = $weekTotal->round(2); $entry->currency = $weekTotal->getCurrency(); $entry->code = $config->code; $entry->description = sprintf('%s to %s (%s %s) @ %s per week', $eventRange->getStart()->format('Y-m-d'), $fullWeekEndDate->format('Y-m-d'), $weeks, $weeks == 1 ? 'week' : 'weeks', $amountPerWeek->format()); $entries->add($entry); } if ($leftoverDays > 0) { // Start 1 day after the end of the previous full week(s), if there // were any $partialWeekStartDate = clone $eventRange->getEnd(); $partialWeekStartDate->modify("-{$leftoverDays} day")->modify('+1 day'); $entry = new Entry(); $entry->amount = $leftoverDayTotal->round(2); $entry->currency = $leftoverDayTotal->getCurrency(); $entry->code = $config->code; $entry->description = sprintf('%s to %s (%s %s) @ %s per week (prorated daily)', $partialWeekStartDate->format('Y-m-d'), $eventRange->getEnd()->format('Y-m-d'), $leftoverDays, $leftoverDays == 1 ? 'day' : 'days', $amountPerWeek->format()); $entries->add($entry); } return $entries; }
/** * Test whether this DateRange includes a DateTime or a DateRange * * If a DateTime is greater than or equal to the start of AND less than * or equal to the end of this DateRange, it is considered included. * * If a DateRange is fully enclosed inside this DateRange, it is * considered included. The test is essentially the same as for the * DateTime except it is performed on both the start and end dates of the * DateRange. * * @param DateTime|DateRange $arg Other object to test * @return bool */ public function includes($arg) { if ($arg instanceof DateTime) { return $this->getStart() <= $arg && $this->getEnd() >= $arg; } elseif ($arg instanceof DateRange) { return $this->includes($arg->getStart()) && $this->includes($arg->getEnd()); } else { throw new InvalidArgumentException('Argument must be an instance of DateTime or ' . __CLASS__); } }
public function isValid($data) { if (!parent::isValid($data)) { return false; } if ($this->isArray()) { $data = $this->_dissolveArrayValue($data, $this->getElementsBelongTo()); } $startDate = new DateTime($data['start']); $endDate = new DateTime($data['end']); if ($startDate > $endDate) { $this->start->addError($this->getTranslator()->translate('The start date must be on or before the end date.')); $this->end->addError($this->getTranslator()->translate('The end date must be on or after the start date.')); return false; } $bookingRange = new DateRange($startDate, $endDate); $bookingFacility = $this->em->find('Tillikum\\Entity\\Facility\\Facility', $data['facility_id']); $person = $this->booking->person; $bookings = $this->em->createQueryBuilder()->select('b')->from('Tillikum\\Entity\\Booking\\Facility\\Facility', 'b')->where('b.start <= :proposedEnd')->andWhere('b.end >= :proposedStart')->andWhere('b.facility = :facility')->andWhere('b.person != :person')->orderBy('b.start')->setParameter('facility', $bookingFacility)->setParameter('person', $person)->setParameter('proposedStart', $bookingRange->getStart())->setParameter('proposedEnd', $bookingRange->getEnd())->getQuery()->getResult(); $configs = $this->em->createQueryBuilder()->select('c')->from('Tillikum\\Entity\\Facility\\Config\\Config', 'c')->where('c.facility = :facility')->orderBy('c.start')->setParameter('facility', $bookingFacility)->getQuery()->getResult(); $holds = $this->em->createQueryBuilder()->select('h')->from('Tillikum\\Entity\\Facility\\Hold\\Hold', 'h')->where('h.start <= :proposedEnd')->andWhere('h.end >= :proposedStart')->andWhere('h.facility = :facility')->orderBy('h.start')->setParameter('facility', $bookingFacility)->setParameter('proposedStart', $bookingRange->getStart())->setParameter('proposedEnd', $bookingRange->getEnd())->getQuery()->getResult(); $occupancyInputs = array(new OccupancyInput($bookingRange->getStart(), -1, sprintf('start of the booking range you specified from %s to %s', $bookingRange->getStart()->format('Y-m-d'), $bookingRange->getEnd()->format('Y-m-d'))), new OccupancyInput(date_modify(clone $bookingRange->getEnd(), '+1 day'), 1, sprintf('end of the booking range you specified from %s to %s', $bookingRange->getStart()->format('Y-m-d'), $bookingRange->getEnd()->format('Y-m-d')))); foreach ($bookings as $booking) { $occupancyInputs[] = new OccupancyInput($booking->start, -1, sprintf('start of a booking from %s to %s', $booking->start->format('Y-m-d'), $booking->end->format('Y-m-d'))); $occupancyInputs[] = new OccupancyInput(date_modify(clone $booking->end, '+1 day'), 1, sprintf('end of a booking from %s to %s', $booking->start->format('Y-m-d'), $booking->end->format('Y-m-d'))); if (!empty($booking->person->gender)) { $bookingGenderSpec = isset($bookingGenderSpec) ? $bookingGenderSpec->andSpec(new GenderMatchSpecification($booking->person->gender)) : new GenderMatchSpecification($booking->person->gender); } } $currentConfigSpace = 0; foreach ($configs as $config) { $occupancyInputs[] = new OccupancyInput($config->start, $config->capacity - $currentConfigSpace, sprintf('start of a facility configuration from %s to %s', $config->start->format('Y-m-d'), $config->end->format('Y-m-d'))); $currentConfigSpace = $config->capacity; if (!empty($config->gender)) { $facilityGenderSpec = isset($facilityGenderSpec) ? $facilityGenderSpec->andSpec(new GenderMatchSpecification($config->gender)) : new GenderMatchSpecification($config->gender); } if (!empty($config->suite)) { $suiteConfigs = $this->em->createQueryBuilder()->select('c')->from('Tillikum\\Entity\\Facility\\Config\\Room\\Room', 'c')->where('c.start <= :proposedEnd')->andWhere('c.end >= :proposedStart')->andWhere('c.suite = :suite')->setParameter('proposedStart', $bookingRange->getStart())->setParameter('proposedEnd', $bookingRange->getEnd())->setParameter('suite', $config->suite)->getQuery()->getResult(); foreach ($suiteConfigs as $suiteConfig) { if (!empty($suiteConfig->gender)) { $suiteGenderSpec = isset($suiteGenderSpec) ? $suiteGenderSpec->andSpec(new GenderMatchSpecification($suiteConfig->gender)) : new GenderMatchSpecification($suiteConfig->gender); } } } } foreach ($holds as $hold) { $occupancyInputs[] = new OccupancyInput($hold->start, $hold->space * -1, sprintf('start of a hold from %s to %s', $hold->start->format('Y-m-d'), $hold->end->format('Y-m-d'))); $occupancyInputs[] = new OccupancyInput(date_modify(clone $hold->end, '+1 day'), $hold->space, sprintf('end of a hold from %s to %s', $hold->start->format('Y-m-d'), $hold->end->format('Y-m-d'))); if (!empty($hold->gender)) { $holdGenderSpec = isset($holdGenderSpec) ? $holdGenderSpec->andSpec(new GenderMatchSpecification($hold->gender)) : new GenderMatchSpecification($hold->gender); } } $occupancyEngine = new OccupancyEngine($occupancyInputs); $occupancyResult = $occupancyEngine->run(); if (!$occupancyResult->getIsSuccess()) { $this->facility_name->addError(sprintf($this->getTranslator()->translate('There is no available space in this facility to book another' . ' resident during the specified time period. The problem' . ' occurred at the %s.'), $occupancyResult->getCulprit()->getDescription())); return false; } if (isset($bookingGenderSpec) && !$bookingGenderSpec->isSatisfiedBy($person->gender)) { $this->addWarning(sprintf($this->getTranslator()->translate('The person you are booking with gender %s did not meet' . ' the gender requirements of the other people booked' . ' to this facility for the desired time period.'), $person->gender)); } if (isset($facilityGenderSpec) && !$facilityGenderSpec->isSatisfiedBy($person->gender)) { $this->addWarning(sprintf($this->getTranslator()->translate('The person you are booking with gender %s did not meet' . ' the gender requirements of the configurations for this' . ' facility for the desired time period.'), $person->gender)); } if (isset($holdGenderSpec) && !$holdGenderSpec->isSatisfiedBy($person->gender)) { $this->addWarning(sprintf($this->getTranslator()->translate('The person you are booking with gender %s did not meet' . ' the gender requirements of the holds on this facility' . ' for the desired time period.'), $person->gender)); } if (isset($suiteGenderSpec) && !$suiteGenderSpec->isSatisfiedBy($person->gender)) { $this->addWarning(sprintf($this->getTranslator()->translate('The person you are booking with gender %s did not meet' . ' the gender requirements of the other people booked to' . ' this suite for the specified time period.'), $person->gender)); } return true; }
public function isValid($data) { if (!parent::isValid($data)) { return false; } $startDate = new DateTime($data['start']); $endDate = new DateTime($data['end']); if ($startDate > $endDate) { $this->start->addError($this->getTranslator()->translate('The start date must be on or before the end date.')); $this->end->addError($this->getTranslator()->translate('The end date must be on or after the start date.')); return false; } $configRange = new DateRange($startDate, $endDate); $facility = $this->em->find('Tillikum\\Entity\\Facility\\Facility', $data['facility_id']); $bookings = $this->em->createQueryBuilder()->select('b')->from('Tillikum\\Entity\\Booking\\Facility\\Facility', 'b')->where('b.start <= :proposedEnd')->andWhere('b.end >= :proposedStart')->andWhere('b.facility = :facility')->orderBy('b.start')->setParameter('facility', $facility)->setParameter('proposedStart', $configRange->getStart())->setParameter('proposedEnd', $configRange->getEnd())->getQuery()->getResult(); $qb = $this->em->createQueryBuilder()->select('c')->from('Tillikum\\Entity\\Facility\\Config\\Config', 'c')->where('c.facility = :facility')->orderBy('c.start')->setParameter('facility', $this->entity->facility); if ($this->entity && isset($this->entity->id)) { $qb->andWhere('c != :entity')->setParameter('entity', $this->entity); } $configs = $qb->getQuery()->getResult(); $overlappingQueryBuilder = $this->em->createQueryBuilder()->select('c')->from('Tillikum\\Entity\\Facility\\Config\\Config', 'c')->where('c.start <= :proposedStart')->andWhere('c.end >= :proposedEnd')->andWhere('c.facility = :facility')->setParameter('proposedStart', $configRange->getStart())->setParameter('proposedEnd', $configRange->getEnd())->setParameter('facility', $this->entity->facility); if ($this->entity && isset($this->entity->id)) { $overlappingQueryBuilder->andWhere('c != :entity')->setParameter('entity', $this->entity); } $overlappingConfigs = $overlappingQueryBuilder->getQuery()->getResult(); $holds = $this->em->createQueryBuilder()->select('h')->from('Tillikum\\Entity\\Facility\\Hold\\Hold', 'h')->where('h.start <= :proposedEnd')->andWhere('h.end >= :proposedStart')->andWhere('h.facility = :facility')->orderBy('h.start')->setParameter('facility', $facility)->setParameter('proposedStart', $configRange->getStart())->setParameter('proposedEnd', $configRange->getEnd())->getQuery()->getResult(); if (count($overlappingConfigs) > 0) { foreach ($overlappingConfigs as $config) { $errorMessage = sprintf($this->getTranslator()->translate('An existing configuration from %s to %s overlaps your intended configuration.'), $config->start->format('Y-m-d'), $config->end->format('Y-m-d')); $this->start->addError($errorMessage); $this->end->addError($errorMessage); } return false; } $occupancyInputs = array(new OccupancyInput($configRange->getStart(), $data['capacity'], sprintf('start of the facility configuration you specified from %s to %s', $configRange->getStart()->format('Y-m-d'), $configRange->getEnd()->format('Y-m-d')))); foreach ($configs as $config) { $occupancyInputs[] = new OccupancyInput($config->start, $config->capacity, sprintf('start of a facility configuration from %s to %s', $config->start->format('Y-m-d'), $config->end->format('Y-m-d'))); if (!empty($data['suite'])) { $suiteConfigs = $this->em->createQueryBuilder()->select('c')->from('Tillikum\\Entity\\Facility\\Config\\Room\\Room', 'c')->join('c.suite', 's')->where('c.start <= :proposedEnd')->andWhere('c.end >= :proposedStart')->andWhere('s.id = :suiteId')->setParameter('proposedStart', $configRange->getStart())->setParameter('proposedEnd', $configRange->getEnd())->setParameter('suiteId', $data['suite'])->getQuery()->getResult(); foreach ($suiteConfigs as $suiteConfig) { if (!empty($suiteConfig->gender)) { $suiteGenderSpec = isset($suiteGenderSpec) ? $suiteGenderSpec->andSpec(new GenderMatchSpecification($suiteConfig->gender)) : new GenderMatchSpecification($suiteConfig->gender); } } } } // We need to re-sort since the user-specified input will not be sorted usort($occupancyInputs, function ($a, $b) { if ($a->getDate() == $b->getDate()) { return 0; } return $a->getDate() < $b->getDate() ? -1 : 1; }); // Actually calculate moments after re-sorting $currentConfigSpace = 0; foreach ($occupancyInputs as $idx => $input) { $oldValue = $input->getValue(); $occupancyInputs[$idx] = new OccupancyInput($input->getDate(), $input->getValue() - $currentConfigSpace, $input->getDescription()); $currentConfigSpace = $oldValue; } foreach ($bookings as $booking) { $occupancyInputs[] = new OccupancyInput($booking->start, -1, sprintf('start of a booking from %s to %s', $booking->start->format('Y-m-d'), $booking->end->format('Y-m-d'))); $occupancyInputs[] = new OccupancyInput(date_modify(clone $booking->end, '+1 day'), 1, sprintf('end of a booking from %s to %s', $booking->start->format('Y-m-d'), $booking->end->format('Y-m-d'))); if (!empty($booking->person->gender)) { $bookingGenderSpec = isset($bookingGenderSpec) ? $bookingGenderSpec->andSpec(new GenderMatchSpecification($booking->person->gender)) : new GenderMatchSpecification($booking->person->gender); } } foreach ($holds as $hold) { $occupancyInputs[] = new OccupancyInput($hold->start, $hold->space * -1, sprintf('start of a hold from %s to %s', $hold->start->format('Y-m-d'), $hold->end->format('Y-m-d'))); $occupancyInputs[] = new OccupancyInput(date_modify(clone $hold->end, '+1 day'), $hold->space, sprintf('end of a hold from %s to %s', $hold->start->format('Y-m-d'), $hold->end->format('Y-m-d'))); if (!empty($hold->gender)) { $holdGenderSpec = isset($holdGenderSpec) ? $holdGenderSpec->andSpec(new GenderMatchSpecification($hold->gender)) : new GenderMatchSpecification($hold->gender); } } $occupancyEngine = new OccupancyEngine($occupancyInputs); $occupancyResult = $occupancyEngine->run(); if (!$occupancyResult->getIsSuccess()) { $this->capacity->addError(sprintf($this->getTranslator()->translate('There are too many claims on space in this facility ' . 'to change the capacity of this configuration. The ' . 'problem occurred at the %s.'), $occupancyResult->getCulprit()->getDescription())); return false; } if (isset($holdGenderSpec) && !$holdGenderSpec->isSatisfiedBy($data['gender'])) { $this->addWarning(sprintf($this->getTranslator()->translate('The desired configuration gender does not meet the' . ' gender requirements of an overlapping facility hold.'))); } if (isset($bookingGenderSpec) && !$bookingGenderSpec->isSatisfiedBy($data['gender'])) { $this->addWarning(sprintf($this->getTranslator()->translate('The desired configuration gender does not meet the' . ' gender requirements of an overlapping booking.'))); } if (isset($suiteGenderSpec) && !$suiteGenderSpec->isSatisfiedBy($data['gender'])) { $this->addWarning(sprintf($this->getTranslator()->translate('The desired configuration gender does not meet the' . ' gender requirements of an overlapping booking in a' . ' related suite.'))); } return true; }