<?php
/**
* Created by Elements.at New Media Solutions GmbH
*
*/
namespace App\Ecommerce\AvailabilitySystem\Event;
use App\Model\Shop\Event\EventProduct;
use Carbon\Carbon;
use Doctrine\DBAL\Query\QueryBuilder;
use Elements\Bundle\TicketShopFrameworkBundle\Model\Shop\Cart\TicketShopCartInterface;
use Elements\Bundle\TicketShopFrameworkBundle\Service\CartService;
use Pimcore\Bundle\EcommerceFrameworkBundle\AvailabilitySystem\AvailabilitySystemInterface;
use Pimcore\Bundle\EcommerceFrameworkBundle\Factory;
use Pimcore\Db;
use Pimcore\Model\DataObject\AbstractObject;
use Pimcore\Model\DataObject\OnlineShopOrder;
use Pimcore\Model\DataObject\OnlineShopOrderItem;
use Pimcore\Model\DataObject\ShopEvent;
class AvailabilitySystem implements AvailabilitySystemInterface
{
private TicketShopCartInterface $cart;
private bool $showCancelledItems = false;
public function __construct(CartService $cartService)
{
$environment = Factory::getInstance()->getEnvironment();
$cartName = $environment->getCurrentCheckoutTenant() === 'b2b' ?: 'cart';
$this->cart = $cartService->getCart($cartName);
}
public function setShowCancelledItems(bool $showCancelledItems): void
{
$this->showCancelledItems = $showCancelledItems;
}
/**
* @param EventProduct $product
* @param int $quantityScale
* @param null $products
*
* @return Availability|null
*/
public function getAvailabilityInfo(
$product,
$quantityScale = 1,
$products = null
): ?Availability {
$returnAvailability = null;
if ($product->getType() == AbstractObject::OBJECT_TYPE_VARIANT) {
$returnAvailability = $this->getAvailableQuota($product, $product->getEventStartDate());
} else {
//search every given availability
$availableList = $this->getAvailableList($product);
foreach ($availableList as $availability) {
if ($availability->isAvailable()) {
$returnAvailability = $availability;
break;
}
}
}
return $returnAvailability;
}
/**
* ermittelt alle verfügbaren tage und kontingente
*
* @param EventProduct $event
* @param Carbon|null $startDate
* @param Carbon|null $endDate
* @param bool $getPastAvailabilities
*
* @return Availability[]
*/
public function getAvailableList(
EventProduct $event,
Carbon $startDate = null,
Carbon $endDate = null,
bool $getPastAvailabilities = false,
bool $ignoreCartItems = false
): array {
if ($availableDates = $this->getAvailableDates($event, $startDate, $endDate, $getPastAvailabilities)) {
return $this->getAvailableQuotas($event, $availableDates, $ignoreCartItems);
}
return [];
}
/**
* Check if event has any availability on the whole day
*
* @param EventProduct $event
* @param Carbon $date
*
* @return bool
*/
public function isAvailableOnDay(EventProduct $event, Carbon $date): bool
{
/** @var EventProduct $event */
$event = $event->getReference();
$availabilities = $this->getAvailableList($event, $date->copy()->startOfDay(), $date->copy()->endOfDay());
return count($availabilities) > 0;
}
/**
* @param EventProduct $event
*
* @return Availability|null
*/
public function getNextAvailability(EventProduct $event): ?Availability
{
$availability = null;
if ($validityDates = $event->getValidityDates()) {
if ($nextOccurrence = $validityDates->getNextOccurrence()) {
$nextDate = Carbon::parse($nextOccurrence);
$availability = $this->getAvailableQuota($event, $nextDate);
}
}
return $availability;
}
/**
* all available dates
*
* @param EventProduct $event
* @param Carbon|null $startDate
* @param Carbon|null $endDate
*
* @return Carbon[]
*/
public function getAvailableDates(EventProduct $event, Carbon $startDate = null, Carbon $endDate = null, bool $getPastAvailabilities = false): array
{
$availableDates = [];
if (!$startDate) {
if ($event->getBookableOnDayOfEvent()) {
$startDate = Carbon::now()->startOfDay();
} else {
$startDate = Carbon::now();
}
}
if (!$endDate) {
$endDate = Carbon::today()->addYears(2);
}
if ($validityDates = $event->getValidityDates()) {
$validDates = $validityDates->getOccurrencesBetween($startDate, $endDate);
foreach ($validDates as $validDate) {
$date = Carbon::parse($validDate);
if ($this->isAvailableByDate($event, $date, $getPastAvailabilities, $startDate)) {
$availableDates[$date->getTimestamp()] = $date;
}
}
}
return $availableDates;
}
/**
* @param EventProduct $event
* @param Carbon $date
* @param bool $ignoreCartItems
*
* @return Availability
*/
public function getAvailableQuota(EventProduct $event, Carbon $date, bool $ignoreCartItems = false): Availability
{
$availabilities = $this->getAvailableQuotas($event, [$date], $ignoreCartItems);
if (!empty($availabilities)) {
return array_shift($availabilities);
} else {
return new Availability($event, false, 0, 0, $date);
}
}
/**
* @param EventProduct $event
* @param array<mixed> $dates
* @param bool $ignoreCartItems
*
* @return Availability[]
*/
protected function getAvailableQuotas(EventProduct $event, array $dates, bool $ignoreCartItems = false): array
{
$results = [];
$totalQuota = $event->getQuota();
array_walk($dates, function (&$option) {
$option = $option->getTimestamp();
});
$queryBuilder = Db::get()->createQueryBuilder();
$queryBuilder->setParameters(['productId' => $event->getReference()->getId()]);
$this->buildQuery($queryBuilder);
$this->addProductCondition($queryBuilder);
$this->addItemStateCondition($queryBuilder);
$this->addDateCondition($queryBuilder, $dates);
$this->addParentStateCondition($queryBuilder);
$list = $queryBuilder->execute()->fetchAllAssociative();
if (!$ignoreCartItems) {
$cartItems = $this->getCartItemsCount();
}
$id = $event->getId();
foreach ($list as $entry) {
$availableQuota = $totalQuota - (int)$entry['quota'];
if (isset($cartItems[$entry['eventStartDate'] . $id])) {
$availableQuota -= $cartItems[$entry['eventStartDate'] . $id];
}
$eventDate = Carbon::createFromTimestamp($entry['eventStartDate']);
$results[$entry['eventStartDate']] = new Availability(
$event,
($availableQuota > 0) && $this->isAvailableByDate($event, $eventDate),
$availableQuota > 0 ? $availableQuota : 0,
intval($totalQuota),
$eventDate);
}
if (!$this->showCancelledItems) {
$tmpResult = [];
foreach ($results as $timestamp => $result) {
if (!$result->isCancelled()) {
$tmpResult[$timestamp] = $result;
}
}
$results = $tmpResult;
}
//if no booked event found, fill the rest with the total quota
foreach ($dates as $date) {
if (!key_exists($date, $results)) {
$availableQuota = $totalQuota;
if (isset($cartItems[$date . $id])) {
$availableQuota -= $cartItems[$date . $id];
}
$eventDate = Carbon::createFromTimestamp($date);
$isAvailable = $this->isAvailableByDate($event, $eventDate);
$availability = new Availability($event, $isAvailable, $availableQuota, intval($totalQuota), $eventDate);
if ($this->showCancelledItems || !$availability->isCancelled()) {
$results[$date] = $availability;
}
}
}
ksort($results);
return $results;
}
private function isAvailableByDate(EventProduct $event, Carbon $eventDate, ?bool $getPastAvailabilities = false, ?Carbon $startDate = null): bool
{
if ($getPastAvailabilities) {
$startDate = $startDate ?? (Carbon::now())->subDays(14);
$currentDateTime = $startDate;
} else {
$currentDateTime = Carbon::now();
//not available before x hours
if ($bookingStopHours = $event->getStopSale()) {
/** @phpstan-ignore-next-line */
$currentDateTime = Carbon::now()->addhours($bookingStopHours);
}
}
return $currentDateTime->lte($event->getBookableOnDayOfEvent() ? $eventDate->endOfDay() : $eventDate);
}
/**
* @return int[]
*/
private function getCartItemsCount(): array
{
$cartItems = [];
$eventCartItems = $this->cart->getItemsByProductClass(EventProduct::class);
foreach ($eventCartItems as $item) {
/** @var EventProduct $product */
$product = $item->getProduct();
$parentProduct = $product->getReference();
if (isset($cartItems[$product->getEventStartDate()->getTimestamp() . $parentProduct->getId()])) {
$cartItems[$product->getEventStartDate()->getTimestamp() . $parentProduct->getId()] += $item->getCount();
} else {
$cartItems[$product->getEventStartDate()->getTimestamp() . $parentProduct->getId()] = $item->getCount();
}
}
return $cartItems;
}
/**
* @param QueryBuilder $queryBuilder
*
* @return void
*/
protected function buildQuery(QueryBuilder $queryBuilder): void
{
$queryBuilder->select('sum(orderItem.amount) as sum', 'eventStartDate', 'sum(eventBrick.realQuota) as quota')
->from('object_' . OnlineShopOrderItem::classId(), 'orderItem')
->join('orderItem', 'object_' . ShopEvent::classId(), 'product', 'product.o_id = orderItem.product__id ')
->join('orderItem', 'object_brick_store_OrderItemCustomizedEvent_' . OnlineShopOrderItem::classId(), 'eventBrick', 'eventBrick.o_id = orderItem.o_id ')
;
}
/**
* @param QueryBuilder $queryBuilder
* @param Carbon[] $startDateList
*
* @return void
*/
protected function addDateCondition(QueryBuilder $queryBuilder, array $startDateList): void
{
$queryBuilder->andWhere('product.eventStartDate IN(' . implode(',', $startDateList) . ')')->addGroupBy('product.eventStartDate');
}
/**
* @param QueryBuilder $queryBuilder
*
* @return void
*/
protected function addProductCondition(QueryBuilder $queryBuilder): void
{
$queryBuilder->andWhere(sprintf('(select o_parentId from object_%s p where p.o_id = product.o_id) = :productId', EventProduct::classId()));
}
/**
* @param QueryBuilder $queryBuilder
*
* @return void
*/
protected function addItemStateCondition(QueryBuilder $queryBuilder): void
{
$queryBuilder->andWhere(' (orderItem.orderState is NULL OR orderItem.orderState = "committed" OR orderItem.orderState = "") ');
}
/**
* @param QueryBuilder $queryBuilder
*
* @return void
*/
protected function addParentStateCondition(QueryBuilder $queryBuilder): void
{
$queryBuilder
->andWhere(sprintf(' (SELECT shopOrder.orderState FROM object_%s shopOrder WHERE shopOrder.o_id = orderItem.o_parentId) IN ("committed", "cancelled")', OnlineShopOrder::classId()));
}
}