<?php
/**
* Created by Elements.at New Media Solutions GmbH
*
*/
namespace App\Model\Shop\Ticket;
use App\Model\Shop\CancelableInterface;
use App\Model\Shop\OrderItem;
use App\Model\Traits\SkidataTrait;
use App\Model\Type\ForwardingStateType;
use App\Model\Type\PoolType;
use Carbon\Carbon;
use Elements\Bundle\SkidataTicketingSwebBundle\Model\Constant\ValidityUnit;
use Elements\Bundle\SkidataTicketingSwebBundle\Skidata\Sweb\Client\SalesChannel\Enum\PermissionStatusEnum;
use Elements\Bundle\SkidataTicketingSwebBundle\Skidata\Sweb\Client\SalesChannel\Enum\SalesStatusEnum;
use Elements\Bundle\TicketShopFrameworkBundle\Model\DataObject\PriceGroupInterface;
use Elements\Bundle\TicketShopFrameworkBundle\Model\DataObject\TicketConsumerCategory;
use Elements\Bundle\TicketShopFrameworkBundle\Model\DataObject\TicketProduct as TSF_TicketProduct;
use Elements\Bundle\TicketShopFrameworkBundle\Model\Shop\Product\PriceGroupAwareProduct;
use Pimcore;
use Pimcore\Model\DataObject\TicketMetaProduct;
use Pimcore\Model\DataObject\TicketshopTicketAdditional;
/**
* Class TicketProduct
*
* @package App\Model\Shop\Ticketing
*
* @method TicketConsumerCategory|null getTicketConsumerCategory()
* @method ShopTicketCatalog|null getCatalogObject()
* @method TicketProduct getReference()
*/
class TicketProduct extends TSF_TicketProduct implements CancelableInterface
{
use SkidataTrait;
const PRODUCT_TYPE = 'ticket';
const SHOP_SYSTEM_NAME = 'ticket';
protected function getShopSystemName(): string
{
return TicketProduct::SHOP_SYSTEM_NAME;
}
/**
* defines the name of the price system for this product.
* there should either be a attribute in pro product object or
* it should be overwritten in mapped sub classes of product classes
*
* @return string
*/
public function getPriceSystemName(): ?string
{
return $this->getShopSystemName();
}
/**
* defines the name of the availability system for this product.
* there should either be a attribute in pro product object or
* it should be overwritten in mapped sub classes of product classes
*
* @return string
*/
public function getAvailabilitySystemName(): ?string
{
return 'default';
}
public function getOrderCatalog(): ?ShopTicketCatalog
{
// load unpublished objects too to display catalog infos in backend attask #419238 - storno-thema
Pimcore\Model\DataObject\AbstractObject::setHideUnpublished(false);
$orderCatalog = $this->getCatalogObject();
Pimcore\Model\DataObject\AbstractObject::setHideUnpublished(true);
return $orderCatalog;
}
/**
* Checks if TicketProduct is bookable
*/
public function isBookable(int $quantity = 1): bool
{
if (!\Pimcore::inDebugMode() && $this->deactivateSkidataRequests()) {
return false;
}
$skidataProduct = $this->getSkidataProduct();
if (null === $skidataProduct) {
return false;
}
if ($this->getOnlyToday()) {
if (Carbon::now()->lt(Carbon::now()->setTime(12, 10, 0, 0))) {
return false;
}
}
return $skidataProduct->getActive() && $this->getReference()->isPublished();
}
public function isSingleRide(): bool
{
return (bool)$this->getSingleRideTicket();
}
public function isCancelable(OrderItem $orderItem = null): bool
{
if ($orderItem) {
$currentAdminUser = \Pimcore\Tool\Admin::getCurrentUser();
$skidataBrick = $orderItem->getSkidataBrick();
// can not cancel if cancelled in Skidata - except for special users
if ($orderItem->isCanceledInSkidata() && !$currentAdminUser?->isAllowed('shop_backend_cancel_cancelled_in_skidata')) {
return false;
}
// can cancel always if not submitted !
if (!$skidataBrick || $skidataBrick->getTransmisssionState() == ForwardingStateType::ERROR || $skidataBrick->getTransmisssionState() == null) {
return true;
}
if (!$this->isCancelableInSkidata($orderItem)) {
return false;
}
if (!$this->isSeasonTicket()) {
if ($validityBrick = $orderItem->getCustomized()->getOrderItemCustomizedValidityRange()) {
// do not allow cancellation in between validity -> enhance to allow until 7 o clock if needed
// this logic shouldn't be applied for seasonal tickets (attask #414766)
// Root Cause for this: the Consumed State is only synced once a day ( at around 23:50) so we can not rely on that -> hence during validity cancellation is locked -> if not consumed it can be cancelled after validity again.
if (Carbon::now()->between($validityBrick->getStartDate(), $validityBrick->getEndDate())) {
return false;
}
}
}
}
return true;
}
public function isCancelableInSkidata(OrderItem $orderItem): bool
{
$skidataBrick = $orderItem->getSkidataBrick();
if ($skidataBrick) {
/**
* Ein "TicketOrderItem" ist stornierbar falls sämtliche TicketItems:
* 1.) die Permission noch nicht "consumed" ist
* 2.) das TicketItem selbst stornierbar ist, entspricht folgendener Abfrage
* Booked ( BOOKED oder BOOKED_AND_REJECTED oder BOOKED_AND_TRANSFERRED )
* ! reserved => wieder stornierbar ( #642504 / #624984)
*
* wenn bereits in Skidata storniert => cancel im Pimcore erlaubt (Pimcore-Storno, RĂĽckerstattung datatrans)
*/
$salesStatus = $skidataBrick->getSkiDataSalesStatus();
$cancelableSalesStates = array_merge(SalesStatusEnum::getBookedSalesStates(), SalesStatusEnum::getCanceledSalesStates());
$cancelableSalesStates[] = SalesStatusEnum::Reserved;
if (!in_array($salesStatus, $cancelableSalesStates)) {
return false;
}
$permissionStatus = $skidataBrick->getSkiDataPermissionStatus();
// not cancelable anymore if they are on this states !
if (in_array($permissionStatus, [PermissionStatusEnum::Consumed, PermissionStatusEnum::Blocked])) {
return false;
}
if ($skidataBrick->getSkiDataPermissionConsumed()) {
return false;
}
}
return true;
}
public function getMetaProduct(): TicketMetaProduct|false
{
$listing = new TicketMetaProduct\Listing();
$listing->addConditionParam("find_in_set({$this->getId()}, relatedTo)");
return $listing->current();
}
public function getInsuranceMetaProduct(): TicketshopTicketAdditional|false
{
$listing = new TicketshopTicketAdditional\Listing();
$listing->addConditionParam("find_in_set({$this->getId()}, products)");
return $listing->current();
}
/**
* @param Carbon|null $startDate
*
* @return Carbon|null
*/
public function getTicketEndDate(Carbon $startDate = null): ?Carbon
{
$duration = null;
$catalog = $this->getCatalogObject();
if ($startDate == null) {
$startDate = $this->getTicketStartDate();
}
if ($catalog && $catalog->getIsAnnualCatalog()) {
$yearlyDate = clone $startDate;
return $yearlyDate->addYear();
} elseif ($this->isSeasonTicket() && $catalog) {
return $catalog->getCurrentDateRangeEndDate();
}
if ($this->getValidityUnit() == ValidityUnit::DAY) {
$duration = $this->getValidityValue() - 1; // substract 1 as we use StartOfDay and EndOfDay hence 1 Day is always on the same days
} else {
//todo - Saisonkarten, Punktekarten? Stundenkarten?
}
// sanity checks that it is at least 0 !! no negative numbers
if ($duration < 0) {
$duration = 0;
}
return $startDate ? $startDate->copy()->addDays($duration)->endOfDay() : null;
}
public function isSkidataBookableFor(TicketConsumerCategory $consumerCategory): bool
{
if ($this->getSkidataProduct() && !empty($this->getSkidataProduct()->getConsumerCategories())) {
$skidataConsumer = $consumerCategory->getSkidataConsumerForSkidataProduct($this->getSkidataProduct());
if ($skidataConsumer) {
return true;
}
}
return false;
}
public function getTicketPool(): string
{
$skiDataProduct = $this->getReference()->getSkidataProduct();
if($skiDataProduct == null) {
return PoolType::UNASSIGNED;
}
$pool = $skiDataProduct->getValidPoolName() ?? PoolType::UNASSIGNED;
if (!PoolType::isValid($pool)) {
$pool = PoolType::UNKNOWN;
}
return $pool;
}
public function getOrCreateCloneWithPriceGroup(PriceGroupInterface $priceGroup): PriceGroupAwareProduct
{
if (!$priceGroup instanceof TicketConsumerCategory) {
throw new \InvalidArgumentException('PriceGroup must be instance of TicketConsumerCategory');
}
/** @phpstan-ignore-next-line */
return $this->getReference()->getOrCreateOrderableObject($priceGroup,
$this->getTicketStartDate(),
$this->getAcquisitionType(),
$this->getCatalogObject());
}
}