src/Ecommerce/AvailabilitySystem/Event/AvailabilitySystem.php line 60

Open in your IDE?
  1. <?php
  2. /**
  3.  * Created by Elements.at New Media Solutions GmbH
  4.  *
  5.  */
  6. namespace App\Ecommerce\AvailabilitySystem\Event;
  7. use App\Ecommerce\CartManager\SessionCartItem;
  8. use App\Model\Shop\Event\EventProduct;
  9. use Carbon\Carbon;
  10. use Doctrine\DBAL\Driver\Exception;
  11. use Doctrine\DBAL\Query\QueryBuilder;
  12. use Elements\Bundle\TicketShopFrameworkBundle\Model\Shop\Cart\TicketShopCartInterface;
  13. use Elements\Bundle\TicketShopFrameworkBundle\Service\CartService;
  14. use Pimcore\Bundle\EcommerceFrameworkBundle\AvailabilitySystem\AvailabilitySystemInterface;
  15. use Pimcore\Bundle\EcommerceFrameworkBundle\Factory;
  16. use Pimcore\Db;
  17. use Pimcore\Model\DataObject\AbstractObject;
  18. use Pimcore\Model\DataObject\OnlineShopOrder;
  19. use Pimcore\Model\DataObject\OnlineShopOrderItem;
  20. use Pimcore\Model\DataObject\ShopEvent;
  21. class AvailabilitySystem implements AvailabilitySystemInterface
  22. {
  23.     private TicketShopCartInterface $cart;
  24.     private bool $showCancelledItems false;
  25.     public function __construct(CartService $cartService)
  26.     {
  27.         $environment Factory::getInstance()->getEnvironment();
  28.         $cartName $environment->getCurrentCheckoutTenant() === 'b2b' 'b2b' 'cart';
  29.         $this->cart $cartService->getCart($cartName);
  30.     }
  31.     public function setShowCancelledItems(bool $showCancelledItems): void
  32.     {
  33.         $this->showCancelledItems $showCancelledItems;
  34.     }
  35.     /**
  36.      * @param EventProduct $product
  37.      * @param int $quantityScale
  38.      * @param null $products
  39.      *
  40.      * @return Availability|null
  41.      */
  42.     public function getAvailabilityInfo(
  43.         $product,
  44.         $quantityScale 1,
  45.         $products null
  46.     ): ?Availability {
  47.         $returnAvailability null;
  48.         if ($product->getType() == AbstractObject::OBJECT_TYPE_VARIANT) {
  49.             $returnAvailability $this->getAvailableQuota($product$product->getEventStartDate());
  50.         } else {
  51.             //search every given availability
  52.             $availableList $this->getAvailableList($product);
  53.             foreach ($availableList as $availability) {
  54.                 if ($availability->isAvailable()) {
  55.                     $returnAvailability $availability;
  56.                     break;
  57.                 }
  58.             }
  59.         }
  60.         return $returnAvailability;
  61.     }
  62.     /**
  63.      * ermittelt alle verfügbaren tage und kontingente
  64.      *
  65.      * @param EventProduct $event
  66.      * @param Carbon|null $startDate
  67.      * @param Carbon|null $endDate
  68.      * @param bool $getPastAvailabilities
  69.      * @param bool $ignoreCartItems
  70.      * @param bool $addPossiblePeopleNumber
  71.      *
  72.      * @return Availability[]
  73.      *
  74.      * @throws Exception
  75.      * @throws \Doctrine\DBAL\Exception
  76.      */
  77.     public function getAvailableList(
  78.         EventProduct $event,
  79.         Carbon $startDate null,
  80.         Carbon $endDate null,
  81.         bool $getPastAvailabilities false,
  82.         bool $ignoreCartItems false,
  83.         bool $addPossiblePeopleNumber false
  84.     ): array {
  85.         if ($availableDates $this->getAvailableDates($event$startDate$endDate$getPastAvailabilities)) {
  86.             return $this->getAvailableQuotas($event$availableDates$ignoreCartItems$addPossiblePeopleNumber);
  87.         }
  88.         return [];
  89.     }
  90.     public function isAvailableForDateAndQuantity(
  91.         EventProduct $event,
  92.         Carbon $startDate,
  93.         Carbon $endDate,
  94.         int $quantity
  95.     ): bool {
  96.         foreach ($this->getAvailableList($event$startDate$endDateaddPossiblePeopleNumbertrue) as $availability) {
  97.             if ($availability->isAvailable() && $availability->getAvailableQuota() >= $quantity) {
  98.                 return true;
  99.             }
  100.         }
  101.         return false;
  102.     }
  103.     /**
  104.      * Check if event has any availability on the whole day
  105.      *
  106.      * @param EventProduct $event
  107.      * @param Carbon $date
  108.      *
  109.      * @return bool
  110.      */
  111.     public function isAvailableOnDay(EventProduct $eventCarbon $date): bool
  112.     {
  113.         /** @var EventProduct $event */
  114.         $event $event->getReference();
  115.         $availabilities $this->getAvailableList($event$date->copy()->startOfDay(), $date->copy()->endOfDay());
  116.         return count($availabilities) > 0;
  117.     }
  118.     /**
  119.      * @param EventProduct $event
  120.      *
  121.      * @return Availability|null
  122.      */
  123.     public function getNextAvailability(EventProduct $event): ?Availability
  124.     {
  125.         $availability null;
  126.         if ($validityDates $event->getValidityDates()) {
  127.             if ($nextOccurrence $validityDates->getNextOccurrence()) {
  128.                 $nextDate Carbon::parse($nextOccurrence);
  129.                 $availability $this->getAvailableQuota($event$nextDate);
  130.             }
  131.         }
  132.         return $availability;
  133.     }
  134.     /**
  135.      * all available dates
  136.      *
  137.      * @param EventProduct $event
  138.      * @param Carbon|null $startDate
  139.      * @param Carbon|null $endDate
  140.      * @param bool $getPastAvailabilities
  141.      *
  142.      * @return Carbon[]
  143.      */
  144.     public function getAvailableDates(
  145.         EventProduct $event,
  146.         Carbon $startDate null,
  147.         Carbon $endDate null,
  148.         bool $getPastAvailabilities false
  149.     ): array {
  150.         $availableDates = [];
  151.         if (!$startDate) {
  152.             if ($event->getBookableOnDayOfEvent()) {
  153.                 $startDate Carbon::now()->startOfDay();
  154.             } else {
  155.                 $startDate Carbon::now();
  156.             }
  157.         }
  158.         if (!$endDate) {
  159.             $endDate Carbon::today()->addYears(2);
  160.         }
  161.         if ($validityDates $event->getValidityDates()) {
  162.             $validDates $validityDates->getOccurrencesBetween($startDate$endDate);
  163.             foreach ($validDates as $validDate) {
  164.                 $date Carbon::parse($validDate);
  165.                 if ($this->isAvailableByDate($event$date$getPastAvailabilities$startDate)) {
  166.                     $availableDates[$date->getTimestamp()] = $date;
  167.                 }
  168.             }
  169.         }
  170.         return $availableDates;
  171.     }
  172.     /**
  173.      * @param EventProduct $event
  174.      * @param Carbon $date
  175.      * @param bool $ignoreCartItems
  176.      *
  177.      * @return Availability
  178.      */
  179.     public function getAvailableQuota(EventProduct $eventCarbon $datebool $ignoreCartItems false): Availability
  180.     {
  181.         $availabilities $this->getAvailableQuotas($event, [$date], $ignoreCartItems);
  182.         if (!empty($availabilities)) {
  183.             return array_shift($availabilities);
  184.         } else {
  185.             return new Availability($eventfalse000$datefalse$event->getPossibleParticipantNumber());
  186.         }
  187.     }
  188.     /**
  189.      * @param EventProduct $event
  190.      * @param array<mixed> $dates
  191.      * @param bool $ignoreCartItems
  192.      * @param bool $addPossiblePeopleNumber
  193.      *
  194.      * @return Availability[]
  195.      *
  196.      * @throws Exception
  197.      * @throws \Doctrine\DBAL\Exception
  198.      */
  199.     protected function getAvailableQuotas(
  200.         EventProduct $event,
  201.         array $dates,
  202.         bool $ignoreCartItems false,
  203.         bool $addPossiblePeopleNumber false
  204.     ): array {
  205.         $results = [];
  206.         $totalQuota $event->getQuota();
  207.         $possiblePeopleQuota $event->getPossibleParticipantNumber();
  208.         array_walk($dates, function (&$option) {
  209.             $option $option->getTimestamp();
  210.         });
  211.         $queryBuilder Db::get()->createQueryBuilder();
  212.         $queryBuilder->setParameters(['productId' => $event->getReference()->getId()]);
  213.         $this->buildQuery($queryBuilder);
  214.         $this->addProductCondition($queryBuilder);
  215.         $this->addItemStateCondition($queryBuilder);
  216.         $this->addDateCondition($queryBuilder$dates);
  217.         $this->addParentStateCondition($queryBuilder);
  218.         $list $queryBuilder->execute()->fetchAllAssociative();
  219.         if (!$ignoreCartItems) {
  220.             $cartItems $this->getCartItemsCount();
  221.         }
  222.         $id $event->getReference()->getId();
  223.         foreach ($list as $entry) {
  224.             $availableQuota $totalQuota - (int)$entry['quota'];
  225.             $hasCartItems false;
  226.             if (isset($cartItems[$entry['eventStartDate'] . $id])) {
  227.                 $hasCartItems true;
  228.                 $availableQuota -= $cartItems[$entry['eventStartDate'] . $id];
  229.             }
  230.             // for cross-selling
  231.             if ($addPossiblePeopleNumber && $possiblePeopleQuota) {
  232.                 $availableQuota *= $possiblePeopleQuota;
  233.             }
  234.             $eventDate Carbon::createFromTimestamp($entry['eventStartDate']);
  235.             $results[$entry['eventStartDate']] = new Availability(
  236.                 $event,
  237.                 $availableQuota && $this->isAvailableByDate($event$eventDate),
  238.                 $availableQuota $availableQuota 0,
  239.                 intval($totalQuota),
  240.                 $entry['sum'],
  241.                 $eventDate,
  242.                 $this->isAvailableWithCartAndTime($hasCartItems$availableQuota$event$eventDate),
  243.                 $possiblePeopleQuota
  244.             );
  245.         }
  246.         if (!$this->showCancelledItems) {
  247.             $tmpResult = [];
  248.             foreach ($results as $timestamp => $result) {
  249.                 if (!$result->isCancelled()) {
  250.                     $tmpResult[$timestamp] = $result;
  251.                 }
  252.             }
  253.             $results $tmpResult;
  254.         }
  255.         //if no booked event found, fill the rest with the total quota
  256.         foreach ($dates as $date) {
  257.             if (!key_exists($date$results)) {
  258.                 $availableQuota $totalQuota;
  259.                 $hasCartItems false;
  260.                 if (isset($cartItems[$date $id])) {
  261.                     $hasCartItems true;
  262.                     $availableQuota -= $cartItems[$date $id];
  263.                 }
  264.                 // for cross-selling
  265.                 if ($addPossiblePeopleNumber && $possiblePeopleQuota) {
  266.                     $availableQuota *= $possiblePeopleQuota;
  267.                 }
  268.                 $eventDate Carbon::createFromTimestamp($date);
  269.                 $availability = new Availability(
  270.                     $event,
  271.                     $availableQuota && $this->isAvailableByDate($event$eventDate),
  272.                     $availableQuota,
  273.                     intval($totalQuota),
  274.                     0,
  275.                     $eventDate,
  276.                     $this->isAvailableWithCartAndTime($hasCartItems$availableQuota$event$eventDate),
  277.                     $possiblePeopleQuota
  278.                 );
  279.                 if ($this->showCancelledItems || !$availability->isCancelled()) {
  280.                     $results[$date] = $availability;
  281.                 }
  282.             }
  283.         }
  284.         ksort($results);
  285.         return $results;
  286.     }
  287.     private function isAvailableByDate(EventProduct $eventCarbon $eventDate, ?bool $getPastAvailabilities false, ?Carbon $startDate null): bool
  288.     {
  289.         if ($getPastAvailabilities) {
  290.             $startDate $startDate ?? (Carbon::now())->subDays(14);
  291.             $currentDateTime $startDate;
  292.         } else {
  293.             $currentDateTime Carbon::now();
  294.             //not available before x hours
  295.             if ($bookingStopHours $event->getStopSale()) {
  296.                 /** @phpstan-ignore-next-line  */
  297.                 $currentDateTime Carbon::now()->addhours($bookingStopHours);
  298.             }
  299.         }
  300.         return $currentDateTime->lte($event->getBookableOnDayOfEvent() ? $eventDate->endOfDay() : $eventDate);
  301.     }
  302.     /**
  303.      * @return int[]
  304.      */
  305.     private function getCartItemsCount(): array
  306.     {
  307.         $cartItems = [];
  308.         $eventCartItems $this->cart->getItemsByProductClass(EventProduct::class);
  309.         /** @var SessionCartItem $item */
  310.         foreach ($eventCartItems as $item) {
  311.             /** @var EventProduct $product */
  312.             $product $item->getProduct();
  313.             $parentProduct $product->getReference();
  314.             if (isset($cartItems[$product->getEventStartDate()->getTimestamp() . $parentProduct->getId()])) {
  315.                 $cartItems[$product->getEventStartDate()->getTimestamp() . $parentProduct->getId()] += $item->getRealEventCount();
  316.             } else {
  317.                 $cartItems[$product->getEventStartDate()->getTimestamp() . $parentProduct->getId()] = $item->getRealEventCount();
  318.             }
  319.         }
  320.         return $cartItems;
  321.     }
  322.     /**
  323.      * @param QueryBuilder $queryBuilder
  324.      *
  325.      * @return void
  326.      */
  327.     protected function buildQuery(QueryBuilder $queryBuilder): void
  328.     {
  329.         $queryBuilder->select('sum(orderItem.amount) as sum''eventStartDate''sum(eventBrick.realQuota) as quota')
  330.             ->from('object_' OnlineShopOrderItem::classId(), 'orderItem')
  331.             ->join('orderItem''object_' ShopEvent::classId(), 'product''product.o_id = orderItem.product__id ')
  332.             ->join('orderItem''object_brick_store_OrderItemCustomizedEvent_' OnlineShopOrderItem::classId(), 'eventBrick''eventBrick.o_id = orderItem.o_id ')
  333.         ;
  334.     }
  335.     /**
  336.      * @param QueryBuilder $queryBuilder
  337.      * @param Carbon[] $startDateList
  338.      *
  339.      * @return void
  340.      */
  341.     protected function addDateCondition(QueryBuilder $queryBuilder, array $startDateList): void
  342.     {
  343.         $queryBuilder->andWhere('product.eventStartDate IN(' implode(','$startDateList) . ')')->addGroupBy('product.eventStartDate');
  344.     }
  345.     /**
  346.      * @param QueryBuilder $queryBuilder
  347.      *
  348.      * @return void
  349.      */
  350.     protected function addProductCondition(QueryBuilder $queryBuilder): void
  351.     {
  352.         $queryBuilder->andWhere(sprintf('(select o_parentId from object_%s p where p.o_id = product.o_id) = :productId'EventProduct::classId()));
  353.     }
  354.     /**
  355.      * @param QueryBuilder $queryBuilder
  356.      *
  357.      * @return void
  358.      */
  359.     protected function addItemStateCondition(QueryBuilder $queryBuilder): void
  360.     {
  361.         $queryBuilder->andWhere(' (orderItem.orderState is NULL OR orderItem.orderState = "committed" OR orderItem.orderState = "") ');
  362.     }
  363.     /**
  364.      * @param QueryBuilder $queryBuilder
  365.      *
  366.      * @return void
  367.      */
  368.     protected function addParentStateCondition(QueryBuilder $queryBuilder): void
  369.     {
  370.         $queryBuilder
  371.             ->andWhere(sprintf(' (SELECT shopOrder.orderState FROM object_%s shopOrder WHERE  shopOrder.o_id = orderItem.o_parentId) IN ("committed", "cancelled")'OnlineShopOrder::classId()));
  372.     }
  373.     /**
  374.      * @param bool $hasCartItems
  375.      * @param float|int|null $availableQuota
  376.      * @param EventProduct $event
  377.      * @param Carbon $eventDate
  378.      *
  379.      * @return bool
  380.      */
  381.     private function isAvailableWithCartAndTime(
  382.         bool $hasCartItems,
  383.         float|int|null $availableQuota,
  384.         EventProduct $event,
  385.         Carbon $eventDate
  386.     ): bool {
  387.         return ($hasCartItems && $availableQuota >= || $availableQuota 0) && $this->isAvailableByDate($event$eventDate);
  388.     }
  389. }