<?php
/**
* Created by Elements.at New Media Solutions GmbH
*
*/
namespace App\Service\Shop;
use App\Ecommerce\AvailabilitySystem\Event\AvailabilitySystem;
use App\Ecommerce\CartManager\SessionCartItem;
use App\Model\Shop\Cart\CartItemGroup as CustomCartItemGroup;
use App\Model\Shop\Event\EventProduct;
use App\Model\Shop\Event\ShopDynamicPrice;
use App\Model\Shop\Merchandise\MerchandiseProduct;
use App\Model\Shop\Ticket\ShopTicketCatalog;
use App\Model\Traits\SkidataTrait;
use App\Service\TicketShopFrameworkBundle\TicketFilter;
use Carbon\Carbon;
use Elements\Bundle\TicketShopFrameworkBundle\Ecommerce\PricingManager\PriceInfo as TSFPricingManagerPriceInfo;
use Elements\Bundle\TicketShopFrameworkBundle\Exception\UnavailableException;
use Elements\Bundle\TicketShopFrameworkBundle\Model\DataObject\TicketConsumerCategory;
use Elements\Bundle\TicketShopFrameworkBundle\Model\DataObject\TicketProduct;
use Elements\Bundle\TicketShopFrameworkBundle\Model\Shop\Cart\CartItemGroup;
use Elements\Bundle\TicketShopFrameworkBundle\Model\Shop\Cart\TicketShopCartInterface;
use Elements\Bundle\TicketShopFrameworkBundle\Model\Shop\Ticketing\TicketCatalogAvailability;
use Endroid\QrCode\Color\Color;
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelHigh;
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelLow;
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelMedium;
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelQuartile;
use Endroid\QrCode\Label\Label;
use Endroid\QrCode\QrCode;
use Endroid\QrCode\Writer\PngWriter;
use Endroid\QrCode\Writer\Result\ResultInterface;
use Pimcore\Bundle\EcommerceFrameworkBundle\PricingManager\PriceInfoInterface;
use Pimcore\Bundle\EcommerceFrameworkBundle\Type\Decimal;
use Pimcore\Logger;
use Pimcore\Model\DataObject\Fieldcollection\Data\SkidataAdditionalProductItem;
use Pimcore\Model\DataObject\Objectbrick\Data\AbstractData;
use Pimcore\Model\DataObject\Objectbrick\Data\PaymentProviderDPG;
use Pimcore\Model\DataObject\Objectbrick\Data\PaymentProviderDPGPayPal;
use Pimcore\Model\DataObject\OnlineShopOrder\PaymentProvider;
use Pimcore\Model\DataObject\SkidataConsumerCategory;
use Pimcore\Model\DataObject\SkidataProduct;
use Pimcore\Model\DataObject\TicketSeason;
use Symfony\Component\HttpFoundation\Request;
class ShopService
{
use SkidataTrait;
public function __construct(
protected ProductService $productService
) {
}
/**
* @param ShopTicketCatalog $catalog
*
* @return array<mixed>
*/
public function getSeasonDates(ShopTicketCatalog $catalog): array
{
return $this->modifySeasonDates($catalog->getTicketSeasons());
}
/**
* @param TicketSeason[]|null $seasons
*
* @return array<mixed>
*/
public function modifySeasonDates(array|null $seasons): array
{
$dates = [];
if (empty($seasons)) {
return $dates;
}
foreach ($seasons as $season) {
foreach ($season->getDates()->getDefinitionArray() as $definition) {
if ($definition['type'] == 'RecurringDate') {
$dates[] = [
'season' => $season->getSeason(),
'from' => Carbon::parse($definition['values']['fromDate']),
'to' => Carbon::parse($definition['values']['toDate']),
];
}
}
}
return $dates;
}
/**
* @param array<mixed>|null $seasonDates
* @param Carbon $date
*
* @return string
*/
public function getSeasonNameForDate(array|null $seasonDates, Carbon $date): string
{
if (!empty($seasonDates)) {
foreach ($seasonDates as $seasonDate) {
if ($date->between($seasonDate['from'], $seasonDate['to'])) {
return $seasonDate['season'];
}
}
}
//todo: default?
return 'midseason';
}
/**
* @param TicketCatalogAvailability $ticketCatalogAvailability
*
* @return array<mixed>
*/
public function getInsurancesForProduct(TicketCatalogAvailability $ticketCatalogAvailability): array
{
$insurances = [];
foreach ($ticketCatalogAvailability->getTicketAvailability() as $availability) {
foreach ($availability->getConsumerAvailabilities() as $consumerAvailability) {
if ($consumerInsurances = $consumerAvailability->getAdditionalProductAvailabilities()) {
$ticket = $availability->getTicketProduct();
foreach ($consumerInsurances as $insurance) {
/** @var \App\Model\Shop\Ticket\TicketProduct $insuranceTicketProduct */
$insuranceTicketProduct = $insurance->getAdditionalProduct();
$insurances['insurances'][$ticket->getId()][] = $insuranceTicketProduct;
$insurances['mappingObject'][$insuranceTicketProduct->getId()] = $insuranceTicketProduct->getInsuranceMetaProduct();
}
/** @phpstan-ignore-next-line */
if (isset($insurances['insurances'])) {
$insurances['insurances'][$ticket->getId()] = array_unique($insurances['insurances'][$ticket->getId()]);
}
}
}
}
return $insurances;
}
public function isInsuranceAllowed(
TicketProduct $insurance,
SkidataProduct $skidataProduct,
SkidataConsumerCategory $category
): bool {
$insuranceUuid = $insurance->getSkidataProduct()?->getUuid();
// find matching item for product / category
$additionalProductItems = $skidataProduct->getAdditionalProductItems();
if ($additionalProductItems) {
/** @var SkidataAdditionalProductItem $additionalProductItem */
foreach ($additionalProductItems as $additionalProductItem) {
if (!$additionalProductItem->getSkiDataProduct() || !$additionalProductItem->getConsumerCategory()) {
continue;
}
if ($category->getUuid() == $additionalProductItem->getConsumerCategory()->getUuid()
&& $insuranceUuid == $additionalProductItem->getSkiDataProduct()->getUuid()) {
return true;
}
}
}
return false;
}
/**
* @param TicketProduct $product
* @param TicketConsumerCategory $consumerCategory
* @param int $count
* @param TicketProduct|null $additional
* @param ShopTicketCatalog|null $ticketCatalog
* @param TicketShopCartInterface $cart
* @param Carbon|null $ticketStartDate
* @param array<mixed> $params
*
* @return array<mixed>
*
* @throws \Exception
*/
public function addTicketItemsToCartPerConsumer(
TicketProduct $product,
TicketConsumerCategory $consumerCategory,
int $count,
?TicketProduct $additional,
?ShopTicketCatalog $ticketCatalog,
TicketShopCartInterface $cart,
Carbon $ticketStartDate = null,
array $params = []
): array {
$productVariation = $product->getOrCreateOrderableObject($consumerCategory, $ticketStartDate, null, $ticketCatalog);
$addedItems = [];
for ($i = 0; $i < $count; $i++) {
$itemKey = sprintf('%d%s', $productVariation->getId(), uniqid());
try {
$priceInfo = $productVariation->getOSPriceInfo();
} catch (UnavailableException $exception) {
Logger::warning(sprintf('trying to add unavailable ticket #%s: %s', $productVariation->getId(),
$exception->getMessage()));
return [];
}
if ($priceInfo instanceof \Elements\Bundle\TicketShopFrameworkBundle\Ecommerce\PriceSystem\Skidata\PriceInfo) {
if ($priceInfo->getTicketBasePrice()->getAmount()->lessThan(Decimal::zero())) {
Logger::warning(sprintf('trying to add ticket with ticket price < 0 : ticket #%s',
$productVariation->getId()));
return [];
}
}
$addedItems[] = $cart->addItem($productVariation, 1, $itemKey, false, $params);
/** @var SessionCartItem $addedItem */
$addedItem = $cart->getItem($itemKey);
if ($additional) {
$subItemKey = $addedItem->getItemKey() . '-option-' . $count;
$addedItem->addSubItem($additional, 1, $subItemKey);
}
if (array_key_exists('person', $params)) {
$addedItem->setPerson($params['person']);
}
$addedItem->setIsUpgradeProduct($params['isUpgradeOption']);
$addedItem->setOriginalProduct($params['originalProduct']);
}
$cart->save();
// if ($count > 0) {
// $trackingManager = Factory::getInstance()->getTrackingManager();
// $trackingManager->trackCartProductActionAdd($cart, $productVariation, $count);
// }
return $addedItems;
}
/**
* @param ShopTicketCatalog $catalog
* @param Carbon $startDate
*
* @return bool
*/
public function isServiceViewHidden(ShopTicketCatalog $catalog, Carbon $startDate, Carbon $endDate = null, bool $exactDate = false): bool
{
if ($catalog->getUpgrades()) {
return false;
}
$firstValidityDateRange = $catalog->getFirstValidDateRange($startDate);
if ($firstValidityDateRange) {
$startDate = $startDate->greaterThan($firstValidityDateRange['from']) ? $startDate : $firstValidityDateRange['from'];
if (!$endDate && !$exactDate) {
$endDate = $firstValidityDateRange['to'];
} elseif (!$endDate && $exactDate) {
//only one day clicked
$endDate = $startDate;
}
$filter = new TicketFilter($startDate, $endDate);
$filter->setExactDuration($exactDate);
//we don't need price calculation here
$filter->setIgnorePrice(true);
//check for insurances
$catalogAvailability = $filter->getTicketCatalogAvailability($catalog, true);
//if more than one ticket is bookable e.g. day-ticket + half-day ticket
if (count($catalogAvailability->getTicketAvailability()) > 1) {
return false;
}
//if insurances are found
foreach ($catalogAvailability->getTicketAvailability() as $ticketAvailability) {
foreach ($ticketAvailability->getConsumerAvailabilities() as $consumerAvailability) {
if ($consumerAvailability->getAdditionalProductAvailabilities()) {
return false;
}
}
}
}
return true;
}
/**
* @param EventProduct $product
* @param ShopDynamicPrice $priceObj
* @param TicketShopCartInterface $cart
* @param Carbon $date
* @param int $quantity
* @param array<mixed> $params
*
* @return SessionCartItem
*
* @throws \Exception
*/
public function addEventToCartPerPriceObject(
EventProduct $product,
ShopDynamicPrice $priceObj,
TicketShopCartInterface $cart,
Carbon $date,
int $quantity,
array $params = [],
bool $allowAddingPastEvents = false
): ?SessionCartItem {
if($product->getBookableOnDayOfEvent() && ($date->isBefore(Carbon::now()->startOfDay()) && !$allowAddingPastEvents)) {
Logger::warning(sprintf('trying to add event with date in the past #%s: %s', $product->getId(), $date->format('c')));
return null;
} elseif ($product->getBookableOnDayOfEvent() && $product->getStopSale() > 0) {
$lastSale = clone $date;
$lastSale->subHours(intval($product->getStopSale()));
if($lastSale->isBefore(Carbon::now())) {
Logger::warning(sprintf('trying to add event with date/time beyond stop sales setting #%s: %s', $product->getId(), $date->format('c')));
return null;
}
} elseif (!$product->getBookableOnDayOfEvent() && ($date->isBefore(Carbon::now()->addDay()->startOfDay()) && !$allowAddingPastEvents)) {
Logger::warning(sprintf('trying to add event with date today or in the past, event is not bookable on day of event #%s: %s', $product->getId(), $date->format('c')));
return null;
}
$productVariation = $product->getOrCreateOrderableObject($date, $priceObj);
$itemKey = sprintf('%d', $productVariation->getId());
try {
$priceInfo = $productVariation->getOSPriceInfo();
} catch (UnavailableException $exception) {
Logger::warning(sprintf('trying to add unavailable event #%s: %s', $productVariation->getId(), $exception->getMessage()));
return null;
}
if ($priceInfo instanceof \Elements\Bundle\TicketShopFrameworkBundle\Ecommerce\PriceSystem\Skidata\PriceInfo) {
if ($priceInfo->getTicketBasePrice()->getAmount()->lessThan(Decimal::zero())) {
Logger::warning(sprintf('trying to add event with price < 0 : event #%s', $productVariation->getId()));
return null;
}
}
$itemKey = $cart->addItem($productVariation, $quantity, $itemKey, false, $params);
/** @var SessionCartItem|null $cartItem */
$cartItem = $cart->getItem($itemKey);
$cart->save();
// if ($quantity > 0) {
// $trackingManager = Factory::getInstance()->getTrackingManager();
// $trackingManager->trackCartProductActionAdd($cart, $productVariation, $quantity);
// }
return $cartItem;
}
/**
* @param MerchandiseProduct $merch
* @param int $quantity
* @param TicketShopCartInterface $cart
*
* @return SessionCartItem|null
*/
public function addMerchandiseToCart(MerchandiseProduct $merch, int $quantity, TicketShopCartInterface $cart): ?SessionCartItem
{
$itemKey = 'merch-' . $merch->getId();
//todo check if is available
$itemKey = $cart->addItem($merch, $quantity, $itemKey);
/** @var SessionCartItem|null $cartItem */
$cartItem = $cart->getItem($itemKey);
$cart->save();
// if ($quantity > 0) {
// $trackingManager = Factory::getInstance()->getTrackingManager();
// $trackingManager->trackCartProductActionAdd($cart, $merch, $quantity);
// }
return $cartItem;
}
public function isValidMaxParticipants(EventProduct $event, int $totalParticipants): bool
{
$quota = $event->getQuota();
if ($maxParticipants = $event->getMaxParticipantPerBooking()) {
return $totalParticipants <= $maxParticipants && $totalParticipants <= $quota;
} else {
return true;
}
}
public function isValidParticipants(EventProduct $event, Carbon $bookedDate, int $realAddedParticipants): bool
{
/** @var AvailabilitySystem $availabilitySystem */
$availabilitySystem = $event->getAvailabilitySystemImplementation();
$availability = $availabilitySystem->getAvailableQuota($event, $bookedDate, true);
if ($availability->isAvailable()) {
return $realAddedParticipants <= $availability->getAvailableQuota();
}
return false;
}
/**
* @param CartItemGroup[] $groupedItems
*
* @return array<mixed>
*/
public function groupCartItemsByClass(array $groupedItems): array
{
$groupedClassItems = [];
foreach ($groupedItems as $item) {
/** @var MerchandiseProduct|EventProduct|TicketProduct $product */
$product = $item->getProduct();
$groupedClassItems[$product->getClassName()][] = $item;
}
return $groupedClassItems;
}
public function getPricingRulesText(PriceInfoInterface $priceInfo): ?string
{
$rules = $priceInfo->getRules();
$rulesText = [];
foreach ($rules as $rule) {
if ($description = $rule->getDescription()) {
$rulesText[] = $description;
}
}
return $rulesText ? implode(' | ', $rulesText) : null;
}
/**
* @param PriceInfoInterface|CartItemGroup $subject
*
* @return string|null
*/
public function getPricingRulesLabel(PriceInfoInterface|CartItemGroup|CustomCartItemGroup $subject): ?string
{
if ($subject instanceof PriceInfoInterface) {
if ($subject instanceof TSFPricingManagerPriceInfo) {
$priceInfo = $subject;
$priceInfo->getPrice();
$originalPriceInfo = $priceInfo->getOriginalPriceInfo();
if ($originalPriceInfo instanceof \App\Ecommerce\PriceSystem\PriceInfoInterface
&& $originalPriceInfo->getDiscountLabel()) {
$prefix = ($originalPriceInfo->isHideDiscountValue() || !$originalPriceInfo->getDiscount()) ? '' : ' ' . $originalPriceInfo->getDiscount() . '% ';
return $prefix . $originalPriceInfo->getDiscountLabel();
}
}
} else {
$labels = [];
foreach ($subject->getItems() as $cartItem) {
$labels[] = $this->getPricingRulesLabel($cartItem->getPriceInfo());
}
//filter null-values from $labels
$labels = array_keys(array_flip(array_filter($labels)));
if (count($labels) == 1) {
return $labels[0];
}
}
return null;
}
/**
* @param string $code
* @param int $size
* @param string|null $label
*
* @return ResultInterface
*/
public function createQrCode(string $code, int $size = 200, string $label = null, int $errorLevel = null): ResultInterface
{
$writer = new PngWriter();
$qrCode = new QrCode($code);
match ($errorLevel) {
1 => $errorLevel = new ErrorCorrectionLevelLow(),
2 => $errorLevel = new ErrorCorrectionLevelMedium(),
3 => $errorLevel = new ErrorCorrectionLevelQuartile(),
default => $errorLevel = new ErrorCorrectionLevelHigh(),
};
$qrCode->setSize($size);
$qrCode->setErrorCorrectionLevel($errorLevel);
$qrCode->setForegroundColor(new Color(0, 0, 0, 0));
$qrCode->setBackgroundColor(new Color(255, 255, 255, 0));
if ($label) {
$label = Label::create($label)->setTextColor(new Color(0, 0, 0));
}
return $writer->write($qrCode, null, $label);
}
/**
* @param array<mixed> $recurringDates
*
* @return ?Carbon
*/
public function getMaxValidityDate(array $recurringDates): ?Carbon
{
$maxDate = null;
foreach ($recurringDates as $date) {
$maxDate = $date['toDate']->greaterThan($maxDate) ? $date['toDate'] : $maxDate;
}
return $maxDate;
}
/**
* @param array<mixed> $recurringDates
*
* @return ?Carbon
*/
public function getMinValidityDate(array $recurringDates): ?Carbon
{
$minDate = null;
foreach ($recurringDates as $date) {
$minDate = (null == $minDate || $date['fromDate']->lessThan($minDate)) ? $date['fromDate'] : $minDate;
}
return $minDate;
}
public function hasUserPermission(string $permission): bool
{
$user = \Pimcore\Tool\Admin::getCurrentUser();
return $user->isAllowed($permission);
}
/**
* @param string $idString
* @param bool $returnInt
*
* @return array<mixed>|int|null
*/
public function extractIdsFromString(?string $idString, bool $returnInt = false): array|int|null
{
if (null == $idString) {
return null;
}
$output = array_map('intval', explode(',', $idString));
return count($output) === 1 && $returnInt ? $output[0] : $output;
}
/**
* @param Request $request
*
* @return array<mixed>
*/
public function getDatesForPriceCalculator(Request $request): array
{
$output = [];
$isMobile = $request->get('isMobile');
$output['startDate'] = $request->get('shownStartYear') && $request->get('shownStartMonth')
? Carbon::create($request->get('shownStartYear'), $request->get('shownStartMonth'))
: Carbon::today()->startOfMonth();
$output['endDate'] = $request->get('shownEndYear') && $request->get('shownEndMonth')
? Carbon::create($request->get('shownEndYear'), $request->get('shownEndMonth'))->endOfMonth()
: ($isMobile === 'false' ? Carbon::today()->addMonthNoOverflow()->endOfMonth() : Carbon::today()->endOfMonth());
return $output;
}
/**
* @param \App\Model\Shop\Ticket\ShopTicketCatalog $catalog
* @param array<int> $priceGroupIds
*
* @return array<int>
*/
public function getCorrectAllowedConsumerIds(ShopTicketCatalog $catalog, array $priceGroupIds): array
{
$allowedConsumerIds = [];
foreach ($catalog->getTicketConsumerCategories() as $consumerCategory) {
$consumerId = $consumerCategory->getId();
$parentId = $consumerCategory->getParentId();
if (in_array($consumerId, $priceGroupIds)) {
$allowedConsumerIds[] = $consumerId;
unset($priceGroupIds[array_search($consumerId, $priceGroupIds)]);
}
if (in_array($parentId, $priceGroupIds)) {
$allowedConsumerIds[] = $consumerId;
unset($priceGroupIds[array_search($parentId, $priceGroupIds)]);
}
}
return $allowedConsumerIds;
}
/**
* @param Carbon $date
* @param Carbon $endDate
* @param Carbon $minDate
* @param array<mixed> $seasonDates
* @param array<mixed> $pressures
*
* @return array<mixed>
*/
public function buildCalendarData(Carbon $date, Carbon $endDate, Carbon $minDate, array $seasonDates, array $pressures): array
{
$output = [];
while ($date->lessThanOrEqualTo($endDate)) {
$dates = [];
$month = $date->month;
$year = $date->year;
$tmpDate = $date->copy();
while ($date->lessThanOrEqualTo($tmpDate->endOfMonth())) {
$data = [
'date' => $date->toDateTimeLocalString(),
'season' => $this->getSeasonNameForDate($seasonDates, $date),
'disabled' => $date->lessThan($minDate),
];
if ($date->gte($minDate)) {
$data['priceTendency'] = $this->productService->getPriceTendency($date->copy(), $pressures);
}
$dates[] = $data;
$date->addDay();
}
$output[] = [
'month' => $month,
'year' => $year,
'dates' => $dates,
];
}
return $output;
}
/**
* @param PaymentProvider|null $providerBrick
*
* @return PaymentProviderDPG|PaymentProviderDPGPayPal|null
*/
public function getDatatransProviderBrick(?PaymentProvider $providerBrick): ?AbstractData
{
$paymentProvider = null;
if (null !== $providerBrick) {
if ($paymentProviderDPG = $providerBrick->getPaymentProviderDPG()) {
$paymentProvider = $paymentProviderDPG;
}
if ($paymentProviderDPGPayPal = $providerBrick->getPaymentProviderDPGPayPal()) {
$paymentProvider = $paymentProviderDPGPayPal;
}
}
return $paymentProvider;
}
}