Amasty Resolution Center

Visual Merchandiser issues

Hello! I want to report some issues with Visual Merchandiser module that we faced on our project and propos possible fixes for them. They still may be store related, but it would be good if you check them.

1) We have a big mismatch between the number of active products in the Magento admin compared to some of our categories.

The first issue was with batch generator in Model/Indexer/Category/Product/Action/MakeTempCategoryProductTable.php
It inserts products in batches of 500 items per query
The first SQL query looks like this


Second SQL query like this


So, each batch uses a condition like entity_id > X to fetch the next "page" of results, where X is the last processed entity_id, and since entity_id can be totally random (products aren't sorted by this entity_id), that's why it just skipped a bunch of products.
Additionally, looks like ccp.position is totally skipped.

It can be fixed like this
--- Model/Indexer/Category/Product/Action/MakeTempCategoryProductTable.php
+++ Model/Indexer/Category/Product/Action/MakeTempCategoryProductTable.php
@@ -365,6 +365,7 @@

// 0 - By Position Sort Method
if ($sortMethod === 0) {
+ $productCollection->getSelect()->columns(['position' => new Zend_Db_Expr('MIN(ccp.position)')]);
$productCollection->getSelect()->order('ccp.position ' . Select::SQL_ASC);
}

@@ -376,7 +377,7 @@
'entity_id',
$productCollection->getSelect(),
$this->rangeStep,
- BatchIteratorInterface::UNIQUE_FIELD_ITERATOR
+ BatchIteratorInterface::NON_UNIQUE_FIELD_ITERATOR
);
}


2) Some categories that were enabled only on non default store become empty after some time.
This issue appeared because dynamic category reindexer loaded categories for just 2 stores: base store (0) and default store (1) and since categories wasn’t enabled there, reindexer just skipped these categories.

It can be fixed like this
--- Model/Indexer/DynamicCategory/Action/DoReindex.php
+++ Model/Indexer/DynamicCategory/Action/DoReindex.php
@@ -89,7 +89,8 @@

