From f9470ae1ec0393eef57c32c4aad83f3da38fc1a0 Mon Sep 17 00:00:00 2001 From: Damien Couchez Date: Fri, 29 Aug 2025 11:19:41 +0200 Subject: [PATCH 01/43] MAGE-1407: fixed indexing queue notices display in phtml --- view/adminhtml/templates/queue/status.phtml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/view/adminhtml/templates/queue/status.phtml b/view/adminhtml/templates/queue/status.phtml index 4f260146c..84bf2a9ad 100644 --- a/view/adminhtml/templates/queue/status.phtml +++ b/view/adminhtml/templates/queue/status.phtml @@ -1,6 +1,9 @@
@@ -30,18 +33,18 @@ isQueueActive()): ?>
- escapeHtml(__('Status of the queue : %1.', $block->getQueueRunnerStatus())); ?> -

escapeHtml(__('Last Update : %1.', $block->getLastQueueUpdate())); ?>

+ escapeHtml(__('Status of the queue : %1.', $block->getQueueRunnerStatus()), $allowedTags); ?> +

escapeHtml(__('Last Update : %1.', $block->getLastQueueUpdate()), $allowedTags); ?>

getNotices() ?> -

escapeHtml($notice) ?>

+

escapeHtml($notice, $allowedTags) ?>

-

escapeHtml(__('See how well your indexing queue is performing by viewing your %1.', 'indexing queue run logs')) ?>

-

escapeHtml(__('Access the %1.', 'Queue Archive')) ?>

+

escapeHtml(__('See how well your indexing queue is performing by viewing your %1.', 'indexing queue run logs'), $allowedTags) ?>

+

escapeHtml(__('Access the %1.', 'Queue Archive'), $allowedTags) ?>

