<?php
namespace DreiwmBrandstetterPlugin\Subscriber;
use DateMalformedStringException;
use DateTime;
use DreiwmBrandstetterPlugin\Core\Checkout\Cart\Custom\Error\CustomerTooLateForPackstationError;
use DreiwmBrandstetterPlugin\DreiwmBrandstetterPlugin;
use DreiwmBrandstetterPlugin\Service\DateValidator;
use DreiwmBrandstetterPlugin\Service\PackingStationService;
use Psr\Log\LoggerInterface;
use Shopware\Core\Checkout\Cart\Event\AfterLineItemAddedEvent;
use Shopware\Core\Content\Category\CategoryEvents;
use Shopware\Core\Content\Product\Events\ProductListingCriteriaEvent;
use Shopware\Core\Content\Product\Events\ProductListingResultEvent;
use Shopware\Core\Content\Product\Events\ProductSearchCriteriaEvent;
use Shopware\Core\Content\Product\ProductEntity;
use Shopware\Core\Content\Product\SalesChannel\ProductListResponse;
use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductCollection;
use Shopware\Core\Content\Seo\SeoUrlPlaceholderHandlerInterface;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\ContainsFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\NotFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\OrFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\RangeFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting;
use Shopware\Core\Framework\Log\LoggerFactory;
use Shopware\Core\System\SalesChannel\Context\AbstractSalesChannelContextFactory;
use Shopware\Core\System\SalesChannel\Context\SalesChannelContextService;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Storefront\Framework\Routing\RequestTransformer;
use Shopware\Storefront\Framework\Routing\StorefrontResponse;
use Shopware\Storefront\Page\GenericPageLoadedEvent;
use Shopware\Storefront\Page\Navigation\NavigationPage;
use Shopware\Storefront\Page\Navigation\NavigationPageLoadedEvent;
use Shopware\Storefront\Page\Product\Configurator\ProductCombinationFinder;
use Shopware\Storefront\Page\Product\ProductPageLoadedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\VarDumper\VarDumper;
class BrandstetterSubscriber implements EventSubscriberInterface
{
private RequestStack $requestStack;
private SeoUrlPlaceholderHandlerInterface $seoUrlPlaceholderHandler;
private EntityRepository $freeLockerRepository;
/**
* @deprecated tag:v6.5.0 - will be removed
*/
private ProductCombinationFinder $productCombinationFinder;
private $salesChannelContextFactory;
private PackingStationService $packingStationService;
private DateValidator $dateValidator;
private LoggerInterface $logger;
private EntityRepository $productRepository;
public function __construct(
RequestStack $requestStack,
ProductCombinationFinder $productCombinationFinder,
SeoUrlPlaceholderHandlerInterface $seoUrlPlaceholderHandler,
AbstractSalesChannelContextFactory $salesChannelContextFactory,
PackingStationService $packingStationService,
$freeLockerRepository,
DateValidator $dateValidator,
EntityRepository $productRepository,
LoggerFactory $loggerFactory
) {
$this->requestStack = $requestStack;
$this->productCombinationFinder = $productCombinationFinder;
$this->seoUrlPlaceholderHandler = $seoUrlPlaceholderHandler;
$this->salesChannelContextFactory = $salesChannelContextFactory;
$this->packingStationService = $packingStationService;
$this->freeLockerRepository = $freeLockerRepository;
$this->dateValidator = $dateValidator;
$this->productRepository = $productRepository;
$this->logger = $loggerFactory->createRotating('dreiwm_brandstetter_subscriber', 7);
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::REQUEST => 'setVariantIdToDisplayFilter',
KernelEvents::RESPONSE => 'setVariantIdToDisplay',
AfterLineItemAddedEvent::class => 'addPaketinLocker',
ProductListingCriteriaEvent::class => ['productListingResult', 500],
];
}
/**
* Filtere die Produkte nach Verfügbarkeit
* @param ProductListingCriteriaEvent $event
* @return void
* @throws DateMalformedStringException
*/
public function productListingResult(ProductListingCriteriaEvent $event): void
{
$this->frontendProductAssociation($event->getCriteria());
// reload the page
// --- 1. Wochentag aus dem aktuellen Datum ermitteln ---
// hole das aktuelle Datum aus dem Cookie
$currentDate = $this->requestStack->getCurrentRequest()->cookies->get('customerSelectedDate');
// hole die ausgewählte Versandart aus dem Cookie
$customerSelectedDeliveryId = $this->requestStack->getCurrentRequest()->cookies->get('customerSelectedDeliveryId');
// Wenn Post ausgewählt ist, dann setze das gewählte Datum auf heute
if ($customerSelectedDeliveryId == DreiwmBrandstetterPlugin::POST_ID) {
// return;
$currentDate = (new DateTime())->format('Y-m-d');
}
// wenn kein aktuelles Datum gesetzt ist und die Versandart nicht Post ist, dann breche ab
if ($currentDate == null && $customerSelectedDeliveryId !== DreiwmBrandstetterPlugin::POST_ID) {
return;
}
// Datum in DateTime umwandeln
$currentDate = (new DateTime($currentDate))->format('Y-m-d');
// Filtere Produkte mit Lagerbestand
$event->getCriteria()->addFilter(new EqualsFilter('product.available', 1));
// Erstelle ein DateTime-Objekt aus dem aktuellen Datum
$dt = new DateTime($currentDate);
// Ermittle den englischen Wochentag, z. B. "Mon", "Tue", etc.
$englishDay = $dt->format('D');
// Mappen des englischen Wochentags auf das deutsche Kürzel
$dayMap = [
'Mon' => 'Mo',
'Tue' => 'Di',
'Wed' => 'Mi',
'Thu' => 'Do',
'Fri' => 'Fr',
'Sat' => 'Sa',
'Sun' => 'So'
];
$desiredDay = $dayMap[$englishDay] ?? null;
// --- 2. Gemeinsamer Datum-Filter ("common date filter") ---
// Dieser Filter deckt die üblichen Fälle ab, wie z.B.:
// - Produkte, die sowohl 'product_available_from' als auch 'product_available_until' gesetzt haben und in den Zeitraum fallen,
// - Produkte, bei denen nur eines der Felder gesetzt ist,
// - Produkte ohne beide Felder (immer verfügbar).
$commonDateFilter = new OrFilter([
// Fall 1: Beide Felder vorhanden und aktuelles Datum liegt dazwischen
new MultiFilter(MultiFilter::CONNECTION_AND, [
new RangeFilter('customFields.product_available_from', [RangeFilter::LTE => $currentDate]),
new RangeFilter('customFields.product_available_until', [RangeFilter::GTE => $currentDate]),
]),
// Fall 2: Nur 'product_available_from' vorhanden
new MultiFilter(MultiFilter::CONNECTION_AND, [
new RangeFilter('customFields.product_available_from', [RangeFilter::LTE => $currentDate]),
new EqualsFilter('customFields.product_available_until', null),
]),
// Fall 3: Nur 'product_available_until' vorhanden
new MultiFilter(MultiFilter::CONNECTION_AND, [
new EqualsFilter('customFields.product_available_from', null),
new RangeFilter('customFields.product_available_until', [RangeFilter::GTE => $currentDate]),
]),
// Fall 4: Keine Datumsfelder gesetzt
new MultiFilter(MultiFilter::CONNECTION_AND, [
new EqualsFilter('customFields.product_available_from', null),
new EqualsFilter('customFields.product_available_until', null),
]),
]);
// --- 3. Filter für Produkte ohne baking_days (Gruppe A) ---
// Diese Produkte dürfen kein baking_days-Feld gesetzt haben und müssen nur den Datumskriterien genügen.
$filterWithoutBakingDays = new MultiFilter(MultiFilter::CONNECTION_AND, [
new EqualsFilter('customFields.baking_days', null),
$commonDateFilter,
]);
// 4. Gruppe B (mit baking_days)
$filterWithBakingDays = new MultiFilter(MultiFilter::CONNECTION_AND, [
// baking_days gesetzt
new NotFilter(NotFilter::CONNECTION_AND, [
new EqualsFilter('customFields.baking_days', null),
]),
// enthält gewähltes Kürzel? (z. B. "Wed")
new ContainsFilter('customFields.baking_days', $desiredDay),
// Datumskriterien
$commonDateFilter,
]);
// 5. Final immer beide Gruppen zusammenführen:
$finalFilter = new OrFilter([
$filterWithoutBakingDays,
$filterWithBakingDays,
]);
$event->getCriteria()->addFilter($finalFilter);
}
private function loadProductCustomField(ProductListingCriteriaEvent $event, string $productNumber): array
{
// ganz simple EINE Abfrage nur für Debug:
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('product.productNumber', $productNumber));
$criteria->addAssociation('customFields');
$product = $this->productRepository->search($criteria, $event->getContext())->first();
return $product ? $product->getCustomFields() : [];
}
/**
* Füge die Association für die Frontend-Produktanzeige hinzu
* @param $criteria
*/
private function frontendProductAssociation($criteria): void
{
$criteria->addAssociation('properties');
$criteria->addAssociation('properties.group');
}
/**
* Erstelle einen SalesChannelContext
* @param string $salesChannelId
* @param string $languageId
* @return SalesChannelContext $salesChannelContext
*/
public function createSalesChannelContext(string $salesChannelId, string $languageId): SalesChannelContext
{
return $this->salesChannelContextFactory->create('', $salesChannelId,
[SalesChannelContextService::LANGUAGE_ID => $languageId]);
}
/**
* Leite auf der Detail-Seite um, wenn eine Variante ausgewählt wurde.
* @param ResponseEvent $event
* @return void
*/
public function setVariantIdToDisplay(ResponseEvent $event): void
{
// prüfe ob die Route frontend.detail.page ist
$currentRoute = $event->getRequest()->attributes->get('_route');
// wenn nicht -> nicht weiterleiten
if ($currentRoute !== 'frontend.detail.page') {
return;
}
/**@var StorefrontResponse $storefrontResponse */
$storefrontResponse = $event->getResponse();
if ($storefrontResponse->getStatusCode() !== 200) {
return;
}
// hole die parentId aus der Route
$parentProductId = $storefrontResponse->getData()['page']->getProduct()->getParentId();
// hole die productId aus der Route
$currentProductId = $storefrontResponse->getData()['page']->getProduct()->getId();
// wenn parentId == null -> dann ist es ein Produkt ohne Varianten -> nicht weiterleiten
if ($parentProductId == null) {
return;
}
// SalesChannelContext holen
$salesChannelContext = $event->getRequest()->attributes->get('sw-sales-channel-context');
// schaue, welche Variante im Cookie gespeichert ist
$variantIdToDisplay = $this->requestStack->getCurrentRequest()->cookies->get('variantIdToDisplay');
// wenn keine Variante im Cookie gespeichert ist -> nicht weiterleiten
if ($variantIdToDisplay == null) {
return;
}
// setze die PropertyGroup und die Optionen für den ProductCombinationFinder um die richtige Variante zu finden
$options = [
DreiwmBrandstetterPlugin::PROPERTY_GROUP_ID => $variantIdToDisplay,
];
// hole das gefundene Produkt
$finderResponse = $this->productCombinationFinder->find(
$parentProductId,
DreiwmBrandstetterPlugin::PROPERTY_GROUP_ID,
$options ?? [],
$salesChannelContext
);
$parentProductId = $finderResponse->getVariantId();
// gefundenes Produkt ist das aktuelle Produkt -> leite nicht um
if ($parentProductId == $currentProductId) {
return;
}
// redirect to the new URL
$host = $this->requestStack->getCurrentRequest()->attributes->get(RequestTransformer::SALES_CHANNEL_ABSOLUTE_BASE_URL)
. $this->requestStack->getCurrentRequest()->attributes->get(RequestTransformer::SALES_CHANNEL_BASE_URL);
$url = $this->seoUrlPlaceholderHandler->replace(
$this->seoUrlPlaceholderHandler->generate(
'frontend.detail.page',
['productId' => $parentProductId]
),
$host,
$salesChannelContext
);
$response = new RedirectResponse($url);
$event->setResponse($response);
}
/**
* Wähle die Variante aus, die angezeigt werden soll. Die ausgewählte Variante wird in einem Cookie gespeichert.
* Greift auf der Listing-Seite
* @param RequestEvent $event
*/
public function setVariantIdToDisplayFilter(RequestEvent $event): void
{
// hole das Cookie für die ausgewählte Variante
$variantIdToDisplay = $this->requestStack->getCurrentRequest()->cookies->get('variantIdToDisplay');
// hole das Cookie für die ausgewählte Versandart
$customerAvailableShippingMethodPropertyId = $this->requestStack->getCurrentRequest()->cookies->get('customerAvailableShippingMethodProperty');
// wird für das Listing benötigt
// nur ausführen, wenn die ausgewählte Variante und die ausgewählte Versandart gesetzt sind
if ($variantIdToDisplay and $customerAvailableShippingMethodPropertyId) {
// hole die Properties aus der URL
$lx_properties = $event->getRequest()->query->get('properties');
// wenn Properties gesetzt sind, dann hänge die ausgewählte Variante und die ausgewählte Versandart an
if ($lx_properties !== null) {
$ls_properties = $lx_properties . '|' . $variantIdToDisplay . '|' . $customerAvailableShippingMethodPropertyId . '|';
// wenn Properties nicht gesetzt sind, dann hänge nur die ausgewählte Variante und die ausgewählte Versandart an
} else {
$ls_properties = $variantIdToDisplay . '|' . $customerAvailableShippingMethodPropertyId . '|';
}
if ($ls_properties !== '') {
$event->getRequest()->query->set('properties', $ls_properties);
}
}
}
/**
* Beim Hinzufügen eines Produkts zum Warenkorb wird geprüft, ob es sich um eine Paketstation handelt.
* Wenn ja, wird ein Fach reserviert und die Daten in der Datenbank gespeichert
* @param AfterLineItemAddedEvent $event
* @return void
* @throws \Exception
*/
public function addPaketinLocker(AfterLineItemAddedEvent $event): void
{
// dd( $event->getCart()->getDeliveries());
$shippingMethodId = $event->getCart()->getDeliveries()->first()->getShippingMethod()->getId();
// wenn nicht Paketstation → nichts machen
if ($shippingMethodId !== DreiwmBrandstetterPlugin::PAKETSTATION_ID) {
return;
}
// hole das Datum aus dem Cookie
$desiredDate = $this->requestStack->getCurrentRequest()->cookies->get('customerSelectedDate');
// prüfe, ob schon ein Fach reserviert wurde
$context = Context::createDefaultContext();
$criteria = new Criteria();
// filtere nach dem Cart-Token und dem Datum
$criteria->addFilter(new MultiFilter(MultiFilter::CONNECTION_AND, [
new EqualsFilter('date', $desiredDate),
new EqualsFilter('cartToken', $event->getCart()->getToken()),
]));
$reservedLocker = $this->freeLockerRepository->searchIds($criteria, $context)->firstId();
// wenn schon ein Fach reserviert wurde → nichts machen
if ($reservedLocker) {
return;
}
// alle Datensätze löschen, die den gleichen Cart-Token haben aber ein anderes Datum
$deleteCriteria = new Criteria();
$deleteCriteria->addFilter(new MultiFilter(MultiFilter::CONNECTION_AND, [
new EqualsFilter('cartToken', $event->getCart()->getToken()),
new NotFilter(NotFilter::CONNECTION_AND, [
new EqualsFilter('date', $desiredDate),
]),
]));
$dataToDelete = $this->freeLockerRepository->search($deleteCriteria, $context);
if($dataToDelete->count() > 0) {
// gehe die Einträge durch. Jeder Eintrag enthält die Daten für ein Fach
foreach ($dataToDelete as $entry) {
$pinsToDelete = [$entry->getMerchantPin(), $entry->getCustomerPin()];
// lösche die Pins für das Fach
$this->packingStationService->deletePins($entry->getBoxId(),$pinsToDelete);
// lösche den Eintrag in der Tabelle dreiwm_free_locker
$this->freeLockerRepository->delete([['id' => $entry->getId()]], $context);
}
}
// reserviere ein Fach
$reservedLockerData = $this->packingStationService->reservePaketinLockerOnDate(new DateTime($desiredDate),
$event->getCart()->getToken());
// wenn kein Fach reserviert werden konnte → Fehlermeldung
if ($reservedLockerData == null) {
// Fehlermeldung CustomerTooLateForPackstationError
$event->getCart()->addErrors(new CustomerTooLateForPackstationError());
return;
}
// speichere das Cart-Token in 3wmfreelocker
$this->freeLockerRepository->upsert([
[
'cartToken' => $event->getCart()->getToken(),
'boxId' => $reservedLockerData['boxId'],
'boxName' => $reservedLockerData['boxName'],
'merchantPin' => $reservedLockerData['merchantPin'],
'customerPin' => $reservedLockerData['customerPin'],
'date' => $desiredDate,
'freeLocker' => 1,
]
], $context);
}
}