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 @@
-
+