src/Helpers/Bookings/BookingHelper.php line 30

Open in your IDE?
  1. <?php
  2. namespace App\Helpers\Bookings;
  3. use App\Entity\Animals\Animal;
  4. use App\Entity\Animals\MixerCategory;
  5. use App\Entity\Animals\Type;
  6. use App\Entity\Bookings\Booking;
  7. use App\Entity\Bookings\Item;
  8. use App\Entity\Bookings\Payment;
  9. use Symfony\Component\DependencyInjection\ContainerInterface as Container;
  10. use Symfony\Component\Yaml\Parser;
  11. use Doctrine\ORM\EntityManagerInterface;
  12. use \App\Helpers\System\ConfigHelper;
  13. use \App\Helpers\Services\ServiceHelper;
  14. use \App\Helpers\Animals\AnimalHelper;
  15. class BookingHelper
  16. {
  17. protected $container;
  18. protected $em;
  19. protected $config;
  20. protected $housingByAnimalType;
  21. public function __construct(Container $container, EntityManagerInterface $em, ConfigHelper $configHelper, ServiceHelper $serviceHelper, AnimalHelper $animalHelper)
  22. {
  23. $this->container = $container;
  24. $this->em = $em;
  25. $this->configHelper = $configHelper;
  26. $this->serviceHelper = $serviceHelper;
  27. $this->animalHelper = $animalHelper;
  28. $this->boardingService = $this->em->getRepository(\App\Entity\Services\Service::class)->findOneFiltered(array(
  29. 'id' => $this->configHelper->getValue("boarding_service_id")
  30. ));
  31. }
  32. public function getItem(\App\Entity\Bookings\Booking $booking, \App\Entity\Animals\Animal $animal, \App\Entity\Services\Service $service)
  33. {
  34. return $this->em->getRepository(\App\Entity\Bookings\Item::class)->findOneFiltered(array(
  35. 'booking' => $booking,
  36. 'animal' => $animal,
  37. 'service' => $service,
  38. 'parent' => null
  39. ));
  40. }
  41. public function getBoardingItemForAnimal(\App\Entity\Bookings\Booking $booking, \App\Entity\Animals\Animal $animal)
  42. {
  43. return $this->em->getRepository(\App\Entity\Bookings\Item::class)->findOneFiltered(array(
  44. 'booking' => $booking,
  45. 'animal' => $animal,
  46. 'service' => $this->getBoardingService(),
  47. 'parent' => null
  48. ));
  49. }
  50. public function getBoardingItems(\App\Entity\Bookings\Booking $booking, $extraCriteria = array())
  51. {
  52. return $this->em->getRepository(\App\Entity\Bookings\Item::class)->findFiltered(array_merge(array(
  53. 'booking' => $booking,
  54. 'service' => $this->getBoardingService(),
  55. 'parent' => null
  56. ), $extraCriteria));
  57. }
  58. public function bookingHasService(\App\Entity\Bookings\Booking $booking, \App\Entity\Animals\Animal $animal, \App\Entity\Services\Service $service)
  59. {
  60. // Get items for this booking
  61. $item = $this->em->getRepository(\App\Entity\Bookings\Item::class)->findOneFiltered(array(
  62. 'booking' => $booking,
  63. 'animal' => $animal,
  64. 'service' => $service
  65. ));
  66. if ($item)
  67. return true;
  68. return false;
  69. }
  70. public function getBoardingService()
  71. {
  72. $boardingService = $this->boardingService;
  73. // Make sure it exists
  74. if (!$boardingService)
  75. throw new \Exception("Boarding service not found!");
  76. return $boardingService;
  77. }
  78. public function calculateBoardingPrice(\App\Entity\Bookings\Item $boardingItem)
  79. {
  80. // Totals
  81. $total = 0;
  82. // Get service
  83. $service = $this->getBoardingService();
  84. // Get animal
  85. $animal = $boardingItem->getAnimal();
  86. // Get start and end date
  87. $arrivalDate = $boardingItem->getArrivalDate();
  88. // Afternoon or morning?
  89. if ($boardingItem->getArrivalTime() == "pm")
  90. $arrivalDate->setTime(13, 0, 0);
  91. else
  92. $arrivalDate->setTime(9, 0, 0);
  93. // Get departure date
  94. $departureDate = $boardingItem->getDepartureDate();
  95. // Afternoon or morning?
  96. if ($boardingItem->getDepartureTime() == "pm")
  97. $departureDate->setTime(13, 0, 0);
  98. else
  99. $departureDate->setTime(9, 0, 0);
  100. // Get global variables
  101. $dayStart = $this->configHelper->getValue('daytime_start');
  102. $nightStart = $this->configHelper->getValue('nightitme_start');
  103. // Calculate the period length
  104. $period = new \DatePeriod($arrivalDate, \DateInterval::createFromDateString('1 day'), $departureDate);
  105. // Keep track of days
  106. $dayCount = 1;
  107. // Loop the days
  108. foreach ($period as $singleDay)
  109. {
  110. // Get the price for this day
  111. $price = $this->serviceHelper->getPrice($service, $animal->getType(), $animal->getSize(), ($boardingItem ? $boardingItem->getMixerCategory() : null), null, $singleDay);
  112. if (!$price)
  113. continue;
  114. // Create the cut off times
  115. $dayPeriodStart = new \DateTime(date("Y-m-d H:i:0", strtotime($singleDay->format("Y-m-d") . ' ' . $dayStart)));
  116. $nightPeriodStart = new \DateTime(date("Y-m-d H:i:0", strtotime($singleDay->format("Y-m-d") . ' ' . $nightStart)));
  117. // Weekend?
  118. if (in_array($singleDay->format("D"), array("Sun", "Sat")))
  119. $isWeekend = true;
  120. else
  121. $isWeekend = false;
  122. // Are we there for the night?
  123. if ($departureDate >= $nightPeriodStart)
  124. $total += floatval($isWeekend ? $price->getWeekendNightPrice() : $price->getWeekdayNightPrice());
  125. // Are we there for the day?
  126. elseif (($arrivalDate > $dayPeriodStart) || (($departureDate <= $nightPeriodStart)))
  127. {
  128. // Only animals staying for 1 day will fall in here.
  129. // We will charge them a day rate for 1 day, otherwise they will be charged a night rate.
  130. if (($singleDay->format("Y-m-d") == $departureDate->format("Y-m-d")) && ($dayCount == 1))
  131. {
  132. $total += floatval($isWeekend ? $price->getWeekendDayPrice() : $price->getWeekdayDayPrice());
  133. }
  134. }
  135. // Increment the day count
  136. $dayCount++;
  137. }
  138. return $total;
  139. }
  140. public function calculatePrice(\App\Entity\Bookings\Item $item)
  141. {
  142. // Define variables
  143. $total = 0;
  144. $serviceDate = null;
  145. $animal = $item->getAnimal();
  146. $service = $item->getService();
  147. // Assume weekday pricing by default
  148. $isWeekend = false;
  149. // Get the day of the service
  150. if ($item->getServiceDate())
  151. $serviceDate = $item->getServiceDate();
  152. $savedPrice = false;
  153. // Get the price for this day
  154. if ($service->getPricingType() == "complex")
  155. {
  156. // Got a saved price?
  157. if ($item->getWeekdayDayPrice())
  158. {
  159. $price = $item->getWeekdayDayPrice();
  160. $savedPrice = true;
  161. }
  162. else
  163. $price = $this->serviceHelper->getPrice($service, $animal->getType(), $animal->getSize(), $item->getMixerCategory(), null, $serviceDate);
  164. }
  165. else
  166. {
  167. // Got a saved price?
  168. if ($item->getWeekdayDayPrice())
  169. {
  170. $price = $item->getWeekdayDayPrice();
  171. $savedPrice = true;
  172. }
  173. else
  174. $price = $this->serviceHelper->getPrice($service, $animal->getType(), $animal->getSize(), null, null, $serviceDate);
  175. }
  176. if ($serviceDate)
  177. {
  178. // Weekend?
  179. if (in_array($serviceDate->format("D"), array("Sun", "Sat")))
  180. $isWeekend = true;
  181. }
  182. // Saved price?
  183. if ($savedPrice)
  184. {
  185. $total += $price;
  186. return array(
  187. 'total' => $total,
  188. 'weekdayDayPrice' => $price,
  189. 'weekdayNightPrice' => 0,
  190. 'weekendDayPrice' => 0,
  191. 'weekendNightPrice' => 0
  192. );
  193. }
  194. else
  195. {
  196. if ($item->getService()->getPricingType() == "complex")
  197. {
  198. // Always assume day pricing for any services other than boarding.
  199. if ($price)
  200. $total += floatval($isWeekend ? $price->getWeekendDayPrice() : $price->getWeekdayDayPrice());
  201. }
  202. else
  203. {
  204. // Simple pricing always uses the weekday day price
  205. if ($price)
  206. $total += floatval($price->getWeekdayDayPrice());
  207. }
  208. return array(
  209. 'total' => $total,
  210. 'weekdayDayPrice' => ($price ? $price->getWeekdayDayPrice() : 0),
  211. 'weekdayNightPrice' => ($price ? $price->getWeekendNightPrice() : 0),
  212. 'weekendDayPrice' => ($price ? $price->getWeekendDayPrice() : 0),
  213. 'weekendNightPrice' => ($price ? $price->getWeekendNightPrice() : 0)
  214. );
  215. }
  216. }
  217. public function getAnimalMeals(\App\Entity\Bookings\Booking $booking, \App\Entity\Animals\Animal $animal)
  218. {
  219. // Missing booking?
  220. if (!$booking)
  221. throw new \Exception("Booking should be set in getAnimalMeals()");
  222. // Missing animal?
  223. if (!$animal)
  224. throw new \Exception("Animal should be set in getAnimalMeals()");
  225. // Array to hold meals
  226. $mealArray = array();
  227. // Get slots
  228. $slots = $this->em->getRepository(\App\Entity\Meals\Slot::class)->findFiltered();
  229. // Loop slots
  230. foreach ($slots as $someSlot)
  231. {
  232. $mealArray[] = array(
  233. "slot" => $someSlot,
  234. "booking_meal" => $this->em->getRepository(\App\Entity\Bookings\Meal::class)->findOneFiltered(array(
  235. 'animal' => $animal,
  236. 'booking' => $booking,
  237. 'slot' => $someSlot
  238. )),
  239. "animal_meal" => $this->em->getRepository(\App\Entity\Animals\Meal::class)->findOneFiltered(array(
  240. 'animal' => $animal,
  241. 'slot' => $someSlot
  242. ))
  243. );
  244. }
  245. return $mealArray;
  246. }
  247. public function getMealByPeriod(Animal $animal, Booking $booking, string $period)
  248. {
  249. // Get slot
  250. $slot = $this->em->getRepository(\App\Entity\Meals\Slot::class)->findOneFiltered([
  251. "period" => $period
  252. ]);
  253. // Not found?
  254. if (!$slot)
  255. return null;
  256. // Grab the meal
  257. return $this->em->getRepository(\App\Entity\Bookings\Meal::class)->findOneFiltered(array(
  258. 'animal' => $animal,
  259. 'booking' => $booking,
  260. 'slot' => $slot
  261. ));
  262. }
  263. public function getMealCheck(\App\Entity\Bookings\Meal $bookingMeal, \DateTime $selectedDate, string $period)
  264. {
  265. // Find item
  266. $item = $this->getItem($bookingMeal->getBooking(), $bookingMeal->getAnimal(), $this->getBoardingService());
  267. if (!$item)
  268. return null;
  269. // Check if this meal has been issued
  270. $mealCheck = $this->em->getRepository(\App\Entity\Bookings\MealCheck::class)->findOneFiltered(array(
  271. "animal" => $bookingMeal->getAnimal(),
  272. "item" => $item,
  273. "period" => $period,
  274. "selectedDate" => new \DateTime(date("Y-m-d", strtotime($selectedDate->format("Y-m-d")))),
  275. ));
  276. return $mealCheck;
  277. }
  278. public function getOtherItems(\App\Entity\Bookings\Booking $booking, \App\Entity\Animals\Animal $animal)
  279. {
  280. // Get items
  281. $items = $this->em->getRepository(\App\Entity\Bookings\Item::class)->findFiltered(array(
  282. "otherItems" => true,
  283. "animal" => $animal,
  284. "booking" => $booking
  285. ));
  286. return $items;
  287. }
  288. public function criteriaMet(\App\Entity\Bookings\Booking $booking)
  289. {
  290. // Get booking animals
  291. $bookingAnimals = $booking->getAnimals();
  292. // Loop animals
  293. foreach ($bookingAnimals as $someAnimal)
  294. {
  295. // Error on the animal?
  296. if (count($this->animalHelper->animalHasErrors($someAnimal)))
  297. return false;
  298. // Get vaccinations
  299. $vaccinations = $this->animalHelper->isVaccinated(null, $someAnimal);
  300. foreach ($vaccinations as $someVaccination)
  301. {
  302. if (!$someVaccination["verified"])
  303. return false;
  304. }
  305. }
  306. return true;
  307. }
  308. public function checkVaccinations(\App\Entity\Bookings\Booking $booking)
  309. {
  310. // Get animals
  311. $animals = $booking->getAnimals();
  312. // Vaccination flag
  313. $fullyVaccinated = true;
  314. // Loop animals
  315. foreach ($animals as $someAnimal)
  316. {
  317. // Get all vaccination for this animal type
  318. $vaccinationTypes = $this->em->getRepository(\App\Entity\Vaccinations\Type::class)->findFiltered(array(
  319. "animalType" => $someAnimal->getType()
  320. ));
  321. // Loop vaccinations
  322. foreach ($vaccinationTypes as $someVaccinationType)
  323. {
  324. if (!$this->animalHelper->isVaccinated($someVaccinationType, $someAnimal))
  325. $fullyVaccinated = false;
  326. }
  327. }
  328. // Fully vaccinated?
  329. if ($fullyVaccinated)
  330. {
  331. // Complete vaccinations
  332. $booking->setVaccinationsCompleted(new \DateTime());
  333. }
  334. else
  335. {
  336. $booking->setVaccinationsCompleted(null);
  337. }
  338. // Persist booking
  339. $this->em->persist($booking);
  340. return $booking;
  341. }
  342. function getDepositRequirement(\App\Entity\Bookings\Item $item)
  343. {
  344. // Default to 0
  345. $depositRequired = 0;
  346. if ($item->getId())
  347. $arrivalDate = $item->getArrivalDate();
  348. else
  349. $arrivalDate = null;
  350. if ($arrivalDate)
  351. {
  352. // Is this item a boarding service?
  353. if ($item->getService() === $this->getBoardingService())
  354. {
  355. // Get the price for one night
  356. $price = $this->serviceHelper->getPrice($item->getService(), $item->getAnimal()->getType(), $item->getAnimal()->getSize(), $item->getMixerCategory(), null, $arrivalDate);
  357. // Found a price?
  358. if ($price && $price->getWeekdayNightPrice())
  359. {
  360. // Less than 4 nights?
  361. if ($item->getDurationInDays() < 4)
  362. {
  363. // Deposit is equal to 1 night
  364. $depositRequired = $price->getWeekdayNightPrice();
  365. }
  366. else
  367. {
  368. // Query for a deposit amount
  369. $deposit = $this->em->getRepository(\App\Entity\Services\Deposit::class)->findOneFiltered(array(
  370. "dateBetween" => $arrivalDate
  371. ));
  372. if ($deposit && $deposit->getDepositPercentage() > 0)
  373. {
  374. // Calculate the deposit required
  375. $depositRequired = $deposit->getDepositPercentage() * $item->getTotal();
  376. }
  377. }
  378. }
  379. else
  380. throw new \Exception("Cannot get price!");
  381. }
  382. else
  383. {
  384. // Deposit is zero
  385. $depositRequired = 0;
  386. }
  387. return $depositRequired;
  388. }
  389. // Query for the default
  390. $deposit = $this->em->getRepository(\App\Entity\Services\Deposit::class)->findOneFiltered(array(
  391. "startDate" => null,
  392. "endDate" => null
  393. ));
  394. if ($deposit && $deposit->getDepositPercentage() > 0)
  395. {
  396. // Calculate the deposit required
  397. $depositRequired = $deposit->getDepositPercentage() * $item->getTotal();
  398. }
  399. // Is this item a boarding service?
  400. if ($item->getService() === $this->getBoardingService())
  401. {
  402. // Deposit too low?
  403. if ($depositRequired < $this->configHelper->getValue('minimum_boarding_deposit'))
  404. $depositRequired = $this->configHelper->getValue('minimum_boarding_deposit');
  405. }
  406. // Make sure the deposit isn't greater than the actual item cost
  407. if ($depositRequired > $item->getTotal())
  408. $depositRequired = $item->getTotal();
  409. return $depositRequired;
  410. }
  411. public function getBookingBuddies(\App\Entity\Bookings\Item $bookingItem)
  412. {
  413. // No animal on booking item?
  414. if (!$bookingItem->getAnimal())
  415. throw new \Exception("No animal on booking item");
  416. // No buddies?
  417. if (!count($bookingItem->getAnimal()->getBuddies()))
  418. return [];
  419. $buddyArray = array();
  420. foreach ($bookingItem->getAnimal()->getBuddies() as $someBuddy)
  421. $buddyArray[] = $someBuddy;
  422. // Find booking items
  423. $otherBookingItems = $this->em->getRepository(\App\Entity\Bookings\Item::class)->findFiltered(array(
  424. "conflictingDates" => array(
  425. $bookingItem->getArrivalDate(),
  426. $bookingItem->getDepartureDate(),
  427. ),
  428. "animal" => array("in", $buddyArray)
  429. ));
  430. // Array to hold matching animals
  431. $animalArray = array();
  432. // Loop bookings
  433. foreach ($otherBookingItems as $someItem)
  434. {
  435. // No animal
  436. if (!$someItem->getAnimal())
  437. continue;
  438. if (in_array($someItem->getAnimal(), $buddyArray))
  439. {
  440. // Not in the array
  441. if (!in_array($someItem->getAnimal(), $animalArray))
  442. $animalArray[] = $someItem->getAnimal();
  443. }
  444. }
  445. return $animalArray;
  446. }
  447. public function getCapacity(\App\Entity\Bookings\Housing $housing, \App\Entity\Animals\Size $size)
  448. {
  449. // Query for capacity
  450. $capacity = $this->em->getRepository(\App\Entity\Bookings\Capacity::class)->findOneFiltered(array(
  451. "housing" => $housing,
  452. "size" => $size
  453. ));
  454. if (!$capacity)
  455. return null;
  456. return $capacity->getValue();
  457. }
  458. public function generateAdditionalCharges(\App\Entity\Bookings\Booking $booking)
  459. {
  460. // Get booking items
  461. $items = $booking->getItems();
  462. // No items?
  463. if (!count($items))
  464. return $booking;
  465. // Get first day of booking
  466. $firstArrivalDate = $booking->getFirstArrivalDate();
  467. $lastDepartureDate = $booking->getLastDepartureDate();
  468. // Not got a valid set of dates?
  469. if (!$firstArrivalDate || !$lastDepartureDate)
  470. {
  471. // No dates to calculate additional charges. This implies there are no animals. Remove everything on the booking
  472. foreach ($items as $someItem)
  473. {
  474. // Remove and delete the item
  475. $booking->removeItem($someItem);
  476. $this->em->remove($someItem);
  477. }
  478. return $booking;
  479. }
  480. // Array to keep track of charges
  481. $chargeArray = array();
  482. // Service only booking?
  483. if ($booking->getIsServiceOnlyBooking())
  484. {
  485. // This is a service only booking.
  486. // Remove any additional charges associated with housing
  487. // Get the the additional boarding charge items for this booking
  488. $boardingAdditionalCharges = $this->em->getRepository(\App\Entity\Bookings\Item::class)->findFiltered(array(
  489. "service" => $this->getBoardingService(),
  490. "additionalCharge" => array("not_null", true), // Not null
  491. "booking" => $booking
  492. ));
  493. // Loop the charges
  494. foreach ($boardingAdditionalCharges as $someItem)
  495. {
  496. // Remove from booking
  497. $booking->removeItem($someItem);
  498. // Remove item
  499. $this->em->remove($someItem);
  500. }
  501. }
  502. else
  503. {
  504. // Loop items
  505. foreach ($items as $someItem)
  506. {
  507. // Not a boarding service?
  508. if ($someItem->getService() != $this->getBoardingService())
  509. {
  510. // Skip it
  511. continue;
  512. }
  513. // Calculate the period length
  514. $period = new \DatePeriod($someItem->getArrivalDate(), \DateInterval::createFromDateString('1 day'), $someItem->getDepartureDate());
  515. // Keep track of the number of days
  516. $dayCount = 1;
  517. // Loop the days
  518. foreach ($period as $singleDay)
  519. {
  520. // Don't charge if this is the last day
  521. if ($singleDay->format("Y-m-d") != $someItem->getDepartureDate()->format("Y-m-d"))
  522. {
  523. // Get any additional charges for this day
  524. $additionalCharges = $this->em->getRepository(\App\Entity\Services\AdditionalCharge::class)->findFiltered(array(
  525. "service" => $this->getBoardingService(),
  526. "animalType" => $someItem->getAnimal()->getType(),
  527. "dateBetween" => $singleDay
  528. ));
  529. // Got any?
  530. if (count($additionalCharges))
  531. {
  532. // Loop additional charges
  533. foreach ($additionalCharges as $someAdditionalCharge)
  534. {
  535. // Not in the array yet?
  536. if (!array_key_exists($someAdditionalCharge->getId() . '_' . $someItem->getAnimal()->getId(), $chargeArray))
  537. {
  538. // Add to charge tracking array
  539. $chargeArray[$someAdditionalCharge->getId() . '_' . $someItem->getAnimal()->getId()] = array(
  540. "additionalCharge" => $someAdditionalCharge,
  541. "animal" => $someItem->getAnimal(),
  542. "quantity" => 0
  543. );
  544. }
  545. // Add onto total
  546. $chargeArray[$someAdditionalCharge->getId() . '_' . $someItem->getAnimal()->getId()]['quantity']++;
  547. }
  548. }
  549. }
  550. // Increment day count
  551. $dayCount++;
  552. }
  553. }
  554. // Loop the charge array
  555. foreach ($chargeArray as $someAdditionalCharge)
  556. {
  557. // Check for existing item
  558. $additionalChargeItem = $this->em->getRepository(\App\Entity\Bookings\Item::class)->findOneFiltered(array(
  559. "parent" => null,
  560. "additionalCharge" => $someAdditionalCharge['additionalCharge'],
  561. "animal" => $someAdditionalCharge['animal'],
  562. "booking" => $booking
  563. ));
  564. // Not found
  565. if (!$additionalChargeItem)
  566. {
  567. // New instance of additional charge item
  568. $additionalChargeItem = new \App\Entity\Bookings\Item(array(
  569. "booking" => $booking,
  570. "description" => $someAdditionalCharge['additionalCharge']->getName() . " - " . $someAdditionalCharge['additionalCharge']->getAnimalType()->getName(),
  571. "additionalCharge" => $someAdditionalCharge['additionalCharge'],
  572. "quantity" => $someAdditionalCharge['quantity'],
  573. "taxRate" => $this->configHelper->getValue("vat_rate"),
  574. "animal" => $someAdditionalCharge['animal']
  575. ));
  576. // Persist additional charge
  577. $this->em->persist($additionalChargeItem);
  578. // Add to item
  579. $booking->addItem($additionalChargeItem);
  580. }
  581. // Update charge item
  582. $additionalChargeItem->setRate($someAdditionalCharge['additionalCharge']->getPrice());
  583. $additionalChargeItem->setQuantity($someAdditionalCharge['quantity']);
  584. // Persist additional charge
  585. $this->em->persist($additionalChargeItem);
  586. }
  587. // Persist booking
  588. $this->em->persist($booking);
  589. }
  590. // Get all other items that aren't boarding
  591. $nonBoardingItems = $this->em->getRepository(\App\Entity\Bookings\Item::class)->findFiltered(array(
  592. "service" => array("not_in", [$this->getBoardingService()]),
  593. "booking" => $booking,
  594. ));
  595. // Loop non boarding items such as walks and washes
  596. foreach ($nonBoardingItems as $someItem)
  597. {
  598. // Is there a service date on the item?
  599. if ($someItem->getServiceDate())
  600. {
  601. // Check for any additional charges
  602. $additionalCharges = $this->em->getRepository(\App\Entity\Services\AdditionalCharge::class)->findFiltered(array(
  603. "service" => $someItem->getService(),
  604. "animalType" => $someItem->getAnimal()->getType(),
  605. "dateBetween" => $someItem->getServiceDate()
  606. ));
  607. // Loop additional charges
  608. foreach ($additionalCharges as $someAdditionalCharge)
  609. {
  610. // Check fo existing item
  611. $additionalChargeItem = $this->em->getRepository(\App\Entity\Bookings\Item::class)->findOneFiltered(array(
  612. "parent" => $someItem,
  613. "additionalCharge" => $someAdditionalCharge
  614. ));
  615. // Not found
  616. if (!$additionalChargeItem)
  617. {
  618. // New instance of additional charge item
  619. $additionalChargeItem = new \App\Entity\Bookings\Item(array(
  620. "animal" => $someItem->getAnimal(),
  621. "booking" => $someItem->getBooking(),
  622. "parent" => $someItem,
  623. "description" => $someAdditionalCharge->getName(),
  624. "service" => null,
  625. "additionalCharge" => $someAdditionalCharge,
  626. "quantity" => 1,
  627. "taxRate" => $this->configHelper->getValue("vat_rate")
  628. ));
  629. // Add to item
  630. $someItem->addChild($additionalChargeItem);
  631. $booking->addItem($additionalChargeItem);
  632. // Persist item
  633. $this->em->persist($someItem);
  634. }
  635. // Update rate
  636. $additionalChargeItem->setRate($someAdditionalCharge->getPrice());
  637. // Persist additional charge
  638. $this->em->persist($additionalChargeItem);
  639. // Persist booking
  640. $this->em->persist($booking);
  641. }
  642. }
  643. }
  644. return $booking;
  645. }
  646. public function refreshItemTotals(Item $item)
  647. {
  648. // Got children
  649. if (count($item->getChildren()))
  650. {
  651. // Loop and refresh totals
  652. foreach ($item->getChildren() as $someChildItem)
  653. $this->refreshItemTotals($someChildItem);
  654. $item->setTotalTax(0);
  655. $item->setTotal(0);
  656. $item->setSubtotal(0);
  657. $item->setRate(0);
  658. $item->setDeposit(0);
  659. }
  660. else
  661. {
  662. $item->setTotalTax(($item->getRate() * $item->getQuantity()) - (($item->getRate() * $item->getQuantity()) / (1 + ($item->getTaxRate() / 100))));
  663. $item->setTotal(($item->getRate() * $item->getQuantity()));
  664. $item->setSubtotal($item->getTotal() - $item->getTotalTax());
  665. }
  666. // Is this the boarding service?
  667. if ($item->getService() === $this->getBoardingService())
  668. {
  669. // Clear it
  670. $item->setServiceTotal(0);
  671. // Set it
  672. $item->setBoardingTotal($item->getTotal());
  673. // Calculate deposit required
  674. $itemDepositRequired = $this->getDepositRequirement($item);
  675. $item->setDeposit($itemDepositRequired);
  676. // Is the deposit too high?
  677. if ($itemDepositRequired > $item->getTotal())
  678. {
  679. // Reduce deposit down to total
  680. $item->setDeposit($item->getTotal());
  681. }
  682. }
  683. else
  684. {
  685. // Clear it
  686. $item->setBoardingTotal(0);
  687. // Set it
  688. $item->setServiceTotal($item->getTotal());
  689. }
  690. // Is this a top level item?
  691. if (!$item->getParent())
  692. {
  693. // Grab payments (for items with child items, payments go against the top/parent item)
  694. $payments = $this->em->getRepository(Payment::class)->findFiltered([
  695. "item" => $item->getParent() ? $item->getParent() : $item
  696. ]);
  697. // Grab payments
  698. $totalPaid = 0;
  699. foreach ($payments as $somePayment)
  700. $totalPaid += $somePayment->getAmount();
  701. // Set it
  702. $item->setPaid($totalPaid);
  703. }
  704. else
  705. {
  706. // Clear paid amount
  707. $item->setPaid(0);
  708. }
  709. }
  710. public function refreshTotals(\App\Entity\Bookings\Booking $booking, bool $flushChanges = false)
  711. {
  712. // Refresh the booking (i.e. pull it fresh from DB)
  713. $this->em->refresh($booking);
  714. // Generate additional charges on booking
  715. $booking = $this->generateAdditionalCharges($booking);
  716. // Calculate boarding and service totals
  717. $animalTotalArray = array();
  718. // Track some things
  719. $discountTotal = 0;
  720. $boardingItemCount = 0;
  721. $total = 0;
  722. $subTotal = 0;
  723. $depositTotal = 0;
  724. // Get the items
  725. $items = $this->em->getRepository(Item::class)->findFiltered([
  726. "booking" => $booking,
  727. "parent" => null
  728. ], [
  729. "i.id" => "ASC"
  730. ]);
  731. // Count how many boarding items there are
  732. foreach ($items as $someItem)
  733. {
  734. // Boarding Service?
  735. if ($someItem->getService() == $this->getBoardingService() && !$booking->getIsServiceOnlyBooking())
  736. $boardingItemCount++;
  737. }
  738. // Loop items
  739. foreach ($items as $someItem)
  740. {
  741. // Boarding Service?
  742. if ($someItem->getService() == $this->getBoardingService())
  743. {
  744. // Service only booking?
  745. if ($booking->getIsServiceOnlyBooking())
  746. {
  747. // Default to 0
  748. $someItem->setRate(0);
  749. }
  750. else
  751. {
  752. // Calculate the boarding price
  753. $boardingRate = $this->calculateBoardingPrice($someItem);
  754. // Does this item have a late checkout?
  755. if ($someItem->isOverdue())
  756. {
  757. // Original total not set?
  758. if (!$someItem->getOriginalTotal())
  759. $someItem->setOriginalTotal($someItem->getTotal());
  760. // Update the overdue charge
  761. $someItem->setOverdueCharge($boardingRate - $someItem->getOriginalTotal());
  762. }
  763. // Percentage discount?
  764. $discountRate = 0;
  765. if ($booking->getDiscountPercentage() > 0)
  766. {
  767. $discountRate = ($boardingRate * (1 + $booking->getDiscountPercentage())) - $boardingRate;
  768. $discountTotal += $discountRate;
  769. }
  770. // Fixed discount?
  771. elseif ($booking->getDiscount() > 0)
  772. {
  773. // We spread the fixed discount across all the booking items
  774. $discountRate = $booking->getDiscount() / $boardingItemCount;
  775. $discountTotal += $discountRate;
  776. }
  777. // Set item rate
  778. $someItem->setRate($boardingRate - $discountRate);
  779. }
  780. }
  781. elseif ($someItem->getService())
  782. {
  783. // Calculate the price
  784. $calculatedPrice = $this->calculatePrice($someItem);
  785. // Set item rate
  786. $someItem->setRate($calculatedPrice['total']);
  787. $someItem->setServiceTotal($calculatedPrice['total']);
  788. // Save prices on item
  789. $someItem->setWeekdayDayPrice($calculatedPrice['weekdayDayPrice']);
  790. $someItem->setWeekdayNightPrice($calculatedPrice['weekdayNightPrice']);
  791. $someItem->setWeekendDayPrice($calculatedPrice['weekendDayPrice']);
  792. $someItem->setWeekendNightPrice($calculatedPrice['weekendNightPrice']);
  793. }
  794. // Refresh the item totals
  795. $this->refreshItemTotals($someItem);
  796. // Grab totals
  797. $total += $someItem->getCalculatedTotal();
  798. $subTotal += $someItem->getSubTotal();
  799. // Persist item
  800. $this->em->persist($someItem);
  801. // Got an animal?
  802. if ($someItem->getAnimal())
  803. {
  804. // Animal not in array yet
  805. if (!array_key_exists($someItem->getAnimal()->getId(), $animalTotalArray))
  806. {
  807. $animalTotalArray[$someItem->getAnimal()->getId()] = array(
  808. "animal" => $someItem->getAnimal(),
  809. "boardingTotal" => 0,
  810. "serviceTotal" => 0
  811. );
  812. // Boarding?
  813. if ($someItem->getService() == $this->getBoardingService())
  814. $animalTotalArray[$someItem->getAnimal()->getId()]["boardingTotal"] += $someItem->getCalculatedTotal();
  815. elseif ($someItem->getService())
  816. $animalTotalArray[$someItem->getAnimal()->getId()]["serviceTotal"] += $someItem->getCalculatedTotal();
  817. }
  818. }
  819. }
  820. // Set totals
  821. $booking->setSubTotal($subTotal);
  822. $booking->setTotal($total);
  823. $booking->setTotalDiscount($discountTotal);
  824. // Is the deposit more than the total?
  825. if ($depositTotal > $booking->getTotal())
  826. $depositTotal = $booking->getTotal();
  827. // Set deposit
  828. $booking->setDeposit($depositTotal);
  829. // Persist the booking
  830. $this->em->persist($booking);
  831. // Flushing?
  832. if ($flushChanges)
  833. {
  834. $this->em->flush();
  835. }
  836. return $booking;
  837. }
  838. public function getTotals(\App\Entity\Bookings\Booking $booking)
  839. {
  840. return array(
  841. "subTotal" => number_format($booking->getSubTotal(), 2, '.', ''),
  842. "total" => number_format($booking->getTotal(), 2, '.', ''),
  843. "outstanding" => number_format($booking->getOutstanding(), 2, '.', ''),
  844. "paid" => number_format($booking->getCalculatedPaid(), 2, '.', ''),
  845. "discount" => number_format($booking->getDiscount(), 2, '.', ''),
  846. "outstandingdeposit" => number_format($booking->getCalculatedOutstandingDeposit(), 2, '.', ''),
  847. "deposit" => number_format($booking->getCalculatedDeposit(), 2, '.', '')
  848. );
  849. }
  850. /* This method is designed to assign a mixer category to each animal that is boarding on this booking. */
  851. public function refreshMixerCategories(\App\Entity\Bookings\Booking $booking)
  852. {
  853. // Get animals
  854. $animals = $booking->getAnimals();
  855. // No animals?
  856. if (!count($animals))
  857. return $booking;
  858. // Array to hold those who can share
  859. $animalsThatCanShare = array();
  860. $animalsThatCannotShare = array();
  861. // Keep track of housing used
  862. $housingUsed = array();
  863. // Loop animals
  864. foreach ($animals as $someAnimal)
  865. {
  866. // Find boarding item
  867. $boardingItem = $this->getItem($booking, $someAnimal, $this->getBoardingService());
  868. if (!$boardingItem)
  869. throw new \Exception("Unable to find boarding item for " . $someAnimal->getName());
  870. // Animal type not added to housing array?
  871. if (!array_key_exists($someAnimal->getType()->getId(), $housingUsed))
  872. $housingUsed[$someAnimal->getType()->getId()] = 0;
  873. // Add to mixer array
  874. $housingUsed[$someAnimal->getType()->getId()]++;
  875. // Get the active agreement for this animal
  876. $activeAgreement = $this->animalHelper->getActiveAgreement($someAnimal);
  877. // Does the agreement allow sharing?
  878. if ($activeAgreement && $activeAgreement->getCanShareHousing())
  879. {
  880. // Add it
  881. $animalsThatCanShare[] = $someAnimal;
  882. }
  883. else
  884. {
  885. // Add it
  886. $animalsThatCannotShare[] = $someAnimal;
  887. }
  888. }
  889. // Built an array of animals which can share?
  890. if (count($animalsThatCanShare))
  891. {
  892. // Used for splitting animals into groups
  893. $groupSet = 1;
  894. $capacityCount = array();
  895. // Group animals that can share
  896. $sharerGroups = $this->groupAnimalHousing($animalsThatCanShare);
  897. // Loop the sharing groups
  898. foreach ($sharerGroups as $key => $someGroup)
  899. {
  900. // Extract the animal type from the key
  901. $animalTypeId = explode("_", $key)[1];
  902. // Animal type not added to housing array?
  903. if (!array_key_exists($animalTypeId, $housingUsed))
  904. $housingUsed[$animalTypeId] = 0;
  905. // Add to mixer array
  906. $housingUsed[$animalTypeId]++;
  907. // Loop the animals
  908. foreach ($someGroup['animals'] as $someAnimal)
  909. {
  910. // Find boarding item
  911. $boardingItem = $this->getItem($booking, $someAnimal, $this->getBoardingService());
  912. if (!$boardingItem)
  913. throw new \Exception("Unable to find boarding item for " . $someAnimal->getName());
  914. // Only 1 in the group?
  915. if (count($someGroup['animals']) == 1)
  916. {
  917. // Find mixer category
  918. $mixerCategory = $this->em->getRepository(\App\Entity\Animals\MixerCategory::class)->findOneFiltered(array(
  919. "quantity" => 1,
  920. "isSameHousehold" => true
  921. ));
  922. if (!$mixerCategory)
  923. throw new \Exception("Unable to find mixer category for solo.");
  924. // Set mixercategory
  925. $boardingItem->setMixerCategory($mixerCategory);
  926. }
  927. else
  928. {
  929. // Find mixer category
  930. $mixerCategory = $this->em->getRepository(\App\Entity\Animals\MixerCategory::class)->findOneFiltered(array(
  931. "quantity" => count($someGroup['animals']),
  932. "isSameHousehold" => true
  933. ));
  934. if (!$mixerCategory)
  935. throw new \Exception("Unable to find mixer category for sharing: " . count($someGroup['animals']));
  936. // Set category
  937. $boardingItem->setMixerCategory($mixerCategory);
  938. }
  939. // Persist
  940. $this->em->persist($boardingItem);
  941. }
  942. }
  943. }
  944. // Loop animals that cannot share
  945. foreach ($animalsThatCannotShare as $key => $someAnimal)
  946. {
  947. // Find boarding item
  948. $boardingItem = $this->getItem($booking, $someAnimal, $this->getBoardingService());
  949. if (!$boardingItem)
  950. throw new \Exception("Unable to find boarding item for " . $someAnimal->getName());
  951. // Get mixer category from animal
  952. $mixerCategory = $someAnimal->getMixerCategory();
  953. // No mixer category?
  954. if(!$mixerCategory)
  955. {
  956. // Find mixer category
  957. $mixerCategory = $this->em->getRepository(\App\Entity\Animals\MixerCategory::class)->findOneFiltered(array(
  958. "quantity" => 1,
  959. "isSameHousehold" => true
  960. ));
  961. }
  962. if (!$mixerCategory)
  963. throw new \Exception("Unable to find mixer category");
  964. // Set mixercategory
  965. $boardingItem->setMixerCategory($mixerCategory);
  966. // Persist
  967. $this->em->persist($boardingItem);
  968. }
  969. // Flush
  970. $this->em->flush();
  971. // Set housing used on booking
  972. $booking->setHousingUsed($housingUsed);
  973. // Persist booking
  974. $this->em->persist($booking);
  975. // Flush
  976. $this->em->flush();
  977. return $booking;
  978. }
  979. public function calculateNumberOfHousingUsed(\DateTime $boardingDate, \App\Entity\Bookings\Booking $booking, bool $excludeDepartures = true)
  980. {
  981. // Get items for this day
  982. $boardingItems = $this->em->getRepository(\App\Entity\Bookings\Item::class)->findFiltered(array(
  983. 'booking' => $booking,
  984. 'service' => $this->getBoardingService(),
  985. 'boardingDate' => $boardingDate
  986. ));
  987. // No boarding items found for this day, so no housing used!
  988. if (!count($boardingItems))
  989. return [];
  990. $housingUsed = array(); // Keep track of housing used
  991. $animalsThatCanShare = array(); // Animals that can share together
  992. // Loop the boarding items
  993. foreach ($boardingItems as $someBoardingItem)
  994. {
  995. // Are we excluding departures and this animal departs today
  996. if ($excludeDepartures && $someBoardingItem->getDepartureDate() == $boardingDate)
  997. continue;
  998. // Got a mixer category?
  999. if (!$someBoardingItem->getMixerCategory())
  1000. continue;
  1001. // Get animal
  1002. $animal = $someBoardingItem->getAnimal();
  1003. // Grab the animal type
  1004. $animalType = $animal->getType();
  1005. // Grab the housing for this animal type
  1006. $housing = $animalType->getHousing();
  1007. // Housing type not found?
  1008. if (!$housing)
  1009. throw new \Exception("Cannot determine housing for animal type: " . $animal->getType()->getId());
  1010. // Index it by housing and animal type (to ensure different animal types wont share a house even if they use the same housing)
  1011. $index = $housing->getId() . "_" . $animalType->getId();
  1012. // Solo mixer category
  1013. if ($someBoardingItem->getMixerCategory()->getQuantity() == 1)
  1014. {
  1015. // Animal housing not added to housing array?
  1016. if (!array_key_exists($index, $housingUsed))
  1017. {
  1018. $housingUsed[$index] = array(
  1019. "housing" => $housing,
  1020. "type" => $animalType,
  1021. "used" => 0,
  1022. "animals" => []
  1023. );
  1024. }
  1025. // Increment housing usage
  1026. $housingUsed[$index]["used"]++;
  1027. $housingUsed[$index]["animals"][] = $animal;
  1028. }
  1029. else
  1030. {
  1031. $animalsThatCanShare[] = $animal;
  1032. }
  1033. }
  1034. // Work out who is sharing with who
  1035. $sharerGroups = $this->groupAnimalHousing($animalsThatCanShare);
  1036. // Loop the sharer group. Each group is a new house, e.g kennel
  1037. foreach ($sharerGroups as $key => $someGroup)
  1038. {
  1039. if (!$someGroup["housing"])
  1040. continue;
  1041. // Animal housing not added to housing array?
  1042. if (!array_key_exists($someGroup["housing"]->getId(), $housingUsed))
  1043. {
  1044. $housingUsed[$someGroup["housing"]->getId()] = array(
  1045. "housing" => $someGroup["housing"],
  1046. "type" => $someGroup['type'],
  1047. "used" => 0,
  1048. "animals" => []
  1049. );
  1050. }
  1051. // Add to count
  1052. $housingUsed[$someGroup["housing"]->getId()]["used"]++;
  1053. $housingUsed[$someGroup["housing"]->getId()]["animals"] = array_merge($housingUsed[$someGroup["housing"]->getId()]["animals"], $someGroup['animals']);
  1054. }
  1055. return $housingUsed;
  1056. }
  1057. public function groupAnimalHousing(array $animalsThatCanShare = array())
  1058. {
  1059. // Track the capacity
  1060. $capacityCount = array();
  1061. // Number each group
  1062. $groupSet = 0;
  1063. // Group animals together
  1064. $sharerGroups = array();
  1065. // Loop animals that can share, try to position them together
  1066. foreach ($animalsThatCanShare as $someAnimal)
  1067. {
  1068. // Grab the animal type
  1069. $animalType = $someAnimal->getType();
  1070. // Grab the housing for this animal type
  1071. $housing = $animalType->getHousing();
  1072. // Index it by housing and animal type (to ensure different animal types wont share a house even if they use the same housing)
  1073. $index = $housing->getId() . "_" . $animalType->getId();
  1074. // Animal type not looked at yet
  1075. if (!array_key_exists($index, $capacityCount))
  1076. $capacityCount[$index] = 0;
  1077. // Add onto capacity count
  1078. $capacityCount[$index] += $this->getCapacityValue($someAnimal);
  1079. // Reached limit of 1.0, put the other animal in a seperate group
  1080. if ($capacityCount[$index] > 1)
  1081. {
  1082. $groupSet += 1;
  1083. $capacityCount[$index] = 0;
  1084. }
  1085. // Group does not exist
  1086. if (!array_key_exists("group_" . $index . "_" . $groupSet, $sharerGroups))
  1087. {
  1088. $sharerGroups["group_" . $index . "_" . $groupSet] = array(
  1089. "housing" => $someAnimal->getType()->getHousing(),
  1090. "animals" => array(),
  1091. "type" => $animalType
  1092. );
  1093. }
  1094. // Add to group
  1095. $sharerGroups["group_" . $index . "_" . $groupSet]["animals"][] = $someAnimal;
  1096. }
  1097. return $sharerGroups;
  1098. }
  1099. public function animalHasConsent(\App\Entity\Bookings\Booking $booking, \App\Entity\Animals\Animal $animal)
  1100. {
  1101. // Find item for animal on booking
  1102. $item = $this->em->getRepository(\App\Entity\Bookings\Item::class)->findOneFiltered(array(
  1103. "booking" => $booking,
  1104. "animal" => $animal,
  1105. "service" => $this->getBoardingService()
  1106. ));
  1107. // Grab the mixer category
  1108. $mixerCategory = $item->getMixerCategory();
  1109. // Got item but no mixer category
  1110. if ($item && !$mixerCategory)
  1111. {
  1112. // Default to single
  1113. $mixerCategory = $this->em->getRepository(MixerCategory::class)->findOneFiltered(array(
  1114. "quantity" => 1,
  1115. "isSameHousehold" => true
  1116. ));
  1117. }
  1118. if ($item && $mixerCategory)
  1119. {
  1120. // Solo?
  1121. if ($mixerCategory->getQuantity() == 1)
  1122. {
  1123. // Find a valid agreement for this animal (solo is always permitted)
  1124. $agreement = $this->em->getRepository(\App\Entity\Customers\Agreement::class)->findOneFiltered(array(
  1125. "customer" => $animal->getCustomer(),
  1126. "animal" => $animal,
  1127. "status" => "completed",
  1128. "expiryDate" => array("gte", new \DateTime())
  1129. ));
  1130. if (!$agreement)
  1131. return false;
  1132. else
  1133. {
  1134. return [
  1135. 'agreement' => $agreement,
  1136. 'validForBooking' => true,
  1137. 'bookingItem' => $item
  1138. ];
  1139. }
  1140. }
  1141. // Share with same household
  1142. if ($mixerCategory->getQuantity() > 1 && $mixerCategory->getIsSameHousehold())
  1143. {
  1144. // Find a valid agreement
  1145. $agreement = $this->em->getRepository(\App\Entity\Customers\Agreement::class)->findOneFiltered(array(
  1146. "customer" => $animal->getCustomer(),
  1147. "animal" => $animal,
  1148. "status" => "completed",
  1149. "expiryDate" => array("gte", new \DateTime())
  1150. ));
  1151. if (!$agreement)
  1152. return false;
  1153. else
  1154. {
  1155. return [
  1156. 'agreement' => $agreement,
  1157. 'validForBooking' => $agreement->getCanShareHousing(),
  1158. 'bookingItem' => $item
  1159. ];
  1160. }
  1161. }
  1162. // Share with different households
  1163. if ($mixerCategory->getQuantity() > 1 && !$mixerCategory->getIsSameHousehold())
  1164. {
  1165. // Find a valid agreement
  1166. $agreement = $this->em->getRepository(\App\Entity\Customers\Agreement::class)->findOneFiltered(array(
  1167. "customer" => $animal->getCustomer(),
  1168. "animal" => $animal,
  1169. "status" => "completed",
  1170. "expiryDate" => array("gte", new \DateTime())
  1171. ));
  1172. if (!$agreement)
  1173. return false;
  1174. else
  1175. {
  1176. return [
  1177. 'agreement' => $agreement,
  1178. 'validForBooking' => $agreement->getCanMixHousing(),
  1179. 'bookingItem' => $item
  1180. ];
  1181. }
  1182. }
  1183. }
  1184. return false;
  1185. }
  1186. public function getCapacityValue(\App\Entity\Animals\Animal $animal)
  1187. {
  1188. // Get housing
  1189. $capacity = $this->em->getRepository(\App\Entity\Bookings\Capacity::class)->findOneFiltered(array(
  1190. "housing" => $animal->getType()->getHousing(),
  1191. "size" => $animal->getSize()
  1192. ));
  1193. if ($capacity)
  1194. return floatval($capacity->getValue());
  1195. return 0;
  1196. }
  1197. public function animalIsCheckedIn(Booking $booking, Animal $animal)
  1198. {
  1199. // Get boarding items for this booking
  1200. $boardingItems = $this->getBoardingItems($booking);
  1201. foreach ($boardingItems as $someItem)
  1202. {
  1203. if ($someItem->isCheckedIn() && $someItem->getAnimal() == $animal)
  1204. return true;
  1205. }
  1206. return false;
  1207. }
  1208. public function bookingIsCheckedIn(Booking $booking, $inFull = false)
  1209. {
  1210. // Get boarding items for this booking
  1211. $boardingItems = $this->getBoardingItems($booking);
  1212. // Got no items
  1213. if (!count($boardingItems))
  1214. return false;
  1215. // Could how many of the items have checked in
  1216. $hasCheckedIn = 0;
  1217. foreach ($boardingItems as $someItem)
  1218. {
  1219. if ($someItem->isCheckedIn())
  1220. $hasCheckedIn++;
  1221. }
  1222. // All checked in or at least one?
  1223. if ($inFull)
  1224. {
  1225. if ($hasCheckedIn == count($boardingItems))
  1226. return true;
  1227. }
  1228. else
  1229. {
  1230. if ($hasCheckedIn > 0)
  1231. return true;
  1232. }
  1233. return false;
  1234. }
  1235. public function bookingIsCheckedOut(\App\Entity\Bookings\Booking $booking, $inFull = false)
  1236. {
  1237. // Get boarding items for this booking
  1238. $boardingItems = $this->getBoardingItems($booking);
  1239. // Got no items
  1240. if (!count($boardingItems))
  1241. return false;
  1242. // Could how many of the items have checked out
  1243. $hasCheckedOut = 0;
  1244. foreach ($boardingItems as $someItem)
  1245. {
  1246. if ($someItem->isCheckedOut())
  1247. $hasCheckedOut++;
  1248. }
  1249. // All checked in or at least one?
  1250. if ($inFull)
  1251. {
  1252. if ($hasCheckedOut == count($boardingItems))
  1253. return true;
  1254. }
  1255. else
  1256. {
  1257. if ($hasCheckedOut > 0)
  1258. return true;
  1259. }
  1260. return false;
  1261. }
  1262. public function bookingHasMedicalRecord(\App\Entity\Bookings\Item $bookingItem, \App\Entity\Animals\MedicalRecord $medicalRecord)
  1263. {
  1264. // Get medical record on booking
  1265. $bookingMedicalRecord = $this->em->getRepository(\App\Entity\Bookings\MedicalRecord::class)->findOneFiltered(array(
  1266. "medicalRecord" => $medicalRecord,
  1267. "bookingItem" => $bookingItem
  1268. ));
  1269. return $bookingMedicalRecord;
  1270. }
  1271. public function animalHasCurrentMedication(Animal $animal)
  1272. {
  1273. // Get medical record on booking
  1274. $animalMedicalRecord = $this->em->getRepository(\App\Entity\Animals\MedicalRecord::class)->findOneFiltered(array(
  1275. "animal" => $animal,
  1276. "requiresCheck" => false,
  1277. "status" => "current"
  1278. ));
  1279. return $animalMedicalRecord ? true : false;
  1280. }
  1281. public function getBookingBreakdown(\App\Entity\Bookings\Booking $booking): array
  1282. {
  1283. // Array to animal breakdowns
  1284. $animalBreakdown = array(
  1285. "animals" => array(),
  1286. "miscItems" => array(),
  1287. "totals" => array(
  1288. "subTotal" => $booking->getSubTotal(),
  1289. "discount" => $booking->getDiscount(),
  1290. "total" => $booking->getTotal(),
  1291. "paid" => $booking->getCalculatedPaid(),
  1292. "deposit" => 0
  1293. )
  1294. );
  1295. // Get the boarding service
  1296. $boardingService = $this->getBoardingService();
  1297. // Loop animals on booking
  1298. if (count($booking->getAnimals()))
  1299. {
  1300. foreach ($booking->getAnimals() as $someAnimal)
  1301. {
  1302. // Is animal in array?
  1303. if (!array_key_exists($someAnimal->getId(), $animalBreakdown))
  1304. {
  1305. // Get boarding item
  1306. $boardingItem = $this->getItem($booking, $someAnimal, $boardingService);
  1307. // Hold eligible service items
  1308. $eligibleServiceItems = [];
  1309. // Get all top level service items for this animals
  1310. $allServiceItems = $this->em->getRepository(\App\Entity\Bookings\Item::class)->findFiltered(array(
  1311. "booking" => $booking,
  1312. "animal" => $someAnimal,
  1313. "parent" => null,
  1314. "service" => array("not_in", [$boardingService])
  1315. ));
  1316. foreach ($allServiceItems as $someItem)
  1317. {
  1318. // Got children?
  1319. if (count($someItem->getChildren()))
  1320. {
  1321. // Loop the children
  1322. foreach ($someItem->getChildren() as $someChildItem)
  1323. {
  1324. // Add it to our eligible array
  1325. $eligibleServiceItems[] = $someChildItem;
  1326. }
  1327. }
  1328. else
  1329. {
  1330. // Add it to our eligible array
  1331. $eligibleServiceItems[] = $someItem;
  1332. }
  1333. }
  1334. $animalBreakdown["animals"][$someAnimal->getId()] = array(
  1335. "animal" => $someAnimal,
  1336. "boardingService" => $boardingItem,
  1337. "services" => $eligibleServiceItems,
  1338. "miscItems" => $this->em->getRepository(\App\Entity\Bookings\Item::class)->findFiltered(array(
  1339. "booking" => $booking,
  1340. "animal" => $someAnimal,
  1341. "otherItems" => true
  1342. )),
  1343. 'totals' => array(
  1344. 'subTotal' => 0,
  1345. 'total' => 0,
  1346. 'deposit' => 0,
  1347. 'paid' => 0,
  1348. 'tax' => 0
  1349. )
  1350. );
  1351. // Get all animal items
  1352. $allAnimalItems = $this->em->getRepository(\App\Entity\Bookings\Item::class)->findFiltered(array(
  1353. "booking" => $booking,
  1354. "animal" => $someAnimal,
  1355. ));
  1356. // Loop the tiems
  1357. foreach ($allAnimalItems as $someItem)
  1358. {
  1359. // Add onto totals
  1360. $animalBreakdown["animals"][$someAnimal->getId()]['totals']['subTotal'] += $someItem->getSubTotal();
  1361. $animalBreakdown["animals"][$someAnimal->getId()]['totals']['total'] += $someItem->getTotal();
  1362. $animalBreakdown["animals"][$someAnimal->getId()]['totals']['deposit'] += $someItem->getDeposit();
  1363. $animalBreakdown["animals"][$someAnimal->getId()]['totals']['paid'] += $someItem->getPaid();
  1364. $animalBreakdown["animals"][$someAnimal->getId()]['totals']['tax'] += $someItem->getTotalTax();
  1365. }
  1366. }
  1367. }
  1368. }
  1369. // Get all non animal specific items
  1370. $animalBreakdown["miscItems"] = $this->em->getRepository(\App\Entity\Bookings\Item::class)->findFiltered(array(
  1371. "booking" => $booking,
  1372. "animal" => null
  1373. ));
  1374. return $animalBreakdown;
  1375. }
  1376. public function calculateRemainingAmounts(\App\Entity\Bookings\Item $boardingItem)
  1377. {
  1378. // Declare totals
  1379. $depositLeftToPay = 0;
  1380. $serviceLeftToPay = 0;
  1381. $itemPaid = floatval($boardingItem->getPaid());
  1382. $itemDeposit = floatval($boardingItem->getDeposit());
  1383. $itemTotal = $boardingItem->getTotal();
  1384. // Paying off the deposit?
  1385. if ($itemPaid < $itemDeposit)
  1386. {
  1387. $depositLeftToPay = $itemDeposit - $itemPaid;
  1388. $serviceLeftToPay = $itemTotal - $itemDeposit;
  1389. }
  1390. else
  1391. {
  1392. // Deposit has been paid off
  1393. $depositLeftToPay = 0;
  1394. // Paid more than the deposit?
  1395. if ($itemPaid > $itemDeposit)
  1396. {
  1397. // Service amount - payments made off service
  1398. $serviceLeftToPay = ($itemTotal - $itemDeposit) - ($itemPaid - $itemDeposit);
  1399. }
  1400. else
  1401. $serviceLeftToPay = 0;
  1402. }
  1403. return array(
  1404. "depositAmount" => $depositLeftToPay,
  1405. "serviceAmount" => $serviceLeftToPay
  1406. );
  1407. }
  1408. /**
  1409. * Build structured array of payments by batching together payments which are for the same item (paid at the same time)
  1410. */
  1411. public function buildPaymentsArray($payments = array())
  1412. {
  1413. $paymentsArray = array();
  1414. // Loop payments
  1415. foreach ($payments as $somePayment)
  1416. {
  1417. // Default batch id
  1418. $batchId = $somePayment->getId();
  1419. // Does the payment have a batch id?
  1420. if ($somePayment->getBatchId())
  1421. $batchId = $somePayment->getBatchId();
  1422. // Payment not in array yet?
  1423. if (!array_key_exists($batchId, $paymentsArray))
  1424. {
  1425. // Add to array
  1426. $paymentsArray[$batchId] = array(
  1427. "payment" => $somePayment,
  1428. "description" => $somePayment->getDescription() ? $somePayment->getDescription() : ($somePayment->getItem() ? $somePayment->getItem()->getDescription() : null),
  1429. "childPayments" => array(),
  1430. "qty" => 0,
  1431. "depositAmount" => 0,
  1432. "serviceAmount" => 0,
  1433. "total" => 0
  1434. );
  1435. }
  1436. // Add to child payments. This is so the expand payment view on the frontend will show all the payments in detail.
  1437. $paymentsArray[$batchId]["childPayments"][] = $somePayment;
  1438. // Add totals, deposits, service charges...
  1439. $paymentsArray[$batchId]["total"] += $somePayment->getAmount();
  1440. $paymentsArray[$batchId]["depositAmount"] += $somePayment->getDepositAmount();
  1441. $paymentsArray[$batchId]["serviceAmount"] += $somePayment->getServiceAmount();
  1442. $paymentsArray[$batchId]["qty"]++;
  1443. }
  1444. return $paymentsArray;
  1445. }
  1446. public function getLongStayChecklistsDue()
  1447. {
  1448. // Get booking items
  1449. $bookingItemsDueChecklist = $this->em->getRepository(\App\Entity\Bookings\Item::class)->findFiltered(array(
  1450. 'service' => $this->getBoardingService(),
  1451. 'isOnSite' => true,
  1452. 'dueLongStayChecklist' => $this->configHelper->getValue('longstay_check_interval')
  1453. ));
  1454. return $bookingItemsDueChecklist;
  1455. }
  1456. public function getHandsOnChecklistsDue()
  1457. {
  1458. // Get booking items
  1459. $bookingItemsDueChecklist = $this->em->getRepository(\App\Entity\Bookings\Item::class)->findFiltered(array(
  1460. 'service' => $this->getBoardingService(),
  1461. //'isOnSite' => true,
  1462. 'dueHandsOnChecklist' => $this->configHelper->getValue('handson_check_interval')
  1463. ));
  1464. return $bookingItemsDueChecklist;
  1465. }
  1466. public function getHousingTicketWeeks(Booking $booking)
  1467. {
  1468. // Grab an array of the days
  1469. $arrayOfDays = [];
  1470. $date = new \DateTime($booking->getFirstArrivalDate()->format("Y-m-d"));
  1471. $endDate = $booking->getLastDepartureDate();
  1472. while ($date <= $endDate)
  1473. {
  1474. $newDate = new \DateTime($date->format("Y-m-d"));
  1475. $arrayOfDays[] = $newDate;
  1476. $date->modify('+1 day');
  1477. }
  1478. // Break it up into weeks
  1479. $arrayOfWeeks = array_chunk($arrayOfDays, 14);
  1480. return $arrayOfWeeks;
  1481. }
  1482. public function getOutstandingBalanceByBookingAndAnimal(Booking $booking, Animal $animal)
  1483. {
  1484. $outstanding = 0;
  1485. // Get booking items
  1486. $items = $this->em->getRepository(\App\Entity\Bookings\Item::class)->findFiltered([
  1487. "booking" => $booking,
  1488. "animal" => $animal
  1489. ]);
  1490. foreach ($items as $someItem)
  1491. $outstanding += $someItem->getCalculatedOutstanding();
  1492. return $outstanding;
  1493. }
  1494. /*
  1495. /* Calculate the capacity used by animal type and housing type for a given date
  1496. */
  1497. public function getCapacityByDate(\DateTime $boardingDate, ?\DateTime $excludedDepartureDate = null)
  1498. {
  1499. // Get items for this day
  1500. $boardingItems = $this->em->getRepository(\App\Entity\Bookings\Item::class)->findFiltered(array(
  1501. 'service' => $this->getBoardingService(),
  1502. 'boardingDate' => $boardingDate,
  1503. 'cancelled' => false
  1504. ));
  1505. // Get all mixer categories
  1506. $mixerCategories = $this->em->getRepository(MixerCategory::class)->findFiltered([], [
  1507. "mc.quantity" => "ASC"
  1508. ]);
  1509. // Get single mixer category
  1510. $singleMixerCategory = $this->em->getRepository(MixerCategory::class)->findOneFiltered(array(
  1511. "quantity" => 1,
  1512. "isSameHousehold" => true
  1513. ));
  1514. // Build an array structure for the mixer categories to be used later
  1515. $mixerCategoryStructure = [];
  1516. foreach ($mixerCategories as $someMixerCategory)
  1517. {
  1518. // Create it
  1519. $mixerCategoryStructure[$someMixerCategory->getId()] = [
  1520. "mixerCategory" => $someMixerCategory,
  1521. "animals" => [],
  1522. "housingGroups" => []
  1523. ];
  1524. }
  1525. // Loop and group by booking
  1526. $itemsByBooking = [];
  1527. foreach ($boardingItems as $someItem)
  1528. {
  1529. // Excluding a departure date?
  1530. if ($excludedDepartureDate && $someItem->getDepartureDate()->format("Y-m-d") == $excludedDepartureDate->format("Y-m-d"))
  1531. continue;
  1532. // Grab the booking
  1533. $booking = $someItem->getBooking();
  1534. // Not got this yet?
  1535. if (!array_key_exists($booking->getId(), $itemsByBooking))
  1536. {
  1537. // Create it
  1538. $itemsByBooking[$booking->getId()] = [
  1539. "booking" => $booking,
  1540. "items" => []
  1541. ];
  1542. }
  1543. // Add the item
  1544. $itemsByBooking[$booking->getId()]["items"][] = $someItem;
  1545. }
  1546. // Array to hold it
  1547. $capacityByAnimalType = [];
  1548. $capacityByHousingType = [];
  1549. // Loop the bookings
  1550. foreach ($itemsByBooking as $someBooking)
  1551. {
  1552. // Loop & extract the animals
  1553. $animals = [];
  1554. foreach ($someBooking['items'] as $someItem)
  1555. {
  1556. if (!in_array($someItem->getAnimal(), $animals))
  1557. $animals[] = $someItem->getAnimal();
  1558. }
  1559. // Grab animals grouped by type
  1560. $animalsByType = $this->groupAnimalsByType($animals);
  1561. // Loop types
  1562. foreach ($animalsByType as $someType)
  1563. {
  1564. // Store the data for this booking only
  1565. $bookingCapacityByMixerCategory = $mixerCategoryStructure;
  1566. // Grab the type
  1567. $type = $someType['type'];
  1568. // Grab the housing
  1569. $housing = $someType['type']->getHousing();
  1570. // Don't have this yet?
  1571. if (!array_key_exists($type->getId(), $capacityByAnimalType))
  1572. {
  1573. // Create it
  1574. $capacityByAnimalType[$type->getId()] = [
  1575. "type" => $type,
  1576. "mixerCategories" => $mixerCategoryStructure,
  1577. "animals" => [],
  1578. "housingGroups" => []
  1579. ];
  1580. }
  1581. // Don't have this housing?
  1582. if (!$housing)
  1583. continue;
  1584. // Don't have this yet?
  1585. if (!array_key_exists($housing->getId(), $capacityByHousingType))
  1586. {
  1587. // Create it
  1588. $capacityByHousingType[$housing->getId()] = [
  1589. "housing" => $housing,
  1590. "animals" => [],
  1591. "housingGroups" => []
  1592. ];
  1593. }
  1594. // Loop animals
  1595. foreach ($someType['animals'] as $someAnimal)
  1596. {
  1597. // Grab the mixer category
  1598. $mixerCategory = $this->getMixerCategoryForBookingByAnimal($someAnimal, $someBooking["booking"]);
  1599. // Not got it from the booking
  1600. if (!$mixerCategory)
  1601. $mixerCategory = $someAnimal->getMixerCategory();
  1602. // Not got it from the animal? Default to single
  1603. if (!$mixerCategory)
  1604. $mixerCategory = $singleMixerCategory;
  1605. // Add the animal into relevant mixer category
  1606. $capacityByAnimalType[$type->getId()]["mixerCategories"][$mixerCategory->getId()]["animals"][] = $someAnimal;
  1607. // Add the animal into more generic array
  1608. $capacityByAnimalType[$type->getId()]["animals"][] = $someAnimal;
  1609. // Add into booking specific array
  1610. $bookingCapacityByMixerCategory[$mixerCategory->getId()]["animals"][] = $someAnimal;
  1611. // Add the animal into the housing array
  1612. $capacityByHousingType[$housing->getId()]["animals"][] = $someAnimal;
  1613. }
  1614. // Loop the mixer categories
  1615. foreach ($bookingCapacityByMixerCategory as $mixerCategoryID => $someMixerCategory)
  1616. {
  1617. // Break into chunks based on how many animals allowed
  1618. $housingGroups = array_chunk($someMixerCategory['animals'], $someMixerCategory['mixerCategory']->getQuantity());
  1619. // Add to array
  1620. $capacityByAnimalType[$type->getId()]["mixerCategories"][$mixerCategoryID]["housingGroups"] = $housingGroups;
  1621. // Add to generic array
  1622. $capacityByAnimalType[$type->getId()]["housingGroups"] = array_merge($capacityByAnimalType[$type->getId()]["housingGroups"], $housingGroups);
  1623. // Add the animal into the housing array
  1624. $capacityByHousingType[$housing->getId()]["housingGroups"] = array_merge($capacityByHousingType[$housing->getId()]["housingGroups"], $housingGroups);
  1625. }
  1626. }
  1627. }
  1628. return [
  1629. "by_type" => $capacityByAnimalType,
  1630. "by_housing" => $capacityByHousingType
  1631. ];
  1632. }
  1633. /*
  1634. /* Get the mixer category for the boarding service item for a particular animal and booking
  1635. */
  1636. public function getMixerCategoryForBookingByAnimal(Animal $animal, Booking $booking)
  1637. {
  1638. // Find the boarding item
  1639. $boardingItem = $this->em->getRepository(\App\Entity\Bookings\Item::class)->findOneFiltered(array(
  1640. "service" => $this->getBoardingService(),
  1641. "booking" => $booking,
  1642. "animal" => $animal
  1643. ));
  1644. // Not found?
  1645. if (!$boardingItem)
  1646. return null;
  1647. return $boardingItem->getMixerCategory();
  1648. }
  1649. /*
  1650. /* Takes an array of animal and returns an array with the animals grouped by type
  1651. */
  1652. public function groupAnimalsByType(iterable $animals)
  1653. {
  1654. // Loop the animals, group by type
  1655. $animalsByType = [];
  1656. foreach ($animals as $someAnimal)
  1657. {
  1658. // Grab the type
  1659. $type = $someAnimal->getType();
  1660. // Not got this one yet?
  1661. if (!array_key_exists($type->getId(), $animalsByType))
  1662. {
  1663. // Create it
  1664. $animalsByType[$type->getId()] = [
  1665. "type" => $type,
  1666. "animals" => []
  1667. ];
  1668. }
  1669. // Add the animal
  1670. $animalsByType[$type->getId()]["animals"][] = $someAnimal;
  1671. }
  1672. return $animalsByType;
  1673. }
  1674. }