<?php
/**
* Created by Elements.at New Media Solutions GmbH
*
*/
namespace App\Service\Shop;
use App\Model\Shop\Merchandise\MerchandiseProduct;
use App\Model\Shop\Ticket\ShopTicketCatalog;
use App\Model\Shop\Ticket\TicketConsumerCategory;
use App\Service\FormService;
use App\Service\LinkGenerator\MerchandiseLinkGenerator;
use App\Service\SiteConfigService;
use Carbon\Carbon;
use Elements\Bundle\RecurringDatesTypeBundle\Templating\RecurringDatesHelper;
use Knp\Component\Pager\PaginatorInterface;
use Pimcore\Log\ApplicationLogger;
use Pimcore\Model\DataObject;
use Pimcore\Model\DataObject\Concrete;
use Pimcore\Model\DataObject\ShopEvent;
use Pimcore\Model\DataObject\ShopNotification;
use Pimcore\Model\DataObject\ShopProductInterest;
use Pimcore\Model\DataObject\SiteConfig;
use Pimcore\Model\DataObject\TicketShopCategory;
use Pimcore\Translation\Translator;
use Symfony\Component\HttpFoundation\Request;
class ProductService
{
private SiteConfig $siteConfig;
public function __construct(
protected MerchandiseLinkGenerator $merchLinkGenerator,
protected Translator $translator,
protected ApplicationLogger $logger,
protected FormService $formService,
protected SmartPricerService $smartPricerService,
protected RecurringDatesHelper $recurringDatesHelper,
SiteConfigService $siteConfigService
) {
$this->siteConfig = $siteConfigService->getSiteConfig();
}
public function getMerchandiseTopTitle(MerchandiseProduct $product): string
{
$toptitle = [];
foreach ($product->getCategories() as $category) {
$toptitle[] = $category->getName();
}
return implode(' | ', $toptitle);
}
/**
* @param MerchandiseProduct $product
*
* @return array<mixed>
*/
public function getMerchandiseVariantOptions(MerchandiseProduct $product, string $currentProductId): array
{
$options = [];
$product = $product->getReference();
foreach ($product->getChildren([DataObject::OBJECT_TYPE_VARIANT]) as $variant) {
if ($variant instanceof MerchandiseProduct) {
$label = $variant->getVariantName();
if (!$variant->getQuota()) {
$label .= ' - ' . $this->translator->trans('shop.detail.not-available');
}
$options[] = [
'label' => $label,
'value' => $variant->getId(),
'selected' => $variant->getId() == $currentProductId,
'disabled' => !$variant->getQuota(),
'attr' => ['data-merchandise-ajax-url' => $this->merchLinkGenerator->generate($variant) . '?reload-variant=' . $variant->getId()],
];
}
}
return $options;
}
public function handleNotificationRequest(string $email, int $productId, string $locale): bool
{
try {
$product = DataObject::getById($productId);
if (!$product instanceof MerchandiseProduct && !$product instanceof DataObject\ShopTicketCatalog && !$product instanceof ShopEvent) {
throw new \Exception('No object with id ' . $productId . ' found');
}
$newNotification = new ShopNotification();
$newNotification->setKey('notification-' . uniqid() . '-' . time());
$path = ($this->siteConfig->getShopNotificationFolder() ?: 'Notifications') . '/' . Carbon::now()->format('Y/m/d');
$newNotification->setParent(DataObject\Service::createFolderByPath($path));
$newNotification->setEmail($email);
$newNotification->setProduct($product);
$newNotification->setLocale($locale);
$newNotification->setSignedForNotification(Carbon::now());
$newNotification->setPublished(true);
$newNotification->save();
return true;
} catch (\Exception $e) {
$this->logger->error('Not able to save ShopNotification: ' . $e->getMessage(), [
'component' => 'ShopNotification',
]);
return false;
}
}
/**
* @param DataObject\Listing\Concrete $listing
* @param Request $request
* @param PaginatorInterface $paginator
* @param String[] $tenants
*
* @return array<mixed>
*/
public function prepareProductData(DataObject\Listing\Concrete $listing, Request $request, PaginatorInterface $paginator, array $tenants = ['b2c']): array
{
$categories = new TicketShopCategory\Listing();
$interests = new ShopProductInterest\Listing();
$returnData['filterLabels'] = [];
$listing->setOrderKey('sort DESC, name ASC', false);
$tenantCondition = [];
foreach ($tenants as $tenant) {
$tenantCondition[] = 'FIND_IN_SET("' . $tenant . '", tenant)';
}
if ($tenantCondition) {
$listing->addConditionParam(implode(' OR ', $tenantCondition));
}
$categories->addConditionParam("o_id IN (SELECT dest_id FROM object_relations_{$listing->getClassId()} WHERE fieldname = 'productCategory' AND src_id IN (" . implode(',', $listing->loadIdList()) . '))');
$interests->addConditionParam("o_id IN (SELECT dest_id FROM object_relations_{$listing->getClassId()} WHERE fieldname = 'interests ' AND src_id IN (" . implode(',', $listing->loadIdList()) . '))');
// FILTER
if ($keyword = $request->get('search')) {
$keyword = htmlspecialchars($keyword);
$listing->addConditionParam('name LIKE :keyword OR shortDescription LIKE :keyword', [
'keyword' => "%$keyword%",
]);
$returnData['filterLabels'][] = [
'label' => $keyword,
'name' => 'search',
'value' => $keyword,
];
}
if ($filterCategoryIds = $request->get('categories')) {
if ($filterCategoryIds = array_filter($filterCategoryIds)) {
array_walk($filterCategoryIds, [$this, 'filterParams']);
$listing->addConditionParam('FIND_IN_SET(productCategory__id, :categoryIds)', [
'categoryIds' => implode(',', $filterCategoryIds),
]);
$returnData['categoryLabels'] = $this->getFilterLabels($filterCategoryIds);
$returnData['filterLabels'] = array_merge($returnData['filterLabels'], $this->getFilterLabels($filterCategoryIds, true, 'categories[]'));
}
}
if ($filterInterestIds = $request->get('interests')) {
if (is_array($filterInterestIds)) {
if ($filterInterestIds = array_filter($filterInterestIds)) {
array_walk($filterInterestIds, [$this, 'filterParams']);
$query = [];
foreach ($filterInterestIds as $interestId) {
$id = intval($interestId);
$query[] = "FIND_IN_SET({$id}, interests)";
}
$listing->addConditionParam(implode(' OR ', $query));
$returnData['interestLabels'] = $this->getFilterLabels($filterInterestIds);
$returnData['filterLabels'] = array_merge($returnData['filterLabels'], $this->getFilterLabels($filterInterestIds, true, 'interests[]'));
}
}
}
$paginator = $paginator->paginate($listing, (int)$request->get('page', 1), 12);
$returnData['products'] = $paginator;
$returnData['categories'] = $categories;
$returnData['interests'] = $interests;
return $returnData;
}
/**
* @param DataObject\Listing\Concrete $listing
* @param Request $request
* @param PaginatorInterface $paginator
*
* @return array<mixed>
*/
public function prepareMerchData(DataObject\Listing\Concrete $listing, Request $request, PaginatorInterface $paginator): array
{
$categories = $this->formService->getRelatedObjects($listing, 'categories');
$returnData['filterLabels'] = [];
// FILTER
if ($keyword = htmlspecialchars($request->get('search'))) {
$listing->addConditionParam('name LIKE :keyword OR description LIKE :keyword', [
'keyword' => "%$keyword%",
]);
$returnData['filterLabels'][] = [
'label' => $keyword,
'name' => 'search',
'value' => $keyword,
];
}
if ($filterCategoryIds = $request->get('categories')) {
if ($filterCategoryIds = array_filter($filterCategoryIds)) {
array_walk($filterCategoryIds, [$this, 'filterParams']);
$query = [];
foreach ($filterCategoryIds as $interestId) {
$query[] = "FIND_IN_SET({$interestId}, categories)";
}
$listing->addConditionParam(implode(' OR ', $query));
$returnData['categoryLabels'] = $this->getFilterLabels($filterCategoryIds);
$returnData['filterLabels'] = array_merge($returnData['filterLabels'], $this->getFilterLabels($filterCategoryIds, true, 'categories[]'));
}
}
$paginator = $paginator->paginate($listing, (int)$request->get('page', 1), 12);
$returnData['products'] = $paginator;
$returnData['categories'] = array_map(function ($c) use ($request) {
return [
'id' => $c->getId(),
'name' => 'categories[]',
'value' => $c->getId(),
'label' => $c->getName(),
'selected' => $request->get('categories') && in_array($c->getId(), $request->get('categories')),
'checked' => $request->get('categories') && in_array($c->getId(), $request->get('categories')),
];
}, $categories);
return $returnData;
}
/**
* @param array<int> $ids
* @param bool $full
* @param string $name
*
* @return array<mixed>
*/
public function getFilterLabels(array $ids, bool $full = false, string $name = ''): array
{
$labels = [];
foreach ($ids as $id) {
$obj = Concrete::getById($id);
if ($obj instanceof TicketShopCategory || $obj instanceof ShopProductInterest) {
if ($full) {
$labels[] = [
'label' => $obj->getName(),
'value' => $obj->getId(),
'name' => $name,
];
} else {
$labels[] = $obj->getName();
}
}
}
return $labels;
}
public function filterParams(mixed &$value): void
{
$value = htmlspecialchars($value);
}
/**
* @param DataObject\Listing\Concrete $listing
* @param String $tenant
*
* @return array<mixed>
*/
public function getAvailableCategoryInterests(DataObject\Listing\Concrete $listing, string $tenant = 'b2c'): array
{
$interests = $categories = $categoriesPerInterest = [];
$listing->addConditionParam("tenant LIKE '%{$tenant}%'");
/** @var ShopTicketCatalog|ShopEvent $item */
foreach ($listing as $item) {
if ($item instanceof ShopTicketCatalog && !$item->isAvailableInDateRange()) {
continue;
}
/** @var ShopProductInterest $interest */
foreach ($item->getInterests() as $interest) {
$category = $item->getProductCategory();
$categoryId = $category->getId();
// add category if not exists yet
if (!key_exists($categoryId, $categories)) {
$categories[$categoryId] = $category;
}
// add interest if not exists yet
if (!key_exists($interest->getId(), $interests)) {
$interests[$interest->getId()] = $interest;
$categoriesPerInterest[$interest->getId()][] = $categoryId;
continue;
}
// add category to interest if not exists yet
if (!in_array($categoryId, $categoriesPerInterest[$interest->getId()])) {
$categoriesPerInterest[$interest->getId()][] = $categoryId;
}
}
}
uasort($categories, [$this, 'orderDescending']);
uasort($interests, [$this, 'orderDescending']);
$returnData['categories'] = $categories;
$returnData['interests'] = $interests;
$returnData['categoriesPerInterest'] = $categoriesPerInterest;
return $returnData;
}
/**
* @param DataObject\Listing\Concrete $listing
* @param int $interestId
* @param int[] $categoryIds
* @param string $tenant
*
* @return array<mixed>
*/
public function getFilteredPriceGroups(DataObject\Listing\Concrete $listing, int $interestId, array $categoryIds, string $tenant = 'b2c'): array
{
$listing->addConditionParam("tenant LIKE '%{$tenant}%'");
$listing->addConditionParam("o_id IN (SELECT src_id FROM object_relations_{$listing->getClassId()} WHERE fieldname = 'interests' AND dest_id = " . $interestId . ')');
$listing->addConditionParam("o_id IN (SELECT src_id FROM object_relations_{$listing->getClassId()} WHERE fieldname = 'productCategory' AND dest_id IN (" . implode(',', $categoryIds) . '))');
$priceGroups = [];
/** @var ShopTicketCatalog|ShopEvent $item */
foreach ($listing as $item) {
if ($item instanceof ShopTicketCatalog && !$item->isAvailableInDateRange()) {
continue;
}
foreach ($item->getTicketConsumerCategories() as $group) {
if (!$group->hasChildren()) { // if $group has no children then it is a child of a Meta Category
$parent = $group->getParent();
if ($parent instanceof TicketConsumerCategory && !key_exists($parent->getId(), $priceGroups)) {
$priceGroups[$parent->getId()] = $parent;
}
}
}
}
uasort($priceGroups, [$this, 'orderDescending']);
return $priceGroups;
}
/**
* @param DataObject\Listing\Concrete $listing
* @param int|null $interestId
* @param int[] $categoryIds
* @param int[] $priceGroupIds
* @param string $tenant
*
* @return DataObject\Listing\Concrete
*/
public function getFilteredCatalogs(
DataObject\Listing\Concrete $listing,
?int $interestId = null,
array $categoryIds = [],
array $priceGroupIds = [],
string $tenant = 'b2c'): DataObject\Listing\Concrete
{
$listing->addConditionParam("tenant LIKE '%{$tenant}%'");
if ($interestId) {
$listing->addConditionParam("o_id IN (SELECT src_id FROM object_relations_{$listing->getClassId()} WHERE fieldname = 'interests' AND dest_id = " . $interestId . ')');
}
if (!empty($categoryIds)) {
$listing->addConditionParam("o_id IN (SELECT src_id FROM object_relations_{$listing->getClassId()} WHERE fieldname = 'productCategory' AND dest_id IN (" . implode(',', $categoryIds) . '))');
}
if (!empty($priceGroupIds)) { // price groups are more complex since it has children and variants
$tempIds = [];
foreach ($priceGroupIds as $priceGroupId) {
$tempIds[] = $priceGroupId; // TODO: maybe find a better algorithm?
$category = TicketConsumerCategory::getById($priceGroupId);
if ($category->hasChildren()) {
foreach ($category->getChildren() as $child) {
if ($child instanceof TicketConsumerCategory && !in_array($child->getId(), $tempIds)) {
$tempIds[] = $child->getId();
}
}
}
}
$listing->addConditionParam("o_id IN (SELECT src_id FROM object_relations_{$listing->getClassId()} WHERE fieldname = 'ticketConsumerCategories' AND dest_id IN (" . implode(',', $tempIds) . '))');
}
return $listing;
}
/**
* Helper function for getting the price tendency. $pressures needs to be fetched from DB and therefore is not in this method (performance reasons).
*
* @param Carbon $date
* @param array<mixed> $pressures
*
* @return string
*/
public function getPriceTendency(Carbon $date, array $pressures): string
{
$priceTendency = 'low';
$demandPressureTimestamp = $date->copy()->startOfDay()->getTimestamp();
if (array_key_exists($demandPressureTimestamp, $pressures) && $pressures[$demandPressureTimestamp] > 0) {
$priceTendency = $this->smartPricerService->getPressureText($pressures[$demandPressureTimestamp]);
}
return $priceTendency;
}
/**
* @param ShopTicketCatalog[] $catalogs
*
* @return array<mixed>
*/
public function getPriceCalculatorData(array $catalogs): array
{
$recurringDates = [];
$useNextDayForAll = true;
$minSelectableDates = 2;
$maxSelectableDates = 1;
$minSelectableDays = $maxSelectableDays = null;
foreach ($catalogs as $catalog) {
$recurringDates = array_merge($this->recurringDatesHelper->getCalculatedDates($catalog, 'getValidityDates'), $recurringDates);
if ($useNextDayForAll && !$catalog->getUseNextDay()) {
$useNextDayForAll = false;
}
if (2 == $minSelectableDates && 1 == $catalog->getMinSelectableDates()) {
$minSelectableDates = 1;
}
if (1 == $maxSelectableDates && 2 == $catalog->getMaxSelectableDates()) {
$maxSelectableDates = 2;
}
if (null == $minSelectableDays || $catalog->getMinSelectableDays() < $minSelectableDays) {
$minSelectableDays = $catalog->getMinSelectableDays();
}
if (null == $maxSelectableDays || $catalog->getMaxSelectableDays() > $maxSelectableDays) {
$maxSelectableDays = $catalog->getMaxSelectableDays();
}
}
return [$recurringDates, $useNextDayForAll, $minSelectableDates, $maxSelectableDates, $minSelectableDays, $maxSelectableDays];
}
/**
* @param Concrete $a
* @param Concrete $b
*
* @return int
*/
private function orderDescending(Concrete $a, Concrete $b)
{
if (method_exists($a, 'getOrder') && method_exists($b, 'getOrder')) {
$orderA = $a->getOrder() ?? 0;
$orderB = $b->getOrder() ?? 0;
return $orderB <=> $orderA;
}
return 0;
}
}