diff --git a/Api/RecommendManagementInterface.php b/Api/RecommendManagementInterface.php new file mode 100644 index 000000000..83d444a83 --- /dev/null +++ b/Api/RecommendManagementInterface.php @@ -0,0 +1,29 @@ +client === null) { + $this->client = RecommendClient::create( + $this->configHelper->getApplicationID(), + $this->configHelper->getAPIKey() + ); + } + return $this->client; + } + + /** + * @param string $productId + * @return array + * @throws NoSuchEntityException + */ + public function getBoughtTogetherRecommendation(string $productId): array + { + return $this->getRecommendations($productId, 'bought-together'); + } + + /** + * @param string $productId + * @return array + * @throws NoSuchEntityException + */ + public function getRelatedProductsRecommendation(string $productId): array + { + return $this->getRecommendations($productId, 'related-products'); + } + + /** + * @return array + * @throws NoSuchEntityException + */ + public function getTrendingItemsRecommendation(): array + { + return $this->getRecommendations('', 'trending-items'); + } + + /** + * @param string $productId + * @return array + * @throws NoSuchEntityException + */ + public function getLookingSimilarRecommendation(string $productId): array + { + return $this->getRecommendations($productId, 'bought-together'); + } + + /** + * @param string $productId + * @param string $model + * @param float|int $threshold + * @return array + * @throws NoSuchEntityException + */ + protected function getRecommendations(string $productId, string $model, float|int $threshold = 50): array + { + $request['indexName'] = $this->indexNameFetcher->getIndexName('_products'); + $request['model'] = $model; + $request['threshold'] = $threshold; + if (!empty($productId)) { + $request['objectID'] = $productId; + } + + $client = $this->getClient(); + $recommendations = $client->getRecommendations( + [ + 'requests' => [ + $request + ], + ], + ); + + return $recommendations['results'][0] ?? []; + } +} diff --git a/Observer/RecommendSettings.php b/Observer/RecommendSettings.php new file mode 100644 index 000000000..a74462447 --- /dev/null +++ b/Observer/RecommendSettings.php @@ -0,0 +1,195 @@ +getData('changed_paths') as $changedPath) { + // Validate before enable FBT on PDP or on cart page + if (( + $changedPath == ConfigHelper::IS_RECOMMEND_FREQUENTLY_BOUGHT_TOGETHER_ENABLED + && $this->configHelper->isRecommendFrequentlyBroughtTogetherEnabled() + ) || ( + $changedPath == ConfigHelper::IS_RECOMMEND_FREQUENTLY_BOUGHT_TOGETHER_ENABLED_ON_CART_PAGE + && $this->configHelper->isRecommendFrequentlyBroughtTogetherEnabledOnCartPage() + )) { + $this->validateFrequentlyBroughtTogether($changedPath); + } + + // Validate before enable related products on PDP or on cart page + if (( + $changedPath == ConfigHelper::IS_RECOMMEND_RELATED_PRODUCTS_ENABLED + && $this->configHelper->isRecommendRelatedProductsEnabled() + ) || ( + $changedPath == ConfigHelper::IS_RECOMMEND_RELATED_PRODUCTS_ENABLED_ON_CART_PAGE + && $this->configHelper->isRecommendRelatedProductsEnabledOnCartPage() + )) { + $this->validateRelatedProducts($changedPath); + } + + // Validate before enable trending items on PDP or on cart page + if (( + $changedPath == ConfigHelper::IS_TREND_ITEMS_ENABLED_IN_PDP + && $this->configHelper->isTrendItemsEnabledInPDP() + ) || ( + $changedPath == ConfigHelper::IS_TREND_ITEMS_ENABLED_IN_SHOPPING_CART + && $this->configHelper->isTrendItemsEnabledInShoppingCart() + )) { + $this->validateTrendingItems($changedPath); + } + + // Validate before enable looking similar on PDP or on cart page + if (( + $changedPath == ConfigHelper::IS_LOOKING_SIMILAR_ENABLED_IN_PDP + && $this->configHelper->isLookingSimilarEnabledInPDP() + ) || ( + $changedPath == ConfigHelper::IS_LOOKING_SIMILAR_ENABLED_IN_SHOPPING_CART + && $this->configHelper->isLookingSimilarEnabledInShoppingCart() + )) { + $this->validateLookingSimilar($changedPath); + } + } + } + + /** + * @param string $changedPath + * @return void + * @throws LocalizedException + */ + protected function validateFrequentlyBroughtTogether(string $changedPath): void + { + $this->validateRecommendation($changedPath, 'getBoughtTogetherRecommendation', 'Frequently Bought Together'); + } + + /** + * @param string $changedPath + * @return void + * @throws LocalizedException + */ + protected function validateRelatedProducts(string $changedPath): void + { + $this->validateRecommendation($changedPath, 'getRelatedProductsRecommendation', 'Related Products'); + } + + /** + * @param string $changedPath + * @return void + * @throws LocalizedException + */ + protected function validateTrendingItems(string $changedPath): void + { + $this->validateRecommendation($changedPath, 'getTrendingItemsRecommendation', 'Trending Items'); + } + + /** + * @param string $changedPath + * @return void + * @throws LocalizedException + */ + protected function validateLookingSimilar(string $changedPath): void + { + $this->validateRecommendation($changedPath, 'getLookingSimilarRecommendation', 'Looking Similar'); + } + + /** + * @param string $changedPath - config path to be reverted if validation failed + * @param string $recommendationMethod - name of method to call to retrieve method from RecommendManagementInterface + * @param string $modelName - user friendly name to refer to model in error messaging + * @return void + * @throws LocalizedException + */ + protected function validateRecommendation(string $changedPath, string $recommendationMethod, string $modelName): void + { + try { + $recommendations = $this->recommendManagement->$recommendationMethod($this->getProductId()); + if (empty($recommendations['renderingContent'])) { + throw new LocalizedException(__( + "It appears that there is no trained model available for Algolia application ID %1.", + $this->configHelper->getApplicationID() + )); + } + } catch (\Exception $e) { + $this->configWriter->save($changedPath, 0); + throw new LocalizedException(__( + "Unable to save %1 Recommend configuration due to the following error: %2", + $modelName, + $e->getMessage() + ) + ); + } + } + + /** + * @return string - Product ID string for use in API calls + * @throws LocalizedException + */ + protected function getProductId(): string + { + if ($this->productId === '') { + $searchCriteria = $this->searchCriteriaBuilder + ->addFilter(self::STATUS, 1) + ->addFilter(self::QUANTITY_AND_STOCK_STATUS, 1) + ->addFilter( + self::VISIBILITY, + [ + Visibility::VISIBILITY_IN_CATALOG, + Visibility::VISIBILITY_IN_SEARCH, + Visibility::VISIBILITY_BOTH + ], + 'in') + ->setPageSize(1) + ->create(); + $result = $this->productRepository->getList($searchCriteria); + $items = $result->getItems(); + $firstProduct = reset($items); + if ($firstProduct) { + $this->productId = (string) $firstProduct->getId(); + } else { + throw new LocalizedException(__("Unable to locate product to validate Recommend model.")); + } + } + + return $this->productId; + } +} diff --git a/README.md b/README.md index 194a90780..141da2392 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.14.1-green) +![Latest version](https://img.shields.io/badge/latest-3.14.2-green) ![Magento 2](https://img.shields.io/badge/Magento-2.4.x-orange) ![PHP](https://img.shields.io/badge/PHP-8.1%2C8.2%2C8.3-blue) @@ -62,7 +62,7 @@ The easiest way to install the extension is to use [Composer](https://getcompose If you would like to stay on a minor version, please upgrade your composer to only accept minor versions. The following example will keep you on the minor version and will update patches automatically. -`"algolia/algoliasearch-magento-2": "~3.14.1"` +`"algolia/algoliasearch-magento-2": "~3.14.2"` ### Customisation diff --git a/composer.json b/composer.json index 983191af6..531855ccd 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.14.1", + "version": "3.14.2", "require": { "php": "~8.1|~8.2|~8.3", "magento/framework": "~103.0", diff --git a/etc/adminhtml/events.xml b/etc/adminhtml/events.xml index cdb5f09df..180028370 100755 --- a/etc/adminhtml/events.xml +++ b/etc/adminhtml/events.xml @@ -30,6 +30,9 @@ + + + diff --git a/etc/di.xml b/etc/di.xml index 2c43c8f87..b8aa0552f 100755 --- a/etc/di.xml +++ b/etc/di.xml @@ -26,6 +26,7 @@ + diff --git a/etc/module.xml b/etc/module.xml index f6707058e..9ae6e168d 100755 --- a/etc/module.xml +++ b/etc/module.xml @@ -1,6 +1,6 @@ - +