<?php
/**
* Created by Elements.at New Media Solutions GmbH
*
*/
namespace App\Model\Shop\Ticket;
use App\Ecommerce\PriceSystem\Ticket\PriceSystem;
use App\Model\Shop\TeaserInfoInterface;
use App\Service\TicketShopFrameworkBundle\TicketFilter;
use Carbon\Carbon;
use Elements\Bundle\RecurringDatesTypeBundle\Templating\RecurringDatesHelper;
use Elements\Bundle\SkidataTicketingSwebBundle\Model\Constant\ValidityUnit;
use Elements\Bundle\TicketShopFrameworkBundle\Model\DataObject\TicketCatalog;
use Elements\Bundle\TicketShopFrameworkBundle\Model\Shop\Ticketing\TicketCatalogAvailability;
use Pimcore\Bundle\EcommerceFrameworkBundle\Model\ProductInterface;
use Pimcore\Bundle\EcommerceFrameworkBundle\PriceSystem\PriceInfoInterface;
use Pimcore\Bundle\EcommerceFrameworkBundle\PriceSystem\PriceInterface;
use Pimcore\Log\ApplicationLogger;
use Pimcore\Model\Asset\Image;
/**
* Class TicketProduct
*
* @package App\Model\Shop\Ticketing
*
* @method TicketProduct[] getTicketProducts()
* @method TicketConsumerCategory[] getTicketConsumerCategories()
* @method null|TicketConsumerCategory getCalendarPriceConsumerCategory()
*/
class ShopTicketCatalog extends TicketCatalog implements TeaserInfoInterface, ProductInterface
{
/**
* @return PriceInterface|null
*/
public function getTeaserPrice(): ?PriceInterface
{
return $this->getPrice(null, true);
}
public function getStartDate(?Carbon $fromDate = null): ?Carbon
{
if (!$fromDate) {
$firstValidityDate = $this->getFirstValidDateRange(Carbon::now()->startOfDay());
$startDate = $firstValidityDate['from'] ?? null;
} else {
$startDate = $fromDate;
}
return $startDate;
}
public function getProduct(?Carbon $fromDate = null): ?TicketProduct
{
$startDate = $this->getStartDate($fromDate);
$products = $this->getTicketProducts();
$consumer = $this->getCalendarPriceConsumerCategory();
$refProduct = null;
if ($products && $consumer && $startDate) {
// get ref product with the shortest duration, sorting in relations-field is important
$refProduct = null;
foreach ($products as $ticketProduct) {
if ($ticketProduct->getSkidataProduct()) {
if ($refProduct == null || $ticketProduct->getValidityUnit() == ValidityUnit::DAY) {
if (($refProduct && $refProduct->getValidityUnit() !== ValidityUnit::DAY) || $refProduct == null || $ticketProduct->getValidityValue() < $refProduct->getValidityValue()) {
$refProduct = $ticketProduct;
if ($refProduct->getValidityValue() == 1 && $refProduct->getValidityValue() == ValidityUnit::DAY) {
break;
}
}
}
}
}
}
return $refProduct;
}
public function getPriceInfoForProduct(TicketProduct $refProduct, ?Carbon $fromDate = null, bool $ignoreSmartPricer = false): ?PriceInfoInterface
{
$consumer = $this->getCalendarPriceConsumerCategory();
$startDate = $this->getStartDate($fromDate);
try {
/** @var PriceSystem $priceSystem */
$priceSystem = $refProduct->getPriceSystemImplementation();
if ($ignoreSmartPricer) {
$priceSystem->setIgnoreSmartPricer();
}
$priceInfo = $priceSystem->getPriceInfo($refProduct, 1, [], $startDate,
$consumer, null, $this);
return $priceInfo;
} catch (\Exception $e) {
//error ticket on this date is not available
ApplicationLogger::getInstance('ticketPrice')->debug($e->getMessage(), [
'relatedObject' => $refProduct,
]);
}
return null;
}
public function getPriceInfo(?Carbon $fromDate = null, bool $ignoreSmartPricer = false): ?PriceInfoInterface
{
$refProduct = $this->getProduct($fromDate);
return $refProduct ?
$this->getPriceInfoForProduct($refProduct, $fromDate, $ignoreSmartPricer) : null;
}
public function getPrice(?Carbon $fromDate = null, bool $ignoreSmartPricer = false): ?PriceInterface
{
return $this->getPriceInfo($fromDate, $ignoreSmartPricer)?->getPrice();
}
public function getPriceForDateRange(?Carbon $fromDate = null, ?Carbon $toDate = null, bool $useBasePrice = false): ?PriceInterface
{
$availabilities = $this->getAvailabilityForDateRange($fromDate, $toDate);
return $this->getLowestPriceInfoForAvailability($availabilities, $useBasePrice)?->getTotalPrice();
}
public function getAvailabilityForDateRange(
?Carbon $fromDate = null,
?Carbon $toDate = null,
bool $exactDuration = true,
bool $ignorePrice = false,
bool $includeInsurances = false,
bool $ignoreIsBookable = false
): TicketCatalogAvailability {
$filter = new TicketFilter($fromDate, $toDate);
$filter->setExactDuration($exactDuration);
$filter->setIgnorePrice($ignorePrice);
if ($calendarConsumer = $this->getCalendarPriceConsumerCategory()) {
$filter->setConsumers([$calendarConsumer->getId()]);
}
$availabilities = $filter->getTicketCatalogAvailability($this, $includeInsurances, $ignoreIsBookable);
return $availabilities;
}
/**
* @param Carbon|null $fromDate
* @param Carbon|null $toDate
* @param array<int> $allowedConsumerIds
* @param bool $ignoreIsBookable
*
* @return TicketCatalogAvailability
*/
public function getAvailabilityForDateRangeWithPriceGroup(array $allowedConsumerIds, ?Carbon $fromDate = null, ?Carbon $toDate = null, bool $ignoreIsBookable = false): TicketCatalogAvailability
{
$filter = new TicketFilter($fromDate, $toDate);
$filter->setExactDuration(true);
$filter->setConsumers($allowedConsumerIds);
$availabilities = $filter->getTicketCatalogAvailability($this, false, $ignoreIsBookable);
return $availabilities;
}
/**
* Checks if it is available in a specific date range. If no end date is given, all future dates get checked
*
* @param ?Carbon $startDate
* @param ?Carbon $endDate
*
* @return bool
*/
public function isAvailableInDateRange(?Carbon $startDate = null, ?Carbon $endDate = null): bool
{
$recurringDates = (new RecurringDatesHelper())->getCalculatedDates($this, 'getValidityDates');
$checkFuture = false;
if (!$startDate) {
$startDate = Carbon::now();
$checkFuture = true;
}
if (!$endDate) {
$endDate = $startDate->copy();
}
foreach ($recurringDates as $recurringDate) {
if ($checkFuture && $recurringDate['fromDate']->isFuture()) {
return true;
} elseif ($recurringDate['fromDate']->lte($startDate) && $recurringDate['toDate']->gte($endDate)) {
return true;
}
}
return false;
}
public function getLowestPriceInfoForAvailability(TicketCatalogAvailability $availabilities, bool $useBasePrice = false, bool $includeIgnoredTickets = false): ?PriceInfoInterface
{
$priceInfo = null;
if ($availabilities->hasAvailability()) {
foreach ($availabilities->getTicketAvailability() as $ticketAvailability) {
$product = $ticketAvailability->getTicketProduct();
//get first, because we already restricted to our adult consumer
$consumerAvailability = $ticketAvailability->getConsumerAvailabilities()[0];
if ($priceInfo == null || ((!$product->getIgnoreForFromPrice() || $includeIgnoredTickets) &&
$priceInfo->getTotalPrice()->getGrossAmount()->asNumeric() > $consumerAvailability->getPriceInfo()->getTotalPrice()->getGrossAmount()->asNumeric())
) {
$priceInfo = $consumerAvailability->getPriceInfo();
}
}
}
if ($useBasePrice && $priceInfo) {
$price = $priceInfo->getOriginalPriceInfo();
} else {
$price = $priceInfo;
}
return $price;
}
public function getTeaserTopTitle(): ?string
{
if ($category = $this->getProductCategory()) {
return $category->getName();
}
return '';
}
public function getDescriptionForTeaser(): ?string
{
return $this->getTeaserDescription() ?: $this->getShortDescription();
}
public function getTeaserImage(): ?Image
{
return $this->getMainImage();
}
/** @return array<mixed> */
public function getTeaserValidity(): array
{
return $this->getFirstValidDateRange(Carbon::now());
}
public function getFirstValidValidityDate(Carbon $date): ?Carbon
{
$validityDate = null;
$firstValidityDateRange = $this->getFirstValidDateRange($date);
if (isset($firstValidityDateRange['from'])) {
/** @var Carbon $fromDateValidity */
$fromDateValidity = $firstValidityDateRange['from'];
$validityDate = $fromDateValidity->lt($date) ? $date : $fromDateValidity;
}
return $validityDate;
}
/**
* @param Carbon $date
*
* @return array<mixed>
*/
public function getFirstValidDateRange(Carbon $date): array
{
if ($validDates = $this->getValidityDates()) {
if ($definitions = $validDates->getDefinitionArray()) {
foreach ($definitions as $definition) {
if (isset($definition['values']['fromDate'])) {
$fromDate = Carbon::parse($definition['values']['fromDate']);
$toDate = Carbon::parse($definition['values']['toDate']);
if ($definition['type'] == 'RecurringDate'
&& ($date->between($fromDate, $toDate) || $fromDate->gte($date))
) {
return [
'from' => $fromDate,
'to' => $toDate,
];
}
}
}
}
}
return [];
}
/**
* returns all valid date ranges now and in the future, if no dateformat is given, dats are returned as timestamp
*
* @param Carbon $date
*
* @return array<mixed>
*/
public function getAllValidDateRanges(?Carbon $date = null, string $dateFormat = null): array
{
$validRanges=[];
if(!$date) {
$date = Carbon::now()->startOfDay();
}
if ($validDates = $this->getValidityDates()) {
if ($definitions = $validDates->getDefinitionArray()) {
foreach ($definitions as $definition) {
$fromDate = Carbon::parse($definition['values']['fromDate']);
$toDate = Carbon::parse($definition['values']['toDate']);
if ($definition['type'] == 'RecurringDate'
&& ($date->between($fromDate, $toDate) || $fromDate->gte($date))
) {
$validRanges[]= [
'from' => $dateFormat ? $fromDate->format($dateFormat) : $fromDate->getTimestamp(),
'to' => $dateFormat ? $toDate->format($dateFormat) : $toDate->getTimestamp(),
];
}
}
}
}
return $validRanges;
}
/**
* @return array<mixed>
*/
public function getValidityDaysNumeric(): array
{
$validityDays = [];
$products = $this->getTicketProducts();
foreach ($products as $product) {
$validityDays[] = $product->getValidity();
//add for flex-tickets e.g. 2in4 days
if ($validTicketDays = $product->getValidDays()) {
$validityDays[] = $validTicketDays;
}
}
$validityDays = array_unique($validityDays);
sort($validityDays);
return $validityDays;
}
public function getCurrentDateRangeStartDate(): ?Carbon
{
$range = $this->getFirstValidDateRange(Carbon::now()->startOfDay());
if (isset($range['from'])) {
return $range['from']->startOfDay();
}
return null;
}
public function getCurrentDateRangeEndDate(): ?Carbon
{
$range = $this->getFirstValidDateRange(Carbon::now()->startOfDay());
if (isset($range['to'])) {
return $range['to']->startOfDay();
}
return null;
}
//for dummy template
public function getDescriptionExpand(): bool
{
return true;
}
public function getMaxSelectableDates(): int
{
$selectableDates = 1;
if ($this->getTicketProducts()) {
foreach ($this->getTicketProducts() as $ticketProduct) {
if (!$ticketProduct->isSingleRide() && $ticketProduct->getValidityValue() > 1) {
$selectableDates = 2;
break;
}
}
}
return $selectableDates;
}
public function getMinSelectableDates(): int
{
$selectableDates = 2;
if ($this->getTicketProducts()) {
foreach ($this->getTicketProducts() as $ticketProduct) {
if ($ticketProduct->isSingleRide() || $ticketProduct->getValidityValue() < 2) {
$selectableDates = 1;
break;
}
}
}
return $selectableDates;
}
public function getMinValidityValue(): int
{
$minValidityValue = null;
if ($this->getTicketProducts()) {
foreach ($this->getTicketProducts() as $ticketProduct) {
if ($minValidityValue === null) {
$minValidityValue = $ticketProduct->getValidityValue();
} elseif ($ticketProduct->getValidityValue() < $minValidityValue) {
$minValidityValue = $ticketProduct->getValidityValue();
}
}
}
return $minValidityValue;
}
public function hasBookableTicketProducts(): bool
{
if ($this->getTicketProducts()) {
foreach ($this->getTicketProducts() as $ticketProduct) {
if ($ticketProduct->isBookable()) {
return true; // return true if at least one TicketProduct is bookable !
}
}
}
return false;
}
/**
* @return bool
*/
public function isBookable(): bool
{
$isPublished = $this->isPublished();
$hasActiveProducts = count($this->getActiveProducts()) > 0;
$isBookable = !$this->getIsNotBookable();
$hasBookableTicketProducts = $this->hasBookableTicketProducts();
$hasCurrentOrFutureDateRanges = $this->hasCurrentOrFutureDateRanges();
return $hasActiveProducts && $isBookable && $isPublished && $hasBookableTicketProducts && $hasCurrentOrFutureDateRanges;
}
public function hasCurrentOrFutureDateRanges(): bool
{
$datesHelper = \Pimcore::getContainer()->get(RecurringDatesHelper::class);
return !empty($datesHelper->getCalculatedDates($this, 'getValidityDates'));
}
//for tracking
public function getOSName(): ?string
{
return $this->getName();
}
//for tracking
public function getOSProductNumber(): ?string
{
return null;
}
public function getMinDuration(): int
{
$products = $this->getTicketProducts();
if (!$this->maxDuration && !$this->minDuration) {
$maxDuration = -1;
$minDuration = 1000;
foreach ($products as $product) {
$minValidityDuration = $product->getValidDays() ?: $product->getValidity();
$maxValidityDuration = $product->getValidity();
if (!$minValidityDuration) {
continue; // skip products where validity can not be calculated
}
if ($maxValidityDuration > $maxDuration) {
$maxDuration = $maxValidityDuration;
}
if ($minValidityDuration < $minDuration) {
$minDuration = $minValidityDuration;
}
}
$this->minDuration = intval($minDuration);
$this->maxDuration = intval($maxDuration);
}
return $this->minDuration;
}
// for b2b
public function getBaseCatalog(): self
{
$id = \Pimcore\Db::getConnection()->fetchOne("
SELECT src_id AS id
FROM object_relations_{$this->getClassId()}
WHERE dest_id = {$this->getId()} and fieldname = 'upgrades'
");
if (!empty($id)) {
return ShopTicketCatalog::getById($id);
}
return $this;
}
}