foreach ($this->storeManager->getStores() as $store) {
$storeId = (int) $store->getId();
- $categories = $this->getCategories((int) $store->getRootCategoryId(), $categoryIds);
+
+ $categories = $this->getCategories((int) $store->getRootCategoryId(), $categoryIds, $storeId);
foreach ($categories as $categoryId => $dynamicConditions) {
$amastyRule->setConditions(null);
$amastyRule->setConditionsSerialized($dynamicConditions);
@@ -120,9 +121,10 @@
/**
* @return array ['category_id' => 'conditions', ...]
*/
- private function getCategories(int $rootCategoryId, ?array $categoryIds): array
+ private function getCategories(int $rootCategoryId, ?array $categoryIds, $storeId): array
{
$categoryCollection = $this->categoryCollectionFactory->create();
+ $categoryCollection->setStoreId($storeId);
$categoryCollection->addAttributeToFilter('amlanding_is_dynamic', 1);
$categoryCollection->addFieldToFilter([
['attribute' => 'path', 'like' => '1/' . $rootCategoryId . '/%'],

--- Model/Indexer/DynamicCategory/Indexer/SyncIndexedData.php
+++ Model/Indexer/DynamicCategory/Indexer/SyncIndexedData.php
@@ -21,6 +21,7 @@
use Magento\Framework\DB\Adapter\DeadlockException;
use Magento\Framework\DB\Ddl\Table;
use Magento\Framework\DB\Select;
+use Magento\Store\Model\StoreManagerInterface;
use Psr\Log\LoggerInterface;

/**
@@ -34,6 +35,11 @@
private $getStoresForRootCategory;

/**
+ * @var StoreManagerInterface
+ */
+ private $storeManager;
+
+ /**
* @var ResourceConnection
*/
private $resourceConnection;
@@ -75,6 +81,7 @@
CategoryIndex $categoryIndex,
SortIds $sortIds,
LoggerInterface $logger,
+ StoreManagerInterface $storeManager,
?DependencyModifier $dependencyModifier = null // TODO move to not optional
) {
$this->getStoresForRootCategory = $getStoresForRootCategory;
@@ -83,6 +90,7 @@
$this->categoryIndex = $categoryIndex;
$this->sortIds = $sortIds;
$this->logger = $logger;
+ $this->storeManager = $storeManager;
$this->dependencyModifier = $dependencyModifier ?? ObjectManager::getInstance()->get(DependencyModifier::class);
}

@@ -101,7 +109,19 @@
}

$isModified = false;
- foreach ($this->getCategories($categoryIds) as $categoryId => $categoryData) {
+
+ $categories = [];
+ foreach ($this->storeManager->getStores() as $store) {
+ $storeId = (int)$store->getId();
+ foreach ($this->getCategories($categoryIds, $storeId) as $categoryId => $categoryData) {
+ if (isset($categories[$categoryId])) {
+ continue;
+ }
+ $categories[$categoryId] = $categoryData;
+ }
+ }
+
+ foreach ($categories as $categoryId => $categoryData) {
if ($this->executeCategory($categoryId, $categoryData)) {
$isModified = true;
}
@@ -161,9 +181,10 @@
/**
* @return array ['category_id' => ['amasty_category_product_sort' => value, 'path' => value], ...]
*/
- private function getCategories(?array $categoryIds): array
+ private function getCategories(?array $categoryIds, $storeId): array
{
$categoryCollection = $this->categoryCollectionFactory->create();
+ $categoryCollection->setStoreId($storeId);
$categoryCollection->addAttributeToFilter('amlanding_is_dynamic', 1);
$categoryCollection->addIsActiveFilter();
if ($categoryIds !== null) {

3) We faced an issue when category become empty after category was saved.
After investigating, it appeared that array of product positions in the session was cleared, and module update category with empty array.

It can be fixed like this
--- Plugin/Catalog/Model/ResourceModel/Category.php
+++ Plugin/Catalog/Model/ResourceModel/Category.php
@@ -109,6 +109,8 @@
}
}

- $category->setPostedProducts($productIds);
+ if (!empty($productIds)) {
+ $category->setPostedProducts($productIds);
+ }
}
}

4) Dynamic categories contained configurable products with 0 price.
If we check class vendor/magento/module-configurable-product/Pricing/Price/LowestPriceOptionsProvider.php, we can find that it loads minimal price for configurable product on PLP, but in fact it doesn't work, because simple products are missing in amasty_merch_dynamic_category_products DB table. This is happening because our child products of configurable parents didn't have the same attributes data, and condition wasn't applied to them.

It can be fixed we preload simple products for configurable products as separate query.
--- Amasty/VisualMerch/Model/DynamicCategory/GetMatchedProductIds.php
+++ Amasty/VisualMerch/Model/DynamicCategory/GetMatchedProductIds.php
@@ -14,6 +14,7 @@
use Amasty\VisualMerch\Model\Rule\Condition\Optimization\ConditionsOptimizerInterface;
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
use Magento\Store\Model\StoreManagerInterface;
+use Magento\Framework\App\ResourceConnection;

class GetMatchedProductIds
{
@@ -32,14 +33,21 @@
*/
private $conditionsOptimizer;

+ /**
+ * @var ResourceConnection
+ */
+ private $resourceConnection;
+
public function __construct(
CollectionFactory $collectionFactory,
StoreManagerInterface $storeManager,
- ConditionsOptimizerInterface $conditionsOptimizer
+ ConditionsOptimizerInterface $conditionsOptimizer,
+ ResourceConnection $resourceConnection
) {
$this->collectionFactory = $collectionFactory;
$this->storeManager = $storeManager;
$this->conditionsOptimizer = $conditionsOptimizer;
+ $this->resourceConnection = $resourceConnection;
}

public function execute(Combine $conditions, int $storeId, ?array $productsFilter = null): array
@@ -68,6 +76,28 @@

$this->storeManager->setCurrentStore($prevStoreId);

- return $productCollection->getAllIds();
+ $configurableProductCollection = clone $productCollection;
+
+ $ids = $productCollection->getAllIds();
+
+ $configurableProductCollection->addFieldToFilter('type_id', 'configurable');
+ $configuredProductIds = $configurableProductCollection->getAllIds();
+
+ $childIds = [];
+ if (!empty($configuredProductIds)) {
+ $connection = $this->resourceConnection->getConnection();
+ $select = $connection->select()
+ ->from(['relation' => 'catalog_product_relation'], ['child_id'])
+ ->join(
+ ['entity' => 'catalog_product_entity'],
+ 'entity.entity_id = relation.child_id',
+ []
+ )
+ ->where('relation.parent_id IN (?)', $configuredProductIds)
+ ->where('entity.type_id = ?', 'simple');
+ $childIds = $connection->fetchCol($select);
+ }
+
+ return array_unique(array_merge($ids, $childIds));
}
}