diff --git a/src/Adapter/MySQL.php b/src/Adapter/MySQL.php index 8af448ce0..b06d93da2 100644 --- a/src/Adapter/MySQL.php +++ b/src/Adapter/MySQL.php @@ -404,8 +404,13 @@ protected function computeOrderByField(array $filterToTableMapping) * to order products by their position in the search results we got from the core, with inverted order */ if ($orderField == 'p.position' && !empty($this->getInitialPopulation()->getFilters()['id_product']['='][0])) { - return 'FIELD(p.id_product,' . implode(',', $this->getInitialPopulation()->getFilters()['id_product']['='][0]) . ') ' . + $orderField = 'FIELD(p.id_product,' . implode(',', $this->getInitialPopulation()->getFilters()['id_product']['='][0]) . ') ' . ($this->getOrderDirection() === 'asc' ? 'DESC' : 'ASC'); + + // Push out-of-stock products to the end on search pages too. Without this, the relevance + // ordering was returned as is and bypassed the "out of stock last" behaviour that the + // category, manufacturer and other listings get below through computeShowLast(). + return $this->computeShowLast($orderField, $filterToTableMapping); } // Alter order by field and add some products to the end of the list, if required diff --git a/tests/php/FacetedSearch/Adapter/MySQLTest.php b/tests/php/FacetedSearch/Adapter/MySQLTest.php index 716c160f6..64f929b2c 100644 --- a/tests/php/FacetedSearch/Adapter/MySQLTest.php +++ b/tests/php/FacetedSearch/Adapter/MySQLTest.php @@ -78,6 +78,41 @@ public function testGetEmptyQuery() ); } + /** + * On search pages the products are ordered by their relevance (their position in the pool + * returned by the core search). The "out of stock last" behaviour must still be applied on + * top of that relevance ordering, exactly like on category/manufacturer listings. + * + * @see https://github.com/PrestaShop/ps_facetedsearch/issues/1236 + */ + public function testGetQueryAppliesOutOfStockLastOnSearchRelevanceOrdering() + { + $configurationMock = Mockery::mock(Configuration::class); + $configurationMock->shouldReceive('get') + ->with('PS_LAYERED_FILTER_SHOW_OUT_OF_STOCK_LAST') + ->andReturn(true); + Configuration::setStaticExpectations($configurationMock); + + $productMock = Mockery::namedMock(Product::class); + $productMock->shouldReceive('isAvailableWhenOutOfStock') + ->with(2) + ->andReturn(false); + + // A search query: the pool of product ids becomes the initial population and the order + // field is the relevance "position". + $this->adapter->addFilter('id_product', [1, 2, 3], '='); + $this->adapter->useFiltersAsInitialPopulation(); + $this->adapter->setOrderField('position'); + $this->adapter->setOrderDirection('asc'); + + $query = $this->adapter->getQuery(); + + // Out-of-stock products are pushed to the end... + $this->assertContains('IFNULL(p.quantity, 0) <= 0', $query); + // ...while the relevance ordering is preserved within each group. + $this->assertContains('FIELD(p.id_product,1,2,3) DESC', $query); + } + /** * @dataProvider oneSelectFieldDataProvider */