<?php
namespace Wbfk\Bundles\Service;
use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Core\System\SystemConfig\SystemConfigService;
use Wbfk\Bundles\Entity\Bundle\BundleProductCollection;
use Wbfk\Bundles\Entity\Bundle\BundleProductEntity;
use Wbfk\Bundles\Helper\IsSalesChannelProductBuyable;
use WbfkExtensions\Core\Checkout\Cart\Delivery\ShippingAndDeliveryInformation\ShippingAndDeliveryDateRange;
use WbfkExtensions\Core\Checkout\Cart\Delivery\ShippingAndDeliveryInformation\ShippingAndDeliveryInformation;
use WbfkExtensions\Core\Checkout\Cart\Delivery\ShippingAndDeliveryInformation\ShippingAndDeliveryInformationCollection;
use WbfkExtensions\Core\Checkout\Cart\Delivery\ShippingAndDeliveryInformationService;
use WbfkExtensions\Core\Content\WbfkProductExtension\WbfkProductExtensionEntity;
class BundleProductDeliveryAndAvailabilityCalculator
{
use IsSalesChannelProductBuyable;
public function __construct(
protected readonly EntityRepository $bundleProductRepository,
protected readonly ShippingAndDeliveryInformationService $expectedProductDeliveryTimeService,
protected readonly SystemConfigService $systemConfigService
) {
}
public function calculatedDeliveryAndAvailability(SalesChannelProductEntity $product, SalesChannelContext $salesChannelContext): void
{
$bundles = $this->getBundles($product->getId(), $salesChannelContext);
if ($bundles->count() === 0) {
return;
}
/** @noinspection PhpDeprecationInspection */
$product->addExtension('wbfk_bundle_product', $bundles);
$this->findDeliveryTimeAndAvailabilityBasedOnChildProducts(
$product,
$bundles,
$salesChannelContext
);
}
public function getBundles(
string $productId,
SalesChannelContext $salesChannelContext
): BundleProductCollection {
$context = $salesChannelContext->getContext();
$criteria = new Criteria();
$criteria->setTitle("Getting bundle products child products data");
$criteria->addFilter(new EqualsFilter('productId', $productId));
$criteria->addAssociation('salesChannelChildProduct.productSuppliers.wbfkSupplier.deliveryTimes');
$productSupplierAssociation = $criteria->getAssociation('salesChannelChildProduct');
$productSupplierAssociation->addFilter(new EqualsFilter('productSuppliers.active', true));
$productSupplierAssociation->addFilter(new EqualsFilter('wbfkSupplier.isActive', true));
/** @var BundleProductCollection|BundleProductEntity[] $bundles */
$bundles = $this->bundleProductRepository->search($criteria, $context)->getEntities();
$this->findChildProductsDeliveryTimes($bundles, $salesChannelContext);
return $bundles;
}
private function findDeliveryTimeAndAvailabilityBasedOnChildProducts(
SalesChannelProductEntity $product,
BundleProductCollection $bundleProducts,
SalesChannelContext $salesChannelContext
): void {
static $sysMaxQuantity = 0;
if ($sysMaxQuantity === 0) {
$sysMaxQuantity = $this->systemConfigService->getInt(
'core.cart.maxQuantity',
$salesChannelContext->getSalesChannel()->getId()
);
}
$minimumDeliveryDate = null;
$maximumDeliveryDate = null;
$fastestDeliveryInformation = null;
// First calculate the max available Quantity from the Parent-Product configuration
// Later we will add the max available Quantity given by the child products
// For reference:
// \Shopware\Core\Content\Product\ProductMaxPurchaseCalculator->calculate()
$calculatedMaxPurchase = (function (SalesChannelProductEntity $product) use ($sysMaxQuantity): int {
$steps = $product->getPurchaseSteps() ?? 1;
$min = $product->getMinPurchase() ?? 1;
$max = $product->getMaxPurchase() ?? $sysMaxQuantity;
// the amount of times the purchase step is fitting in between min and max added to the minimum
return \floor(($max - $min) / $steps) * $steps + $min;
})($product);
// Also calculate the stock. This is only derived by the children products
$stock = PHP_INT_MAX;
$parentCloseOut = $product->getIsCloseout();
$areAllChildrenDeliverable = true;
$areAllChildrenBuyable = true;
foreach ($bundleProducts as $bundleProduct) {
$childProduct = $bundleProduct->getSalesChannelChildProduct();
if (!$this->isBuyable($childProduct)) {
$areAllChildrenBuyable = false;
$calculatedMaxPurchase = 0;
}
/*
* Calculate the Bundle-Stock from the child products
* If we have a stock in our own Product-Extension, use this
* It is set for products using the warehouses system from shopware commerce
*/
$stockByChild = (function (BundleProductEntity $bundleProduct, SalesChannelProductEntity $childProduct) {
$stock = (int)$childProduct->getAvailableStock();
/** @var WbfkProductExtensionEntity $wpe */
if ($wpe = $childProduct->getExtension('WbfkProductExtension')) {
$stock = $wpe->getSuppliersTotalStock();
}
// Also consider if this child is multiple times in the bundle
return (int)($stock / $bundleProduct->getQuantity());
})($bundleProduct, $childProduct);
$stock = min($stock, $stockByChild);
/*
* 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 max purchase quantities on the child, since quantities should be configured on the bundle product itself.
* We might not sell more than 100 cables. But we might sell more than 10 Bundle-Products each containing 10 cables or more.
*/
if ($childProduct->getIsCloseout() || $parentCloseOut) {
$calculatedMaxPurchase = min($calculatedMaxPurchase, $stockByChild);
}
/** @var ShippingAndDeliveryInformationCollection $shippingAndDeliveryInformations */
$shippingAndDeliveryInformations = $childProduct->getExtension('shippingAndDeliveryInformations');
$fastestDeliveryInformation = $shippingAndDeliveryInformations->fastestDeliveryInformation();
if ($fastestDeliveryInformation === null) {
$areAllChildrenDeliverable = false;
continue;
}
/*
* Delivery time of the bundle is given by its slowest component / article
* Get the latest minimum delivery time
* Get the latest maximum delivery time
*/
if ($minimumDeliveryDate === null || $minimumDeliveryDate < $fastestDeliveryInformation->earliestDeliveryDate()) {
$minimumDeliveryDate = $fastestDeliveryInformation->earliestDeliveryDate();
}
if ($maximumDeliveryDate === null || $maximumDeliveryDate < $fastestDeliveryInformation->latestDeliveryDate()) {
$maximumDeliveryDate = $fastestDeliveryInformation->latestDeliveryDate();
}
}
if ($areAllChildrenBuyable === false) {
$product->setCalculatedMaxPurchase(0);
return;
}
$product->setCalculatedMaxPurchase($calculatedMaxPurchase);
$lastProductDeliveryInformation = $fastestDeliveryInformation;
if ($areAllChildrenDeliverable && $lastProductDeliveryInformation) {
$shippingAndDeliveryDateRange = new ShippingAndDeliveryDateRange(
null,
null,
$minimumDeliveryDate,
$maximumDeliveryDate
);
$shippingAndDeliveryInformation = new ShippingAndDeliveryInformation(
$product->getId(),
$lastProductDeliveryInformation->supplierId,
$lastProductDeliveryInformation->supplierDisplayName,
$lastProductDeliveryInformation->supplierPriority,
$stock,
$shippingAndDeliveryDateRange,
$shippingAndDeliveryDateRange
);
$shippingAndDeliveryInformationsForBundle = new ShippingAndDeliveryInformationCollection([$shippingAndDeliveryInformation]);
/** @noinspection PhpDeprecationInspection */
$product->addExtension('shippingAndDeliveryInformations', $shippingAndDeliveryInformationsForBundle);
}
}
private function findChildProductsDeliveryTimes(
BundleProductCollection $bundles,
SalesChannelContext $context
): void {
$bundles->map(function (BundleProductEntity $bundle) use ($context) {
$product = $bundle->getSalesChannelChildProduct();
/** @noinspection PhpDeprecationInspection */
$product->addExtension(
'shippingAndDeliveryInformations',
$this->expectedProductDeliveryTimeService->getShippingAndDeliveryInformationSortedBySupplierPriority(
$product,
$context
)
);
});
}
}