<?php declare(strict_types=1);
namespace Shopware\B2B\InStock\BridgePlatform;
use Shopware\B2B\Shop\BridgePlatform\SalesChannelProductExtension;
use Shopware\B2B\StoreFrontAuthentication\Framework\AuthenticationService;
use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity;
use Shopware\Core\Framework\DataAbstractionLayer\Search\AggregationResult\AggregationResultCollection;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\EntitySearchResult;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\AntiJoinFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\Filter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\NotFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\RangeFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\IdSearchResult;
use Shopware\Core\System\SalesChannel\Entity\SalesChannelRepositoryInterface;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Core\System\SystemConfig\SystemConfigService;
use function end;
use function explode;
use function get_class;
use function in_array;
use function min;
class SalesChannelProductRepositoryDecorator implements SalesChannelRepositoryInterface
{
private const MAX_LINE_ITEM_QUANTITY_IN_CART_ROUTE = 'core.cart.maxQuantity';
private const PRODUCT_IN_STOCKS_ID_FIELD = 'product.b2bInStock.id';
private const PRODUCT_IN_STOCKS_AUTH_ID_FIELD = 'product.b2bInStock.authId';
private const PRODUCT_IN_STOCKS_IN_STOCK_FIELD = 'product.b2bInStock.inStock';
private const IN_STOCK_IN_STOCK_FIELD = 'inStock';
private const IN_STOCK_AUTH_ID_FIELD = 'authId';
private const PRODUCT_DEFINITION_STOCK_FIELDS = [
'availableStock',
'stock',
];
/**
* @var SalesChannelRepositoryInterface
*/
private $decorated;
/**
* @var AuthenticationService
*/
private $authenticationService;
/**
* @var SystemConfigService
*/
private $systemConfigService;
public function __construct(
SalesChannelRepositoryInterface $decorated,
AuthenticationService $authenticationService,
SystemConfigService $systemConfigService
) {
$this->decorated = $decorated;
$this->authenticationService = $authenticationService;
$this->systemConfigService = $systemConfigService;
}
public function search(Criteria $criteria, SalesChannelContext $salesChannelContext): EntitySearchResult
{
if (!$this->authenticationService->isB2b()) {
return $this->decorated->search($criteria, $salesChannelContext);
}
$this->prepareCriteriaForB2bInStock($criteria);
$searchResult = $this->decorated->search($criteria, $salesChannelContext);
$searchResult = $this->getUpdatedSearchResult($searchResult, $salesChannelContext);
return $searchResult;
}
public function aggregate(Criteria $criteria, SalesChannelContext $salesChannelContext): AggregationResultCollection
{
return $this->decorated->aggregate($criteria, $salesChannelContext);
}
public function searchIds(Criteria $criteria, SalesChannelContext $salesChannelContext): IdSearchResult
{
if (!$this->authenticationService->isB2b()) {
return $this->decorated->searchIds($criteria, $salesChannelContext);
}
$this->prepareCriteriaForB2bInStock($criteria);
return $this->decorated->searchIds($criteria, $salesChannelContext);
}
/**
* @internal
*/
protected function prepareCriteriaForB2bInStock(Criteria $criteria): void
{
$filters = $criteria->getFilters();
$criteria->addAssociation(SalesChannelProductExtension::B2B_IN_STOCK_EXTENSION_NAME);
$associationCriteria = $criteria->getAssociation(SalesChannelProductExtension::B2B_IN_STOCK_EXTENSION_NAME);
$associationCriteria->addFilter(new EqualsAnyFilter(self::IN_STOCK_AUTH_ID_FIELD, [
$this->authenticationService->getIdentity()->getAuthId()->getValue(),
$this->authenticationService->getIdentity()->getContextAuthId()->getValue(),
]));
$criteria->resetFilters();
foreach ($filters as $filter) {
$criteria->addFilter($this->mapFilter($filter));
if ($associationFilter = $this->getAssociationFilter($filter)) {
$associationCriteria->addFilter($associationFilter);
}
}
}
/**
* @internal
*/
protected function mapFilter(Filter $filter): Filter
{
// ignore inherited MultiFilter
if (in_array(
$class = get_class($filter),
[MultiFilter::class, NotFilter::class, AntiJoinFilter::class],
true
)) {
$queries = [];
foreach ($filter->getQueries() as $query) {
$queries[] = $this->mapFilter($query);
}
return new $class($filter->getOperator(), $queries);
}
if (!$this->isInStockFilter($filter)) {
return $filter;
}
return $this->buildInStockFilter($filter);
}
/**
* @internal
*/
protected function isInStockFilter(Filter $filter): bool
{
return ($filter instanceof EqualsFilter || $filter instanceof RangeFilter || $filter instanceof EqualsAnyFilter)
&& in_array($this->getNecessaryFieldValue($filter->getField()), self::PRODUCT_DEFINITION_STOCK_FIELDS, true);
}
/**
* @internal
*/
private function buildInStockFilter(Filter $filter): Filter
{
return new MultiFilter(
MultiFilter::CONNECTION_OR,
[
new MultiFilter(MultiFilter::CONNECTION_AND, [
$filter,
new EqualsFilter(self::PRODUCT_IN_STOCKS_ID_FIELD, null),
]),
new MultiFilter(MultiFilter::CONNECTION_AND, [
$this->mapInStockFilter(self::PRODUCT_IN_STOCKS_IN_STOCK_FIELD, $filter),
new NotFilter(NotFilter::CONNECTION_AND, [new EqualsFilter(self::PRODUCT_IN_STOCKS_ID_FIELD, null)]),
new EqualsAnyFilter(self::PRODUCT_IN_STOCKS_AUTH_ID_FIELD, [
$this->authenticationService->getIdentity()->getAuthId()->getValue(),
$this->authenticationService->getIdentity()->getContextAuthId()->getValue(),
]),
]),
]
);
}
/**
* @internal
*/
protected function mapInStockFilter(string $fieldName, Filter $filter): Filter
{
if ($filter instanceof RangeFilter) {
return new RangeFilter($fieldName, $filter->getParameters());
}
if ($filter instanceof EqualsFilter) {
return new EqualsFilter($fieldName, $filter->getValue());
}
if ($filter instanceof EqualsAnyFilter) {
return new EqualsAnyFilter($fieldName, $filter->getValue());
}
return $filter;
}
/**
* @internal
*/
protected function getAssociationFilter(Filter $filter): ?Filter
{
// ignore inherited MultiFilter
if (in_array(
$class = get_class($filter),
[MultiFilter::class, NotFilter::class, AntiJoinFilter::class],
true
)) {
$queries = [];
foreach ($filter->getQueries() as $query) {
$associationFilter = $this->getAssociationFilter($query);
if (!$associationFilter) {
continue;
}
$queries[] = $associationFilter;
}
if (!$queries) {
return null;
}
return new $class($filter->getOperator(), $queries);
}
if (!$this->isInStockFilter($filter)) {
return null;
}
return $this->mapInStockFilter(self::IN_STOCK_IN_STOCK_FIELD, $filter);
}
/**
* @internal
*/
protected function getUpdatedSearchResult(
EntitySearchResult $searchResult,
SalesChannelContext $salesChannelContext
): EntitySearchResult {
$maxLineItemQuantityInCart = (int) $this->systemConfigService
->get(self::MAX_LINE_ITEM_QUANTITY_IN_CART_ROUTE, $salesChannelContext->getSalesChannel()->getId());
/** @var SalesChannelProductEntity $product */
foreach ($searchResult->getIterator() as $product) {
/** @var B2bInStockCollection $b2bInStocks */
if (!($b2bInStocks = $product->getExtension(SalesChannelProductExtension::B2B_IN_STOCK_EXTENSION_NAME))
|| $b2bInStocks->count() === 0) {
continue;
}
$inStock = $b2bInStocks->getBestMatchByAuthId(
$this->authenticationService->getIdentity()->getAuthId()
);
$product->setAvailableStock($inStock);
$product->setStock($inStock);
$product->setAvailable((bool) $inStock);
$product->setMaxPurchase(min($inStock, $maxLineItemQuantityInCart));
}
return $searchResult;
}
/**
* @internal
*/
protected function getNecessaryFieldValue(string $field): string
{
$explodedField = explode('.', $field);
return end($explodedField);
}
}