<?php
declare(strict_types=1);
namespace Wbfk\Bundles\Subscriber;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Exception;
use Shopware\Core\Content\Product\ProductEvents;
use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity;
use Shopware\Core\Defaults;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenEvent;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\System\SalesChannel\Entity\SalesChannelEntityLoadedEvent;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Core\System\SystemConfig\SystemConfigService;
use Shopware\Storefront\Page\Product\ProductPageCriteriaEvent;
use Shopware\Storefront\Page\Product\ProductPageLoadedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Wbfk\Bundles\Core\Content\Product\DataAbstractionLayer\BundleFilter;
use Wbfk\Bundles\Core\Content\Product\DataAbstractionLayer\BundleStockUpdater;
use Wbfk\Bundles\Entity\Bundle\BundleProductCollection;
use Wbfk\Bundles\Service\BundleProductDeliveryAndAvailabilityCalculator;
use WbfkExtensions\Core\Content\WbfkProductExtension\WbfkProductExtensionEntity;
use ApplifactionPriceMachine\Service\ProductPriceCalculationService;
class ProductSubscriber implements EventSubscriberInterface
{
public function __construct(
protected readonly BundleProductDeliveryAndAvailabilityCalculator $bundleProductDeliveryAndAvailabilityCalculator,
protected readonly EntityRepository $bundleProductRepository,
protected readonly SystemConfigService $systemConfigService,
protected readonly Connection $connection,
protected readonly BundleFilter $bundleFilter,
protected readonly BundleStockUpdater $bundleStockUpdater,
protected readonly ?ProductPriceCalculationService $productPriceCalculationService
) {
}
public static function getSubscribedEvents(): array
{
return [
'sales_channel.product.loaded' => 'onProductLoaded',
ProductPageCriteriaEvent::class => 'onProductCriteriaLoaded',
ProductPageLoadedEvent::class => 'onProductPageLoaded',
ProductEvents::PRODUCT_WRITTEN_EVENT => [['queueBundleParentPriceCalculation'], ['updateBundleCloseoutStatus']],
];
}
public function getBundles(string $productId, SalesChannelContext $scContext): BundleProductCollection
{
$context = $scContext->getContext();
$criteria = new Criteria();
$criteria->setTitle("Getting bundle products child products data");
$criteria->addAssociation('childProduct');
$criteria->addFilter(new EqualsFilter('productId', $productId));
/** @var BundleProductCollection $bundles */
$bundles = $this->bundleProductRepository->search($criteria, $context)->getEntities();
return $bundles;
}
public function onProductLoaded(SalesChannelEntityLoadedEvent $event)
{
$products = $event->getEntities();
$scContext = $event->getSalesChannelContext();
$sysMaxQuantity = $this->systemConfigService->getInt(
'core.cart.maxQuantity',
$scContext->getSalesChannel()->getId()
);
/** @var SalesChannelProductEntity $scProduct */
foreach ($products as $scProduct) {
$bundles = $this->getBundles($scProduct->getId(), $scContext);
if ($bundles->count() === 0) {
return null;
}
/** @noinspection PhpDeprecationInspection */
$scProduct->addExtension('wbfk_bundle_product', $bundles);
// Calculate the max available Quantity.
$steps = $scProduct->get('purchaseSteps') ?? 1;
$min = $scProduct->get('minPurchase') ?? 1;
$max = $scProduct->get('maxPurchase') ?? $sysMaxQuantity;
// If the max Quantity is set to "0", we ignore it.
// #1124: ANG12299 kann nicht angenommen werden
$max = $max <= 0 ? $sysMaxQuantity : $max;
// the amount of times the purchase step is fitting in between min and max added to the minimum
$max = \floor(($max - $min) / $steps) * $steps + $min;
$maxPurchase = [$max];
// Consider Child-Articles
$parentCloseOut = $scProduct->get('isCloseout');
foreach ($bundles as $bundleProduct) {
$childProduct = $bundleProduct->getChildProduct();
/*
* If the bundle or the child is in clearance sale (closeout) we restrict the bundle by the stock of the child article.
* But we ignore the configured quantities on the child, since quantities can and should be configured on the bundle product.
*/
if ($childProduct->get('isCloseout') || $parentCloseOut) {
$stock = (int)$childProduct->get('availableStock');
/** @var WbfkProductExtensionEntity $wpe */
if ($wpe = $childProduct->getExtension('WbfkProductExtension')) {
$stock = $wpe->getSuppliersTotalStock();
}
// Also consider if this child is multiple times in the bundle
$maxPurchase[] = floor($stock / $bundleProduct->getQuantity());
}
}
$scProduct->setCalculatedMaxPurchase((int)min($maxPurchase));
}
}
public function onProductCriteriaLoaded(ProductPageCriteriaEvent $event): void
{
$event->getCriteria()->addAssociation('wbfk_bundle_product');
}
public function onProductPageLoaded(ProductPageLoadedEvent $event): void
{
$product = $event->getPage()->getProduct();
if (!$product->getExtension('wbfk_bundle_product')->count()) {
return;
}
$this->bundleProductDeliveryAndAvailabilityCalculator->calculatedDeliveryAndAvailability($product, $event->getSalesChannelContext());
}
public function queueBundleParentPriceCalculation(EntityWrittenEvent $event): void
{
// Only queue the price calculation of bundle parents, if the Price Machine Plugin is installed.
if (!$this->productPriceCalculationService) {
return;
}
$bundleParentIdSql = '
SELECT
LOWER(HEX(wbp.product_id)) AS parent_product_id
FROM wbfk_bundle_product wbp
WHERE LOWER(HEX(wbp.child_product_id)) = :childProductId
AND LOWER(HEX(wbp.product_version_id)) = :versionId
AND LOWER(HEX(wbp.child_product_version_id)) = :versionId
GROUP BY wbp.product_id';
foreach ($event->getWriteResults() as $entityWriteResult) {
$bundleChildProductId = $entityWriteResult->getPrimaryKey();
$bundleParentIdResults = $this->connection->fetchAllAssociative($bundleParentIdSql, [
'childProductId' => $bundleChildProductId,
'versionId' => Defaults::LIVE_VERSION,
]);
foreach ($bundleParentIdResults as $bundleParentIdResult) {
$this->productPriceCalculationService->queueProductPriceCalculation($bundleParentIdResult['parent_product_id']);
}
}
}
/**
* @throws Exception
*/
public function updateBundleCloseoutStatus(EntityWrittenEvent $event): void
{
$closeoutUpdatedProductIds = array_reduce($event->getPayloads(), function (array $closeOutUpdatedProductIds, array $payload) {
if (isset($payload['isCloseout'])) {
$closeOutUpdatedProductIds[] = $payload['id'];
}
return $closeOutUpdatedProductIds;
}, []);
$bundleChildrenIds = $this->bundleFilter->filterBundleChildrenIds($closeoutUpdatedProductIds, $event->getContext());
$bundleIds = $this->bundleFilter->findParentIds($bundleChildrenIds, $event->getContext());
$this->bundleStockUpdater->updateBundleStockAndAvailability($bundleIds, $event->getContext());
}
}