custom/plugins/DreiwmBrandstetterPlugin/src/Subscriber/BrandstetterSubscriber.php line 343

Open in your IDE?
  1. <?php
  2. namespace DreiwmBrandstetterPlugin\Subscriber;
  3. use DateMalformedStringException;
  4. use DateTime;
  5. use DreiwmBrandstetterPlugin\Core\Checkout\Cart\Custom\Error\CustomerTooLateForPackstationError;
  6. use DreiwmBrandstetterPlugin\DreiwmBrandstetterPlugin;
  7. use DreiwmBrandstetterPlugin\Service\DateValidator;
  8. use DreiwmBrandstetterPlugin\Service\PackingStationService;
  9. use Psr\Log\LoggerInterface;
  10. use Shopware\Core\Checkout\Cart\Event\AfterLineItemAddedEvent;
  11. use Shopware\Core\Content\Category\CategoryEvents;
  12. use Shopware\Core\Content\Product\Events\ProductListingCriteriaEvent;
  13. use Shopware\Core\Content\Product\Events\ProductListingResultEvent;
  14. use Shopware\Core\Content\Product\Events\ProductSearchCriteriaEvent;
  15. use Shopware\Core\Content\Product\ProductEntity;
  16. use Shopware\Core\Content\Product\SalesChannel\ProductListResponse;
  17. use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductCollection;
  18. use Shopware\Core\Content\Seo\SeoUrlPlaceholderHandlerInterface;
  19. use Shopware\Core\Framework\Context;
  20. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
  21. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  22. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\ContainsFilter;
  23. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  24. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter;
  25. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\NotFilter;
  26. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\OrFilter;
  27. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\RangeFilter;
  28. use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting;
  29. use Shopware\Core\Framework\Log\LoggerFactory;
  30. use Shopware\Core\System\SalesChannel\Context\AbstractSalesChannelContextFactory;
  31. use Shopware\Core\System\SalesChannel\Context\SalesChannelContextService;
  32. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  33. use Shopware\Storefront\Framework\Routing\RequestTransformer;
  34. use Shopware\Storefront\Framework\Routing\StorefrontResponse;
  35. use Shopware\Storefront\Page\GenericPageLoadedEvent;
  36. use Shopware\Storefront\Page\Navigation\NavigationPage;
  37. use Shopware\Storefront\Page\Navigation\NavigationPageLoadedEvent;
  38. use Shopware\Storefront\Page\Product\Configurator\ProductCombinationFinder;
  39. use Shopware\Storefront\Page\Product\ProductPageLoadedEvent;
  40. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  41. use Symfony\Component\HttpFoundation\RedirectResponse;
  42. use Symfony\Component\HttpFoundation\RequestStack;
  43. use Symfony\Component\HttpKernel\Event\RequestEvent;
  44. use Symfony\Component\HttpKernel\Event\ResponseEvent;
  45. use Symfony\Component\HttpKernel\KernelEvents;
  46. use Symfony\Component\VarDumper\VarDumper;
  47. class BrandstetterSubscriber implements EventSubscriberInterface
  48. {
  49.     private RequestStack $requestStack;
  50.     private SeoUrlPlaceholderHandlerInterface $seoUrlPlaceholderHandler;
  51.     private EntityRepository $freeLockerRepository;
  52.     /**
  53.      * @deprecated tag:v6.5.0 - will be removed
  54.      */
  55.     private ProductCombinationFinder $productCombinationFinder;
  56.     private $salesChannelContextFactory;
  57.     private PackingStationService $packingStationService;
  58.     private DateValidator $dateValidator;
  59.     private LoggerInterface $logger;
  60.     private EntityRepository $productRepository;
  61.     public function __construct(
  62.         RequestStack $requestStack,
  63.         ProductCombinationFinder $productCombinationFinder,
  64.         SeoUrlPlaceholderHandlerInterface $seoUrlPlaceholderHandler,
  65.         AbstractSalesChannelContextFactory $salesChannelContextFactory,
  66.         PackingStationService $packingStationService,
  67.         $freeLockerRepository,
  68.         DateValidator $dateValidator,
  69.         EntityRepository $productRepository,
  70.         LoggerFactory $loggerFactory
  71.     ) {
  72.         $this->requestStack $requestStack;
  73.         $this->productCombinationFinder $productCombinationFinder;
  74.         $this->seoUrlPlaceholderHandler $seoUrlPlaceholderHandler;
  75.         $this->salesChannelContextFactory $salesChannelContextFactory;
  76.         $this->packingStationService $packingStationService;
  77.         $this->freeLockerRepository $freeLockerRepository;
  78.         $this->dateValidator $dateValidator;
  79.         $this->productRepository $productRepository;
  80.         $this->logger $loggerFactory->createRotating('dreiwm_brandstetter_subscriber'7);
  81.     }
  82.     public static function getSubscribedEvents(): array
  83.     {
  84.         return [
  85.             KernelEvents::REQUEST => 'setVariantIdToDisplayFilter',
  86.             KernelEvents::RESPONSE => 'setVariantIdToDisplay',
  87.             AfterLineItemAddedEvent::class => 'addPaketinLocker',
  88.             ProductListingCriteriaEvent::class => ['productListingResult'500],
  89.         ];
  90.     }
  91.     /**
  92.      * Filtere die Produkte nach Verfügbarkeit
  93.      * @param ProductListingCriteriaEvent $event
  94.      * @return void
  95.      * @throws DateMalformedStringException
  96.      */
  97.     public function productListingResult(ProductListingCriteriaEvent $event): void
  98.     {
  99.         $this->frontendProductAssociation($event->getCriteria());
  100.         // reload the page
  101.         // --- 1. Wochentag aus dem aktuellen Datum ermitteln ---
  102.         // hole das aktuelle Datum aus dem Cookie
  103.         $currentDate $this->requestStack->getCurrentRequest()->cookies->get('customerSelectedDate');
  104.         // hole die ausgewählte Versandart aus dem Cookie
  105.         $customerSelectedDeliveryId $this->requestStack->getCurrentRequest()->cookies->get('customerSelectedDeliveryId');
  106.         // Wenn Post ausgewählt ist, dann setze das gewählte Datum auf heute
  107.         if ($customerSelectedDeliveryId == DreiwmBrandstetterPlugin::POST_ID) {
  108. //            return;
  109.             $currentDate = (new DateTime())->format('Y-m-d');
  110.         }
  111.         // wenn kein aktuelles Datum gesetzt ist und die Versandart nicht Post ist, dann breche ab
  112.         if ($currentDate == null && $customerSelectedDeliveryId !== DreiwmBrandstetterPlugin::POST_ID) {
  113.             return;
  114.         }
  115.         // Datum in DateTime umwandeln
  116.         $currentDate = (new DateTime($currentDate))->format('Y-m-d');
  117.         // Filtere Produkte mit Lagerbestand
  118.         $event->getCriteria()->addFilter(new EqualsFilter('product.available'1));
  119.         // Erstelle ein DateTime-Objekt aus dem aktuellen Datum
  120.         $dt = new DateTime($currentDate);
  121.         // Ermittle den englischen Wochentag, z. B. "Mon", "Tue", etc.
  122.         $englishDay $dt->format('D');
  123.         // Mappen des englischen Wochentags auf das deutsche Kürzel
  124.         $dayMap = [
  125.             'Mon' => 'Mo',
  126.             'Tue' => 'Di',
  127.             'Wed' => 'Mi',
  128.             'Thu' => 'Do',
  129.             'Fri' => 'Fr',
  130.             'Sat' => 'Sa',
  131.             'Sun' => 'So'
  132.         ];
  133.         $desiredDay $dayMap[$englishDay] ?? null;
  134.         // --- 2. Gemeinsamer Datum-Filter ("common date filter") ---
  135.         // Dieser Filter deckt die üblichen Fälle ab, wie z.B.:
  136.         // - Produkte, die sowohl 'product_available_from' als auch 'product_available_until' gesetzt haben und in den Zeitraum fallen,
  137.         // - Produkte, bei denen nur eines der Felder gesetzt ist,
  138.         // - Produkte ohne beide Felder (immer verfügbar).
  139.         $commonDateFilter = new OrFilter([
  140.             // Fall 1: Beide Felder vorhanden und aktuelles Datum liegt dazwischen
  141.             new MultiFilter(MultiFilter::CONNECTION_AND, [
  142.                 new RangeFilter('customFields.product_available_from', [RangeFilter::LTE => $currentDate]),
  143.                 new RangeFilter('customFields.product_available_until', [RangeFilter::GTE => $currentDate]),
  144.             ]),
  145.             // Fall 2: Nur 'product_available_from' vorhanden
  146.             new MultiFilter(MultiFilter::CONNECTION_AND, [
  147.                 new RangeFilter('customFields.product_available_from', [RangeFilter::LTE => $currentDate]),
  148.                 new EqualsFilter('customFields.product_available_until'null),
  149.             ]),
  150.             // Fall 3: Nur 'product_available_until' vorhanden
  151.             new MultiFilter(MultiFilter::CONNECTION_AND, [
  152.                 new EqualsFilter('customFields.product_available_from'null),
  153.                 new RangeFilter('customFields.product_available_until', [RangeFilter::GTE => $currentDate]),
  154.             ]),
  155.             // Fall 4: Keine Datumsfelder gesetzt
  156.             new MultiFilter(MultiFilter::CONNECTION_AND, [
  157.                 new EqualsFilter('customFields.product_available_from'null),
  158.                 new EqualsFilter('customFields.product_available_until'null),
  159.             ]),
  160.         ]);
  161.         // --- 3. Filter für Produkte ohne baking_days (Gruppe A) ---
  162.         // Diese Produkte dürfen kein baking_days-Feld gesetzt haben und müssen nur den Datumskriterien genügen.
  163.         $filterWithoutBakingDays = new MultiFilter(MultiFilter::CONNECTION_AND, [
  164.             new EqualsFilter('customFields.baking_days'null),
  165.             $commonDateFilter,
  166.         ]);
  167.         // 4. Gruppe B (mit baking_days)
  168.         $filterWithBakingDays = new MultiFilter(MultiFilter::CONNECTION_AND, [
  169.             // baking_days gesetzt
  170.             new NotFilter(NotFilter::CONNECTION_AND, [
  171.                 new EqualsFilter('customFields.baking_days'null),
  172.             ]),
  173.             // enthält gewähltes Kürzel? (z. B. "Wed")
  174.             new ContainsFilter('customFields.baking_days'$desiredDay),
  175.             // Datumskriterien
  176.             $commonDateFilter,
  177.         ]);
  178.         // 5. Final immer beide Gruppen zusammenführen:
  179.         $finalFilter = new OrFilter([
  180.             $filterWithoutBakingDays,
  181.             $filterWithBakingDays,
  182.         ]);
  183.         $event->getCriteria()->addFilter($finalFilter);
  184.     }
  185.     private function loadProductCustomField(ProductListingCriteriaEvent $eventstring $productNumber): array
  186.     {
  187.         // ganz simple EINE Abfrage nur für Debug:
  188.         $criteria = new Criteria();
  189.         $criteria->addFilter(new EqualsFilter('product.productNumber'$productNumber));
  190.         $criteria->addAssociation('customFields');
  191.         $product $this->productRepository->search($criteria$event->getContext())->first();
  192.         return $product $product->getCustomFields() : [];
  193.     }
  194.     /**
  195.      * Füge die Association für die Frontend-Produktanzeige hinzu
  196.      * @param $criteria
  197.      */
  198.     private function frontendProductAssociation($criteria): void
  199.     {
  200.         $criteria->addAssociation('properties');
  201.         $criteria->addAssociation('properties.group');
  202.     }
  203.     /**
  204.      * Erstelle einen SalesChannelContext
  205.      * @param string $salesChannelId
  206.      * @param string $languageId
  207.      * @return SalesChannelContext $salesChannelContext
  208.      */
  209.     public function createSalesChannelContext(string $salesChannelIdstring $languageId): SalesChannelContext
  210.     {
  211.         return $this->salesChannelContextFactory->create(''$salesChannelId,
  212.             [SalesChannelContextService::LANGUAGE_ID => $languageId]);
  213.     }
  214.     /**
  215.      * Leite auf der Detail-Seite um, wenn eine Variante ausgewählt wurde.
  216.      * @param ResponseEvent $event
  217.      * @return void
  218.      */
  219.     public function setVariantIdToDisplay(ResponseEvent $event): void
  220.     {
  221.         // prüfe ob die Route frontend.detail.page ist
  222.         $currentRoute $event->getRequest()->attributes->get('_route');
  223.         // wenn nicht -> nicht weiterleiten
  224.         if ($currentRoute !== 'frontend.detail.page') {
  225.             return;
  226.         }
  227.         /**@var StorefrontResponse $storefrontResponse */
  228.         $storefrontResponse $event->getResponse();
  229.         if ($storefrontResponse->getStatusCode() !== 200) {
  230.             return;
  231.         }
  232.         // hole die parentId aus der Route
  233.         $parentProductId $storefrontResponse->getData()['page']->getProduct()->getParentId();
  234.         // hole die productId aus der Route
  235.         $currentProductId $storefrontResponse->getData()['page']->getProduct()->getId();
  236.         // wenn parentId == null -> dann ist es ein Produkt ohne Varianten -> nicht weiterleiten
  237.         if ($parentProductId == null) {
  238.             return;
  239.         }
  240.         // SalesChannelContext holen
  241.         $salesChannelContext $event->getRequest()->attributes->get('sw-sales-channel-context');
  242.         // schaue, welche Variante im Cookie gespeichert ist
  243.         $variantIdToDisplay $this->requestStack->getCurrentRequest()->cookies->get('variantIdToDisplay');
  244.         // wenn keine Variante im Cookie gespeichert ist -> nicht weiterleiten
  245.         if ($variantIdToDisplay == null) {
  246.             return;
  247.         }
  248.         // setze die PropertyGroup und die Optionen für den ProductCombinationFinder um die richtige Variante zu finden
  249.         $options = [
  250.             DreiwmBrandstetterPlugin::PROPERTY_GROUP_ID => $variantIdToDisplay,
  251.         ];
  252.         // hole das gefundene Produkt
  253.         $finderResponse $this->productCombinationFinder->find(
  254.             $parentProductId,
  255.             DreiwmBrandstetterPlugin::PROPERTY_GROUP_ID,
  256.             $options ?? [],
  257.             $salesChannelContext
  258.         );
  259.         
  260.         $parentProductId $finderResponse->getVariantId();
  261.         // gefundenes Produkt ist das aktuelle Produkt -> leite nicht um
  262.         if ($parentProductId == $currentProductId) {
  263.             return;
  264.         }
  265.         // redirect to the new URL
  266.         $host $this->requestStack->getCurrentRequest()->attributes->get(RequestTransformer::SALES_CHANNEL_ABSOLUTE_BASE_URL)
  267.             . $this->requestStack->getCurrentRequest()->attributes->get(RequestTransformer::SALES_CHANNEL_BASE_URL);
  268.         $url $this->seoUrlPlaceholderHandler->replace(
  269.             $this->seoUrlPlaceholderHandler->generate(
  270.                 'frontend.detail.page',
  271.                 ['productId' => $parentProductId]
  272.             ),
  273.             $host,
  274.             $salesChannelContext
  275.         );
  276.         $response = new RedirectResponse($url);
  277.         $event->setResponse($response);
  278.     }
  279.     /**
  280.      * Wähle die Variante aus, die angezeigt werden soll. Die ausgewählte Variante wird in einem Cookie gespeichert.
  281.      * Greift auf der Listing-Seite
  282.      * @param RequestEvent $event
  283.      */
  284.     public function setVariantIdToDisplayFilter(RequestEvent $event): void
  285.     {
  286.         // hole das Cookie für die ausgewählte Variante
  287.         $variantIdToDisplay $this->requestStack->getCurrentRequest()->cookies->get('variantIdToDisplay');
  288.         // hole das Cookie für die ausgewählte Versandart
  289.         $customerAvailableShippingMethodPropertyId $this->requestStack->getCurrentRequest()->cookies->get('customerAvailableShippingMethodProperty');
  290.         // wird für das Listing benötigt
  291.         // nur ausführen, wenn die ausgewählte Variante und die ausgewählte Versandart gesetzt sind
  292.         if ($variantIdToDisplay and $customerAvailableShippingMethodPropertyId) {
  293.             // hole die Properties aus der URL
  294.             $lx_properties $event->getRequest()->query->get('properties');
  295.             // wenn Properties gesetzt sind, dann hänge die ausgewählte Variante und die ausgewählte Versandart an
  296.             if ($lx_properties !== null) {
  297.                 $ls_properties $lx_properties '|' $variantIdToDisplay '|' $customerAvailableShippingMethodPropertyId '|';
  298.                 // wenn Properties nicht gesetzt sind, dann hänge nur die ausgewählte Variante und die ausgewählte Versandart an
  299.             } else {
  300.                 $ls_properties $variantIdToDisplay '|' $customerAvailableShippingMethodPropertyId '|';
  301.             }
  302.             if ($ls_properties !== '') {
  303.                 $event->getRequest()->query->set('properties'$ls_properties);
  304.             }
  305.         }
  306.     }
  307.     /**
  308.      * Beim Hinzufügen eines Produkts zum Warenkorb wird geprüft, ob es sich um eine Paketstation handelt.
  309.      * Wenn ja, wird ein Fach reserviert und die Daten in der Datenbank gespeichert
  310.      * @param AfterLineItemAddedEvent $event
  311.      * @return void
  312.      * @throws \Exception
  313.      */
  314.     public function addPaketinLocker(AfterLineItemAddedEvent $event): void
  315.     {
  316. //        dd( $event->getCart()->getDeliveries());
  317.         $shippingMethodId $event->getCart()->getDeliveries()->first()->getShippingMethod()->getId();
  318.         // wenn nicht Paketstation → nichts machen
  319.         if ($shippingMethodId !== DreiwmBrandstetterPlugin::PAKETSTATION_ID) {
  320.             return;
  321.         }
  322.         // hole das Datum aus dem Cookie
  323.         $desiredDate $this->requestStack->getCurrentRequest()->cookies->get('customerSelectedDate');
  324.         // prüfe, ob schon ein Fach reserviert wurde
  325.         $context Context::createDefaultContext();
  326.         $criteria = new Criteria();
  327.         // filtere nach dem Cart-Token und dem Datum
  328.         $criteria->addFilter(new MultiFilter(MultiFilter::CONNECTION_AND, [
  329.             new EqualsFilter('date'$desiredDate),
  330.             new EqualsFilter('cartToken'$event->getCart()->getToken()),
  331.         ]));
  332.         $reservedLocker $this->freeLockerRepository->searchIds($criteria$context)->firstId();
  333.         // wenn schon ein Fach reserviert wurde → nichts machen
  334.         if ($reservedLocker) {
  335.             return;
  336.         }
  337.         // alle Datensätze löschen, die den gleichen Cart-Token haben aber ein anderes Datum
  338.         $deleteCriteria = new Criteria();
  339.         $deleteCriteria->addFilter(new MultiFilter(MultiFilter::CONNECTION_AND, [
  340.             new EqualsFilter('cartToken'$event->getCart()->getToken()),
  341.             new NotFilter(NotFilter::CONNECTION_AND, [
  342.                 new EqualsFilter('date'$desiredDate),
  343.             ]),
  344.         ]));
  345.         $dataToDelete $this->freeLockerRepository->search($deleteCriteria$context);
  346.         if($dataToDelete->count() > 0) {
  347.             // gehe die Einträge durch. Jeder Eintrag enthält die Daten für ein Fach
  348.             foreach ($dataToDelete as $entry) {
  349.                 $pinsToDelete = [$entry->getMerchantPin(), $entry->getCustomerPin()];
  350.                 // lösche die Pins für das Fach
  351.                 $this->packingStationService->deletePins($entry->getBoxId(),$pinsToDelete);
  352.                 // lösche den Eintrag in der Tabelle dreiwm_free_locker
  353.                 $this->freeLockerRepository->delete([['id' => $entry->getId()]], $context);
  354.             }
  355.         }
  356.         // reserviere ein Fach
  357.         $reservedLockerData $this->packingStationService->reservePaketinLockerOnDate(new DateTime($desiredDate),
  358.             $event->getCart()->getToken());
  359.         // wenn kein Fach reserviert werden konnte → Fehlermeldung
  360.         if ($reservedLockerData == null) {
  361.             // Fehlermeldung CustomerTooLateForPackstationError
  362.             $event->getCart()->addErrors(new CustomerTooLateForPackstationError());
  363.             return;
  364.         }
  365.         // speichere das Cart-Token in 3wmfreelocker
  366.         $this->freeLockerRepository->upsert([
  367.             [
  368.                 'cartToken' => $event->getCart()->getToken(),
  369.                 'boxId' => $reservedLockerData['boxId'],
  370.                 'boxName' => $reservedLockerData['boxName'],
  371.                 'merchantPin' => $reservedLockerData['merchantPin'],
  372.                 'customerPin' => $reservedLockerData['customerPin'],
  373.                 'date' => $desiredDate,
  374.                 'freeLocker' => 1,
  375.             ]
  376.         ], $context);
  377.     }
  378. }