From 4c1c3089a3f1003e621147b5fc40cd831b3dbe25 Mon Sep 17 00:00:00 2001 From: Damien Couchez Date: Tue, 19 Aug 2025 13:47:43 +0200 Subject: [PATCH 02/43] MAGE-1110: Fix queue merging --- Model/Job.php | 111 ++++-------- Test/Integration/Indexing/Queue/QueueTest.php | 166 +++++++++--------- 2 files changed, 119 insertions(+), 158 deletions(-) diff --git a/Model/Job.php b/Model/Job.php index 645bef395..c1ac8e680 100644 --- a/Model/Job.php +++ b/Model/Job.php @@ -3,6 +3,12 @@ namespace Algolia\AlgoliaSearch\Model; use Algolia\AlgoliaSearch\Api\Data\JobInterface; +use Magento\Framework\Data\Collection\AbstractDb; +use Magento\Framework\Exception\AlreadyExistsException; +use Magento\Framework\Model\Context; +use Magento\Framework\Model\ResourceModel\AbstractResource; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; /** * @api @@ -26,24 +32,24 @@ class Job extends \Magento\Framework\Model\AbstractModel implements JobInterface { protected $_eventPrefix = 'algoliasearch_queue_job'; - /** @var \Magento\Framework\ObjectManagerInterface */ - protected $objectManager; + /** @var ObjectManagerInterface */ + protected ObjectManagerInterface $objectManager; /** - * @param \Magento\Framework\Model\Context $context - * @param \Magento\Framework\Registry $registry - * @param \Magento\Framework\ObjectManagerInterface $objectManager - * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource - * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection - * @param array $data + * @param Context $context + * @param Registry $registry + * @param ObjectManagerInterface $objectManager + * @param AbstractResource|null $resource + * @param AbstractDb|null $resourceCollection + * @param array $data */ public function __construct( - \Magento\Framework\Model\Context $context, - \Magento\Framework\Registry $registry, - \Magento\Framework\ObjectManagerInterface $objectManager, - ?\Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, - ?\Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [] + Context $context, + Registry $registry, + ObjectManagerInterface $objectManager, + ?AbstractResource $resource = null, + ?AbstractDb $resourceCollection = null, + array $data = [] ) { parent::__construct($context, $registry, $resource, $resourceCollection, $data); @@ -57,13 +63,13 @@ public function __construct( */ protected function _construct() { - $this->_init(\Algolia\AlgoliaSearch\Model\ResourceModel\Job::class); + $this->_init(ResourceModel\Job::class); } /** - * @throws \Magento\Framework\Exception\AlreadyExistsException - * * @return $this + * @throws AlreadyExistsException|\Exception + * */ public function execute() { @@ -97,6 +103,10 @@ public function prepare() if (isset($decodedData['store_id'])) { $this->setStoreId($decodedData['store_id']); } + + if (isset($decodedData['storeId'])) { + $this->setStoreId($decodedData['storeId']); + } } return $this; @@ -124,44 +134,17 @@ public function canMerge(Job $job, $maxJobDataSize) $decodedData = $this->getDecodedData(); - // @todo Remove legacy checks on 3.16.0 - if ((!isset($decodedData['product_ids']) || count($decodedData['product_ids']) <= 0) - && (!isset($decodedData['category_ids']) || count($decodedData['category_ids']) < 0) - && (!isset($decodedData['entity_ids']) || count($decodedData['entity_ids']) < 0) - && (!isset($decodedData['page_ids']) || count($decodedData['page_ids']) < 0)) { + if (!isset($decodedData['entityIds']) || count($decodedData['entityIds']) <= 0) { return false; } $candidateDecodedData = $job->getDecodedData(); - // @todo Remove legacy checks on 3.16.0 - if ((!isset($candidateDecodedData['product_ids']) || count($candidateDecodedData['product_ids']) <= 0) - && (!isset($candidateDecodedData['category_ids']) || count($candidateDecodedData['category_ids']) < 0) - && (!isset($candidateDecodedData['entity_ids']) || count($candidateDecodedData['entity_ids']) < 0) - && (!isset($candidateDecodedData['page_ids']) || count($candidateDecodedData['page_ids']) < 0)) { - return false; - } - - // @todo Remove on 3.16.0 - if (isset($decodedData['product_ids']) - && count($decodedData['product_ids']) + count($candidateDecodedData['product_ids']) > $maxJobDataSize) { - return false; - } - - // @todo Remove on 3.16.0 - if (isset($decodedData['category_ids']) - && count($decodedData['category_ids']) + count($candidateDecodedData['category_ids']) > $maxJobDataSize) { + if (!isset($candidateDecodedData['entityIds']) || count($candidateDecodedData['entityIds']) <= 0) { return false; } - // @todo Remove on 3.16.0 - if (isset($decodedData['page_ids']) - && count($decodedData['page_ids']) + count($candidateDecodedData['page_ids']) > $maxJobDataSize) { - return false; - } - - if (isset($decodedData['entity_ids']) - && count($decodedData['entity_ids']) + count($candidateDecodedData['entity_ids']) > $maxJobDataSize) { + if (count($decodedData['entityIds']) + count($candidateDecodedData['entityIds']) > $maxJobDataSize) { return false; } @@ -185,35 +168,13 @@ public function merge(Job $mergedJob) $dataSize = $this->getDataSize(); - // @todo Remove useless code on 3.16.0 - if (isset($decodedData['product_ids'])) { - $decodedData['product_ids'] = array_unique(array_merge( - $decodedData['product_ids'], - $mergedJobDecodedData['product_ids'] - )); - - $dataSize = count($decodedData['product_ids']); - } elseif (isset($decodedData['category_ids'])) { - $decodedData['category_ids'] = array_unique(array_merge( - $decodedData['category_ids'], - $mergedJobDecodedData['category_ids'] - )); - - $dataSize = count($decodedData['category_ids']); - } elseif (isset($decodedData['page_ids'])) { - $decodedData['page_ids'] = array_unique(array_merge( - $decodedData['page_ids'], - $mergedJobDecodedData['page_ids'] - )); - - $dataSize = count($decodedData['page_ids']); - } elseif (isset($decodedData['entity_ids'])) { - $decodedData['entity_ids'] = array_unique(array_merge( - $decodedData['entity_ids'], - $mergedJobDecodedData['entity_ids'] + if (isset($decodedData['entityIds'])) { + $decodedData['entityIds'] = array_unique(array_merge( + $decodedData['entityIds'], + $mergedJobDecodedData['entityIds'] )); - $dataSize = count($decodedData['entity_ids']); + $dataSize = count($decodedData['entityIds']); } $this->setDecodedData($decodedData); @@ -253,7 +214,7 @@ public function getStatus() /** * @param \Exception $e * - * @throws \Magento\Framework\Exception\AlreadyExistsException + * @throws AlreadyExistsException * * @return Job */ diff --git a/Test/Integration/Indexing/Queue/QueueTest.php b/Test/Integration/Indexing/Queue/QueueTest.php index c851b05cf..eec61344f 100644 --- a/Test/Integration/Indexing/Queue/QueueTest.php +++ b/Test/Integration/Indexing/Queue/QueueTest.php @@ -227,7 +227,7 @@ public function testMerging() 'pid' => null, 'class' => \Algolia\AlgoliaSearch\Service\Category\IndexBuilder::class, 'method' => 'buildIndexList', - 'data' => '{"store_id":"1","entity_ids":["9","22"]}', + 'data' => '{"storeId":"1","entityIds":["9","22"]}', 'max_retries' => 3, 'retries' => 0, 'error_log' => '', @@ -238,7 +238,7 @@ public function testMerging() 'pid' => null, 'class' => \Algolia\AlgoliaSearch\Service\Category\IndexBuilder::class, 'method' => 'buildIndexList', - 'data' => '{"store_id":"2","entity_ids":["9","22"]}', + 'data' => '{"storeId":"2","entityIds":["9","22"]}', 'max_retries' => 3, 'retries' => 0, 'error_log' => '', @@ -249,7 +249,7 @@ public function testMerging() 'pid' => null, 'class' => \Algolia\AlgoliaSearch\Service\Category\IndexBuilder::class, 'method' => 'buildIndexList', - 'data' => '{"store_id":"3","entity_ids":["9","22"]}', + 'data' => '{"storeId":"3","entityIds":["9","22"]}', 'max_retries' => 3, 'retries' => 0, 'error_log' => '', @@ -260,7 +260,7 @@ public function testMerging() 'pid' => null, 'class' => \Algolia\AlgoliaSearch\Service\Product\IndexBuilder::class, 'method' => 'buildIndexList', - 'data' => '{"store_id":"1","entity_ids":["448"]}', + 'data' => '{"storeId":"1","entityIds":["448"]}', 'max_retries' => 3, 'retries' => 0, 'error_log' => '', @@ -271,7 +271,7 @@ public function testMerging() 'pid' => null, 'class' => \Algolia\AlgoliaSearch\Service\Product\IndexBuilder::class, 'method' => 'buildIndexList', - 'data' => '{"store_id":"2","entity_ids":["448"]}', + 'data' => '{"storeId":"2","entityIds":["448"]}', 'max_retries' => 3, 'retries' => 0, 'error_log' => '', @@ -282,7 +282,7 @@ public function testMerging() 'pid' => null, 'class' => \Algolia\AlgoliaSearch\Service\Product\IndexBuilder::class, 'method' => 'buildIndexList', - 'data' => '{"store_id":"3","entity_ids":["448"]}', + 'data' => '{"storeId":"3","entityIds":["448"]}', 'max_retries' => 3, 'retries' => 0, 'error_log' => '', @@ -293,7 +293,7 @@ public function testMerging() 'pid' => null, 'class' => \Algolia\AlgoliaSearch\Service\Category\IndexBuilder::class, 'method' => 'buildIndexList', - 'data' => '{"store_id":"1","entity_ids":["40"]}', + 'data' => '{"storeId":"1","entityIds":["40"]}', 'max_retries' => 3, 'retries' => 0, 'error_log' => '', @@ -304,7 +304,7 @@ public function testMerging() 'pid' => null, 'class' => \Algolia\AlgoliaSearch\Service\Category\IndexBuilder::class, 'method' => 'buildIndexList', - 'data' => '{"store_id":"2","entity_ids":["40"]}', + 'data' => '{"storeId":"2","entityIds":["40"]}', 'max_retries' => 3, 'retries' => 0, 'error_log' => '', @@ -315,7 +315,7 @@ public function testMerging() 'pid' => null, 'class' => \Algolia\AlgoliaSearch\Service\Category\IndexBuilder::class, 'method' => 'buildIndexList', - 'data' => '{"store_id":"3","entity_ids":["40"]}', + 'data' => '{"storeId":"3","entityIds":["40"]}', 'max_retries' => 3, 'retries' => 0, 'error_log' => '', @@ -326,7 +326,7 @@ public function testMerging() 'pid' => null, 'class' => \Algolia\AlgoliaSearch\Service\Product\IndexBuilder::class, 'method' => 'buildIndexList', - 'data' => '{"store_id":"1","entity_ids":["405"]}', + 'data' => '{"storeId":"1","entityIds":["405"]}', 'max_retries' => 3, 'retries' => 0, 'error_log' => '', @@ -337,7 +337,7 @@ public function testMerging() 'pid' => null, 'class' => \Algolia\AlgoliaSearch\Service\Product\IndexBuilder::class, 'method' => 'buildIndexList', - 'data' => '{"store_id":"2","entity_ids":["405"]}', + 'data' => '{"storeId":"2","entityIds":["405"]}', 'max_retries' => 3, 'retries' => 0, 'error_log' => '', @@ -348,7 +348,7 @@ public function testMerging() 'pid' => null, 'class' => \Algolia\AlgoliaSearch\Service\Product\IndexBuilder::class, 'method' => 'buildIndexList', - 'data' => '{"store_id":"3","entity_ids":["405"]}', + 'data' => '{"storeId":"3","entityIds":["405"]}', 'max_retries' => 3, 'retries' => 0, 'error_log' => '', @@ -370,7 +370,7 @@ public function testMerging() 'pid' => null, 'class' => \Algolia\AlgoliaSearch\Service\Category\IndexBuilder::class, 'method' => 'buildIndexList', - 'data' => '{"store_id":"1","entity_ids":["9","22"]}', + 'data' => '{"storeId":"1","entityIds":["9","22"]}', 'max_retries' => '3', 'retries' => '0', 'error_log' => '', @@ -379,8 +379,8 @@ public function testMerging() 'store_id' => '1', 'is_full_reindex' => '0', 'decoded_data' => [ - 'store_id' => '1', - 'entity_ids' => [ + 'storeId' => '1', + 'entityIds' => [ 0 => '9', 1 => '22', 2 => '40', @@ -400,7 +400,7 @@ public function testMerging() 'pid' => null, 'class' => \Algolia\AlgoliaSearch\Service\Product\IndexBuilder::class, 'method' => 'buildIndexList', - 'data' => '{"store_id":"1","entity_ids":["448"]}', + 'data' => '{"storeId":"1","entityIds":["448"]}', 'max_retries' => '3', 'retries' => '0', 'error_log' => '', @@ -409,8 +409,8 @@ public function testMerging() 'store_id' => '1', 'is_full_reindex' => '0', 'decoded_data' => [ - 'store_id' => '1', - 'entity_ids' => [ + 'storeId' => '1', + 'entityIds' => [ 0 => '448', 1 => '405', ], @@ -434,7 +434,7 @@ public function testMergingWithStaticMethods() 'pid' => null, 'class' => \Algolia\AlgoliaSearch\Service\Category\IndexBuilder::class, 'method' => 'buildIndexList', - 'data' => '{"store_id":"1","entity_ids":["9","22"]}', + 'data' => '{"storeId":"1","entityIds":["9","22"]}', 'max_retries' => 3, 'retries' => 0, 'error_log' => '', @@ -444,7 +444,7 @@ public function testMergingWithStaticMethods() 'pid' => null, 'class' => \Algolia\AlgoliaSearch\Service\Category\IndexBuilder::class, 'method' => 'buildIndexList', - 'data' => '{"store_id":"2","entity_ids":["9","22"]}', + 'data' => '{"storeId":"2","entityIds":["9","22"]}', 'max_retries' => 3, 'retries' => 0, 'error_log' => '', @@ -454,7 +454,7 @@ public function testMergingWithStaticMethods() 'pid' => null, 'class' => \Algolia\AlgoliaSearch\Service\Category\IndexBuilder::class, 'method' => 'buildIndexList', - 'data' => '{"store_id":"3","entity_ids":["9","22"]}', + 'data' => '{"storeId":"3","entityIds":["9","22"]}', 'max_retries' => 3, 'retries' => 0, 'error_log' => '', @@ -464,7 +464,7 @@ public function testMergingWithStaticMethods() 'pid' => null, 'class' => \Algolia\AlgoliaSearch\Helper\Data::class, 'method' => 'deleteObjects', - 'data' => '{"store_id":"1","product_ids":["448"]}', + 'data' => '{"storeId":"1","product_ids":["448"]}', 'max_retries' => 3, 'retries' => 0, 'error_log' => '', @@ -474,7 +474,7 @@ public function testMergingWithStaticMethods() 'pid' => null, 'class' => \Algolia\AlgoliaSearch\Service\Product\IndexBuilder::class, 'method' => 'buildIndexList', - 'data' => '{"store_id":"2","entity_ids":["448"]}', + 'data' => '{"storeId":"2","entityIds":["448"]}', 'max_retries' => 3, 'retries' => 0, 'error_log' => '', @@ -484,7 +484,7 @@ public function testMergingWithStaticMethods() 'pid' => null, 'class' => \Algolia\AlgoliaSearch\Service\Product\IndexBuilder::class, 'method' => 'buildIndexList', - 'data' => '{"store_id":"3","entity_ids":["448"]}', + 'data' => '{"storeId":"3","entityIds":["448"]}', 'max_retries' => 3, 'retries' => 0, 'error_log' => '', @@ -494,7 +494,7 @@ public function testMergingWithStaticMethods() 'pid' => null, 'class' => IndicesConfigurator::class, 'method' => 'saveConfigurationToAlgolia', - 'data' => '{"store_id":"1","entity_ids":["40"]}', + 'data' => '{"storeId":"1"}', 'max_retries' => 3, 'retries' => 0, 'error_log' => '', @@ -504,7 +504,7 @@ public function testMergingWithStaticMethods() 'pid' => null, 'class' => \Algolia\AlgoliaSearch\Service\Category\IndexBuilder::class, 'method' => 'buildIndexList', - 'data' => '{"store_id":"2","entity_ids":["40"]}', + 'data' => '{"storeId":"2","entityIds":["40"]}', 'max_retries' => 3, 'retries' => 0, 'error_log' => '', @@ -514,7 +514,7 @@ public function testMergingWithStaticMethods() 'pid' => null, 'class' => \Algolia\AlgoliaSearch\Model\IndexMover::class, 'method' => 'moveIndexWithSetSettings', - 'data' => '{"store_id":"3","entity_ids":["40"]}', + 'data' => '{"storeId":"3"}', 'max_retries' => 3, 'retries' => 0, 'error_log' => '', @@ -524,7 +524,7 @@ public function testMergingWithStaticMethods() 'pid' => null, 'class' => \Algolia\AlgoliaSearch\Service\Product\IndexBuilder::class, 'method' => 'buildIndexList', - 'data' => '{"store_id":"1","entity_ids":["405"]}', + 'data' => '{"storeId":"1","entityIds":["405"]}', 'max_retries' => 3, 'retries' => 0, 'error_log' => '', @@ -534,7 +534,7 @@ public function testMergingWithStaticMethods() 'pid' => null, 'class' => \Algolia\AlgoliaSearch\Model\IndexMover::class, 'method' => 'moveIndexWithSetSettings', - 'data' => '{"store_id":"2","entity_ids":["405"]}', + 'data' => '{"storeId":"2"}', 'max_retries' => 3, 'retries' => 0, 'error_log' => '', @@ -544,7 +544,7 @@ public function testMergingWithStaticMethods() 'pid' => null, 'class' => \Algolia\AlgoliaSearch\Service\Product\IndexBuilder::class, 'method' => 'buildIndexList', - 'data' => '{"store_id":"3","entity_ids":["405"]}', + 'data' => '{"storeId":"3","entityIds":["405"]}', 'max_retries' => 3, 'retries' => 0, 'error_log' => '', @@ -583,9 +583,9 @@ public function testGetJobs() 'job_id' => 1, 'created' => '2017-09-01 12:00:00', 'pid' => null, - 'class' => \Algolia\AlgoliaSearch\Helper\Data::class, - 'method' => 'rebuildStoreCategoryIndex', - 'data' => '{"store_id":"1","category_ids":["9","22"]}', + 'class' => \Algolia\AlgoliaSearch\Service\Category\IndexBuilder::class, + 'method' => 'buildIndexList', + 'data' => '{"storeId":"1","entityIds":["9","22"]}', 'max_retries' => 3, 'retries' => 0, 'error_log' => '', @@ -594,9 +594,9 @@ public function testGetJobs() 'job_id' => 2, 'created' => '2017-09-01 12:00:00', 'pid' => null, - 'class' => \Algolia\AlgoliaSearch\Helper\Data::class, - 'method' => 'rebuildStoreCategoryIndex', - 'data' => '{"store_id":"2","category_ids":["9","22"]}', + 'class' => \Algolia\AlgoliaSearch\Service\Category\IndexBuilder::class, + 'method' => 'buildIndexList', + 'data' => '{"storeId":"2","entityIds":["9","22"]}', 'max_retries' => 3, 'retries' => 0, 'error_log' => '', @@ -605,9 +605,9 @@ public function testGetJobs() 'job_id' => 3, 'created' => '2017-09-01 12:00:00', 'pid' => null, - 'class' => \Algolia\AlgoliaSearch\Helper\Data::class, - 'method' => 'rebuildStoreCategoryIndex', - 'data' => '{"store_id":"3","category_ids":["9","22"]}', + 'class' => \Algolia\AlgoliaSearch\Service\Category\IndexBuilder::class, + 'method' => 'buildIndexList', + 'data' => '{"storeId":"3","entityIds":["9","22"]}', 'max_retries' => 3, 'retries' => 0, 'error_log' => '', @@ -616,9 +616,9 @@ public function testGetJobs() 'job_id' => 4, 'created' => '2017-09-01 12:00:00', 'pid' => null, - 'class' => \Algolia\AlgoliaSearch\Helper\Data::class, - 'method' => 'rebuildStoreProductIndex', - 'data' => '{"store_id":"1","product_ids":["448"]}', + 'class' => \Algolia\AlgoliaSearch\Service\Product\IndexBuilder::class, + 'method' => 'buildIndexList', + 'data' => '{"storeId":"1","entityIds":["448"]}', 'max_retries' => 3, 'retries' => 0, 'error_log' => '', @@ -627,9 +627,9 @@ public function testGetJobs() 'job_id' => 5, 'created' => '2017-09-01 12:00:00', 'pid' => null, - 'class' => \Algolia\AlgoliaSearch\Helper\Data::class, - 'method' => 'rebuildStoreProductIndex', - 'data' => '{"store_id":"2","product_ids":["448"]}', + 'class' => \Algolia\AlgoliaSearch\Service\Product\IndexBuilder::class, + 'method' => 'buildIndexList', + 'data' => '{"storeId":"2","entityIds":["448"]}', 'max_retries' => 3, 'retries' => 0, 'error_log' => '', @@ -638,9 +638,9 @@ public function testGetJobs() 'job_id' => 6, 'created' => '2017-09-01 12:00:00', 'pid' => null, - 'class' => \Algolia\AlgoliaSearch\Helper\Data::class, - 'method' => 'rebuildStoreProductIndex', - 'data' => '{"store_id":"3","product_ids":["448"]}', + 'class' => \Algolia\AlgoliaSearch\Service\Product\IndexBuilder::class, + 'method' => 'buildIndexList', + 'data' => '{"storeId":"3","entityIds":["448"]}', 'max_retries' => 3, 'retries' => 0, 'error_log' => '', @@ -649,9 +649,9 @@ public function testGetJobs() 'job_id' => 7, 'created' => '2017-09-01 12:00:00', 'pid' => null, - 'class' => \Algolia\AlgoliaSearch\Helper\Data::class, - 'method' => 'rebuildStoreCategoryIndex', - 'data' => '{"store_id":"1","category_ids":["40"]}', + 'class' => \Algolia\AlgoliaSearch\Service\Category\IndexBuilder::class, + 'method' => 'buildIndexList', + 'data' => '{"storeId":"1","entityIds":["40"]}', 'max_retries' => 3, 'retries' => 0, 'error_log' => '', @@ -660,9 +660,9 @@ public function testGetJobs() 'job_id' => 8, 'created' => '2017-09-01 12:00:00', 'pid' => null, - 'class' => \Algolia\AlgoliaSearch\Helper\Data::class, - 'method' => 'rebuildStoreCategoryIndex', - 'data' => '{"store_id":"2","category_ids":["40"]}', + 'class' => \Algolia\AlgoliaSearch\Service\Category\IndexBuilder::class, + 'method' => 'buildIndexList', + 'data' => '{"storeId":"2","entityIds":["40"]}', 'max_retries' => 3, 'retries' => 0, 'error_log' => '', @@ -671,9 +671,9 @@ public function testGetJobs() 'job_id' => 9, 'created' => '2017-09-01 12:00:00', 'pid' => null, - 'class' => \Algolia\AlgoliaSearch\Helper\Data::class, - 'method' => 'rebuildStoreCategoryIndex', - 'data' => '{"store_id":"3","category_ids":["40"]}', + 'class' => \Algolia\AlgoliaSearch\Service\Category\IndexBuilder::class, + 'method' => 'buildIndexList', + 'data' => '{"storeId":"3","entityIds":["40"]}', 'max_retries' => 3, 'retries' => 0, 'error_log' => '', @@ -682,9 +682,9 @@ public function testGetJobs() 'job_id' => 10, 'created' => '2017-09-01 12:00:00', 'pid' => null, - 'class' => \Algolia\AlgoliaSearch\Helper\Data::class, - 'method' => 'rebuildStoreProductIndex', - 'data' => '{"store_id":"1","product_ids":["405"]}', + 'class' => \Algolia\AlgoliaSearch\Service\Product\IndexBuilder::class, + 'method' => 'buildIndexList', + 'data' => '{"storeId":"1","entityIds":["405"]}', 'max_retries' => 3, 'retries' => 0, 'error_log' => '', @@ -693,9 +693,9 @@ public function testGetJobs() 'job_id' => 11, 'created' => '2017-09-01 12:00:00', 'pid' => null, - 'class' => \Algolia\AlgoliaSearch\Helper\Data::class, - 'method' => 'rebuildStoreProductIndex', - 'data' => '{"store_id":"2","product_ids":["405"]}', + 'class' => \Algolia\AlgoliaSearch\Service\Product\IndexBuilder::class, + 'method' => 'buildIndexList', + 'data' => '{"storeId":"2","entityIds":["405"]}', 'max_retries' => 3, 'retries' => 0, 'error_log' => '', @@ -704,9 +704,9 @@ public function testGetJobs() 'job_id' => 12, 'created' => '2017-09-01 12:00:00', 'pid' => null, - 'class' => \Algolia\AlgoliaSearch\Helper\Data::class, - 'method' => 'rebuildStoreProductIndex', - 'data' => '{"store_id":"3","product_ids":["405"]}', + 'class' => \Algolia\AlgoliaSearch\Service\Product\IndexBuilder::class, + 'method' => 'buildIndexList', + 'data' => '{"storeId":"3","entityIds":["405"]}', 'max_retries' => 3, 'retries' => 0, 'error_log' => '', @@ -723,14 +723,14 @@ public function testGetJobs() $expectedFirstJob = [ 'job_id' => '1', 'pid' => $pid, - 'class' => \Algolia\AlgoliaSearch\Helper\Data::class, - 'method' => 'rebuildStoreCategoryIndex', - 'data' => '{"store_id":"1","category_ids":["9","22"]}', + 'class' => \Algolia\AlgoliaSearch\Service\Category\IndexBuilder::class, + 'method' => 'buildIndexList', + 'data' => '{"storeId":"1","entityIds":["9","22"]}', 'merged_ids' => ['1', '7'], 'store_id' => '1', 'decoded_data' => [ - 'store_id' => '1', - 'category_ids' => [ + 'storeId' => '1', + 'entityIds' => [ 0 => '9', 1 => '22', 2 => '40', @@ -741,14 +741,14 @@ public function testGetJobs() $expectedLastJob = [ 'job_id' => '6', 'pid' => $pid, - 'class' => \Algolia\AlgoliaSearch\Helper\Data::class, - 'method' => 'rebuildStoreProductIndex', - 'data' => '{"store_id":"3","product_ids":["448"]}', + 'class' => \Algolia\AlgoliaSearch\Service\Product\IndexBuilder::class, + 'method' => 'buildIndexList', + 'data' => '{"storeId":"3","entityIds":["448"]}', 'merged_ids' => ['6', '12'], 'store_id' => '3', 'decoded_data' => [ - 'store_id' => '3', - 'product_ids' => [ + 'storeId' => '3', + 'entityIds' => [ 0 => '448', 1 => '405', ], @@ -799,8 +799,8 @@ public function testHugeJob() $this->connection->query('TRUNCATE TABLE algoliasearch_queue'); $this->connection->query('INSERT INTO `algoliasearch_queue` (`job_id`, `pid`, `class`, `method`, `data`, `max_retries`, `retries`, `error_log`, `data_size`) VALUES - (1, NULL, \'class\', \'rebuildStoreProductIndex\', \'{"store_id":"1","product_ids":' . $jsonProductIds . '}\', 3, 0, \'\', 5000), - (2, NULL, \'class\', \'rebuildStoreProductIndex\', \'{"store_id":"2","product_ids":["9","22"]}\', 3, 0, \'\', 2);'); + (1, NULL, \'class\', \'buildIndexList\', \'{"storeId":"1","entityIds":' . $jsonProductIds . '}\', 3, 0, \'\', 5000), + (2, NULL, \'class\', \'buildIndexList\', \'{"storeId":"2","entityIds":["9","22"]}\', 3, 0, \'\', 2);'); $pid = getmypid(); /** @var Job[] $jobs */ @@ -810,7 +810,7 @@ public function testHugeJob() $job = reset($jobs); $this->assertEquals(5000, $job->getDataSize()); - $this->assertEquals(5000, count($job->getDecodedData()['product_ids'])); + $this->assertEquals(5000, count($job->getDecodedData()['entityIds'])); $dbJobs = $this->connection->query('SELECT * FROM algoliasearch_queue')->fetchAll(); @@ -837,8 +837,8 @@ public function testMaxSingleJobSize() $this->connection->query('TRUNCATE TABLE algoliasearch_queue'); $this->connection->query('INSERT INTO `algoliasearch_queue` (`job_id`, `pid`, `class`, `method`, `data`, `max_retries`, `retries`, `error_log`, `data_size`) VALUES - (1, NULL, \'class\', \'rebuildStoreProductIndex\', \'{"store_id":"1","product_ids":' . $jsonProductIds . '}\', 3, 0, \'\', 99), - (2, NULL, \'class\', \'rebuildStoreProductIndex\', \'{"store_id":"2","product_ids":["9","22"]}\', 3, 0, \'\', 2);'); + (1, NULL, \'class\', \'buildIndexList\', \'{"storeId":"1","entityIds":' . $jsonProductIds . '}\', 3, 0, \'\', 99), + (2, NULL, \'class\', \'buildIndexList\', \'{"storeId":"2","entityIds":["9","22"]}\', 3, 0, \'\', 2);'); $pid = getmypid(); @@ -851,10 +851,10 @@ public function testMaxSingleJobSize() $lastJob = end($jobs); $this->assertEquals(99, $firstJob->getDataSize()); - $this->assertEquals(99, count($firstJob->getDecodedData()['product_ids'])); + $this->assertEquals(99, count($firstJob->getDecodedData()['entityIds'])); $this->assertEquals(2, $lastJob->getDataSize()); - $this->assertEquals(2, count($lastJob->getDecodedData()['product_ids'])); + $this->assertEquals(2, count($lastJob->getDecodedData()['entityIds'])); $dbJobs = $this->connection->query('SELECT * FROM algoliasearch_queue')->fetchAll(); From bf9d0db2b57ff72a572e750e25f25f470772664f Mon Sep 17 00:00:00 2001 From: Damien Couchez Date: Wed, 20 Aug 2025 16:37:33 +0200 Subject: [PATCH 03/43] MAGE-1396: fix fetchJobs method --- Model/Queue.php | 68 +++++++++++++++++++++++++------------------------ 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/Model/Queue.php b/Model/Queue.php index ca8e54f82..697cd5d4f 100644 --- a/Model/Queue.php +++ b/Model/Queue.php @@ -114,7 +114,7 @@ public function __construct( * @param int $dataSize * @param bool $isFullReindex */ - public function addToQueue($className, $method, array $data, $dataSize = 1, $isFullReindex = false) + public function addToQueue(string $className, string $method, array $data, int $dataSize = 1, bool $isFullReindex = false): void { if (is_object($className)) { $className = $className::class; @@ -146,7 +146,7 @@ public function addToQueue($className, $method, array $data, $dataSize = 1, $isF * * @return float|null */ - public function getAverageProcessingTime() + public function getAverageProcessingTime(): ?float { $select = $this->db->select() ->from($this->logTable, ['number_of_runs' => 'COUNT(duration)', 'average_time' => 'AVG(duration)']) @@ -165,7 +165,7 @@ public function getAverageProcessingTime() * * @throws Exception */ - public function runCron($nbJobs = null, $force = false) + public function runCron(int $nbJobs = null, bool $force = false): void { if (!$this->configHelper->isQueueActive() && $force === false) { return; @@ -213,7 +213,8 @@ public function runCron($nbJobs = null, $force = false) * @param Job $job * @return string */ - protected function jobToWhereClause(Job $job): string { + protected function jobToWhereClause(Job $job): string + { return sprintf('job_id IN (%s)',implode(',', $job->getMergedIds())); } @@ -222,7 +223,8 @@ protected function jobToWhereClause(Job $job): string { * @return void * @throws \Exception */ - protected function processJob(Job $job): void { + protected function processJob(Job $job): void + { $job->execute(); $where = $this->jobToWhereClause($job); @@ -242,7 +244,8 @@ protected function processJob(Job $job): void { * @param Exception $e * @return void */ - protected function handleFailedJob(Job $job, Exception $e): void { + protected function handleFailedJob(Job $job, Exception $e): void + { $this->noOfFailedJobs++; // Log error information @@ -284,7 +287,7 @@ protected function handleFailedJob(Job $job, Exception $e): void { * * @throws Exception */ - public function run($maxJobs) + public function run(int $maxJobs): void { $this->clearOldFailingJobs(); @@ -375,12 +378,12 @@ protected function archiveSuccessfulJobs(string $whereClause): void { /** * @param int $maxJobs * - * @throws Exception - * * @return Job[] * + * @throws Exception + * */ - protected function getJobs($maxJobs) + protected function getJobs(int $maxJobs): array { $maxJobs = ($maxJobs === -1) ? $this->configHelper->getNumberOfJobToRun() : $maxJobs; @@ -432,14 +435,14 @@ protected function getJobs($maxJobs) * * @return Job[] */ - protected function fetchJobs($jobsLimit, $fetchFullReindexJobs = false, $lastJobId = null) + protected function fetchJobs(int $jobsLimit, bool $fetchFullReindexJobs = false, int $lastJobId = null): array { $jobs = []; $actualBatchSize = 0; - $maxBatchSize = $this->configHelper->getNumberOfElementByPage() * $jobsLimit; + $maxBatchSize = $this->configHelper->getNumberOfElementByPage(); - $limit = $maxJobs = $jobsLimit; + $limit = $jobsLimit; $offset = 0; $fetchFullReindexJobs = $fetchFullReindexJobs ? 1 : 0; @@ -470,27 +473,26 @@ protected function fetchJobs($jobsLimit, $fetchFullReindexJobs = false, $lastJob $rawJobsCount = count($rawJobs); $offset += $limit; - $limit = max(0, $maxJobs - $rawJobsCount); + $limit = max(0, $jobsLimit - $rawJobsCount); // $jobs will always be completely set from $rawJobs // Without resetting not-merged jobs would be stacked $jobs = []; - if (count($rawJobs) === $maxJobs) { + // At this point, if this condition is true, this means that no merge was possible in the last iteration + if (count($rawJobs) === $jobsLimit) { $jobs = $rawJobs; break; } + $jobSizes = []; + foreach ($rawJobs as $job) { $jobSize = (int) $job->getDataSize(); - - if ($actualBatchSize + $jobSize <= $maxBatchSize || !$jobs) { - $jobs[] = $job; - $actualBatchSize += $jobSize; - } else { - break 2; - } + $jobSizes[$job->getId()] = $jobSize; + $actualBatchSize = array_sum($jobSizes); + $jobs[] = $job; } } @@ -502,7 +504,7 @@ protected function fetchJobs($jobsLimit, $fetchFullReindexJobs = false, $lastJob * * @return Job[] */ - protected function mergeJobs(array $unmergedJobs) + protected function mergeJobs(array $unmergedJobs): array { $unmergedJobs = $this->sortJobs($unmergedJobs); @@ -539,7 +541,7 @@ protected function mergeJobs(array $unmergedJobs) * * @return Job[] */ - protected function sortJobs(array $jobs) + protected function sortJobs(array $jobs): array { $sortedJobs = []; @@ -571,7 +573,7 @@ protected function sortJobs(array $jobs) * * @return array */ - protected function stackSortedJobs(array $sortedJobs, array $tempSortableJobs, ?Job $job = null) + protected function stackSortedJobs(array $sortedJobs, array $tempSortableJobs, ?Job $job = null): array { if ($tempSortableJobs && $tempSortableJobs !== []) { $tempSortableJobs = $this->jobSort( @@ -599,7 +601,7 @@ protected function stackSortedJobs(array $sortedJobs, array $tempSortableJobs, ? /** * @return array */ - protected function jobSort() + protected function jobSort(): array { $args = func_get_args(); @@ -631,7 +633,7 @@ protected function jobSort() /** * @param Job[] $jobs */ - protected function lockJobs(array $jobs) + protected function lockJobs(array $jobs): void { $jobsIds = $this->getJobsIdsFromMergedJobs($jobs); @@ -657,7 +659,7 @@ protected function lockJobs(array $jobs) * * @return string[] */ - protected function getJobsIdsFromMergedJobs(array $mergedJobs) + protected function getJobsIdsFromMergedJobs(array $mergedJobs): array { $jobsIds = []; foreach ($mergedJobs as $job) { @@ -670,7 +672,7 @@ protected function getJobsIdsFromMergedJobs(array $mergedJobs) /** * @return void */ - protected function clearOldFailingJobs() + protected function clearOldFailingJobs(): void { // Enhanced archive will have already logged this failure if (!$this->configHelper->isEnhancedQueueArchiveEnabled()) { @@ -684,7 +686,7 @@ protected function clearOldFailingJobs() /** * @throws Zend_Db_Statement_Exception */ - protected function clearOldLogRecords() + protected function clearOldLogRecords(): void { $select = $this->db->select() ->from($this->logTable, ['id']) @@ -701,7 +703,7 @@ protected function clearOldLogRecords() /** * @return void */ - protected function clearOldArchiveRecords() + protected function clearOldArchiveRecords(): void { $archiveLogClearLimit = $this->configHelper->getArchiveLogClearLimit(); // Adding a fallback in case this configuration was not set in a consistent way @@ -718,7 +720,7 @@ protected function clearOldArchiveRecords() /** * @return void */ - protected function unlockStackedJobs() + protected function unlockStackedJobs(): void { $this->db->update($this->table, [ 'locked_at' => null, @@ -729,7 +731,7 @@ protected function unlockStackedJobs() /** * @return bool */ - protected function shouldEmptyQueue() + protected function shouldEmptyQueue(): bool { if (getenv('PROCESS_FULL_QUEUE') && getenv('PROCESS_FULL_QUEUE') === '1') { return true; From a991b18002d374c0a497c95134a62211108d6932 Mon Sep 17 00:00:00 2001 From: Damien Couchez Date: Wed, 20 Aug 2025 16:37:50 +0200 Subject: [PATCH 04/43] MAGE-1396: fix fetchJobs method --- Model/Queue.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Model/Queue.php b/Model/Queue.php index 697cd5d4f..cd4eb978a 100644 --- a/Model/Queue.php +++ b/Model/Queue.php @@ -486,6 +486,8 @@ protected function fetchJobs(int $jobsLimit, bool $fetchFullReindexJobs = false, break; } + // Introduced an array of job sizes to determine the total batch size currently processed (sum of all jobs contained in the run) + // This will determine if we can continue to loop over the jobs $jobSizes = []; foreach ($rawJobs as $job) { From aa1362e464220e32f8f500e90503486339063657 Mon Sep 17 00:00:00 2001 From: Damien Couchez Date: Wed, 20 Aug 2025 16:50:06 +0200 Subject: [PATCH 05/43] MAGE-1396: update queue notice --- Helper/Configuration/NoticeHelper.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Helper/Configuration/NoticeHelper.php b/Helper/Configuration/NoticeHelper.php index d3541b638..1648bfadf 100644 --- a/Helper/Configuration/NoticeHelper.php +++ b/Helper/Configuration/NoticeHelper.php @@ -99,6 +99,8 @@ protected function getQueueNotice() in approx. ' . $eta . '. You may want to clear the queue or configure indexing queue.

+ Depending on your configuration set on "Advanced > Maximum number of records processed per indexing job" and if the jobs can be merged into batches, you can expect higher performances. +

Find out more about Indexing Queue in documentation.'; } From 9ae681ffb53624a45944754418e6f1c600214bb5 Mon Sep 17 00:00:00 2001 From: Damien Couchez Date: Wed, 20 Aug 2025 17:01:11 +0200 Subject: [PATCH 06/43] MAGE-1396: small fix --- Model/Queue.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Model/Queue.php b/Model/Queue.php index cd4eb978a..7209bdd6e 100644 --- a/Model/Queue.php +++ b/Model/Queue.php @@ -493,9 +493,10 @@ protected function fetchJobs(int $jobsLimit, bool $fetchFullReindexJobs = false, foreach ($rawJobs as $job) { $jobSize = (int) $job->getDataSize(); $jobSizes[$job->getId()] = $jobSize; - $actualBatchSize = array_sum($jobSizes); $jobs[] = $job; } + + $actualBatchSize = array_sum($jobSizes); } return $jobs; From 522ebedf4d08e4196ceae10e2ff786ac76c7c33d Mon Sep 17 00:00:00 2001 From: Damien Couchez Date: Mon, 25 Aug 2025 10:52:24 +0200 Subject: [PATCH 07/43] MAGE-1100: cleanup --- Model/Job.php | 45 ++++++++++++++------------------------------- Model/Queue.php | 47 ++++++----------------------------------------- 2 files changed, 20 insertions(+), 72 deletions(-) diff --git a/Model/Job.php b/Model/Job.php index c1ac8e680..f5a007d10 100644 --- a/Model/Job.php +++ b/Model/Job.php @@ -32,28 +32,15 @@ class Job extends \Magento\Framework\Model\AbstractModel implements JobInterface { protected $_eventPrefix = 'algoliasearch_queue_job'; - /** @var ObjectManagerInterface */ - protected ObjectManagerInterface $objectManager; - - /** - * @param Context $context - * @param Registry $registry - * @param ObjectManagerInterface $objectManager - * @param AbstractResource|null $resource - * @param AbstractDb|null $resourceCollection - * @param array $data - */ public function __construct( - Context $context, - Registry $registry, - ObjectManagerInterface $objectManager, - ?AbstractResource $resource = null, - ?AbstractDb $resourceCollection = null, - array $data = [] + protected Context $context, + protected Registry $registry, + protected ObjectManagerInterface $objectManager, + protected ?AbstractResource $resource = null, + protected ?AbstractDb $resourceCollection = null, + array $data = [] ) { parent::__construct($context, $registry, $resource, $resourceCollection, $data); - - $this->objectManager = $objectManager; } /** @@ -61,7 +48,7 @@ public function __construct( * * @return void */ - protected function _construct() + protected function _construct(): void { $this->_init(ResourceModel\Job::class); } @@ -71,7 +58,7 @@ protected function _construct() * @throws AlreadyExistsException|\Exception * */ - public function execute() + public function execute(): Job { $model = $this->objectManager->get($this->getClass()); $method = $this->getMethod(); @@ -89,7 +76,7 @@ public function execute() /** * @return $this */ - public function prepare() + public function prepare(): Job { if ($this->getMergedIds() === null) { $this->setMergedIds([$this->getId()]); @@ -100,10 +87,6 @@ public function prepare() $this->setDecodedData($decodedData); - if (isset($decodedData['store_id'])) { - $this->setStoreId($decodedData['store_id']); - } - if (isset($decodedData['storeId'])) { $this->setStoreId($decodedData['storeId']); } @@ -118,7 +101,7 @@ public function prepare() * * @return bool */ - public function canMerge(Job $job, $maxJobDataSize) + public function canMerge(Job $job, $maxJobDataSize): bool { if ($this->getClass() !== $job->getClass()) { return false; @@ -156,7 +139,7 @@ public function canMerge(Job $job, $maxJobDataSize) * * @return Job */ - public function merge(Job $mergedJob) + public function merge(Job $mergedJob): Job { $mergedIds = $this->getMergedIds(); array_push($mergedIds, $mergedJob->getId()); @@ -186,7 +169,7 @@ public function merge(Job $mergedJob) /** * @return array */ - public function getDefaultValues() + public function getDefaultValues(): array { $values = []; @@ -196,7 +179,7 @@ public function getDefaultValues() /** * @return string */ - public function getStatus() + public function getStatus(): string { $status = JobInterface::STATUS_PROCESSING; @@ -218,7 +201,7 @@ public function getStatus() * * @return Job */ - public function saveError(\Exception $e) + public function saveError(\Exception $e): Job { $this->setErrorLog($e->getMessage()); $this->getResource()->save($this); diff --git a/Model/Queue.php b/Model/Queue.php index 7209bdd6e..3c5dea6f9 100644 --- a/Model/Queue.php +++ b/Model/Queue.php @@ -21,9 +21,6 @@ class Queue public const UNLOCK_STACKED_JOBS_AFTER_MINUTES = 15; public const CLEAR_ARCHIVE_LOGS_AFTER_DAYS = 30; - public const SUCCESS_LOG = 'algoliasearch_queue_log.txt'; - public const ERROR_LOG = 'algoliasearch_queue_errors.log'; - public const FAILED_JOB_ARCHIVE_CRITERIA = 'retries >= max_retries'; public const MOVE_INDEX_METHOD_NAME = 'moveIndexWithSetSettings'; @@ -39,23 +36,9 @@ class Queue /** @var string */ protected $archiveTable; - /** @var ObjectManagerInterface */ - protected $objectManager; - - /** @var ConsoleOutput */ - protected $output; - /** @var int */ protected $elementsPerPage; - /** @var ConfigHelper */ - protected $configHelper; - - /** @var DiagnosticsLogger */ - protected $logger; - - protected $jobCollectionFactory; - /** @var int */ protected $maxSingleJobDataSize; @@ -72,38 +55,20 @@ class Queue /** @var array */ protected $logRecord; - /** - * @param ConfigHelper $configHelper - * @param DiagnosticsLogger $logger - * @param JobCollectionFactory $jobCollectionFactory - * @param ResourceConnection $resourceConnection - * @param ObjectManagerInterface $objectManager - * @param ConsoleOutput $output - */ public function __construct( - ConfigHelper $configHelper, - DiagnosticsLogger $logger, - JobCollectionFactory $jobCollectionFactory, - ResourceConnection $resourceConnection, - ObjectManagerInterface $objectManager, - ConsoleOutput $output + protected ConfigHelper $configHelper, + protected DiagnosticsLogger $logger, + protected JobCollectionFactory $jobCollectionFactory, + protected ResourceConnection $resourceConnection, + protected ObjectManagerInterface $objectManager, + protected ConsoleOutput $output ) { - $this->configHelper = $configHelper; - $this->logger = $logger; - $this->jobCollectionFactory = $jobCollectionFactory; - $this->table = $resourceConnection->getTableName('algoliasearch_queue'); $this->logTable = $resourceConnection->getTableName('algoliasearch_queue_log'); $this->archiveTable = $resourceConnection->getTableName('algoliasearch_queue_archive'); - - //$this->db = $resourceConnection->getConnection(); - - $this->objectManager = $objectManager; $this->db = $objectManager->create(ResourceConnection::class)->getConnection('core_write'); - $this->output = $output; $this->elementsPerPage = $this->configHelper->getNumberOfElementByPage(); - $this->maxSingleJobDataSize = $this->configHelper->getNumberOfElementByPage(); } From 9e57d80189045e4440a2eda24f3ec4f5bdabf920 Mon Sep 17 00:00:00 2001 From: Damien Couchez Date: Mon, 25 Aug 2025 13:04:43 +0200 Subject: [PATCH 08/43] MAGE-1100: add new max batch size calculation based on stores --- Model/Queue.php | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/Model/Queue.php b/Model/Queue.php index 3c5dea6f9..7a379f433 100644 --- a/Model/Queue.php +++ b/Model/Queue.php @@ -55,6 +55,12 @@ class Queue /** @var array */ protected $logRecord; + /** @var int */ + protected $maxBatchSize = 0; + + /** @var array */ + protected array $storeMaxBatchSizes; + public function __construct( protected ConfigHelper $configHelper, protected DiagnosticsLogger $logger, @@ -404,15 +410,13 @@ protected function fetchJobs(int $jobsLimit, bool $fetchFullReindexJobs = false, { $jobs = []; - $actualBatchSize = 0; - $maxBatchSize = $this->configHelper->getNumberOfElementByPage(); - + $actualBatchSize = -1; $limit = $jobsLimit; $offset = 0; $fetchFullReindexJobs = $fetchFullReindexJobs ? 1 : 0; - while ($actualBatchSize < $maxBatchSize) { + while ($actualBatchSize < $this->maxBatchSize) { $jobsCollection = $this->jobCollectionFactory->create(); $jobsCollection ->addFieldToFilter('pid', ['null' => true]) @@ -455,18 +459,36 @@ protected function fetchJobs(int $jobsLimit, bool $fetchFullReindexJobs = false, // This will determine if we can continue to loop over the jobs $jobSizes = []; + $this->maxBatchSize = 0; + foreach ($rawJobs as $job) { $jobSize = (int) $job->getDataSize(); $jobSizes[$job->getId()] = $jobSize; $jobs[] = $job; + $this->maxBatchSize += $this->getStoreMaxBatchSize($job->getStoreId()); } + // Final calculation for the loop $actualBatchSize = array_sum($jobSizes); + $this->maxBatchSize = round($this->maxBatchSize / count($jobSizes)); } return $jobs; } + /** + * @param int $storeId + * @return int + */ + protected function getStoreMaxBatchSize(int $storeId): int + { + if (!isset($this->storeMaxBatchSizes[$storeId])) { + $this->storeMaxBatchSizes[$storeId] = $this->configHelper->getNumberOfElementByPage($storeId); + } + + return $this->storeMaxBatchSizes[$storeId]; + } + /** * @param Job[] $unmergedJobs * From 030f028e67dfa4173738a056f54f8e599d607e7f Mon Sep 17 00:00:00 2001 From: Damien Couchez Date: Mon, 25 Aug 2025 19:05:50 +0200 Subject: [PATCH 09/43] MAGE-1100: fix tests --- Model/Queue.php | 7 ++++++- Test/Integration/Indexing/Queue/QueueTest.php | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Model/Queue.php b/Model/Queue.php index 7a379f433..5f0c5f1d5 100644 --- a/Model/Queue.php +++ b/Model/Queue.php @@ -483,7 +483,12 @@ protected function fetchJobs(int $jobsLimit, bool $fetchFullReindexJobs = false, protected function getStoreMaxBatchSize(int $storeId): int { if (!isset($this->storeMaxBatchSizes[$storeId])) { - $this->storeMaxBatchSizes[$storeId] = $this->configHelper->getNumberOfElementByPage($storeId); + try { + $this->storeMaxBatchSizes[$storeId] = $this->configHelper->getNumberOfElementByPage($storeId); + } catch (\Exception $e) { + // In case a job was created before a store deletion + $this->storeMaxBatchSizes[$storeId] = $this->configHelper->getNumberOfElementByPage(); + } } return $this->storeMaxBatchSizes[$storeId]; diff --git a/Test/Integration/Indexing/Queue/QueueTest.php b/Test/Integration/Indexing/Queue/QueueTest.php index eec61344f..85b8a22f5 100644 --- a/Test/Integration/Indexing/Queue/QueueTest.php +++ b/Test/Integration/Indexing/Queue/QueueTest.php @@ -806,7 +806,7 @@ public function testHugeJob() /** @var Job[] $jobs */ $jobs = $this->invokeMethod($this->queue, 'getJobs', ['maxJobs' => 10]); - $this->assertEquals(1, count($jobs)); + $this->assertEquals(2, count($jobs)); $job = reset($jobs); $this->assertEquals(5000, $job->getDataSize()); @@ -820,7 +820,7 @@ public function testHugeJob() $lastJob = end($dbJobs); $this->assertEquals($pid, $firstJob['pid']); - $this->assertNull($lastJob['pid']); + $this->assertEquals($pid, $lastJob['pid']); } /** From 01dede8b1e1fea94e58c1afc62e8779d845b8442 Mon Sep 17 00:00:00 2001 From: Damien Couchez Date: Tue, 26 Aug 2025 11:14:34 +0200 Subject: [PATCH 10/43] MAGE-1110: some more cleaning --- Model/Queue.php | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/Model/Queue.php b/Model/Queue.php index 5f0c5f1d5..b615bda99 100644 --- a/Model/Queue.php +++ b/Model/Queue.php @@ -36,12 +36,6 @@ class Queue /** @var string */ protected $archiveTable; - /** @var int */ - protected $elementsPerPage; - - /** @var int */ - protected $maxSingleJobDataSize; - /** @var int */ protected $noOfFailedJobs = 0; @@ -73,9 +67,6 @@ public function __construct( $this->logTable = $resourceConnection->getTableName('algoliasearch_queue_log'); $this->archiveTable = $resourceConnection->getTableName('algoliasearch_queue_archive'); $this->db = $objectManager->create(ResourceConnection::class)->getConnection('core_write'); - - $this->elementsPerPage = $this->configHelper->getNumberOfElementByPage(); - $this->maxSingleJobDataSize = $this->configHelper->getNumberOfElementByPage(); } /** @@ -513,7 +504,7 @@ protected function mergeJobs(array $unmergedJobs): array if (count($unmergedJobs) > 0) { $nextJob = array_shift($unmergedJobs); - if ($currentJob->canMerge($nextJob, $this->maxSingleJobDataSize)) { + if ($currentJob->canMerge($nextJob, $this->getStoreMaxBatchSize($currentJob->getStoreId()))) { $currentJob->merge($nextJob); continue; From 74a270a3090a5b25c8bc10e20f62bf88168f32c1 Mon Sep 17 00:00:00 2001 From: Damien Couchez Date: Wed, 27 Aug 2025 10:23:03 +0200 Subject: [PATCH 11/43] MAGE-1110: address feedback --- Model/Job.php | 8 ++++---- Model/Queue.php | 26 ++++++++++++++++++-------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/Model/Job.php b/Model/Job.php index f5a007d10..8661dc56f 100644 --- a/Model/Job.php +++ b/Model/Job.php @@ -33,11 +33,11 @@ class Job extends \Magento\Framework\Model\AbstractModel implements JobInterface protected $_eventPrefix = 'algoliasearch_queue_job'; public function __construct( - protected Context $context, - protected Registry $registry, + Context $context, + Registry $registry, protected ObjectManagerInterface $objectManager, - protected ?AbstractResource $resource = null, - protected ?AbstractDb $resourceCollection = null, + ?AbstractResource $resource = null, + ?AbstractDb $resourceCollection = null, array $data = [] ) { parent::__construct($context, $registry, $resource, $resourceCollection, $data); diff --git a/Model/Queue.php b/Model/Queue.php index b615bda99..e9047cbb0 100644 --- a/Model/Queue.php +++ b/Model/Queue.php @@ -49,9 +49,6 @@ class Queue /** @var array */ protected $logRecord; - /** @var int */ - protected $maxBatchSize = 0; - /** @var array */ protected array $storeMaxBatchSizes; @@ -402,12 +399,13 @@ protected function fetchJobs(int $jobsLimit, bool $fetchFullReindexJobs = false, $jobs = []; $actualBatchSize = -1; + $maxBatchSize = 0; $limit = $jobsLimit; $offset = 0; $fetchFullReindexJobs = $fetchFullReindexJobs ? 1 : 0; - while ($actualBatchSize < $this->maxBatchSize) { + while ($actualBatchSize < $maxBatchSize) { $jobsCollection = $this->jobCollectionFactory->create(); $jobsCollection ->addFieldToFilter('pid', ['null' => true]) @@ -450,23 +448,35 @@ protected function fetchJobs(int $jobsLimit, bool $fetchFullReindexJobs = false, // This will determine if we can continue to loop over the jobs $jobSizes = []; - $this->maxBatchSize = 0; - foreach ($rawJobs as $job) { $jobSize = (int) $job->getDataSize(); $jobSizes[$job->getId()] = $jobSize; $jobs[] = $job; - $this->maxBatchSize += $this->getStoreMaxBatchSize($job->getStoreId()); } // Final calculation for the loop $actualBatchSize = array_sum($jobSizes); - $this->maxBatchSize = round($this->maxBatchSize / count($jobSizes)); + $maxBatchSize = $this->calculateMaxBatchSize($jobs); } return $jobs; } + /** + * @param Job[] $jobs + * @return int + */ + protected function calculateMaxBatchSize(array $jobs): int + { + $maxBatchSize = 0; + + foreach ($jobs as $job) { + $maxBatchSize += $this->getStoreMaxBatchSize($job->getStoreId()); + } + + return round($maxBatchSize / count($jobs)); + } + /** * @param int $storeId * @return int From 870b79b4354097540b3ea6260c35791741e4352b Mon Sep 17 00:00:00 2001 From: Damien Couchez Date: Wed, 3 Sep 2025 14:28:04 +0200 Subject: [PATCH 12/43] MAGE-1110: add missing store scope for max batch size --- Service/Category/BatchQueueProcessor.php | 2 +- Service/Category/IndexBuilder.php | 4 ++-- Service/Product/BatchQueueProcessor.php | 2 +- Service/Suggestion/IndexBuilder.php | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Service/Category/BatchQueueProcessor.php b/Service/Category/BatchQueueProcessor.php index bd193a276..f1715db95 100644 --- a/Service/Category/BatchQueueProcessor.php +++ b/Service/Category/BatchQueueProcessor.php @@ -41,7 +41,7 @@ public function processBatch(int $storeId, ?array $entityIds = null): void return; } - $categoriesPerPage = $this->configHelper->getNumberOfElementByPage(); + $categoriesPerPage = $this->configHelper->getNumberOfElementByPage($storeId); if (is_array($entityIds) && count($entityIds) > 0) { $this->processSpecificCategories($entityIds, $categoriesPerPage, $storeId); diff --git a/Service/Category/IndexBuilder.php b/Service/Category/IndexBuilder.php index 17eddee9c..66c8a5bd3 100644 --- a/Service/Category/IndexBuilder.php +++ b/Service/Category/IndexBuilder.php @@ -117,14 +117,14 @@ protected function rebuildEntityIds($storeId, $categoryIds = null): void } if ($size > 0) { - $pages = ceil($size / $this->configHelper->getNumberOfElementByPage()); + $pages = ceil($size / $this->configHelper->getNumberOfElementByPage($storeId)); $page = 1; while ($page <= $pages) { $this->buildIndexPage( $storeId, $collection, $page, - $this->configHelper->getNumberOfElementByPage(), + $this->configHelper->getNumberOfElementByPage($storeId), $categoryIds ); $page++; diff --git a/Service/Product/BatchQueueProcessor.php b/Service/Product/BatchQueueProcessor.php index 15462e5d3..8092608f2 100644 --- a/Service/Product/BatchQueueProcessor.php +++ b/Service/Product/BatchQueueProcessor.php @@ -54,7 +54,7 @@ public function processBatch(int $storeId, ?array $entityIds = null): void $this->areParentsLoaded = true; } - $productsPerPage = $this->configHelper->getNumberOfElementByPage(); + $productsPerPage = $this->configHelper->getNumberOfElementByPage($storeId); if (is_array($entityIds) && count($entityIds) > 0) { foreach (array_chunk($entityIds, $productsPerPage) as $chunk) { diff --git a/Service/Suggestion/IndexBuilder.php b/Service/Suggestion/IndexBuilder.php index 779344683..5a41a6f39 100644 --- a/Service/Suggestion/IndexBuilder.php +++ b/Service/Suggestion/IndexBuilder.php @@ -75,7 +75,7 @@ public function buildIndex(int $storeId, ?array $entityIds, ?array $options): vo $size = $collection->getSize(); if ($size > 0) { - $pages = ceil($size / $this->configHelper->getNumberOfElementByPage()); + $pages = ceil($size / $this->configHelper->getNumberOfElementByPage($storeId)); $collection->clear(); $page = 1; @@ -84,7 +84,7 @@ public function buildIndex(int $storeId, ?array $entityIds, ?array $options): vo $storeId, $collection, $page, - $this->configHelper->getNumberOfElementByPage() + $this->configHelper->getNumberOfElementByPage($storeId) ); $page++; } From 6fe831a2d8e7287922f1676e6d615580a89bad70 Mon Sep 17 00:00:00 2001 From: Eric Wright Date: Thu, 11 Sep 2025 15:25:43 -0400 Subject: [PATCH 13/43] MAGE-1413 Fix implicit nullable types for PHP 8.4 --- Model/Queue.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Model/Queue.php b/Model/Queue.php index e9047cbb0..9a1a08d7e 100644 --- a/Model/Queue.php +++ b/Model/Queue.php @@ -124,7 +124,7 @@ public function getAverageProcessingTime(): ?float * * @throws Exception */ - public function runCron(int $nbJobs = null, bool $force = false): void + public function runCron(?int $nbJobs = null, bool $force = false): void { if (!$this->configHelper->isQueueActive() && $force === false) { return; @@ -394,7 +394,7 @@ protected function getJobs(int $maxJobs): array * * @return Job[] */ - protected function fetchJobs(int $jobsLimit, bool $fetchFullReindexJobs = false, int $lastJobId = null): array + protected function fetchJobs(int $jobsLimit, bool $fetchFullReindexJobs = false, ?int $lastJobId = null): array { $jobs = []; From 7dcbf14c5a7986b8755192dfc6d5951f3959d790 Mon Sep 17 00:00:00 2001 From: Nathan Day Date: Wed, 30 Apr 2025 11:14:01 +0100 Subject: [PATCH 14/43] Refactor Product With Children to remove Double Conversion --- .../PriceManager/ProductWithChildren.php | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/Helper/Entity/Product/PriceManager/ProductWithChildren.php b/Helper/Entity/Product/PriceManager/ProductWithChildren.php index 0cffb9690..b929edd97 100755 --- a/Helper/Entity/Product/PriceManager/ProductWithChildren.php +++ b/Helper/Entity/Product/PriceManager/ProductWithChildren.php @@ -58,8 +58,17 @@ protected function getMinMaxPrices(Product $product, $withTax, $subProducts, $cu } else { $minPrice = $specialPrice[0]; } - $price = $minPrice ?? $this->getTaxPrice($product, $subProduct->getFinalPrice(), $withTax); - $basePrice = $this->getTaxPrice($product, $subProduct->getPrice(), $withTax); + + $finalPrice = $subProduct->getFinalPrice(); + $basePrice = $subProduct->getPrice(); + + if ($currencyCode !== $this->baseCurrencyCode) { + $finalPrice = $this->convertPrice($finalPrice, $currencyCode); + $basePrice = $this->convertPrice($basePrice, $currencyCode); + } + + $price = $minPrice ?? $this->getTaxPrice($product, $finalPrice, $withTax); + $basePrice = $this->getTaxPrice($product, $basePrice, $withTax); $min = min($min, $price); $original = min($original, $basePrice); $max = max($max, $price); @@ -68,14 +77,7 @@ protected function getMinMaxPrices(Product $product, $withTax, $subProducts, $cu } else { $originalMax = $original = $min = $max; } - if ($currencyCode !== $this->baseCurrencyCode) { - $min = $this->convertPrice($min, $currencyCode); - $original = $this->convertPrice($original, $currencyCode); - if ($min !== $max) { - $max = $this->convertPrice($max, $currencyCode); - $originalMax = $this->convertPrice($originalMax, $currencyCode); - } - } + return [$min, $max, $original, $originalMax]; } From 30a2a10d030e4ef76b3f9106c33a1ba6f31fc4e4 Mon Sep 17 00:00:00 2001 From: Damien Couchez Date: Fri, 19 Sep 2025 10:08:50 +0200 Subject: [PATCH 15/43] MAGE-1404: add checks on configuration migration processed on data patches --- .../Patch/Data/MigrateIndexingConfigPatch.php | 10 +++---- .../Data/MigrateInstantSearchConfigPatch.php | 11 ++++---- Setup/Patch/DataMigrationTrait.php | 28 +++++++++++++++++++ 3 files changed, 37 insertions(+), 12 deletions(-) create mode 100644 Setup/Patch/DataMigrationTrait.php diff --git a/Setup/Patch/Data/MigrateIndexingConfigPatch.php b/Setup/Patch/Data/MigrateIndexingConfigPatch.php index 172e749d0..55bab9154 100644 --- a/Setup/Patch/Data/MigrateIndexingConfigPatch.php +++ b/Setup/Patch/Data/MigrateIndexingConfigPatch.php @@ -3,12 +3,15 @@ namespace Algolia\AlgoliaSearch\Setup\Patch\Data; use Algolia\AlgoliaSearch\Helper\ConfigHelper; +use Algolia\AlgoliaSearch\Setup\Patch\DataMigrationTrait; use Magento\Framework\Setup\ModuleDataSetupInterface; use Magento\Framework\Setup\Patch\DataPatchInterface; use Magento\Framework\Setup\Patch\PatchInterface; class MigrateIndexingConfigPatch implements DataPatchInterface { + use DataMigrationTrait; + public function __construct( protected ModuleDataSetupInterface $moduleDataSetup, ) {} @@ -39,12 +42,7 @@ protected function moveIndexingSettings(): void 'algoliasearch_credentials/credentials/enable_pages_index' => ConfigHelper::ENABLE_PAGES_INDEX, ]; - $connection = $this->moduleDataSetup->getConnection(); - foreach ($movedConfig as $from => $to) { - $configDataTable = $this->moduleDataSetup->getTable('core_config_data'); - $whereConfigPath = $connection->quoteInto('path = ?', $from); - $connection->update($configDataTable, ['path' => $to], $whereConfigPath); - } + $this->migrateConfig($movedConfig); } /** diff --git a/Setup/Patch/Data/MigrateInstantSearchConfigPatch.php b/Setup/Patch/Data/MigrateInstantSearchConfigPatch.php index c43c87832..737a9f95d 100644 --- a/Setup/Patch/Data/MigrateInstantSearchConfigPatch.php +++ b/Setup/Patch/Data/MigrateInstantSearchConfigPatch.php @@ -2,12 +2,15 @@ namespace Algolia\AlgoliaSearch\Setup\Patch\Data; +use Algolia\AlgoliaSearch\Setup\Patch\DataMigrationTrait; use Magento\Framework\Setup\ModuleDataSetupInterface; use Magento\Framework\Setup\Patch\DataPatchInterface; use Magento\Framework\Setup\Patch\PatchInterface; class MigrateInstantSearchConfigPatch implements DataPatchInterface { + use DataMigrationTrait; + public function __construct( protected ModuleDataSetupInterface $moduleDataSetup, ) {} @@ -44,12 +47,8 @@ protected function moveInstantSearchSettings(): void 'algoliasearch_instant/instant/infinite_scroll_enable' => 'algoliasearch_instant/instant_options/infinite_scroll_enable', 'algoliasearch_instant/instant/hide_pagination' => 'algoliasearch_instant/instant_options/hide_pagination' ]; - $connection = $this->moduleDataSetup->getConnection(); - foreach ($movedConfig as $from => $to) { - $configDataTable = $this->moduleDataSetup->getTable('core_config_data'); - $whereConfigPath = $connection->quoteInto('path = ?', $from); - $connection->update($configDataTable, ['path' => $to], $whereConfigPath); - } + + $this->migrateConfig($movedConfig); } /** diff --git a/Setup/Patch/DataMigrationTrait.php b/Setup/Patch/DataMigrationTrait.php new file mode 100644 index 000000000..3832f1612 --- /dev/null +++ b/Setup/Patch/DataMigrationTrait.php @@ -0,0 +1,28 @@ +moduleDataSetup->getConnection(); + foreach ($configurations as $from => $to) { + $configDataTable = $this->moduleDataSetup->getTable('core_config_data'); + $whereConfigPathFrom = $connection->quoteInto('path = ?', $from); + + $select = $connection->select() + ->from($configDataTable) + ->where('path = ?', $to); + $existingValues = $connection->fetchAll($select); + + if (count($existingValues) === 0) { + $connection->update($configDataTable, ['path' => $to], $whereConfigPathFrom); + } + } + } +} From 7b80678b545f08eef71b68b53db678620f845a20 Mon Sep 17 00:00:00 2001 From: Eric Wright Date: Fri, 19 Sep 2025 11:16:59 -0400 Subject: [PATCH 16/43] Add dev release version tag --- composer.json | 2 +- etc/module.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index f7809dc49..495329623 100755 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "description": "Algolia Search & Discovery extension for Magento 2", "type": "magento2-module", "license": ["MIT"], - "version": "3.16.0", + "version": "3.16.1-dev", "require": { "php": "~8.2|~8.3|~8.4", "magento/framework": "~103.0", diff --git a/etc/module.xml b/etc/module.xml index 26870a9f1..5d8ff68f3 100755 --- a/etc/module.xml +++ b/etc/module.xml @@ -1,6 +1,6 @@ - + From 9afe5875075e23075b6d7766672746c0ff3c2400 Mon Sep 17 00:00:00 2001 From: Eric Wright Date: Fri, 19 Sep 2025 11:02:59 -0400 Subject: [PATCH 17/43] MAGE-1426 Add unit test experiment with state machine --- .gitignore | 1 + .../Unit/Service/IndexSettingsHandlerTest.php | 183 +++++++++++++++++- 2 files changed, 183 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 6319cde4d..8d7d00605 100755 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ vendor/ .php_cs.cache .vscode .idea +node_modules \ No newline at end of file diff --git a/Test/Unit/Service/IndexSettingsHandlerTest.php b/Test/Unit/Service/IndexSettingsHandlerTest.php index 62cd3c393..fbec227ab 100644 --- a/Test/Unit/Service/IndexSettingsHandlerTest.php +++ b/Test/Unit/Service/IndexSettingsHandlerTest.php @@ -18,17 +18,80 @@ class IndexSettingsHandlerTest extends TestCase private ?IndexSettingsHandler $handler = null; + /** + * State machine to track pending operations per store ID + * Format: [storeId => ['totalCalls' => int, 'waitCalled' => bool, 'batchesCompleted' => int]] + */ + private array $operationState = []; + protected function setUp(): void { $this->connector = $this->createMock(AlgoliaConnector::class); $this->config = $this->createMock(ConfigHelper::class); $this->indexOptions = $this->createMock(IndexOptionsInterface::class); + // Configure the mock to use our state machine + $this->setupStateMachineMock(); + $this->handler = new IndexSettingsHandlerTestable($this->connector, $this->config); } + private function setupStateMachineMock(): void + { + $this->connector->method('setSettings') + ->willReturnCallback(function($indexOptions, $settings, $forwardToReplicas, $mergeSettings, $mergeFrom = '') { + $storeId = $indexOptions->getStoreId(); + + // Initialize state if not exists + if (!isset($this->operationState[$storeId])) { + $this->operationState[$storeId] = [ + 'totalCalls' => 0, + 'waitCalled' => false, + 'batchesCompleted' => 0 + ]; + } + + // Check if we have completed batches that haven't been waited for + if ($this->operationState[$storeId]['batchesCompleted'] > 0 && + !$this->operationState[$storeId]['waitCalled']) { + throw new \RuntimeException( + "Cannot call setSettings on store $storeId: previous operation still pending. Call waitLastTask first." + ); + } + + // Increment call count + $this->operationState[$storeId]['totalCalls']++; + $this->operationState[$storeId]['waitCalled'] = false; + }); + + $this->connector->method('waitLastTask') + ->willReturnCallback(function($storeId = null) { + if ($storeId !== null && isset($this->operationState[$storeId])) { + // Mark that wait has been called + $this->operationState[$storeId]['waitCalled'] = true; + } + }); + } + + /** + * Call this after IndexSettingsHandler.setSettings() completes to mark the batch as done + */ + private function markBatchCompleted(int $storeId): void + { + if (isset($this->operationState[$storeId])) { + $this->operationState[$storeId]['batchesCompleted']++; + } + } + + private function resetOperationState(): void + { + $this->operationState = []; + } + public function testSetSettingsWithForwardingEnabledAndMixedSettings(): void { + $this->resetOperationState(); + $storeId = 1; $settings = [ 'customRanking' => ['desc(price)'], @@ -44,7 +107,7 @@ public function testSetSettingsWithForwardingEnabledAndMixedSettings(): void $this->connector->expects($this->exactly(2)) ->method('setSettings') ->willReturnCallback( - function($indexOptions, $indexSettings, $forwardToReplicas, $mergeSettings, $mergeFrom) use (&$invocationCount) { + function($indexOptions, $indexSettings, $forwardToReplicas, $mergeSettings, $mergeFrom = '') use (&$invocationCount) { $invocationCount++; switch ($invocationCount) { @@ -62,10 +125,13 @@ function($indexOptions, $indexSettings, $forwardToReplicas, $mergeSettings, $mer }); $this->handler->setSettings($this->indexOptions, $settings); + $this->markBatchCompleted($storeId); } public function testSetSettingsWithForwardingEnabledOnlyExcludedSettings(): void { + $this->resetOperationState(); + $storeId = 1; $settings = [ 'ranking' => ['asc(name)'], @@ -88,10 +154,13 @@ public function testSetSettingsWithForwardingEnabledOnlyExcludedSettings(): void ); $this->handler->setSettings($this->indexOptions, $settings); + $this->markBatchCompleted($storeId); } public function testSetSettingsWithForwardingEnabledOnlyForwardableSettings(): void { + $this->resetOperationState(); + $storeId = 1; $settings = [ 'attributesToHighlight' => ['title'], @@ -113,10 +182,13 @@ public function testSetSettingsWithForwardingEnabledOnlyForwardableSettings(): v ); $this->handler->setSettings($this->indexOptions, $settings); + $this->markBatchCompleted($storeId); } public function testSetSettingsWithForwardingDisabled(): void { + $this->resetOperationState(); + $storeId = 1; $settings = [ 'customRanking' => ['desc(price)'], @@ -138,10 +210,13 @@ public function testSetSettingsWithForwardingDisabled(): void ); $this->handler->setSettings($this->indexOptions, $settings); + $this->markBatchCompleted($storeId); } public function testForwardSettingsWithEmptyInput(): void { + $this->resetOperationState(); + $storeId = 1; $settings = []; @@ -153,6 +228,7 @@ public function testForwardSettingsWithEmptyInput(): void $this->connector->expects($this->never())->method('setSettings'); $this->handler->setSettings($this->indexOptions, $settings); + $this->markBatchCompleted($storeId); } @@ -172,4 +248,109 @@ public function testSplitSettings(): void 'ranking' => ['asc(name)'] ], $noForward); } + + public function testSubsequentSetSettingsWithoutWaitThrowsException(): void + { + $this->resetOperationState(); + + $storeId = 1; + $settings = ['attributesToRetrieve' => ['name']]; + + $this->indexOptions->method('getStoreId')->willReturn($storeId); + $this->config->method('shouldForwardPrimaryIndexSettingsToReplicas') + ->willReturn(false); + + // First call should succeed + $this->handler->setSettings($this->indexOptions, $settings); + $this->markBatchCompleted($storeId); + + // Second call without wait should throw exception + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage("Cannot call setSettings on store $storeId: previous operation still pending. Call waitLastTask first."); + + $this->handler->setSettings($this->indexOptions, $settings); + } + + public function testSubsequentSetSettingsAfterWaitSucceeds(): void + { + $this->resetOperationState(); + + $storeId = 1; + $settings = ['attributesToRetrieve' => ['name']]; + + $this->indexOptions->method('getStoreId')->willReturn($storeId); + $this->config->method('shouldForwardPrimaryIndexSettingsToReplicas') + ->willReturn(false); + + // Configure connector to expect two setSettings calls and one waitLastTask call + $this->connector->expects($this->exactly(2)) + ->method('setSettings'); + + $this->connector->expects($this->once()) + ->method('waitLastTask') + ->with($storeId); + + // First call should succeed + $this->handler->setSettings($this->indexOptions, $settings); + $this->markBatchCompleted($storeId); + + // Wait for the task + $this->connector->waitLastTask($storeId); + + // Second call after wait should succeed + $this->handler->setSettings($this->indexOptions, $settings); + $this->markBatchCompleted($storeId); + } + + public function testDifferentStoreIdsDontInterfere(): void + { + $this->resetOperationState(); + + $storeId1 = 1; + $storeId2 = 2; + $settings = ['attributesToRetrieve' => ['name']]; + + $indexOptions1 = $this->createMock(IndexOptionsInterface::class); + $indexOptions1->method('getStoreId')->willReturn($storeId1); + + $indexOptions2 = $this->createMock(IndexOptionsInterface::class); + $indexOptions2->method('getStoreId')->willReturn($storeId2); + + $this->config->method('shouldForwardPrimaryIndexSettingsToReplicas') + ->willReturn(false); + + // Both calls should succeed as they use different store IDs + $this->connector->expects($this->exactly(2)) + ->method('setSettings'); + + $this->handler->setSettings($indexOptions1, $settings); + $this->markBatchCompleted($storeId1); + $this->handler->setSettings($indexOptions2, $settings); + $this->markBatchCompleted($storeId2); + } + + public function testForwardingEnabledMultipleCallsRequireWait(): void + { + $this->resetOperationState(); + + $storeId = 1; + $settings = [ + 'customRanking' => ['desc(price)'], + 'attributesToRetrieve' => ['name'] + ]; + + $this->indexOptions->method('getStoreId')->willReturn($storeId); + $this->config->method('shouldForwardPrimaryIndexSettingsToReplicas') + ->willReturn(true); + + // First call makes two internal setSettings calls + $this->handler->setSettings($this->indexOptions, $settings); + $this->markBatchCompleted($storeId); + + // Second call should fail because no wait was called + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage("Cannot call setSettings on store $storeId: previous operation still pending. Call waitLastTask first."); + + $this->handler->setSettings($this->indexOptions, $settings); + } } From bbd3330aa34dedf4c250a26937956f2e295aa949 Mon Sep 17 00:00:00 2001 From: Eric Wright Date: Fri, 19 Sep 2025 11:42:51 -0400 Subject: [PATCH 18/43] MAGE-1426 Add wait operation --- Service/IndexSettingsHandler.php | 1 + 1 file changed, 1 insertion(+) diff --git a/Service/IndexSettingsHandler.php b/Service/IndexSettingsHandler.php index f17859674..577135273 100644 --- a/Service/IndexSettingsHandler.php +++ b/Service/IndexSettingsHandler.php @@ -55,6 +55,7 @@ public function setSettings( true, false ); + $this->connector->waitLastTask($indexOptions->getStoreId()); } if ($noForward) { $this->connector->setSettings( From 0f21fcb484c0bdddd568948c38fd17f37d7b6039 Mon Sep 17 00:00:00 2001 From: Eric Wright Date: Fri, 19 Sep 2025 12:14:50 -0400 Subject: [PATCH 19/43] MAGE-1426 Simplify state machine and clarify intent of unit tests --- .../Unit/Service/IndexSettingsHandlerTest.php | 99 +++++++++---------- 1 file changed, 48 insertions(+), 51 deletions(-) diff --git a/Test/Unit/Service/IndexSettingsHandlerTest.php b/Test/Unit/Service/IndexSettingsHandlerTest.php index fbec227ab..efbbc3f53 100644 --- a/Test/Unit/Service/IndexSettingsHandlerTest.php +++ b/Test/Unit/Service/IndexSettingsHandlerTest.php @@ -41,48 +41,36 @@ private function setupStateMachineMock(): void $this->connector->method('setSettings') ->willReturnCallback(function($indexOptions, $settings, $forwardToReplicas, $mergeSettings, $mergeFrom = '') { $storeId = $indexOptions->getStoreId(); - + // Initialize state if not exists if (!isset($this->operationState[$storeId])) { $this->operationState[$storeId] = [ - 'totalCalls' => 0, - 'waitCalled' => false, - 'batchesCompleted' => 0 + 'setSettingsCalled' => false, + 'waitCalled' => false ]; } - - // Check if we have completed batches that haven't been waited for - if ($this->operationState[$storeId]['batchesCompleted'] > 0 && + + // Check that setSettings is not stacked for this $storeId + if ($this->operationState[$storeId]['setSettingsCalled'] && !$this->operationState[$storeId]['waitCalled']) { throw new \RuntimeException( "Cannot call setSettings on store $storeId: previous operation still pending. Call waitLastTask first." ); } - - // Increment call count - $this->operationState[$storeId]['totalCalls']++; + + // Update state + $this->operationState[$storeId]['setSettingsCalled'] = true; $this->operationState[$storeId]['waitCalled'] = false; }); $this->connector->method('waitLastTask') ->willReturnCallback(function($storeId = null) { if ($storeId !== null && isset($this->operationState[$storeId])) { - // Mark that wait has been called $this->operationState[$storeId]['waitCalled'] = true; } }); } - /** - * Call this after IndexSettingsHandler.setSettings() completes to mark the batch as done - */ - private function markBatchCompleted(int $storeId): void - { - if (isset($this->operationState[$storeId])) { - $this->operationState[$storeId]['batchesCompleted']++; - } - } - private function resetOperationState(): void { $this->operationState = []; @@ -91,7 +79,7 @@ private function resetOperationState(): void public function testSetSettingsWithForwardingEnabledAndMixedSettings(): void { $this->resetOperationState(); - + $storeId = 1; $settings = [ 'customRanking' => ['desc(price)'], @@ -125,13 +113,12 @@ function($indexOptions, $indexSettings, $forwardToReplicas, $mergeSettings, $mer }); $this->handler->setSettings($this->indexOptions, $settings); - $this->markBatchCompleted($storeId); } public function testSetSettingsWithForwardingEnabledOnlyExcludedSettings(): void { $this->resetOperationState(); - + $storeId = 1; $settings = [ 'ranking' => ['asc(name)'], @@ -154,13 +141,12 @@ public function testSetSettingsWithForwardingEnabledOnlyExcludedSettings(): void ); $this->handler->setSettings($this->indexOptions, $settings); - $this->markBatchCompleted($storeId); } public function testSetSettingsWithForwardingEnabledOnlyForwardableSettings(): void { $this->resetOperationState(); - + $storeId = 1; $settings = [ 'attributesToHighlight' => ['title'], @@ -182,13 +168,12 @@ public function testSetSettingsWithForwardingEnabledOnlyForwardableSettings(): v ); $this->handler->setSettings($this->indexOptions, $settings); - $this->markBatchCompleted($storeId); } public function testSetSettingsWithForwardingDisabled(): void { $this->resetOperationState(); - + $storeId = 1; $settings = [ 'customRanking' => ['desc(price)'], @@ -210,13 +195,12 @@ public function testSetSettingsWithForwardingDisabled(): void ); $this->handler->setSettings($this->indexOptions, $settings); - $this->markBatchCompleted($storeId); } public function testForwardSettingsWithEmptyInput(): void { $this->resetOperationState(); - + $storeId = 1; $settings = []; @@ -228,7 +212,6 @@ public function testForwardSettingsWithEmptyInput(): void $this->connector->expects($this->never())->method('setSettings'); $this->handler->setSettings($this->indexOptions, $settings); - $this->markBatchCompleted($storeId); } @@ -249,70 +232,77 @@ public function testSplitSettings(): void ], $noForward); } + /** + * Ensure the state machine is working as expected by disabling replica forwarding + * and explicitly invoking subsequent setSettings operations + */ public function testSubsequentSetSettingsWithoutWaitThrowsException(): void { $this->resetOperationState(); - + $storeId = 1; $settings = ['attributesToRetrieve' => ['name']]; $this->indexOptions->method('getStoreId')->willReturn($storeId); + + // Disable forwarding for explicit test $this->config->method('shouldForwardPrimaryIndexSettingsToReplicas') ->willReturn(false); // First call should succeed $this->handler->setSettings($this->indexOptions, $settings); - $this->markBatchCompleted($storeId); - + // Second call without wait should throw exception $this->expectException(\RuntimeException::class); $this->expectExceptionMessage("Cannot call setSettings on store $storeId: previous operation still pending. Call waitLastTask first."); - + $this->handler->setSettings($this->indexOptions, $settings); } + /** + * Explicitly test the state machine succeeds by disabling replica forwarding + * and explicitly invoking the wait operation + * */ public function testSubsequentSetSettingsAfterWaitSucceeds(): void { $this->resetOperationState(); - + $storeId = 1; $settings = ['attributesToRetrieve' => ['name']]; $this->indexOptions->method('getStoreId')->willReturn($storeId); + // Disable forwarding for explicit test $this->config->method('shouldForwardPrimaryIndexSettingsToReplicas') ->willReturn(false); - // Configure connector to expect two setSettings calls and one waitLastTask call $this->connector->expects($this->exactly(2)) ->method('setSettings'); - + $this->connector->expects($this->once()) ->method('waitLastTask') ->with($storeId); // First call should succeed $this->handler->setSettings($this->indexOptions, $settings); - $this->markBatchCompleted($storeId); - + // Wait for the task $this->connector->waitLastTask($storeId); - + // Second call after wait should succeed $this->handler->setSettings($this->indexOptions, $settings); - $this->markBatchCompleted($storeId); } public function testDifferentStoreIdsDontInterfere(): void { $this->resetOperationState(); - + $storeId1 = 1; $storeId2 = 2; $settings = ['attributesToRetrieve' => ['name']]; $indexOptions1 = $this->createMock(IndexOptionsInterface::class); $indexOptions1->method('getStoreId')->willReturn($storeId1); - + $indexOptions2 = $this->createMock(IndexOptionsInterface::class); $indexOptions2->method('getStoreId')->willReturn($storeId2); @@ -324,15 +314,19 @@ public function testDifferentStoreIdsDontInterfere(): void ->method('setSettings'); $this->handler->setSettings($indexOptions1, $settings); - $this->markBatchCompleted($storeId1); $this->handler->setSettings($indexOptions2, $settings); - $this->markBatchCompleted($storeId2); } + /** + * Replica forwarding should abstract the wait operation internally + * However require caller to invoke wait for subsequent ops + * This is *by design* to minimize unnecessary IO blocking + * This test ensures this logic stays in place + */ public function testForwardingEnabledMultipleCallsRequireWait(): void { $this->resetOperationState(); - + $storeId = 1; $settings = [ 'customRanking' => ['desc(price)'], @@ -343,14 +337,17 @@ public function testForwardingEnabledMultipleCallsRequireWait(): void $this->config->method('shouldForwardPrimaryIndexSettingsToReplicas') ->willReturn(true); + // 2 internal calls + 1 explicit call + $this->connector->expects($this->exactly(3)) + ->method('setSettings'); + // First call makes two internal setSettings calls $this->handler->setSettings($this->indexOptions, $settings); - $this->markBatchCompleted($storeId); - - // Second call should fail because no wait was called + + // Second call to handler should fail because no wait was called $this->expectException(\RuntimeException::class); $this->expectExceptionMessage("Cannot call setSettings on store $storeId: previous operation still pending. Call waitLastTask first."); - + $this->handler->setSettings($this->indexOptions, $settings); } } From 3a92122ad0b818b80f4edc914beab794e5e114dc Mon Sep 17 00:00:00 2001 From: Eric Wright Date: Fri, 19 Sep 2025 14:02:32 -0400 Subject: [PATCH 20/43] MAGE-1427 Allow override of facet sort by renderingContent --- Service/Product/FacetBuilder.php | 2 +- view/frontend/web/js/instantsearch.js | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Service/Product/FacetBuilder.php b/Service/Product/FacetBuilder.php index 244e464aa..474faf826 100644 --- a/Service/Product/FacetBuilder.php +++ b/Service/Product/FacetBuilder.php @@ -95,7 +95,7 @@ protected function getRenderingContentValues(array $attributes): array { return array_combine( $attributes, - array_fill(0, count($attributes), [ 'sortRemainingBy' => 'alpha' ]) + array_fill(0, count($attributes), [ 'sortRemainingBy' => 'count' ]) ); } diff --git a/view/frontend/web/js/instantsearch.js b/view/frontend/web/js/instantsearch.js index 4cf433935..045bfed26 100644 --- a/view/frontend/web/js/instantsearch.js +++ b/view/frontend/web/js/instantsearch.js @@ -663,14 +663,21 @@ define([ }, getRefinementListOptions(facet) { - return { + const options = { container : this.getFacetContainer(facet), attribute : facet.attribute, limit : algoliaConfig.maxValuesPerFacet, templates : this.getRefinementsListTemplates(), - sortBy : ['count:desc', 'name:asc'], panelOptions: this.getRefinementFacetPanelOptions(facet) }; + if (!algoliaConfig.instant.isDynamicFacetsEnabled) { + options['sortBy'] = this.getFacetSortBy() + } + return options; + }, + + getFacetSortBy() { + return ['count:desc', 'name:asc']; }, getRefinementFacetPanelOptions(facet) { From c00a843b53adf3af30eae1478d66ac0e68553794 Mon Sep 17 00:00:00 2001 From: Eric Wright Date: Fri, 19 Sep 2025 17:54:43 -0400 Subject: [PATCH 21/43] MAGE-1426 Add integration test for renderingContent --- .../Indexing/Config/ConfigTest.php | 88 ++++++++++++++----- 1 file changed, 66 insertions(+), 22 deletions(-) diff --git a/Test/Integration/Indexing/Config/ConfigTest.php b/Test/Integration/Indexing/Config/ConfigTest.php index d20367662..2f6af8375 100644 --- a/Test/Integration/Indexing/Config/ConfigTest.php +++ b/Test/Integration/Indexing/Config/ConfigTest.php @@ -3,18 +3,24 @@ namespace Algolia\AlgoliaSearch\Test\Integration\Indexing\Config; use Algolia\AlgoliaSearch\Exceptions\AlgoliaException; +use Algolia\AlgoliaSearch\Exceptions\ExceededRetriesException; use Algolia\AlgoliaSearch\Model\IndicesConfigurator; use Algolia\AlgoliaSearch\Test\Integration\TestCase; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; class ConfigTest extends TestCase { + + /** + * @throws NoSuchEntityException + * @throws ExceededRetriesException + * @throws AlgoliaException + * @throws LocalizedException + */ public function testFacets() { - /** @var IndicesConfigurator $indicesConfigurator */ - $indicesConfigurator = $this->getObjectManager()->create(IndicesConfigurator::class); - $indicesConfigurator->saveConfigurationToAlgolia(1); - - $this->algoliaConnector->waitLastTask(); + $this->syncSettingsToAlgolia(); $indexOptions = $this->indexOptionsBuilder->buildWithEnforcedIndex($this->indexPrefix . 'default_products'); $indexSettings = $this->algoliaConnector->getSettings($indexOptions); @@ -22,13 +28,30 @@ public function testFacets() $this->assertEquals($this->assertValues->attributesForFaceting, count($indexSettings['attributesForFaceting'])); } - public function testQueryRules() + public function testRenderingContent() { - /** @var IndicesConfigurator $indicesConfigurator */ - $indicesConfigurator = $this->getObjectManager()->create(IndicesConfigurator::class); - $indicesConfigurator->saveConfigurationToAlgolia(1); + $this->setConfig('algoliasearch_instant/instant_facets/enable_dynamic_facets', '1'); - $this->algoliaConnector->waitLastTask(); + $this->syncSettingsToAlgolia(); + + $indexOptions = $this->indexOptionsBuilder->buildWithEnforcedIndex($this->indexPrefix . 'default_products'); + $indexSettings = $this->algoliaConnector->getSettings($indexOptions); + + $renderingContent = $indexSettings['renderingContent']['facetOrdering']['values'] ?? null; + $this->assertNotNull($renderingContent, "Rendering content not found in product index"); + $this->assertEqualsCanonicalizing(['categories.level0', 'color', 'price.EUR.default', 'price.USD.default'], array_keys($renderingContent), "Expected facets not found in renderingContent"); + $this->assertEquals('count', $renderingContent['color']['sortRemainingBy'], "Default sort not set to count"); + } + + /** + * @throws NoSuchEntityException + * @throws ExceededRetriesException + * @throws AlgoliaException + * @throws LocalizedException + */ + public function testQueryRules() + { + $this->syncSettingsToAlgolia(); $client = $this->algoliaConnector->getClient(); @@ -94,6 +117,12 @@ public function testReplicaCreationWithCustomerGroups() $this->replicaCreationTest(true); } + /** + * @throws ExceededRetriesException + * @throws AlgoliaException + * @throws LocalizedException + * @throws NoSuchEntityException + */ private function replicaCreationTest($withCustomerGroups = false) { $enableCustomGroups = '0'; @@ -133,11 +162,7 @@ private function replicaCreationTest($withCustomerGroups = false) $this->indexPrefix . 'default_products_created_at_desc' => 'desc(created_at)', ]; - /** @var IndicesConfigurator $indicesConfigurator */ - $indicesConfigurator = $this->getObjectManager()->create(IndicesConfigurator::class); - $indicesConfigurator->saveConfigurationToAlgolia(1); - - $this->algoliaConnector->waitLastTask(); + $this->syncSettingsToAlgolia(); $indices = $this->algoliaConnector->listIndexes(); $indicesNames = array_map(fn($indexData) => $indexData['name'], $indices['items']); @@ -151,13 +176,15 @@ private function replicaCreationTest($withCustomerGroups = false) } } + /** + * @throws ExceededRetriesException + * @throws LocalizedException + * @throws NoSuchEntityException + * @throws AlgoliaException + */ public function testExtraSettings() { - /** @var IndicesConfigurator $indicesConfigurator */ - $indicesConfigurator = $this->getObjectManager()->create(IndicesConfigurator::class); - - $indicesConfigurator->saveConfigurationToAlgolia(1); - $this->algoliaConnector->waitLastTask(); + $this->syncSettingsToAlgolia(); $sections = ['products', 'categories', 'pages', 'suggestions']; @@ -182,8 +209,7 @@ public function testExtraSettings() $this->setConfig('algoliasearch_extra_settings/extra_settings/' . $section . '_extra_settings', '{"exactOnSingleWordQuery":"word"}'); } - $indicesConfigurator->saveConfigurationToAlgolia(1); - $this->algoliaConnector->waitLastTask(); + $this->syncSettingsToAlgolia(); foreach ($sections as $section) { $indexName = $this->indexPrefix . 'default_' . $section; @@ -223,4 +249,22 @@ public function testInvalidExtraSettings() $this->fail('AlgoliaException was not raised'); } + + /** + * @throws NoSuchEntityException + * @throws ExceededRetriesException + * @throws AlgoliaException + * @throws LocalizedException + */ + protected function syncSettingsToAlgolia(int $storeId = 1): IndicesConfigurator + { + /** @var IndicesConfigurator $indicesConfigurator */ + $indicesConfigurator = $this->getObjectManager()->get(IndicesConfigurator::class); + $indicesConfigurator->saveConfigurationToAlgolia($storeId); + + $this->algoliaConnector->waitLastTask(); + + return $indicesConfigurator; // return for reuse (as needed) + } + } From 9f08182efd122ea4793de06c8f56daeb3c5c78a3 Mon Sep 17 00:00:00 2001 From: Eric Wright Date: Fri, 19 Sep 2025 18:24:25 -0400 Subject: [PATCH 22/43] MAGE-1426 Suppress Codacy false positive --- Test/Unit/Service/IndexSettingsHandlerTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/Test/Unit/Service/IndexSettingsHandlerTest.php b/Test/Unit/Service/IndexSettingsHandlerTest.php index efbbc3f53..628db33af 100644 --- a/Test/Unit/Service/IndexSettingsHandlerTest.php +++ b/Test/Unit/Service/IndexSettingsHandlerTest.php @@ -54,6 +54,7 @@ private function setupStateMachineMock(): void if ($this->operationState[$storeId]['setSettingsCalled'] && !$this->operationState[$storeId]['waitCalled']) { throw new \RuntimeException( + // phpcs:ignore "Cannot call setSettings on store $storeId: previous operation still pending. Call waitLastTask first." ); } From 52b13dfb1df9c5789583f10f60f68062f57ec9d7 Mon Sep 17 00:00:00 2001 From: Eric Wright Date: Thu, 25 Sep 2025 08:30:16 -0400 Subject: [PATCH 23/43] MAGE-1422 Product URL fix for 3.16.1 (#1825) * MAGE-1422 Restrict CI to release and main * MAGE-1422 Fix product getUrl method by aligning with Magento core behavior (#1796) This commit adds the UrlRewrite::REDIRECT_TYPE => 0 parameter to the Algolia product URL filter data, ensuring that only non-redirect URL rewrites are considered when generating product URLs. This aligns the Algolia URL generation with Magento's core URL handling behavior and prevents potential issues with redirect loops or incorrect URL generation. * MAGE-1422 Update change log * MAGE-1422 Relax branch match pattern * MAGE-1422 Remove workflow branch filter * MAGE-1422 Fix branch matching --------- Co-authored-by: fasimana --- .circleci/config.yml | 6 +++++- CHANGELOG.md | 5 +++++ Model/Product/Url.php | 1 + 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index b3c389a1c..2e26777a0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -159,7 +159,7 @@ jobs: working_directory: ~/Sites command: | bin/cli bash -c "cd ./dev/tests/integration && export $(cat .env | xargs) && ../../../vendor/bin/phpunit --debug --exclude-group problematic ../../../vendor/algolia/algoliasearch-magento-2/Test/Integration/" - + notify: docker: - image: cimg/base:current @@ -173,6 +173,10 @@ jobs: workflows: magento-build-and-test-workflow: + when: + matches: + pattern: "^(feat|fix|chore)/MAGE.*" + value: << pipeline.git.branch >> jobs: - magento-build: matrix: diff --git a/CHANGELOG.md b/CHANGELOG.md index 3464ba3c1..7c86b3e53 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # CHANGE LOG +## 3.16.1 + +### Bug fixes +- Ensure that only non-redirect URL rewrites are considered when generating product URLs - thank you @fasimana + ## 3.16.0 ### Features diff --git a/Model/Product/Url.php b/Model/Product/Url.php index 53f92e564..a43da214b 100755 --- a/Model/Product/Url.php +++ b/Model/Product/Url.php @@ -67,6 +67,7 @@ public function getUrl(Product $product, $params = []) UrlRewrite::ENTITY_ID => $product->getId(), UrlRewrite::ENTITY_TYPE => ProductUrlRewriteGenerator::ENTITY_TYPE, UrlRewrite::STORE_ID => $storeId, + UrlRewrite::REDIRECT_TYPE => 0, ]; if ($categoryId) { $filterData[UrlRewrite::METADATA]['category_id'] = $categoryId; From 8616ed3011015042a51513244290c5385f8be9c5 Mon Sep 17 00:00:00 2001 From: PromInc Date: Thu, 25 Sep 2025 08:13:07 -0500 Subject: [PATCH 24/43] MAGE-1419 Prevent 422 Insights Event - Add to Cart with Discount (#1780) Adding a product to the cart may trigger a PHP and Algolia error due to the discount amount being too large. ## PHP Error ``` main.CRITICAL: Unable to send add to cart event due to Algolia events model misconfiguration: Discount must be a valid decimal number and total length must be no longer than 16 characters [] [] ``` --- Service/Insights/EventProcessor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Service/Insights/EventProcessor.php b/Service/Insights/EventProcessor.php index 0c2bb2ba1..9b207ec15 100644 --- a/Service/Insights/EventProcessor.php +++ b/Service/Insights/EventProcessor.php @@ -262,7 +262,7 @@ protected function getQuoteItemSalePrice(Item $item): float */ protected function getQuoteItemDiscount(Item $item): float { - return floatval($item->getProduct()->getPrice()) - $this->getQuoteItemSalePrice($item); + return round(floatval($item->getProduct()->getPrice()) - $this->getQuoteItemSalePrice($item),2); } /** From 083380753572ea7f65fb2c0b3543b4cb41e77fdb Mon Sep 17 00:00:00 2001 From: Eric Wright Date: Fri, 26 Sep 2025 17:28:18 -0400 Subject: [PATCH 25/43] MAGE-1419 Add unit test class for EventProcessor --- .../Service/Insights/EventProcessorTest.php | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 Test/Unit/Service/Insights/EventProcessorTest.php diff --git a/Test/Unit/Service/Insights/EventProcessorTest.php b/Test/Unit/Service/Insights/EventProcessorTest.php new file mode 100644 index 000000000..b5a8d5b38 --- /dev/null +++ b/Test/Unit/Service/Insights/EventProcessorTest.php @@ -0,0 +1,77 @@ +insightsClient = $this->createMock(InsightsClient::class); + $this->storeManager = $this->createMock(StoreManagerInterface::class); + $this->store = $this->createMock(Store::class); + $this->currency = $this->createMock(Currency::class); + + $this->eventProcessor = new EventProcessor(); + } + + // Test dependency validation and setup methods + + public function testConvertedObjectIDsAfterSearchThrowsExceptionWhenClientMissing(): void + { + $this->expectException(AlgoliaException::class); + $this->expectExceptionMessage("Events model is missing necessary dependencies to function."); + + $this->eventProcessor->convertedObjectIDsAfterSearch( + 'test-event', + 'test-index', + ['1', '2', '3'], + 'query-123' + ); + } + + public function testConvertedObjectIDsAfterSearchThrowsExceptionWhenUserTokenMissing(): void + { + $this->eventProcessor->setInsightsClient($this->insightsClient); + + $this->expectException(AlgoliaException::class); + $this->expectExceptionMessage("Events model is missing necessary dependencies to function."); + + $this->eventProcessor->convertedObjectIDsAfterSearch( + 'test-event', + 'test-index', + ['1', '2', '3'], + 'query-123' + ); + } + + public function testConvertedObjectIDsAfterSearchThrowsExceptionWhenStoreManagerMissing(): void + { + $this->eventProcessor + ->setInsightsClient($this->insightsClient) + ->setAnonymousUserToken('user-token'); + + $this->expectException(AlgoliaException::class); + $this->expectExceptionMessage("Events model is missing necessary dependencies to function."); + + $this->eventProcessor->convertedObjectIDsAfterSearch( + 'test-event', + 'test-index', + ['1', '2', '3'], + 'query-123' + ); + } +} From 26ac911c633eb6b23840fd35a6acf7d09f12cc83 Mon Sep 17 00:00:00 2001 From: Eric Wright Date: Sat, 27 Sep 2025 12:15:18 -0400 Subject: [PATCH 26/43] MAGE-1419 Test add to cart conversion --- .../Service/Insights/EventProcessorTest.php | 197 ++++++++++++++++++ 1 file changed, 197 insertions(+) diff --git a/Test/Unit/Service/Insights/EventProcessorTest.php b/Test/Unit/Service/Insights/EventProcessorTest.php index b5a8d5b38..a35e3fdb9 100644 --- a/Test/Unit/Service/Insights/EventProcessorTest.php +++ b/Test/Unit/Service/Insights/EventProcessorTest.php @@ -2,10 +2,17 @@ namespace Algolia\AlgoliaSearch\Test\Unit\Service\Insights; +use Algolia\AlgoliaSearch\Api\Insights\EventProcessorInterface; use Algolia\AlgoliaSearch\Api\InsightsClient; use Algolia\AlgoliaSearch\Exceptions\AlgoliaException; +use Algolia\AlgoliaSearch\Helper\InsightsHelper; use Algolia\AlgoliaSearch\Service\Insights\EventProcessor; +use Magento\Catalog\Model\Product; use Magento\Directory\Model\Currency; +use Magento\Framework\Exception\LocalizedException; +use Magento\Quote\Model\Quote\Item; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Item as OrderItem; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; use PHPUnit\Framework\TestCase; @@ -74,4 +81,194 @@ public function testConvertedObjectIDsAfterSearchThrowsExceptionWhenStoreManager 'query-123' ); } + + // Test convertedObjectIDsAfterSearch + + public function testConvertedObjectIDsAfterSearchWithAllDependencies(): void + { + $this->setupFullyConfiguredEventProcessor(); + + $this->insightsClient + ->expects($this->once()) + ->method('pushEvents') + ->with( + $this->callback(function ($payload) { + $this->assertArrayHasKey('events', $payload); + $this->assertCount(1, $payload['events']); + + $event = $payload['events'][0]; + $this->assertEquals('conversion', $event['eventType']); + $this->assertEquals('test-event', $event['eventName']); + $this->assertEquals('test-index', $event['index']); + $this->assertEquals('user-token', $event['userToken']); + $this->assertEquals(['1', '2', '3'], $event['objectIDs']); + $this->assertEquals('query-123', $event['queryID']); + + return true; + }), + [] + ) + ->willReturn(['status' => 'ok']); + + $result = $this->eventProcessor->convertedObjectIDsAfterSearch( + 'test-event', + 'test-index', + ['1', '2', '3'], + 'query-123' + ); + } + + public function testConvertedObjectIDsAfterSearchWithAuthenticatedToken(): void + { + $this->setupFullyConfiguredEventProcessor(); + $this->eventProcessor->setAuthenticatedUserToken('auth-token-123'); + + $this->insightsClient + ->expects($this->once()) + ->method('pushEvents') + ->with( + $this->callback(function ($payload) { + $event = $payload['events'][0]; + $this->assertEquals('auth-token-123', $event['authenticatedUserToken']); + return true; + }), + [] + ) + ->willReturn(['status' => 'ok']); + + $this->eventProcessor->convertedObjectIDsAfterSearch( + 'test-event', + 'test-index', + ['1'], + 'query-123' + ); + } + + // Test convertedObjectIDs + + public function testConvertedObjectIDs(): void + { + $this->setupFullyConfiguredEventProcessor(); + + $this->insightsClient + ->expects($this->once()) + ->method('pushEvents') + ->with( + $this->callback(function ($payload) { + $event = $payload['events'][0]; + $this->assertEquals('conversion', $event['eventType']); + $this->assertEquals('test-event', $event['eventName']); + $this->assertEquals('test-index', $event['index']); + $this->assertEquals(['1', '2'], $event['objectIDs']); + $this->assertArrayNotHasKey('queryID', $event); + + return true; + }), + [] + ) + ->willReturn(['status' => 'ok']); + + $this->eventProcessor->convertedObjectIDs( + 'test-event', + 'test-index', + ['1', '2'] + ); + } + + // Test convertAddToCart + + public function testConvertAddToCart(): void + { + $this->setupFullyConfiguredEventProcessor(); + + $product = $this->createMock(Product::class); + $product->method('getId')->willReturn('123'); + $product->method('getPrice')->willReturn(100.0); + + $item = $this->createMock(Item::class); + $item->method('getProduct')->willReturn($product); + $item->method('getData') + ->willReturnMap([ + ['base_price', null, 85.0], + ['qty_to_add', null, 2] + ]); + $item->method('getPrice')->willReturn(85.0); + + $this->insightsClient + ->expects($this->once()) + ->method('pushEvents') + ->with( + $this->callback(function ($payload) { + $event = $payload['events'][0]; + $this->assertEquals('addToCart', $event['eventSubtype']); + $this->assertEquals(['123'], $event['objectIDs']); + $this->assertEquals('USD', $event['currency']); + $this->assertEquals(170.0, $event['value']); // 85 * 2 + $this->assertEquals([['price' => 85.0, 'discount' => 15.0, 'quantity' => 2]], $event['objectData']); + $this->assertEquals('query-456', $event['queryID']); + + return true; + }), + [] + ) + ->willReturn(['status' => 'ok']); + + $this->eventProcessor->convertAddToCart( + 'add-to-cart-event', + 'products-index', + $item, + 'query-456' + ); + } + + public function testConvertAddToCartWithoutQueryID(): void + { + $this->setupFullyConfiguredEventProcessor(); + + $product = $this->createMock(Product::class); + $product->method('getId')->willReturn('123'); + $product->method('getPrice')->willReturn(100.0); + + $item = $this->createMock(Item::class); + $item->method('getProduct')->willReturn($product); + $item->method('getData') + ->willReturnMap([ + ['base_price', null, 80.0], + ['qty_to_add', null, 1] + ]); + $item->method('getPrice')->willReturn(80.0); + + $this->insightsClient + ->expects($this->once()) + ->method('pushEvents') + ->with( + $this->callback(function ($payload) { + $event = $payload['events'][0]; + $this->assertArrayNotHasKey('queryID', $event); + return true; + }), + [] + ) + ->willReturn(['status' => 'ok']); + + $this->eventProcessor->convertAddToCart( + 'add-to-cart-event', + 'products-index', + $item + ); + } + + // Helper methods + + private function setupFullyConfiguredEventProcessor(): void + { + $this->currency->method('getCode')->willReturn('USD'); + $this->store->method('getCurrentCurrency')->willReturn($this->currency); + $this->storeManager->method('getStore')->willReturn($this->store); + + $this->eventProcessor + ->setInsightsClient($this->insightsClient) + ->setAnonymousUserToken('user-token') + ->setStoreManager($this->storeManager); + } } From bdcf3a1dca69e3ae7cf5b4f952ee208b0d1284e4 Mon Sep 17 00:00:00 2001 From: Eric Wright Date: Sat, 27 Sep 2025 12:53:06 -0400 Subject: [PATCH 27/43] MAGE-1419 Test purchase conversion --- .../Service/Insights/EventProcessorTest.php | 99 ++++++++++++++++++- 1 file changed, 98 insertions(+), 1 deletion(-) diff --git a/Test/Unit/Service/Insights/EventProcessorTest.php b/Test/Unit/Service/Insights/EventProcessorTest.php index a35e3fdb9..a4aed2726 100644 --- a/Test/Unit/Service/Insights/EventProcessorTest.php +++ b/Test/Unit/Service/Insights/EventProcessorTest.php @@ -87,7 +87,7 @@ public function testConvertedObjectIDsAfterSearchThrowsExceptionWhenStoreManager public function testConvertedObjectIDsAfterSearchWithAllDependencies(): void { $this->setupFullyConfiguredEventProcessor(); - + $this->insightsClient ->expects($this->once()) ->method('pushEvents') @@ -258,6 +258,84 @@ public function testConvertAddToCartWithoutQueryID(): void ); } + // Test convertPurchaseForItems + public function testConvertPurchaseForItems(): void + { + $this->setupFullyConfiguredEventProcessor(); + + $items = $this->createOrderItems([ + ['id' => '1', 'price' => 50.0, 'originalPrice' => 60.0, 'cartDiscountAmount' => 10.0, 'qtyOrdered' => 2], + ['id' => '2', 'price' => 30.0, 'originalPrice' => 35.0, 'cartDiscountAmount' => 5.0, 'qtyOrdered' => 1], + ]); + + $this->insightsClient + ->expects($this->once()) + ->method('pushEvents') + ->with( + $this->callback(function ($payload) { + $event = $payload['events'][0]; + $this->assertEquals('purchase', $event['eventSubtype']); + $this->assertEquals(['1', '2'], $event['objectIDs']); + $this->assertEquals('USD', $event['currency']); + $this->assertEquals(115.0, $event['value']); // (50 * 2 - 10) + (30 - 5) + $this->assertEquals('query-789', $event['queryID']); + + $objectData = $event['objectData']; + $this->assertCount(2, $objectData); + $this->assertEquals(45, $objectData[0]['price']); // 50 - (10/2) + $this->assertEquals(15, $objectData[0]['discount']); // 30 - (5/1) + $this->assertEquals(2, $objectData[0]['quantity']); + + return true; + }), + [] + ) + ->willReturn(['status' => 'ok']); + + $this->eventProcessor->convertPurchaseForItems( + 'purchase-event', + 'products-index', + $items, + 'query-789' + ); + } + + public function testConvertPurchaseForItemsEnforcesObjectLimit(): void + { + $this->setupFullyConfiguredEventProcessor(); + + // Create more items than the limit allows + $itemsData = []; + for ($i = 1; $i <= 25; $i++) { + $itemsData[] = ['id' => (string) $i, 'price' => 10.0, 'originalPrice' => 10.0, 'cartDiscountAmount' => 0.0, 'qtyOrdered' => 1]; + } + $items = $this->createOrderItems($itemsData); + + $this->insightsClient + ->expects($this->once()) + ->method('pushEvents') + ->with( + $this->callback(function ($payload) { + $event = $payload['events'][0]; + // Should be limited to MAX_OBJECT_IDS_PER_EVENT (20) + $this->assertCount(EventProcessorInterface::MAX_OBJECT_IDS_PER_EVENT, $event['objectIDs']); + $this->assertCount(EventProcessorInterface::MAX_OBJECT_IDS_PER_EVENT, $event['objectData']); + // But value should include all 25 items + $this->assertEquals(250.0, $event['value']); // 25 * 10 + + return true; + }), + [] + ) + ->willReturn(['status' => 'ok']); + + $this->eventProcessor->convertPurchaseForItems( + 'purchase-event', + 'products-index', + $items + ); + } + // Helper methods private function setupFullyConfiguredEventProcessor(): void @@ -271,4 +349,23 @@ private function setupFullyConfiguredEventProcessor(): void ->setAnonymousUserToken('user-token') ->setStoreManager($this->storeManager); } + + private function createOrderItems(array $itemsData): array + { + $items = []; + foreach ($itemsData as $data) { + $product = $this->createMock(Product::class); + $product->method('getId')->willReturn($data['id']); + + $item = $this->createMock(OrderItem::class); + $item->method('getProduct')->willReturn($product); + $item->method('getPrice')->willReturn($data['price']); + $item->method('getOriginalPrice')->willReturn($data['originalPrice']); + $item->method('getDiscountAmount')->willReturn($data['cartDiscountAmount']); + $item->method('getQtyOrdered')->willReturn($data['qtyOrdered']); + + $items[] = $item; + } + return $items; + } } From b6a9eb0b1e900eb797b7d96d0613d14ca26db721 Mon Sep 17 00:00:00 2001 From: Eric Wright Date: Sat, 27 Sep 2025 18:14:33 -0400 Subject: [PATCH 28/43] MAGE-1419 Refactor and merge unit tests for 3.16 --- Service/Insights/EventProcessor.php | 36 +- Test/Unit/Service/EventProcessorTest.php | 188 --------- .../Service/Insights/EventProcessorTest.php | 382 +++++++++++++++++- .../{ => Insights}/EventProcessorTestable.php | 2 +- 4 files changed, 405 insertions(+), 203 deletions(-) delete mode 100644 Test/Unit/Service/EventProcessorTest.php rename Test/Unit/Service/{ => Insights}/EventProcessorTestable.php (86%) diff --git a/Service/Insights/EventProcessor.php b/Service/Insights/EventProcessor.php index 9b207ec15..7b56681b1 100644 --- a/Service/Insights/EventProcessor.php +++ b/Service/Insights/EventProcessor.php @@ -18,6 +18,14 @@ class EventProcessor implements EventProcessorInterface /** @var string */ protected const NO_QUERY_ID_KEY = '__NO_QUERY_ID__'; + /** + * A higher precision is used by default for currency rounding + * KWD (Kuwaiti Dinar), BHD (Bahraini Dinar), JOD (Jordanian Dinar) require up to 3 decimal places + * Override this as needed or apply plugin on the applyPrecision method + */ + /** @var int */ + protected const DECIMAL_PRECISION_SCALE = 3; + public function __construct( protected TaxConfig $taxConfig, protected ?InsightsClient $client = null, @@ -238,10 +246,11 @@ protected function restrictMaxObjectsPerEvent(array $items): array */ protected function getTotalRevenueForEvent(array $objectData): float { - return array_reduce( + $total = array_reduce( $objectData, fn($carry, $item) => floatval($carry) + floatval($item['quantity']) * floatval($item['price']) ); + return $this->applyPrecision($total); } /** @@ -262,7 +271,7 @@ protected function getQuoteItemSalePrice(Item $item): float */ protected function getQuoteItemDiscount(Item $item): float { - return round(floatval($item->getProduct()->getPrice()) - $this->getQuoteItemSalePrice($item),2); + return $this->applyPrecision($item->getProduct()->getPrice() - $this->getQuoteItemSalePrice($item)); } /** @@ -271,18 +280,21 @@ protected function getQuoteItemDiscount(Item $item): float */ protected function getOrderItemSalePrice(OrderItem $item): float { - return $this->taxConfig->priceIncludesTax($this->storeManager->getStore()->getId()) ? + $value = $this->taxConfig->priceIncludesTax($this->storeManager->getStore()->getId()) ? floatval($item->getPriceInclTax()) - $this->getOrderItemCartDiscount($item): floatval($item->getPrice()) - $this->getOrderItemCartDiscount($item); + return $this->applyPrecision($value); } /** + * Get discount for line item for a single product (qty = 1) which is what Algolia uses + * Line item discount retrieved from Magento for a cart rule is for all products (discount * qty) in the line item * @param OrderItem $item * @return float */ protected function getOrderItemCartDiscount(OrderItem $item): float { - return floatval($item->getDiscountAmount()) / intval($item->getQtyOrdered()); + return $this->applyPrecision(floatval($item->getDiscountAmount()) / intval($item->getQtyOrdered())); } /** @@ -294,7 +306,7 @@ protected function getOrderItemDiscount(OrderItem $item): float $itemDiscount = $this->taxConfig->priceIncludesTax($this->storeManager->getStore()->getId()) ? floatval($item->getOriginalPrice()) - floatval($item->getPriceInclTax()) : floatval($item->getOriginalPrice()) - floatval($item->getPrice()); - return $itemDiscount + $this->getOrderItemCartDiscount($item); + return $this->applyPrecision($itemDiscount + $this->getOrderItemCartDiscount($item)); } /** @@ -348,4 +360,18 @@ protected function getItemsByQueryId(Order $order): array return $itemsByQueryId; } + + /** + * A public method is provided to easily override this behavior as needed via plugins + * as different currencies may have different precision requirements + * e.g. + * Some currencies have rounding rules (e.g., CHF (Swiss Franc) often rounds to 0.05 for cash) + * KWD (Kuwaiti Dinar), BHD (Bahraini Dinar), JOD (Jordanian Dinar) → have 1,000 fils per unit + * JPY (Japanese Yen), KRW (Korean Won) do not use cents at all + * + */ + public function applyPrecision(float $value): float + { + return round($value, self::DECIMAL_PRECISION_SCALE); + } } diff --git a/Test/Unit/Service/EventProcessorTest.php b/Test/Unit/Service/EventProcessorTest.php deleted file mode 100644 index b05ddec05..000000000 --- a/Test/Unit/Service/EventProcessorTest.php +++ /dev/null @@ -1,188 +0,0 @@ -client = $this->createMock(InsightsClient::class); - $this->userToken = 'foo'; - $this->authenticatedUserToken = 'authenticated-foo'; - $this->storeManager = $this->createMock(StoreManagerInterface::class); - - $store = $this->createMock(Store::class); - $store->method('getId')->willReturn(1); - $this->storeManager->method('getStore')->willReturn($store); - $this->taxConfig = $this->createMock(TaxConfig::class); - - $this->eventProcessor = new EventProcessorTestable( - $this->taxConfig, - $this->client, - $this->userToken, - $this->authenticatedUserToken, - $this->storeManager - ); - } - - /** - * @dataProvider orderItemsProvider - */ - public function testObjectDataForPurchase($priceIncludesTax, $orderItemsData, $expectedResult, $expectedTotalRevenue): void - { - $this->taxConfig->method('priceIncludesTax')->willReturn($priceIncludesTax); - - $orderItems = []; - - foreach ($orderItemsData as $orderItemData) { - $orderItem = $this->getMockBuilder(OrderItem::class) - ->disableOriginalConstructor() - ->getMock(); - - foreach ($orderItemData as $method => $value){ - $orderItem->method($method)->willReturn($value); - } - - $orderItems[] = $orderItem; - } - - $object = $this->eventProcessor->getObjectDataForPurchase($orderItems); - $this->assertEquals($expectedResult, $object); - - $totalRevenue = $this->eventProcessor->getTotalRevenueForEvent($object); - $this->assertEquals($expectedTotalRevenue, $totalRevenue); - } - - public static function orderItemsProvider(): array - { - return [ - [ // One item - 'priceIncludesTax' => true, - 'orderItemsData' => [ - [ - 'getPrice' => 32.00, - 'getPriceInclTax' => 32.00, - 'getOriginalPrice' => 32.00, - 'getDiscountAmount' => 0.00, - 'getQtyOrdered' => 1, - ] - ], - 'expectedResult' => [ - [ - 'price' => 32.00, - 'discount' => 0.00, - 'quantity' => 1, - ] - ], - 'expectedTotalRevenue' => 32.00 - ], - [ // One item (tax excluded) - 'priceIncludesTax' => false, - 'orderItemsData' => [ - [ - 'getPrice' => 25.00, - 'getPriceInclTax' => 32.00, - 'getOriginalPrice' => 25.00, - 'getDiscountAmount' => 0.00, - 'getQtyOrdered' => 1, - ] - ], - 'expectedResult' => [ - [ - 'price' => 25.00, - 'discount' => 0.00, - 'quantity' => 1, - ] - ], - 'expectedTotalRevenue' => 25.00 - ], - [ // One item with discount - 'priceIncludesTax' => true, - 'orderItemsData' => [ - [ - 'getPrice' => 32.00, - 'getPriceInclTax' => 32.00, - 'getOriginalPrice' => 32.00, - 'getDiscountAmount' => 7.00, - 'getQtyOrdered' => 1, - ] - ], - 'expectedResult' => [ - [ - 'price' => 25.00, - 'discount' => 7.00, - 'quantity' => 1, - ] - ], - 'expectedTotalRevenue' => 25.00 - ], - [ // One item with discount (tax excluded) - 'priceIncludesTax' => false, - 'orderItemsData' => [ - [ - 'getPrice' => 25.00, - 'getPriceInclTax' => 32.00, - 'getOriginalPrice' => 25.00, - 'getDiscountAmount' => 7.00, - 'getQtyOrdered' => 1, - ] - ], - 'expectedResult' => [ - [ - 'price' => 18.00, - 'discount' => 7.00, - 'quantity' => 1, - ] - ], - 'expectedTotalRevenue' => 18.00 - ], - [ // Two items - 'priceIncludesTax' => true, - 'orderItemsData' => [ - [ - 'getPrice' => 32.00, - 'getPriceInclTax' => 32.00, - 'getOriginalPrice' => 32.00, - 'getDiscountAmount' => 7.00, - 'getQtyOrdered' => 1, - ], - [ - 'getPrice' => 32.00, - 'getPriceInclTax' => 32.00, - 'getOriginalPrice' => 32.00, - 'getDiscountAmount' => 0.00, - 'getQtyOrdered' => 2, - ], - ], - 'expectedResult' => [ - [ - 'price' => 25.00, - 'discount' => 7.00, - 'quantity' => 1, - ], - [ - 'price' => 32.00, - 'discount' => 0.00, - 'quantity' => 2, - ] - ], - 'expectedTotalRevenue' => 89.00 // 25 + 32*2 - ], - ]; - } -} diff --git a/Test/Unit/Service/Insights/EventProcessorTest.php b/Test/Unit/Service/Insights/EventProcessorTest.php index a4aed2726..f557ab3bd 100644 --- a/Test/Unit/Service/Insights/EventProcessorTest.php +++ b/Test/Unit/Service/Insights/EventProcessorTest.php @@ -9,30 +9,35 @@ use Algolia\AlgoliaSearch\Service\Insights\EventProcessor; use Magento\Catalog\Model\Product; use Magento\Directory\Model\Currency; -use Magento\Framework\Exception\LocalizedException; use Magento\Quote\Model\Quote\Item; use Magento\Sales\Model\Order; use Magento\Sales\Model\Order\Item as OrderItem; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; +use Magento\Tax\Model\Config as TaxConfig; use PHPUnit\Framework\TestCase; class EventProcessorTest extends TestCase { - private ?EventProcessor $eventProcessor = null; - private ?InsightsClient $insightsClient = null; - private ?StoreManagerInterface $storeManager = null; - private ?Store $store = null; - private ?Currency $currency = null; + protected ?TaxConfig $taxConfig; + protected ?EventProcessor $eventProcessor = null; + protected ?InsightsClient $insightsClient = null; + protected ?StoreManagerInterface $storeManager = null; + protected ?Store $store = null; + protected ?Currency $currency = null; public function setUp(): void { $this->insightsClient = $this->createMock(InsightsClient::class); + $this->storeManager = $this->createMock(StoreManagerInterface::class); $this->store = $this->createMock(Store::class); + $this->store->method('getId')->willReturn(1); + $this->currency = $this->createMock(Currency::class); + $this->taxConfig = $this->createMock(TaxConfig::class); - $this->eventProcessor = new EventProcessor(); + $this->eventProcessor = new EventProcessorTestable($this->taxConfig); } // Test dependency validation and setup methods @@ -258,6 +263,44 @@ public function testConvertAddToCartWithoutQueryID(): void ); } + public function testConvertAddToCartFloatingPointPrecision(): void + { + $this->setupFullyConfiguredEventProcessor(); + + $product = $this->createMock(Product::class); + $product->method('getId')->willReturn('123'); + $product->method('getPrice')->willReturn(23.99); + + $item = $this->createMock(Item::class); + $item->method('getProduct')->willReturn($product); + $item->method('getData') + ->willReturnMap([ + ['base_price', null, 23.93], + ['qty_to_add', null, 1] + ]); + $item->method('getPrice')->willReturn(23.93); + + $this->insightsClient + ->expects($this->once()) + ->method('pushEvents') + ->with( + $this->callback(function ($payload) { + $event = $payload['events'][0]; + $this->assertEquals([['price' => 23.93, 'discount' => .06, 'quantity' => 1]], $event['objectData']); + return true; + }), + [] + ) + ->willReturn(['status' => 'ok']); + + $this->eventProcessor->convertAddToCart( + 'add-to-cart-event', + 'products-index', + $item + ); + + } + // Test convertPurchaseForItems public function testConvertPurchaseForItems(): void { @@ -336,9 +379,308 @@ public function testConvertPurchaseForItemsEnforcesObjectLimit(): void ); } + public function testConvertPurchaseForItemsFloatingPointPrecision(): void + { + $this->setupFullyConfiguredEventProcessor(); + + // These additions should trigger floating point precision errors if rounding is not applied + $items = $this->createOrderItems([ + ['id' => '1', 'price' => 10.10, 'originalPrice' => 15.00, 'cartDiscountAmount' => 0, 'qtyOrdered' => 1], + ['id' => '2', 'price' => 33.20, 'originalPrice' => 35.00, 'cartDiscountAmount' => 0, 'qtyOrdered' => 1], + ]); + + $this->insightsClient + ->expects($this->once()) + ->method('pushEvents') + ->with( + $this->callback(function ($payload) { + $event = $payload['events'][0]; + $this->assertEquals(43.30, $event['value']); // 10.10 + 33.20 + + $objectData = $event['objectData']; + $this->assertEquals(['price' => 10.10, 'discount' => 4.90, 'quantity' => 1], $objectData[0]); // 15.00 - 10.10 + $this->assertEquals(['price' => 33.20, 'discount' => 1.80, 'quantity' => 1], $objectData[1]); // 35.00 - 33.20 + + return true; + }), + [] + ) + ->willReturn(['status' => 'ok']); + + $this->eventProcessor->convertPurchaseForItems( + 'purchase-event', + 'products-index', + $items, + 'query-123' + ); + } + + /** + * The way discounts are recorded in Algolia are per product but Magento is per line item + * This test ensures the expected values are returned and that binary math does not impact the final value + */ + public function testConvertPurchaseForItemsFloatingPointPrecisionWithCartDiscount(): void + { + $this->setupFullyConfiguredEventProcessor(); + + // These additions should trigger floating point precision errors if rounding is not applied + $items = $this->createOrderItems([ + ['id' => '1', 'price' => 10.00, 'originalPrice' => 10.00, 'cartDiscountAmount' => .30, 'qtyOrdered' => 3], + ]); + + $this->insightsClient + ->expects($this->once()) + ->method('pushEvents') + ->with( + $this->callback(function ($payload) { + $event = $payload['events'][0]; + $this->assertEquals(29.70, $event['value']); // 10.00 * 3 + + $objectData = $event['objectData']; + $this->assertEquals(['price' => 9.90, 'discount' => .10, 'quantity' => 3], $objectData[0]); + + return true; + }), + [] + ) + ->willReturn(['status' => 'ok']); + + $this->eventProcessor->convertPurchaseForItems( + 'purchase-event', + 'products-index', + $items, + 'query-123' + ); + } + + // Test convertPurchase + + public function testConvertPurchaseGroupsByQueryID(): void + { + $this->setupFullyConfiguredEventProcessor(); + + $order = $this->createMock(Order::class); + + $items = [ + $this->createOrderItemWithQueryId('1', 'query-1', 50.0, 1), + $this->createOrderItemWithQueryId('2', 'query-1', 30.0, 1), + $this->createOrderItemWithQueryId('3', 'query-2', 25.0, 2), + $this->createOrderItemWithQueryId('4', null, 15.0, 1), // No query ID + ]; + + $order->method('getAllVisibleItems')->willReturn($items); + + $this->insightsClient + ->expects($this->once()) + ->method('pushEvents') + ->with( + $this->callback(function ($payload) { + // There should be 3 different events for each query ID scenario: query-1, query-2, and no-query + $events = $payload['events']; + $this->assertCount(3, $events); + $this->assertEquals(80.0, $events[0]['value']); // 50 + 30 + $this->assertEquals(50.0, $events[1]['value']); // 25 * 2 + $this->assertEquals(15.0, $events[2]['value']); // missing query ID should be final event + return true; + }), + [] + ) + ->willReturn(['status' => 'ok']); + + $result = $this->eventProcessor->convertPurchase( + 'purchase-event', + 'products-index', + $order + ); + // 3 events in one batch + $this->assertCount(1, $result); + } + + public function testConvertPurchaseHandlesLargeOrders(): void + { + $this->setupFullyConfiguredEventProcessor(); + + $order = $this->createMock(Order::class); + + // Create more events than MAX_EVENTS_PER_REQUEST allows + $items = []; + for ($i = 1; $i <= 1500; $i++) { + $items[] = $this->createOrderItemWithQueryId((string)$i, "query-$i", 10.0, 1); + } + + $order->method('getAllVisibleItems')->willReturn($items); + + // Should be called twice due to chunking (1000 + 500) + $this->insightsClient + ->expects($this->exactly(2)) + ->method('pushEvents') + ->willReturn(['status' => 'ok']); + + $result = $this->eventProcessor->convertPurchase( + 'purchase-event', + 'products-index', + $order + ); + + $this->assertCount(2, $result); // 2 chunks + } + + + // Test protected methods + + /** + * @dataProvider orderItemsProvider + */ + public function testObjectDataForPurchase($priceIncludesTax, $orderItemsData, $expectedResult, $expectedTotalRevenue): void + { + $this->setupFullyConfiguredEventProcessor(); + + $this->taxConfig->method('priceIncludesTax')->willReturn($priceIncludesTax); + + $orderItems = []; + + foreach ($orderItemsData as $orderItemData) { + $orderItem = $this->getMockBuilder(OrderItem::class) + ->disableOriginalConstructor() + ->getMock(); + + foreach ($orderItemData as $method => $value){ + $orderItem->method($method)->willReturn($value); + } + + $orderItems[] = $orderItem; + } + + $object = $this->eventProcessor->getObjectDataForPurchase($orderItems); + $this->assertEquals($expectedResult, $object); + + $totalRevenue = $this->eventProcessor->getTotalRevenueForEvent($object); + $this->assertEquals($expectedTotalRevenue, $totalRevenue); + } + + // Data providers + + public static function orderItemsProvider(): array + { + return [ + [ // One item + 'priceIncludesTax' => true, + 'orderItemsData' => [ + [ + 'getPrice' => 32.00, + 'getPriceInclTax' => 32.00, + 'getOriginalPrice' => 32.00, + 'getDiscountAmount' => 0.00, + 'getQtyOrdered' => 1, + ] + ], + 'expectedResult' => [ + [ + 'price' => 32.00, + 'discount' => 0.00, + 'quantity' => 1, + ] + ], + 'expectedTotalRevenue' => 32.00 + ], + [ // One item (tax excluded) + 'priceIncludesTax' => false, + 'orderItemsData' => [ + [ + 'getPrice' => 25.00, + 'getPriceInclTax' => 32.00, + 'getOriginalPrice' => 25.00, + 'getDiscountAmount' => 0.00, + 'getQtyOrdered' => 1, + ] + ], + 'expectedResult' => [ + [ + 'price' => 25.00, + 'discount' => 0.00, + 'quantity' => 1, + ] + ], + 'expectedTotalRevenue' => 25.00 + ], + [ // One item with discount + 'priceIncludesTax' => true, + 'orderItemsData' => [ + [ + 'getPrice' => 32.00, + 'getPriceInclTax' => 32.00, + 'getOriginalPrice' => 32.00, + 'getDiscountAmount' => 7.00, + 'getQtyOrdered' => 1, + ] + ], + 'expectedResult' => [ + [ + 'price' => 25.00, + 'discount' => 7.00, + 'quantity' => 1, + ] + ], + 'expectedTotalRevenue' => 25.00 + ], + [ // One item with discount (tax excluded) + 'priceIncludesTax' => false, + 'orderItemsData' => [ + [ + 'getPrice' => 25.00, + 'getPriceInclTax' => 32.00, + 'getOriginalPrice' => 25.00, + 'getDiscountAmount' => 7.00, + 'getQtyOrdered' => 1, + ] + ], + 'expectedResult' => [ + [ + 'price' => 18.00, + 'discount' => 7.00, + 'quantity' => 1, + ] + ], + 'expectedTotalRevenue' => 18.00 + ], + [ // Two items + 'priceIncludesTax' => true, + 'orderItemsData' => [ + [ + 'getPrice' => 32.00, + 'getPriceInclTax' => 32.00, + 'getOriginalPrice' => 32.00, + 'getDiscountAmount' => 7.00, + 'getQtyOrdered' => 1, + ], + [ + 'getPrice' => 32.00, + 'getPriceInclTax' => 32.00, + 'getOriginalPrice' => 32.00, + 'getDiscountAmount' => 0.00, + 'getQtyOrdered' => 2, + ], + ], + 'expectedResult' => [ + [ + 'price' => 25.00, + 'discount' => 7.00, + 'quantity' => 1, + ], + [ + 'price' => 32.00, + 'discount' => 0.00, + 'quantity' => 2, + ] + ], + 'expectedTotalRevenue' => 89.00 // 25 + 32*2 + ], + ]; + } + // Helper methods - private function setupFullyConfiguredEventProcessor(): void + protected function setupFullyConfiguredEventProcessor(): void { $this->currency->method('getCode')->willReturn('USD'); $this->store->method('getCurrentCurrency')->willReturn($this->currency); @@ -350,7 +692,7 @@ private function setupFullyConfiguredEventProcessor(): void ->setStoreManager($this->storeManager); } - private function createOrderItems(array $itemsData): array + protected function createOrderItems(array $itemsData): array { $items = []; foreach ($itemsData as $data) { @@ -368,4 +710,26 @@ private function createOrderItems(array $itemsData): array } return $items; } + + protected function createOrderItemWithQueryId(string $id, ?string $queryId, float $price, int $qty): OrderItem + { + $product = $this->createMock(Product::class); + $product->method('getId')->willReturn($id); + + $item = $this->createMock(OrderItem::class); + $item->method('getProduct')->willReturn($product); + $item->method('getPrice')->willReturn($price); + $item->method('getOriginalPrice')->willReturn($price); + $item->method('getDiscountAmount')->willReturn(0.0); + $item->method('getQtyOrdered')->willReturn($qty); + + if ($queryId !== null) { + $item->method('hasData')->with(InsightsHelper::QUOTE_ITEM_QUERY_PARAM)->willReturn(true); + $item->method('getData')->with(InsightsHelper::QUOTE_ITEM_QUERY_PARAM)->willReturn($queryId); + } else { + $item->method('hasData')->with(InsightsHelper::QUOTE_ITEM_QUERY_PARAM)->willReturn(false); + } + + return $item; + } } diff --git a/Test/Unit/Service/EventProcessorTestable.php b/Test/Unit/Service/Insights/EventProcessorTestable.php similarity index 86% rename from Test/Unit/Service/EventProcessorTestable.php rename to Test/Unit/Service/Insights/EventProcessorTestable.php index 5a1e68493..e7ef1e2b5 100644 --- a/Test/Unit/Service/EventProcessorTestable.php +++ b/Test/Unit/Service/Insights/EventProcessorTestable.php @@ -1,6 +1,6 @@ Date: Sun, 28 Sep 2025 23:42:20 -0400 Subject: [PATCH 29/43] MAGE-1419 Switch to type casting --- Service/Insights/EventProcessor.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Service/Insights/EventProcessor.php b/Service/Insights/EventProcessor.php index 7b56681b1..b28f561dd 100644 --- a/Service/Insights/EventProcessor.php +++ b/Service/Insights/EventProcessor.php @@ -143,7 +143,7 @@ public function convertAddToCart(string $eventName, string $indexName, Item $ite $this->checkDependencies(); $price = $this->getQuoteItemSalePrice($item); - $qty = intval($item->getData('qty_to_add')); + $qty = (int) $item->getData('qty_to_add'); $event = [ self::EVENT_KEY_SUBTYPE => self::EVENT_SUBTYPE_CART, @@ -248,7 +248,7 @@ protected function getTotalRevenueForEvent(array $objectData): float { $total = array_reduce( $objectData, - fn($carry, $item) => floatval($carry) + floatval($item['quantity']) * floatval($item['price']) + fn($carry, $item) => (float) $carry + (float) $item['quantity'] * (float) $item['price'] ); return $this->applyPrecision($total); } @@ -262,7 +262,7 @@ protected function getTotalRevenueForEvent(array $objectData): float */ protected function getQuoteItemSalePrice(Item $item): float { - return floatval($item->getData('base_price') ?? $item->getPrice()); + return (float) ($item->getData('base_price') ?? $item->getPrice()); } /** @@ -281,8 +281,8 @@ protected function getQuoteItemDiscount(Item $item): float protected function getOrderItemSalePrice(OrderItem $item): float { $value = $this->taxConfig->priceIncludesTax($this->storeManager->getStore()->getId()) ? - floatval($item->getPriceInclTax()) - $this->getOrderItemCartDiscount($item): - floatval($item->getPrice()) - $this->getOrderItemCartDiscount($item); + (float) $item->getPriceInclTax() - $this->getOrderItemCartDiscount($item): + (float) $item->getPrice() - $this->getOrderItemCartDiscount($item); return $this->applyPrecision($value); } @@ -294,7 +294,7 @@ protected function getOrderItemSalePrice(OrderItem $item): float */ protected function getOrderItemCartDiscount(OrderItem $item): float { - return $this->applyPrecision(floatval($item->getDiscountAmount()) / intval($item->getQtyOrdered())); + return $this->applyPrecision((float) $item->getDiscountAmount() / (int) $item->getQtyOrdered()); } /** @@ -304,8 +304,8 @@ protected function getOrderItemCartDiscount(OrderItem $item): float protected function getOrderItemDiscount(OrderItem $item): float { $itemDiscount = $this->taxConfig->priceIncludesTax($this->storeManager->getStore()->getId()) ? - floatval($item->getOriginalPrice()) - floatval($item->getPriceInclTax()) : - floatval($item->getOriginalPrice()) - floatval($item->getPrice()); + (float) $item->getOriginalPrice() - (float) $item->getPriceInclTax() : + (float) $item->getOriginalPrice() - (float) $item->getPrice(); return $this->applyPrecision($itemDiscount + $this->getOrderItemCartDiscount($item)); } @@ -322,7 +322,7 @@ protected function getObjectDataForPurchase(array $items): array return array_map(fn($item) => [ 'price' => $this->getOrderItemSalePrice($item), 'discount' => max(0, $this->getOrderItemDiscount($item)), - 'quantity' => intval($item->getQtyOrdered()) + 'quantity' => (int) $item->getQtyOrdered() ], $items); } @@ -332,7 +332,7 @@ protected function getObjectDataForPurchase(array $items): array */ protected function getObjectIdsForPurchase(array $items): array { - return array_map(fn($item) => $item->getProduct()->getId(), $items); + return array_map(fn($item) => (int) $item->getProduct()->getId(), $items); } From 7e53e39eb4d5078b78ca1701d79330f2872734bc Mon Sep 17 00:00:00 2001 From: pikulsky Date: Mon, 29 Sep 2025 18:50:19 +0400 Subject: [PATCH 30/43] Fix store id for queue job (#1769) * Follow camel case for store ID * Fix using queue job's store ID for jobs sorting --- Model/Queue.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Model/Queue.php b/Model/Queue.php index 9a1a08d7e..b3891fe0e 100644 --- a/Model/Queue.php +++ b/Model/Queue.php @@ -578,7 +578,7 @@ protected function stackSortedJobs(array $sortedJobs, array $tempSortableJobs, ? SORT_ASC, 'method', SORT_ASC, - 'store_id', + 'storeId', SORT_ASC, 'job_id', SORT_ASC From 4a2216dc0d953785f0afa1d6e8435668ad7304ff Mon Sep 17 00:00:00 2001 From: Damien Couchez Date: Mon, 29 Sep 2025 17:05:47 +0200 Subject: [PATCH 31/43] MAGE-1420: update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c86b3e53..f35a495ae 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Bug fixes - Ensure that only non-redirect URL rewrites are considered when generating product URLs - thank you @fasimana +- Fix store id for queue jobs sorting - thank you @pikulsky ## 3.16.0 From 5d195b8c0099ef988273c19c62d3ba9d3efd3643 Mon Sep 17 00:00:00 2001 From: Eric Wright Date: Mon, 29 Sep 2025 16:28:33 -0400 Subject: [PATCH 32/43] MAGE-1434 Remove StoreManager as runtime dependency --- Api/Insights/EventProcessorInterface.php | 1 + Service/Insights/EventProcessor.php | 10 ++++----- .../Service/Insights/EventProcessorTest.php | 21 +++++++------------ 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/Api/Insights/EventProcessorInterface.php b/Api/Insights/EventProcessorInterface.php index 54e5107e0..91ddc10c7 100644 --- a/Api/Insights/EventProcessorInterface.php +++ b/Api/Insights/EventProcessorInterface.php @@ -43,6 +43,7 @@ public function setAuthenticatedUserToken(string $token): EventProcessorInterfac public function setAnonymousUserToken(string $token): EventProcessorInterface; + /** @deprecated Store Manager now handled as injected dependency */ public function setStoreManager(StoreManagerInterface $storeManager): EventProcessorInterface; /** diff --git a/Service/Insights/EventProcessor.php b/Service/Insights/EventProcessor.php index b28f561dd..fac26e68a 100644 --- a/Service/Insights/EventProcessor.php +++ b/Service/Insights/EventProcessor.php @@ -27,11 +27,11 @@ class EventProcessor implements EventProcessorInterface protected const DECIMAL_PRECISION_SCALE = 3; public function __construct( - protected TaxConfig $taxConfig, - protected ?InsightsClient $client = null, - protected ?string $userToken = null, - protected ?string $authenticatedUserToken = null, - protected ?StoreManagerInterface $storeManager = null + protected TaxConfig $taxConfig, + protected StoreManagerInterface $storeManager, + protected ?InsightsClient $client = null, + protected ?string $userToken = null, + protected ?string $authenticatedUserToken = null, ) {} public function setInsightsClient(InsightsClient $client): EventProcessorInterface diff --git a/Test/Unit/Service/Insights/EventProcessorTest.php b/Test/Unit/Service/Insights/EventProcessorTest.php index f557ab3bd..bc64d00ea 100644 --- a/Test/Unit/Service/Insights/EventProcessorTest.php +++ b/Test/Unit/Service/Insights/EventProcessorTest.php @@ -19,7 +19,7 @@ class EventProcessorTest extends TestCase { - protected ?TaxConfig $taxConfig; + protected ?TaxConfig $taxConfig = null; protected ?EventProcessor $eventProcessor = null; protected ?InsightsClient $insightsClient = null; protected ?StoreManagerInterface $storeManager = null; @@ -28,21 +28,17 @@ class EventProcessorTest extends TestCase public function setUp(): void { - $this->insightsClient = $this->createMock(InsightsClient::class); - + $this->taxConfig = $this->createMock(TaxConfig::class); $this->storeManager = $this->createMock(StoreManagerInterface::class); $this->store = $this->createMock(Store::class); - $this->store->method('getId')->willReturn(1); - $this->currency = $this->createMock(Currency::class); - $this->taxConfig = $this->createMock(TaxConfig::class); - - $this->eventProcessor = new EventProcessorTestable($this->taxConfig); + $this->insightsClient = $this->createMock(InsightsClient::class); + $this->eventProcessor = new EventProcessorTestable($this->taxConfig, $this->storeManager); } // Test dependency validation and setup methods - public function testConvertedObjectIDsAfterSearchThrowsExceptionWhenClientMissing(): void + public function testConvertedObjectIDsAfterSearchThrowsExceptionWhenMissingDependencies(): void { $this->expectException(AlgoliaException::class); $this->expectExceptionMessage("Events model is missing necessary dependencies to function."); @@ -70,10 +66,9 @@ public function testConvertedObjectIDsAfterSearchThrowsExceptionWhenUserTokenMis ); } - public function testConvertedObjectIDsAfterSearchThrowsExceptionWhenStoreManagerMissing(): void + public function testConvertedObjectIDsAfterSearchThrowsExceptionWhenInsightsClientMissing(): void { $this->eventProcessor - ->setInsightsClient($this->insightsClient) ->setAnonymousUserToken('user-token'); $this->expectException(AlgoliaException::class); @@ -684,12 +679,12 @@ protected function setupFullyConfiguredEventProcessor(): void { $this->currency->method('getCode')->willReturn('USD'); $this->store->method('getCurrentCurrency')->willReturn($this->currency); + $this->store->method('getId')->willReturn(1); $this->storeManager->method('getStore')->willReturn($this->store); $this->eventProcessor ->setInsightsClient($this->insightsClient) - ->setAnonymousUserToken('user-token') - ->setStoreManager($this->storeManager); + ->setAnonymousUserToken('user-token'); } protected function createOrderItems(array $itemsData): array From 7740a03b435a824c24360a681e090db36c91617e Mon Sep 17 00:00:00 2001 From: Eric Wright Date: Mon, 29 Sep 2025 16:58:45 -0400 Subject: [PATCH 33/43] MAGE-1434 Utilize Magento derived locale for determining decimial precision --- Service/Insights/EventProcessor.php | 26 ++++++++++++------- .../Service/Insights/EventProcessorTest.php | 9 ++++--- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/Service/Insights/EventProcessor.php b/Service/Insights/EventProcessor.php index fac26e68a..19408c652 100644 --- a/Service/Insights/EventProcessor.php +++ b/Service/Insights/EventProcessor.php @@ -7,6 +7,8 @@ use Algolia\AlgoliaSearch\Exceptions\AlgoliaException; use Algolia\AlgoliaSearch\Helper\InsightsHelper; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Locale\FormatInterface as LocalFormatInterface; +use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Quote\Model\Quote\Item; use Magento\Sales\Model\Order; use Magento\Sales\Model\Order\Item as OrderItem; @@ -18,21 +20,24 @@ class EventProcessor implements EventProcessorInterface /** @var string */ protected const NO_QUERY_ID_KEY = '__NO_QUERY_ID__'; - /** - * A higher precision is used by default for currency rounding - * KWD (Kuwaiti Dinar), BHD (Bahraini Dinar), JOD (Jordanian Dinar) require up to 3 decimal places - * Override this as needed or apply plugin on the applyPrecision method - */ - /** @var int */ - protected const DECIMAL_PRECISION_SCALE = 3; + protected int $decimalPrecision; public function __construct( protected TaxConfig $taxConfig, protected StoreManagerInterface $storeManager, + protected LocalFormatInterface $localeFormat, protected ?InsightsClient $client = null, protected ?string $userToken = null, protected ?string $authenticatedUserToken = null, - ) {} + ) { + $this->initDecimalPrecision(); + } + + private function initDecimalPrecision(): void + { + $this->decimalPrecision = $this->localeFormat->getPriceFormat()['precision'] + ?? PriceCurrencyInterface::DEFAULT_PRECISION; + } public function setInsightsClient(InsightsClient $client): EventProcessorInterface { @@ -364,6 +369,9 @@ protected function getItemsByQueryId(Order $order): array /** * A public method is provided to easily override this behavior as needed via plugins * as different currencies may have different precision requirements + * Default behavior is to rely on the store locale and currency configuration via + * \Magento\Framework\Locale\FormatInterface + * * e.g. * Some currencies have rounding rules (e.g., CHF (Swiss Franc) often rounds to 0.05 for cash) * KWD (Kuwaiti Dinar), BHD (Bahraini Dinar), JOD (Jordanian Dinar) → have 1,000 fils per unit @@ -372,6 +380,6 @@ protected function getItemsByQueryId(Order $order): array */ public function applyPrecision(float $value): float { - return round($value, self::DECIMAL_PRECISION_SCALE); + return round($value, $this->decimalPrecision); } } diff --git a/Test/Unit/Service/Insights/EventProcessorTest.php b/Test/Unit/Service/Insights/EventProcessorTest.php index bc64d00ea..1297daddc 100644 --- a/Test/Unit/Service/Insights/EventProcessorTest.php +++ b/Test/Unit/Service/Insights/EventProcessorTest.php @@ -16,24 +16,27 @@ use Magento\Store\Model\StoreManagerInterface; use Magento\Tax\Model\Config as TaxConfig; use PHPUnit\Framework\TestCase; +use Magento\Framework\Locale\FormatInterface as LocaleFormatInterface; class EventProcessorTest extends TestCase { protected ?TaxConfig $taxConfig = null; - protected ?EventProcessor $eventProcessor = null; - protected ?InsightsClient $insightsClient = null; protected ?StoreManagerInterface $storeManager = null; protected ?Store $store = null; + protected ?LocaleFormatInterface $localeFormat = null; protected ?Currency $currency = null; + protected ?InsightsClient $insightsClient = null; + protected ?EventProcessor $eventProcessor = null; public function setUp(): void { $this->taxConfig = $this->createMock(TaxConfig::class); $this->storeManager = $this->createMock(StoreManagerInterface::class); $this->store = $this->createMock(Store::class); + $this->localeFormat = $this->createMock(LocaleFormatInterface::class); $this->currency = $this->createMock(Currency::class); $this->insightsClient = $this->createMock(InsightsClient::class); - $this->eventProcessor = new EventProcessorTestable($this->taxConfig, $this->storeManager); + $this->eventProcessor = new EventProcessorTestable($this->taxConfig, $this->storeManager, $this->localeFormat); } // Test dependency validation and setup methods From 96725aa09089e32fbef68f8576d6e5711ebe5de2 Mon Sep 17 00:00:00 2001 From: Eric Wright Date: Mon, 29 Sep 2025 18:57:06 -0400 Subject: [PATCH 34/43] MAGE-1434 Add tests for variable precision per locale --- Helper/InsightsHelper.php | 3 +- Service/Insights/EventProcessor.php | 6 +- .../Service/Insights/EventProcessorTest.php | 124 +++++++++++++----- .../Insights/EventProcessorTestable.php | 5 + 4 files changed, 98 insertions(+), 40 deletions(-) diff --git a/Helper/InsightsHelper.php b/Helper/InsightsHelper.php index c79088f66..35cd37d34 100644 --- a/Helper/InsightsHelper.php +++ b/Helper/InsightsHelper.php @@ -91,8 +91,7 @@ public function getEventProcessor(): EventProcessorInterface $this->eventProcessor = $this->eventProcessorFactory->create([ 'client' => $this->getInsightsClient(), 'userToken' => $this->getAnonymousUserToken(), - 'authenticatedUserToken' => $this->getAuthenticatedUserToken(), - 'storeManager' => $this->storeManager + 'authenticatedUserToken' => $this->getAuthenticatedUserToken() ]); } return $this->eventProcessor; diff --git a/Service/Insights/EventProcessor.php b/Service/Insights/EventProcessor.php index 19408c652..11c0b3fdc 100644 --- a/Service/Insights/EventProcessor.php +++ b/Service/Insights/EventProcessor.php @@ -33,9 +33,9 @@ public function __construct( $this->initDecimalPrecision(); } - private function initDecimalPrecision(): void + protected function initDecimalPrecision(): void { - $this->decimalPrecision = $this->localeFormat->getPriceFormat()['precision'] + $this->decimalPrecision = $this->localeFormat->getPriceFormat()['requiredPrecision'] ?? PriceCurrencyInterface::DEFAULT_PRECISION; } @@ -267,7 +267,7 @@ protected function getTotalRevenueForEvent(array $objectData): float */ protected function getQuoteItemSalePrice(Item $item): float { - return (float) ($item->getData('base_price') ?? $item->getPrice()); + return $this->applyPrecision((float) ($item->getData('base_price') ?? $item->getPrice())); } /** diff --git a/Test/Unit/Service/Insights/EventProcessorTest.php b/Test/Unit/Service/Insights/EventProcessorTest.php index 1297daddc..b6d40b4cf 100644 --- a/Test/Unit/Service/Insights/EventProcessorTest.php +++ b/Test/Unit/Service/Insights/EventProcessorTest.php @@ -184,18 +184,8 @@ public function testConvertAddToCart(): void { $this->setupFullyConfiguredEventProcessor(); - $product = $this->createMock(Product::class); - $product->method('getId')->willReturn('123'); - $product->method('getPrice')->willReturn(100.0); - - $item = $this->createMock(Item::class); - $item->method('getProduct')->willReturn($product); - $item->method('getData') - ->willReturnMap([ - ['base_price', null, 85.0], - ['qty_to_add', null, 2] - ]); - $item->method('getPrice')->willReturn(85.0); + $product = $this->createMockProduct('123', 100.0); + $item = $this->createMockItem($product, 85.0, 2); $this->insightsClient ->expects($this->once()) @@ -228,18 +218,8 @@ public function testConvertAddToCartWithoutQueryID(): void { $this->setupFullyConfiguredEventProcessor(); - $product = $this->createMock(Product::class); - $product->method('getId')->willReturn('123'); - $product->method('getPrice')->willReturn(100.0); - - $item = $this->createMock(Item::class); - $item->method('getProduct')->willReturn($product); - $item->method('getData') - ->willReturnMap([ - ['base_price', null, 80.0], - ['qty_to_add', null, 1] - ]); - $item->method('getPrice')->willReturn(80.0); + $product = $this->createMockProduct('123', 100.0); + $item = $this->createMockItem($product, 80.0, 1); $this->insightsClient ->expects($this->once()) @@ -265,18 +245,36 @@ public function testConvertAddToCartFloatingPointPrecision(): void { $this->setupFullyConfiguredEventProcessor(); - $product = $this->createMock(Product::class); - $product->method('getId')->willReturn('123'); - $product->method('getPrice')->willReturn(23.99); + $product = $this->createMockProduct('123', 23.99); + $item = $this->createMockItem($product, 23.93, 1); - $item = $this->createMock(Item::class); - $item->method('getProduct')->willReturn($product); - $item->method('getData') - ->willReturnMap([ - ['base_price', null, 23.93], - ['qty_to_add', null, 1] - ]); - $item->method('getPrice')->willReturn(23.93); + $this->insightsClient + ->expects($this->once()) + ->method('pushEvents') + ->with( + $this->callback(function ($payload) { + $event = $payload['events'][0]; + $this->assertEquals([['price' => 23.93, 'discount' => .06, 'quantity' => 1]], $event['objectData']); + return true; + }), + [] + ) + ->willReturn(['status' => 'ok']); + + $this->eventProcessor->convertAddToCart( + 'add-to-cart-event', + 'products-index', + $item + ); + } + + public function testConvertAddToCartWithStandardDecimalPrecision(): void + { + $this->setupFullyConfiguredEventProcessor(); + $this->setupCurrencyPrecision(2); + + $product = $this->createMockProduct('123', 23.992); + $item = $this->createMockItem($product, 23.931, 1); $this->insightsClient ->expects($this->once()) @@ -296,7 +294,34 @@ public function testConvertAddToCartFloatingPointPrecision(): void 'products-index', $item ); + } + + public function testConvertAddToCartWith3PointDecimalPrecision(): void + { + $this->setupFullyConfiguredEventProcessor(); + $this->setupCurrencyPrecision(3); + + $product = $this->createMockProduct('123', 23.992); + $item = $this->createMockItem($product, 23.931, 1); + $this->insightsClient + ->expects($this->once()) + ->method('pushEvents') + ->with( + $this->callback(function ($payload) { + $event = $payload['events'][0]; + $this->assertEquals([['price' => 23.931, 'discount' => .061, 'quantity' => 1]], $event['objectData']); + return true; + }), + [] + ) + ->willReturn(['status' => 'ok']); + + $this->eventProcessor->convertAddToCart( + 'add-to-cart-event', + 'products-index', + $item + ); } // Test convertPurchaseForItems @@ -690,6 +715,35 @@ protected function setupFullyConfiguredEventProcessor(): void ->setAnonymousUserToken('user-token'); } + protected function setupCurrencyPrecision(int $decimalPrecision = \Magento\Framework\Pricing\PriceCurrencyInterface::DEFAULT_PRECISION): void + { + $this->localeFormat->method('getPriceFormat')->willReturn([ + 'requiredPrecision' => $decimalPrecision + ]); + $this->eventProcessor->initDecimalPrecision(); + } + + protected function createMockProduct(string $id, float $price): Product + { + $product = $this->createMock(Product::class); + $product->method('getId')->willReturn($id); + $product->method('getPrice')->willReturn($price); + return $product; + } + + protected function createMockItem(Product $product, float $salePrice, int $qtyToAdd): Item + { + $item = $this->createMock(Item::class); + $item->method('getProduct')->willReturn($product); + $item->method('getData') + ->willReturnMap([ + ['base_price', null, $salePrice], + ['qty_to_add', null, $qtyToAdd] + ]); + $item->method('getPrice')->willReturn($salePrice); + return $item; + } + protected function createOrderItems(array $itemsData): array { $items = []; diff --git a/Test/Unit/Service/Insights/EventProcessorTestable.php b/Test/Unit/Service/Insights/EventProcessorTestable.php index e7ef1e2b5..334cc708c 100644 --- a/Test/Unit/Service/Insights/EventProcessorTestable.php +++ b/Test/Unit/Service/Insights/EventProcessorTestable.php @@ -15,4 +15,9 @@ public function getTotalRevenueForEvent(...$params): float { return parent::getTotalRevenueForEvent(...$params); } + + public function initDecimalPrecision(): void + { + parent::initDecimalPrecision(); + } } From 1f4c27086b29c20b94e788db95732ed54f5fb9d3 Mon Sep 17 00:00:00 2001 From: Eric Wright Date: Mon, 29 Sep 2025 18:57:29 -0400 Subject: [PATCH 35/43] MAGE-1434 Fix convert purchase object ID type casting bug --- Service/Insights/EventProcessor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Service/Insights/EventProcessor.php b/Service/Insights/EventProcessor.php index 11c0b3fdc..2ab37b66a 100644 --- a/Service/Insights/EventProcessor.php +++ b/Service/Insights/EventProcessor.php @@ -337,7 +337,7 @@ protected function getObjectDataForPurchase(array $items): array */ protected function getObjectIdsForPurchase(array $items): array { - return array_map(fn($item) => (int) $item->getProduct()->getId(), $items); + return array_map(fn($item) => (string) $item->getProduct()->getId(), $items); } From f2658ad450d0a7006fc893cac17a6899cb0b2649 Mon Sep 17 00:00:00 2001 From: Eric Wright Date: Mon, 29 Sep 2025 19:13:00 -0400 Subject: [PATCH 36/43] MAGE-1434 Update change log --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c86b3e53..d0467d382 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,13 @@ ## 3.16.1 +### Updates + +- `EventProcessor` now calculates decimal precision on currency based on `Magento\Framework\Locale\FormatInterface` + ### Bug fixes -- Ensure that only non-redirect URL rewrites are considered when generating product URLs - thank you @fasimana +- Ensure that only non-redirect URL rewrites are considered when generating product URLs - thank you @fasimana +- Apply rounding to insight events revenue values to avoid floating point precision errors - thank you @PromInc ## 3.16.0 From 295fe17ae8e22c01ba48f19ec6eea62c258ead01 Mon Sep 17 00:00:00 2001 From: Eric Wright Date: Mon, 29 Sep 2025 21:22:05 -0400 Subject: [PATCH 37/43] MAGE-1434 Add unit test for string IDs on request payload --- .../Service/Insights/EventProcessorTest.php | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/Test/Unit/Service/Insights/EventProcessorTest.php b/Test/Unit/Service/Insights/EventProcessorTest.php index b6d40b4cf..b47ca3829 100644 --- a/Test/Unit/Service/Insights/EventProcessorTest.php +++ b/Test/Unit/Service/Insights/EventProcessorTest.php @@ -548,6 +548,42 @@ public function testConvertPurchaseHandlesLargeOrders(): void $this->assertCount(2, $result); // 2 chunks } + /** Insights API requires that `objectIDs` be submitted as strings */ + public function testConvertPurchaseUsesStringIds(): void + { + $this->setupFullyConfiguredEventProcessor(); + + // These additions should trigger floating point precision errors if rounding is not applied + $items = $this->createOrderItems([ + ['id' => 10, 'price' => 10.00, 'originalPrice' => 10.00, 'cartDiscountAmount' => 0, 'qtyOrdered' => 1], + ['id' => '20', 'price' => 20.00, 'originalPrice' => 20.00, 'cartDiscountAmount' => 0, 'qtyOrdered' => 1], + ['id' => 30.0, 'price' => 30.00, 'originalPrice' => 30.00, 'cartDiscountAmount' => 0, 'qtyOrdered' => 1], + ]); + + $this->insightsClient + ->expects($this->once()) + ->method('pushEvents') + ->with( + $this->callback(function ($payload) { + $event = $payload['events'][0]; + $objectIds = $event['objectIDs']; + foreach ($objectIds as $objectId) { + $this->assertIsString($objectId); + } + return true; + }), + [] + ) + ->willReturn(['status' => 'ok']); + + $this->eventProcessor->convertPurchaseForItems( + 'purchase-event', + 'products-index', + $items, + 'query-123' + ); + } + // Test protected methods From 5bfab732abaf7e3fa9ade361a40a6ffa967cc13f Mon Sep 17 00:00:00 2001 From: Damien Couchez Date: Tue, 30 Sep 2025 16:09:33 +0200 Subject: [PATCH 38/43] MAGE-1416: update version files and changelog --- CHANGELOG.md | 11 +++++++++-- README.md | 2 +- composer.json | 2 +- etc/module.xml | 2 +- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c7b12f79..d61d555b1 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,13 +3,20 @@ ## 3.16.1 ### Updates - +- Add checks on configuration migration processed on data patch - `EventProcessor` now calculates decimal precision on currency based on `Magento\Framework\Locale\FormatInterface` +- Updated various unit/integration tests ### Bug fixes +- Fixed Indexing Queue display in backend templates. +- Fixed Indexing Queue merging mechanism, it should now have way better performances with delta indexing (updates) jobs. +- Fixed implicit nullable types for PHP 8.4 - Ensure that only non-redirect URL rewrites are considered when generating product URLs - thank you @fasimana - Apply rounding to insight events revenue values to avoid floating point precision errors - thank you @PromInc -- Fix store id for queue jobs sorting - thank you @pikulsky +- Fix store id for queue jobs sorting - thank you @pikulsky +- Fix issue where double conversion occurred during price indexing in case of multi currency stores - thank you @natedawg92 +- Fix issue where non-clickable links where rendered on InstantSearch - thank you @PromInc +- Fixed some Codacy issues ## 3.16.0 diff --git a/README.md b/README.md index d839f286d..c5bf56c57 100755 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Algolia Search & Discovery extension for Magento 2 ================================================== -![Latest version](https://img.shields.io/badge/latest-3.16.0-green) +![Latest version](https://img.shields.io/badge/latest-3.16.1-green) ![Magento 2](https://img.shields.io/badge/Magento-2.4.7+-orange) ![PHP](https://img.shields.io/badge/PHP-8.1%2C8.2%2C8.3%2C8.4-blue) diff --git a/composer.json b/composer.json index 495329623..3cc0eb501 100755 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "description": "Algolia Search & Discovery extension for Magento 2", "type": "magento2-module", "license": ["MIT"], - "version": "3.16.1-dev", + "version": "3.16.1", "require": { "php": "~8.2|~8.3|~8.4", "magento/framework": "~103.0", diff --git a/etc/module.xml b/etc/module.xml index 5d8ff68f3..bd179ae67 100755 --- a/etc/module.xml +++ b/etc/module.xml @@ -1,6 +1,6 @@ - + From 8db89f8212bd265eca31805672e3a8938e9eb24c Mon Sep 17 00:00:00 2001 From: Damien Couchez Date: Wed, 1 Oct 2025 14:07:29 +0200 Subject: [PATCH 39/43] MAGE-1420: rollback change on store_id job property --- CHANGELOG.md | 1 - Model/Queue.php | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d61d555b1..55aacba88 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,6 @@ - Fixed implicit nullable types for PHP 8.4 - Ensure that only non-redirect URL rewrites are considered when generating product URLs - thank you @fasimana - Apply rounding to insight events revenue values to avoid floating point precision errors - thank you @PromInc -- Fix store id for queue jobs sorting - thank you @pikulsky - Fix issue where double conversion occurred during price indexing in case of multi currency stores - thank you @natedawg92 - Fix issue where non-clickable links where rendered on InstantSearch - thank you @PromInc - Fixed some Codacy issues diff --git a/Model/Queue.php b/Model/Queue.php index b3891fe0e..9a1a08d7e 100644 --- a/Model/Queue.php +++ b/Model/Queue.php @@ -578,7 +578,7 @@ protected function stackSortedJobs(array $sortedJobs, array $tempSortableJobs, ? SORT_ASC, 'method', SORT_ASC, - 'storeId', + 'store_id', SORT_ASC, 'job_id', SORT_ASC From c65f2b1c9c2bda91b422d318d9a804869262439e Mon Sep 17 00:00:00 2001 From: Eric Wright Date: Thu, 2 Oct 2025 03:16:55 -0400 Subject: [PATCH 40/43] MAGE-1383 Add Codacy ignore patterns --- .codacy.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .codacy.yml diff --git a/.codacy.yml b/.codacy.yml new file mode 100644 index 000000000..ffa14d552 --- /dev/null +++ b/.codacy.yml @@ -0,0 +1,5 @@ +--- +exclude_paths: + - ".php-cs-fixer.php" + - "dev/**" + - "Test/**" From e7cc59f9084bc4a0a48f960edf20e089d15b0fc6 Mon Sep 17 00:00:00 2001 From: Damien Couchez Date: Fri, 14 Nov 2025 15:15:08 +0100 Subject: [PATCH 41/43] MAGE-1460: update version files and changelog --- CHANGELOG.md | 22 ++++++---------------- README.md | 2 +- composer.json | 2 +- etc/module.xml | 2 +- 4 files changed, 9 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf5766790..b16cdf9cb 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,21 +1,6 @@ # CHANGE LOG -## 3.17.0-dev - -### Bug fixes -- Fixed issue where missing pricing keys were not handled gracefully in the Autocomplete product template -- Fixed issue where category was not properly checked in the configuration block - thank you @benjamin-volle - -## 3.17.0-beta.2 - -### Updates -- Removed all `is_null` occurrences - -### Bug fixes -- Fixed 3.17 setup:upgrade on PHP 8.4 -- Fixed many Codacy issues - -## 3.17.0-beta.1 +## 3.17.0 ### Features - Added an Algolia indexing cache for storing metadata to prevent extra queries. Large collections that run periodic full indexes can benefit from this cache. @@ -30,9 +15,14 @@ - Updated default "Maximum number of records sent per indexing request" to 1000 (previously 300). - Updated `ConfigHelper` class, it now has more methods deprecated and ported to separate helper classes. - Updated Unit and Integration tests. +- Removed all `is_null` occurrences ### Bug fixes - Fixed indexing queue templates escaping. +- Fixed 3.17 setup:upgrade on PHP 8.4 +- Fixed many Codacy issues +- Fixed issue where missing pricing keys were not handled gracefully in the Autocomplete product template +- Fixed issue where category was not properly checked in the configuration block - thank you @benjamin-volle ## 3.16.1 diff --git a/README.md b/README.md index 558854773..da50999dd 100755 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Algolia Search & Discovery extension for Magento 2 ================================================== -![Latest version](https://img.shields.io/badge/latest-3.16.0-green) +![Latest version](https://img.shields.io/badge/latest-3.17.0-green) ![Magento 2](https://img.shields.io/badge/Magento-2.4.7+-orange) ![Beta version](https://img.shields.io/badge/beta-3.17.0--beta.1-purple) diff --git a/composer.json b/composer.json index 506307b03..3ad156dc2 100755 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "description": "Algolia Search & Discovery extension for Magento 2", "type": "magento2-module", "license": ["MIT"], - "version": "3.17.0-beta.2", + "version": "3.17.0", "require": { "php": "~8.2|~8.3|~8.4", "magento/framework": "~103.0", diff --git a/etc/module.xml b/etc/module.xml index b22b86847..f450526eb 100755 --- a/etc/module.xml +++ b/etc/module.xml @@ -1,6 +1,6 @@ - + From c3c441fbeb3d4249d23f0b8de9f12462ad93a6bd Mon Sep 17 00:00:00 2001 From: Damien Couchez Date: Tue, 18 Nov 2025 14:12:47 +0100 Subject: [PATCH 42/43] MAGE-1460: fix integration tests --- Test/Integration/Indexing/Config/ConfigTest.php | 8 +++++++- Test/Integration/Indexing/Queue/QueueTest.php | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Test/Integration/Indexing/Config/ConfigTest.php b/Test/Integration/Indexing/Config/ConfigTest.php index 2f6af8375..3cc32e90e 100644 --- a/Test/Integration/Indexing/Config/ConfigTest.php +++ b/Test/Integration/Indexing/Config/ConfigTest.php @@ -32,7 +32,13 @@ public function testRenderingContent() { $this->setConfig('algoliasearch_instant/instant_facets/enable_dynamic_facets', '1'); - $this->syncSettingsToAlgolia(); + try { + $this->syncSettingsToAlgolia(); + } catch (AlgoliaException $e) { + // Skip this test if the renderingContent feature isn't enabled on the application + $this->setConfig('algoliasearch_instant/instant_facets/enable_dynamic_facets', '0'); + $this->markTestSkipped($e->getMessage()); + } $indexOptions = $this->indexOptionsBuilder->buildWithEnforcedIndex($this->indexPrefix . 'default_products'); $indexSettings = $this->algoliaConnector->getSettings($indexOptions); diff --git a/Test/Integration/Indexing/Queue/QueueTest.php b/Test/Integration/Indexing/Queue/QueueTest.php index 00559137e..9eb931127 100644 --- a/Test/Integration/Indexing/Queue/QueueTest.php +++ b/Test/Integration/Indexing/Queue/QueueTest.php @@ -188,6 +188,7 @@ public function testSettings() ]); $this->setConfig(QueueHelper::IS_ACTIVE, '1'); + $this->setConfig(ConfigHelper::ENABLE_INDEXER_QUEUE, '1'); $this->connection->query('DELETE FROM algoliasearch_queue'); @@ -221,6 +222,7 @@ public function testSettings() public function testMergeSettings() { $this->setConfig(QueueHelper::IS_ACTIVE, '1'); + $this->setConfig(ConfigHelper::ENABLE_INDEXER_QUEUE, '1'); $this->setConfig(QueueHelper::NUMBER_OF_JOB_TO_RUN, 1); $this->setConfig(ConfigHelper::NUMBER_OF_ELEMENT_BY_PAGE, 300); From 5f62820e716ee6b3bdd0fcd203515df06f2941dd Mon Sep 17 00:00:00 2001 From: Damien Couchez Date: Tue, 18 Nov 2025 14:52:58 +0100 Subject: [PATCH 43/43] MAGE-1460: changes after review --- CHANGELOG.md | 2 +- README.md | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b16cdf9cb..95dfd4afa 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ ### Bug fixes - Fixed indexing queue templates escaping. - Fixed 3.17 setup:upgrade on PHP 8.4 -- Fixed many Codacy issues +- Performed code sanitization for Codacy compliance - Fixed issue where missing pricing keys were not handled gracefully in the Autocomplete product template - Fixed issue where category was not properly checked in the configuration block - thank you @benjamin-volle diff --git a/README.md b/README.md index da50999dd..eaead7ab1 100755 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ Algolia Search & Discovery extension for Magento 2 ![Latest version](https://img.shields.io/badge/latest-3.17.0-green) ![Magento 2](https://img.shields.io/badge/Magento-2.4.7+-orange) -![Beta version](https://img.shields.io/badge/beta-3.17.0--beta.1-purple) ![PHP](https://img.shields.io/badge/PHP-8.1%2C8.2%2C8.3%2C8.4-blue)