diff --git a/Console/Command/NostoClearQueueCommand.php b/Console/Command/NostoClearQueueCommand.php
index 87febbca3..31f71802a 100644
--- a/Console/Command/NostoClearQueueCommand.php
+++ b/Console/Command/NostoClearQueueCommand.php
@@ -27,6 +27,20 @@ class NostoClearQueueCommand extends Command
*/
public const NOSTO_DELETE_MESSAGE_QUEUE = 'nosto_product_sync.delete';
+ /**
+ * Nosto Product Sync Partial Price label.
+ *
+ * @var string
+ */
+ public const NOSTO_PRODUCT_PARTIAL_PRICE_QUEUE = 'nosto_product_sync.partial_price_update';
+
+ /**
+ * Nosto Product Sync Partial Inventory label.
+ *
+ * @var string
+ */
+ public const NOSTO_PRODUCT_PARTIAL_INVENTORY_QUEUE = 'nosto_product_sync.partial_inventory_update';
+
/**
* @var ConsumerConfig
*/
@@ -40,6 +54,8 @@ class NostoClearQueueCommand extends Command
private array $consumers = [
self::NOSTO_DELETE_MESSAGE_QUEUE,
self::NOSTO_UPDATE_SYNC_MESSAGE_QUEUE,
+ self::NOSTO_PRODUCT_PARTIAL_PRICE_QUEUE,
+ self::NOSTO_PRODUCT_PARTIAL_INVENTORY_QUEUE
];
/**
diff --git a/Model/Indexer/Partial/ProductInventoryIndexer.php b/Model/Indexer/Partial/ProductInventoryIndexer.php
new file mode 100644
index 000000000..84ae9c1c6
--- /dev/null
+++ b/Model/Indexer/Partial/ProductInventoryIndexer.php
@@ -0,0 +1,145 @@
+
+ * @copyright 2020 Nosto Solutions Ltd
+ * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause
+ *
+ */
+
+namespace Nosto\Tagging\Model\Indexer;
+
+use Exception;
+use Magento\Indexer\Model\ProcessManager;
+use Magento\Store\Model\App\Emulation;
+use Magento\Store\Model\Store;
+use Nosto\NostoException;
+use Nosto\Tagging\Helper\Scope as NostoHelperScope;
+use Nosto\Tagging\Logger\Logger as NostoLogger;
+use Nosto\Tagging\Model\Indexer\Dimensions\Product\ModeSwitcher as ProductModeSwitcher;
+use Nosto\Tagging\Model\Indexer\Dimensions\ModeSwitcherInterface;
+use Nosto\Tagging\Model\Indexer\Dimensions\StoreDimensionProvider;
+use Nosto\Tagging\Model\ResourceModel\Magento\Product\Collection as ProductCollection;
+use Nosto\Tagging\Model\ResourceModel\Magento\Product\CollectionBuilder;
+use Nosto\Tagging\Model\Service\Indexer\IndexerStatusServiceInterface;
+use Symfony\Component\Console\Input\InputInterface;
+
+/**
+ * Class ProductInventoryIndexer
+ * Fetches product ID's from CL tables and create entries in the message queue
+ */
+class ProductInventoryIndexer extends AbstractIndexer
+{
+ public const INDEXER_ID = 'nosto_index_partial_product_inventory';
+
+ /** @var CollectionBuilder */
+ private CollectionBuilder $productCollectionBuilder;
+
+ /** @var ProductModeSwitcher */
+ private ProductModeSwitcher $modeSwitcher;
+
+ /**
+ * Invalidate constructor.
+ * @param NostoHelperScope $nostoHelperScope
+ * @param NostoLogger $logger
+ * @param CollectionBuilder $productCollectionBuilder
+ * @param ProductModeSwitcher $modeSwitcher
+ * @param StoreDimensionProvider $dimensionProvider
+ * @param Emulation $storeEmulation
+ * @param ProcessManager $processManager
+ * @param InputInterface $input
+ * @param IndexerStatusServiceInterface $indexerStatusService
+ */
+ public function __construct(
+ NostoHelperScope $nostoHelperScope,
+ NostoLogger $logger,
+ CollectionBuilder $productCollectionBuilder,
+ ProductModeSwitcher $modeSwitcher,
+ StoreDimensionProvider $dimensionProvider,
+ Emulation $storeEmulation,
+ ProcessManager $processManager,
+ InputInterface $input,
+ IndexerStatusServiceInterface $indexerStatusService
+ ) {
+ $this->productCollectionBuilder = $productCollectionBuilder;
+ $this->modeSwitcher = $modeSwitcher;
+ parent::__construct(
+ $nostoHelperScope,
+ $logger,
+ $dimensionProvider,
+ $storeEmulation,
+ $input,
+ $indexerStatusService,
+ $processManager
+ );
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getModeSwitcher(): ModeSwitcherInterface
+ {
+ return $this->modeSwitcher;
+ }
+
+ /**
+ * @inheritDoc
+ * @throws NostoException
+ * @throws Exception
+ */
+ public function doIndex(Store $store, array $ids = [])
+ {
+ $collection = $this->getCollection($store, $ids);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getIndexerId(): string
+ {
+ return self::INDEXER_ID;
+ }
+
+ /**
+ * @param Store $store
+ * @param array $ids
+ * @return ProductCollection
+ */
+ public function getCollection(Store $store, array $ids = []): ProductCollection
+ {
+ $this->productCollectionBuilder->initDefault($store);
+ if (!empty($ids)) {
+ $this->productCollectionBuilder->withIds($ids);
+ } else {
+ $this->productCollectionBuilder->withDefaultVisibility($store);
+ }
+ return $this->productCollectionBuilder->build();
+ }
+}
diff --git a/Model/Indexer/Partial/ProductPriceIndexer.php b/Model/Indexer/Partial/ProductPriceIndexer.php
new file mode 100644
index 000000000..a7d8702e3
--- /dev/null
+++ b/Model/Indexer/Partial/ProductPriceIndexer.php
@@ -0,0 +1,145 @@
+
+ * @copyright 2020 Nosto Solutions Ltd
+ * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause
+ *
+ */
+
+namespace Nosto\Tagging\Model\Indexer;
+
+use Exception;
+use Magento\Indexer\Model\ProcessManager;
+use Magento\Store\Model\App\Emulation;
+use Magento\Store\Model\Store;
+use Nosto\NostoException;
+use Nosto\Tagging\Helper\Scope as NostoHelperScope;
+use Nosto\Tagging\Logger\Logger as NostoLogger;
+use Nosto\Tagging\Model\Indexer\Dimensions\Product\ModeSwitcher as ProductModeSwitcher;
+use Nosto\Tagging\Model\Indexer\Dimensions\ModeSwitcherInterface;
+use Nosto\Tagging\Model\Indexer\Dimensions\StoreDimensionProvider;
+use Nosto\Tagging\Model\ResourceModel\Magento\Product\Collection as ProductCollection;
+use Nosto\Tagging\Model\ResourceModel\Magento\Product\CollectionBuilder;
+use Nosto\Tagging\Model\Service\Indexer\IndexerStatusServiceInterface;
+use Symfony\Component\Console\Input\InputInterface;
+
+/**
+ * Class ProductPriceIndexer
+ * Fetches product ID's from CL tables and create entries in the message queue
+ */
+class ProductPriceIndexer extends AbstractIndexer
+{
+ public const INDEXER_ID = 'nosto_index_partial_product_price';
+
+ /** @var CollectionBuilder */
+ private CollectionBuilder $productCollectionBuilder;
+
+ /** @var ProductModeSwitcher */
+ private ProductModeSwitcher $modeSwitcher;
+
+ /**
+ * Invalidate constructor.
+ * @param NostoHelperScope $nostoHelperScope
+ * @param NostoLogger $logger
+ * @param CollectionBuilder $productCollectionBuilder
+ * @param ProductModeSwitcher $modeSwitcher
+ * @param StoreDimensionProvider $dimensionProvider
+ * @param Emulation $storeEmulation
+ * @param ProcessManager $processManager
+ * @param InputInterface $input
+ * @param IndexerStatusServiceInterface $indexerStatusService
+ */
+ public function __construct(
+ NostoHelperScope $nostoHelperScope,
+ NostoLogger $logger,
+ CollectionBuilder $productCollectionBuilder,
+ ProductModeSwitcher $modeSwitcher,
+ StoreDimensionProvider $dimensionProvider,
+ Emulation $storeEmulation,
+ ProcessManager $processManager,
+ InputInterface $input,
+ IndexerStatusServiceInterface $indexerStatusService
+ ) {
+ $this->productCollectionBuilder = $productCollectionBuilder;
+ $this->modeSwitcher = $modeSwitcher;
+ parent::__construct(
+ $nostoHelperScope,
+ $logger,
+ $dimensionProvider,
+ $storeEmulation,
+ $input,
+ $indexerStatusService,
+ $processManager
+ );
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getModeSwitcher(): ModeSwitcherInterface
+ {
+ return $this->modeSwitcher;
+ }
+
+ /**
+ * @inheritDoc
+ * @throws NostoException
+ * @throws Exception
+ */
+ public function doIndex(Store $store, array $ids = [])
+ {
+ $collection = $this->getCollection($store, $ids);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getIndexerId(): string
+ {
+ return self::INDEXER_ID;
+ }
+
+ /**
+ * @param Store $store
+ * @param array $ids
+ * @return ProductCollection
+ */
+ public function getCollection(Store $store, array $ids = []): ProductCollection
+ {
+ $this->productCollectionBuilder->initDefault($store);
+ if (!empty($ids)) {
+ $this->productCollectionBuilder->withIds($ids);
+ } else {
+ $this->productCollectionBuilder->withDefaultVisibility($store);
+ }
+ return $this->productCollectionBuilder->build();
+ }
+}
diff --git a/Model/Product/Builder.php b/Model/Product/Builder.php
index a9cae9432..77340bd65 100644
--- a/Model/Product/Builder.php
+++ b/Model/Product/Builder.php
@@ -405,7 +405,7 @@ private function amendAttributeTags(Product $product, NostoProduct $nostoProduct
* @param Store $store
* @return string
*/
- private function buildAvailability(Product $product, Store $store)
+ public function buildAvailability(Product $product, Store $store)
{
$availability = ProductInterface::OUT_OF_STOCK;
$isInStock = $this->availabilityService->isInStock($product, $store);
diff --git a/Model/Product/Partial/InventoryProduct.php b/Model/Product/Partial/InventoryProduct.php
new file mode 100644
index 000000000..8c95a8caa
--- /dev/null
+++ b/Model/Product/Partial/InventoryProduct.php
@@ -0,0 +1,105 @@
+
+ * @copyright 2020 Nosto Solutions Ltd
+ * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause
+ *
+ */
+
+namespace Nosto\Tagging\Model\Product\Partial;
+
+use Magento\Catalog\Model\Product;
+use Magento\Store\Model\Store;
+use Nosto\Model\Product\Product as NostoProduct;
+use Nosto\Tagging\Helper\Data as NostoDataHelper;
+use Nosto\Tagging\Model\Product\Sku\Collection as NostoSkuCollection;
+use Nosto\Tagging\Model\Product\Url\Builder as NostoUrlBuilder;
+use Nosto\Tagging\Model\Service\Stock\StockService;
+use Nosto\Tagging\Model\Product\Builder as FullProductBuilder;
+
+class InventoryProduct
+{
+ /** @var NostoDataHelper */
+ private NostoDataHelper $nostoDataHelper;
+
+ /** @var NostoUrlBuilder */
+ private NostoUrlBuilder $urlBuilder;
+
+ /** @var StockService */
+ private StockService $stockService;
+
+ /** @var FullProductBuilder */
+ private FullProductBuilder $fullProductBuilder;
+
+ /** @var NostoSkuCollection */
+ private NostoSkuCollection $skuCollection;
+
+ /**
+ * Builder constructor.
+ * @param NostoDataHelper $nostoDataHelper
+ * @param NostoUrlBuilder $urlBuilder
+ * @param StockService $stockService
+ * @param FullProductBuilder $fullProductBuilder
+ * @param NostoSkuCollection $skuCollection
+ */
+ public function __construct(
+ NostoDataHelper $nostoDataHelper,
+ NostoUrlBuilder $urlBuilder,
+ StockService $stockService,
+ FullProductBuilder $fullProductBuilder,
+ NostoSkuCollection $skuCollection
+ ) {
+ $this->nostoDataHelper = $nostoDataHelper;
+ $this->urlBuilder = $urlBuilder;
+ $this->stockService = $stockService;
+ $this->fullProductBuilder = $fullProductBuilder;
+ $this->skuCollection = $skuCollection;
+ }
+
+ public function build(
+ Product $product,
+ Store $store
+ ) {
+ $nostoProduct = new NostoProduct();
+ $nostoProduct->setProductId((string)$product->getId());
+ $nostoProduct->setUrl($this->urlBuilder->getUrlInStore($product, $store));
+ if ($this->nostoDataHelper->isInventoryTaggingEnabled($store)) {
+ $inventoryLevel = $this->stockService->getQuantity($product, $store);
+ $nostoProduct->setInventoryLevel($inventoryLevel);
+ }
+ $nostoProduct->setAvailability($this->fullProductBuilder->buildAvailability($product, $store));
+ if ($this->nostoDataHelper->isVariationTaggingEnabled($store)) {
+ // We need the full set of SKU's here, otherwise Nosto will remove the SKU's from the product
+ $nostoProduct->setSkus($this->skuCollection->build($product, $store));
+ }
+ return $nostoProduct;
+ }
+}
diff --git a/Model/Product/Partial/PriceProduct.php b/Model/Product/Partial/PriceProduct.php
new file mode 100644
index 000000000..bafd48e22
--- /dev/null
+++ b/Model/Product/Partial/PriceProduct.php
@@ -0,0 +1,125 @@
+
+ * @copyright 2020 Nosto Solutions Ltd
+ * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause
+ *
+ */
+
+namespace Nosto\Tagging\Model\Product\Partial;
+
+use Exception;
+use Magento\Catalog\Model\Product;
+use Magento\Store\Model\Store;
+use Nosto\Exception\NonBuildableProductException;
+use Nosto\Model\Product\Product as NostoProduct;
+use Nosto\Tagging\Helper\Currency as CurrencyHelper;
+use Nosto\Tagging\Helper\Data as NostoDataHelper;
+use Nosto\Tagging\Helper\Price as NostoPriceHelper;
+use Nosto\Tagging\Model\Product\Url\Builder as NostoUrlBuilder;
+use Nosto\Tagging\Model\Product\Variation\Collection as PriceVariationCollection;
+
+class PriceProduct
+{
+ /** @var NostoDataHelper */
+ private NostoDataHelper $nostoDataHelper;
+
+ /** @var NostoPriceHelper */
+ private NostoPriceHelper $nostoPriceHelper;
+
+ /** @var NostoUrlBuilder */
+ private NostoUrlBuilder $urlBuilder;
+
+ /** @var CurrencyHelper */
+ private CurrencyHelper $nostoCurrencyHelper;
+
+ /** @var PriceVariationCollection */
+ private PriceVariationCollection $priceVariationCollection;
+ /**
+ * Builder constructor.
+ * @param NostoDataHelper $nostoDataHelper
+ * @param NostoPriceHelper $priceHelper
+ * @param NostoUrlBuilder $urlBuilder
+ * @param CurrencyHelper $nostoCurrencyHelper
+ * @param PriceVariationCollection $priceVariationCollection
+ */
+ public function __construct(
+ NostoDataHelper $nostoDataHelper,
+ NostoPriceHelper $priceHelper,
+ NostoUrlBuilder $urlBuilder,
+ CurrencyHelper $nostoCurrencyHelper,
+ PriceVariationCollection $priceVariationCollection
+ ) {
+ $this->nostoDataHelper = $nostoDataHelper;
+ $this->nostoPriceHelper = $priceHelper;
+ $this->urlBuilder = $urlBuilder;
+ $this->nostoCurrencyHelper = $nostoCurrencyHelper;
+ $this->priceVariationCollection = $priceVariationCollection;
+ }
+
+ public function build(
+ Product $product,
+ Store $store
+ ) {
+ $nostoProduct = new NostoProduct();
+ $nostoProduct->setProductId((string)$product->getId());
+ $nostoProduct->setUrl($this->urlBuilder->getUrlInStore($product, $store));
+ try {
+ $price = $this->nostoCurrencyHelper->convertToTaggingPrice(
+ $this->nostoPriceHelper->getProductFinalDisplayPrice(
+ $product,
+ $store
+ ),
+ $store
+ );
+ if ($this->nostoDataHelper->isPricingVariationEnabled($store)
+ && $this->nostoDataHelper->isMultiCurrencyDisabled($store)
+ ) {
+ $nostoProduct->setVariations(
+ $this->priceVariationCollection->build($product, $nostoProduct, $store)
+ );
+ }
+ $nostoProduct->setPrice($price);
+ $listPrice = $this->nostoCurrencyHelper->convertToTaggingPrice(
+ $this->nostoPriceHelper->getProductDisplayPrice(
+ $product,
+ $store
+ ),
+ $store
+ );
+ $nostoProduct->setListPrice($listPrice);
+ } catch (Exception $e) {
+ $msg = sprintf("Could not set price for partial product product with id: %s", $product->getId());
+ throw new NonBuildableProductException($msg, $e);
+ }
+ return $nostoProduct;
+ }
+}
diff --git a/Model/Service/Product/Partial/InventoryProductService.php b/Model/Service/Product/Partial/InventoryProductService.php
new file mode 100644
index 000000000..3a7003360
--- /dev/null
+++ b/Model/Service/Product/Partial/InventoryProductService.php
@@ -0,0 +1,113 @@
+
+ * @copyright 2020 Nosto Solutions Ltd
+ * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause
+ *
+ */
+
+namespace Nosto\Tagging\Model\Service\Product\Partial;
+
+use Exception;
+use Magento\Catalog\Api\Data\ProductInterface;
+use Magento\Catalog\Model\Product;
+use Magento\Store\Api\Data\StoreInterface;
+use Magento\Store\Model\Store;
+use Nosto\Exception\FilteredProductException;
+use Nosto\Exception\NonBuildableProductException;
+use Nosto\Model\Product\Product as NostoProduct;
+use Nosto\Tagging\Logger\Logger as NostoLogger;
+use Nosto\Tagging\Model\Product\Partial\InventoryProduct as NostoProductBuilder;
+use Nosto\Tagging\Model\Product\Repository as NostoProductRepository;
+use Nosto\Tagging\Model\Service\Product\ProductServiceInterface;
+
+class InventoryProductService implements ProductServiceInterface
+{
+
+ /** @var NostoProductBuilder */
+ private NostoProductBuilder $nostoProductBuilder;
+
+ /** @var NostoLogger */
+ private NostoLogger $logger;
+
+ /** @var NostoProductRepository */
+ private NostoProductRepository $nostoProductRepository;
+
+ /**
+ * DefaultProductService constructor.
+ * @param NostoProductBuilder $nostoProductBuilder
+ * @param NostoProductRepository $nostoProductRepository
+ * @param NostoLogger $logger
+ */
+ public function __construct(
+ NostoProductBuilder $nostoProductBuilder,
+ NostoProductRepository $nostoProductRepository,
+ NostoLogger $logger
+ ) {
+ $this->nostoProductBuilder = $nostoProductBuilder;
+ $this->nostoProductRepository = $nostoProductRepository;
+ $this->logger = $logger;
+ }
+
+ /**
+ * @param ProductInterface $product
+ * @param StoreInterface $store
+ * @return NostoProduct|null
+ * @suppress PhanTypeMismatchArgument
+ * @throws Exception
+ */
+ public function getProduct(ProductInterface $product, StoreInterface $store)
+ {
+ //@TODO: Can be better abstracted
+ /** @var Product $product */
+ /** @var Store $store */
+ try {
+ return $this->nostoProductBuilder->build(
+ $this->nostoProductRepository->reloadProduct(
+ $product->getId(),
+ $store->getId()
+ ),
+ $store
+ );
+ } catch (NonBuildableProductException $e) {
+ $this->logger->exception($e);
+ return null;
+ } catch (FilteredProductException $e) {
+ $this->logger->debug(
+ sprintf(
+ 'Product filtered out with message: %s',
+ $e->getMessage()
+ )
+ );
+ return null;
+ }
+ }
+}
diff --git a/Model/Service/Product/Partial/PriceProductService.php b/Model/Service/Product/Partial/PriceProductService.php
new file mode 100644
index 000000000..c95551d05
--- /dev/null
+++ b/Model/Service/Product/Partial/PriceProductService.php
@@ -0,0 +1,113 @@
+
+ * @copyright 2020 Nosto Solutions Ltd
+ * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause
+ *
+ */
+
+namespace Nosto\Tagging\Model\Service\Product\Partial;
+
+use Exception;
+use Magento\Catalog\Api\Data\ProductInterface;
+use Magento\Catalog\Model\Product;
+use Magento\Store\Api\Data\StoreInterface;
+use Magento\Store\Model\Store;
+use Nosto\Exception\FilteredProductException;
+use Nosto\Exception\NonBuildableProductException;
+use Nosto\Model\Product\Product as NostoProduct;
+use Nosto\Tagging\Logger\Logger as NostoLogger;
+use Nosto\Tagging\Model\Product\Partial\PriceProduct as NostoProductBuilder;
+use Nosto\Tagging\Model\Product\Repository as NostoProductRepository;
+use Nosto\Tagging\Model\Service\Product\ProductServiceInterface;
+
+class PriceProductService implements ProductServiceInterface
+{
+
+ /** @var NostoProductBuilder */
+ private NostoProductBuilder $nostoProductBuilder;
+
+ /** @var NostoLogger */
+ private NostoLogger $logger;
+
+ /** @var NostoProductRepository */
+ private NostoProductRepository $nostoProductRepository;
+
+ /**
+ * DefaultProductService constructor.
+ * @param NostoProductBuilder $nostoProductBuilder
+ * @param NostoProductRepository $nostoProductRepository
+ * @param NostoLogger $logger
+ */
+ public function __construct(
+ NostoProductBuilder $nostoProductBuilder,
+ NostoProductRepository $nostoProductRepository,
+ NostoLogger $logger
+ ) {
+ $this->nostoProductBuilder = $nostoProductBuilder;
+ $this->nostoProductRepository = $nostoProductRepository;
+ $this->logger = $logger;
+ }
+
+ /**
+ * @param ProductInterface $product
+ * @param StoreInterface $store
+ * @return NostoProduct|null
+ * @suppress PhanTypeMismatchArgument
+ * @throws Exception
+ */
+ public function getProduct(ProductInterface $product, StoreInterface $store)
+ {
+ //@TODO: Can be better abstracted
+ /** @var Product $product */
+ /** @var Store $store */
+ try {
+ return $this->nostoProductBuilder->build(
+ $this->nostoProductRepository->reloadProduct(
+ $product->getId(),
+ $store->getId()
+ ),
+ $store
+ );
+ } catch (NonBuildableProductException $e) {
+ $this->logger->exception($e);
+ return null;
+ } catch (FilteredProductException $e) {
+ $this->logger->debug(
+ sprintf(
+ 'Product filtered out with message: %s',
+ $e->getMessage()
+ )
+ );
+ return null;
+ }
+ }
+}
diff --git a/Model/Service/Sync/Partial/InventoryBulkConsumer.php b/Model/Service/Sync/Partial/InventoryBulkConsumer.php
new file mode 100644
index 000000000..eccfff270
--- /dev/null
+++ b/Model/Service/Sync/Partial/InventoryBulkConsumer.php
@@ -0,0 +1,109 @@
+
+ * @copyright 2020 Nosto Solutions Ltd
+ * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause
+ *
+ */
+
+namespace Nosto\Tagging\Model\Service\Sync\Partial;
+
+use Magento\Framework\EntityManager\EntityManager;
+use Magento\Framework\Json\Helper\Data as JsonHelper;
+use Magento\Store\Model\App\Emulation;
+use Nosto\Exception\MemoryOutOfBoundsException;
+use Nosto\NostoException;
+use Nosto\Tagging\Helper\Scope as NostoScopeHelper;
+use Nosto\Tagging\Logger\Logger;
+use Nosto\Tagging\Model\ResourceModel\Magento\Product\CollectionFactory;
+use Nosto\Tagging\Model\Service\Sync\AbstractBulkConsumer;
+use Nosto\Tagging\Model\Service\Sync\Partial\InventoryService as SyncService;
+
+/**
+ * Inventory Bulk Consumer
+ * Used for bulk operations when doing partial inventory updates
+ *
+ */
+class InventoryBulkConsumer extends AbstractBulkConsumer
+{
+ /** @var SyncService */
+ private SyncService $syncService;
+
+ /** @var NostoScopeHelper */
+ private NostoScopeHelper $nostoScopeHelper;
+
+ /** @var CollectionFactory */
+ private CollectionFactory $collectionFactory;
+
+ /**
+ * AsyncBulkConsumer constructor.
+ * @param SyncService $syncService
+ * @param NostoScopeHelper $nostoScopeHelper
+ * @param CollectionFactory $collectionFactory
+ * @param JsonHelper $jsonHelper
+ * @param EntityManager $entityManager
+ * @param Emulation $storeEmulation
+ * @param Logger $logger
+ */
+ public function __construct(
+ SyncService $syncService,
+ NostoScopeHelper $nostoScopeHelper,
+ CollectionFactory $collectionFactory,
+ JsonHelper $jsonHelper,
+ EntityManager $entityManager,
+ Emulation $storeEmulation,
+ Logger $logger
+ ) {
+ $this->syncService = $syncService;
+ $this->nostoScopeHelper = $nostoScopeHelper;
+ $this->collectionFactory = $collectionFactory;
+ parent::__construct(
+ $logger,
+ $jsonHelper,
+ $entityManager,
+ $storeEmulation
+ );
+ }
+
+ /**
+ * @inheritDoc
+ * @throws MemoryOutOfBoundsException
+ * @throws NostoException
+ */
+ public function doOperation(array $productIds, string $storeId)
+ {
+ $store = $this->nostoScopeHelper->getStore($storeId);
+ $productCollection = $this->collectionFactory->create()
+ ->addIdsToFilter($productIds)
+ ->addStoreFilter($storeId);
+ $this->syncService->syncProducts($productCollection, $store);
+ }
+}
diff --git a/Model/Service/Sync/Partial/InventoryBulkPublisher.php b/Model/Service/Sync/Partial/InventoryBulkPublisher.php
new file mode 100644
index 000000000..dbe5539cf
--- /dev/null
+++ b/Model/Service/Sync/Partial/InventoryBulkPublisher.php
@@ -0,0 +1,78 @@
+
+ * @copyright 2020 Nosto Solutions Ltd
+ * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause
+ *
+ */
+
+namespace Nosto\Tagging\Model\Service\Sync\Partial;
+
+use Nosto\Tagging\Model\Service\Sync\AbstractBulkPublisher;
+
+// @codingStandardsIgnoreFile
+class InventoryBulkPublisher extends AbstractBulkPublisher
+{
+ public const NOSTO_SYNC_MESSAGE_QUEUE = 'nosto_product_sync.partial_inventory_update';
+ public const BULK_SIZE = 100;
+
+ /**
+ * @inheritDoc
+ */
+ public function getTopicName(): string
+ {
+ return self::NOSTO_SYNC_MESSAGE_QUEUE;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getBulkSize(): int
+ {
+ return self::BULK_SIZE;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getBulkDescription(): string
+ {
+ return sprintf('Sync price for %d Nosto products', 2);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getMetaData(): string
+ {
+ return 'Sync price for Nosto products';
+ }
+}
diff --git a/Model/Service/Sync/Partial/InventoryService.php b/Model/Service/Sync/Partial/InventoryService.php
new file mode 100644
index 000000000..30edcba6e
--- /dev/null
+++ b/Model/Service/Sync/Partial/InventoryService.php
@@ -0,0 +1,182 @@
+
+ * @copyright 2020 Nosto Solutions Ltd
+ * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause
+ *
+ */
+
+namespace Nosto\Tagging\Model\Service\Sync\Partial;
+
+use Exception;
+use Magento\Catalog\Model\Product;
+use Magento\Store\Model\Store;
+use Nosto\Exception\MemoryOutOfBoundsException;
+use Nosto\NostoException;
+use Nosto\Operation\UpsertProduct;
+use Nosto\Operation\Product\InventoryUpdate;
+use Nosto\Request\Http\Exception\AbstractHttpException;
+use Nosto\Tagging\Helper\Account as NostoHelperAccount;
+use Nosto\Tagging\Helper\Data as NostoDataHelper;
+use Nosto\Tagging\Helper\Url as NostoHelperUrl;
+use Nosto\Tagging\Logger\Logger as NostoLogger;
+use Nosto\Tagging\Model\ResourceModel\Magento\Product\Collection as ProductCollection;
+use Nosto\Tagging\Model\Service\AbstractService;
+use Nosto\Tagging\Model\Service\Cache\CacheService;
+use Nosto\Tagging\Model\Service\Product\Partial\InventoryProductService as ProductService;
+use Nosto\Tagging\Util\PagingIterator;
+use Nosto\Tagging\Model\Product\Repository as ProductRepository;
+
+class InventoryService extends AbstractService
+{
+ public const BENCHMARK_SYNC_NAME = 'nosto_product_partial_upsert_inventory';
+ public const BENCHMARK_SYNC_BREAKPOINT = 1;
+
+ /** @var NostoHelperAccount */
+ private NostoHelperAccount $nostoHelperAccount;
+
+ /** @var NostoHelperUrl */
+ private NostoHelperUrl $nostoHelperUrl;
+
+ /** @var NostoDataHelper */
+ private NostoDataHelper $nostoDataHelper;
+
+ /** @var ProductService */
+ private ProductService $productService;
+
+ /** @var CacheService */
+ private CacheService $cacheService;
+
+ /** @var int */
+ private int $apiBatchSize;
+
+ /** @var int */
+ private int $apiTimeout;
+
+ /** @var ProductRepository */
+ private ProductRepository $productRepository;
+
+ /**
+ * Sync constructor.
+ * @param NostoHelperAccount $nostoHelperAccount
+ * @param NostoHelperUrl $nostoHelperUrl
+ * @param NostoLogger $logger
+ * @param NostoDataHelper $nostoDataHelper
+ * @param ProductService $productService
+ * @param CacheService $cacheService
+ * @param ProductRepository $productRepository
+ * @param $apiBatchSize
+ * @param $apiTimeout
+ */
+ public function __construct(
+ NostoHelperAccount $nostoHelperAccount,
+ NostoHelperUrl $nostoHelperUrl,
+ NostoLogger $logger,
+ NostoDataHelper $nostoDataHelper,
+ ProductService $productService,
+ CacheService $cacheService,
+ ProductRepository $productRepository,
+ $apiBatchSize,
+ $apiTimeout
+ ) {
+ parent::__construct($nostoDataHelper, $nostoHelperAccount, $logger);
+ $this->productService = $productService;
+ $this->nostoHelperAccount = $nostoHelperAccount;
+ $this->nostoHelperUrl = $nostoHelperUrl;
+ $this->nostoDataHelper = $nostoDataHelper;
+ $this->cacheService = $cacheService;
+ $this->productRepository = $productRepository;
+ $this->apiBatchSize = $apiBatchSize;
+ $this->apiTimeout = $apiTimeout;
+ }
+
+ /**
+ * @param ProductCollection $collection
+ * @param Store $store
+ * @throws MemoryOutOfBoundsException
+ * @throws NostoException
+ * @throws AbstractHttpException
+ * @throws Exception
+ */
+ public function syncProducts(ProductCollection $collection, Store $store)
+ {
+ if (!$this->nostoDataHelper->isProductUpdatesEnabled($store)) {
+ $this->logDebugWithStore(
+ 'Nosto product sync is disabled - skipping upserting products to Nosto',
+ $store
+ );
+ return;
+ }
+ $account = $this->nostoHelperAccount->findAccount($store);
+ $this->startBenchmark(self::BENCHMARK_SYNC_NAME, self::BENCHMARK_SYNC_BREAKPOINT);
+
+ $collection->setPageSize($this->apiBatchSize);
+ $iterator = new PagingIterator($collection);
+
+ /** @var ProductCollection $page */
+ foreach ($iterator as $page) {
+ $productIdsInBatch = [];
+ $this->checkMemoryConsumption('product sync');
+ $op = new InventoryUpdate($account, $this->nostoHelperUrl->getActiveDomain($store));
+ $op->setResponseTimeout($this->apiTimeout);
+ $products = $this->productRepository->getByIds(
+ $page->getAllIds(
+ $this->apiBatchSize,
+ ($iterator->getCurrentPageNumber() - 1) * $this->apiBatchSize
+ )
+ );
+ /** @var Product $product */
+ foreach ($products->getItems() as $product) {
+ $productIdsInBatch[] = $product->getId();
+ $nostoProduct = $this->productService->getProduct($product, $store);
+ if ($nostoProduct === null) {
+ throw new NostoException('Could not get product from the product service.');
+ }
+ $op->addProduct($nostoProduct);
+ // phpcs:ignore
+ $this->cacheService->save($nostoProduct, $store);
+ $this->tickBenchmark(self::BENCHMARK_SYNC_NAME);
+ }
+
+ $this->logDebugWithStore(
+ sprintf(
+ 'Upserting batch of %d (%s) - API timeout is set to %d seconds',
+ $this->apiBatchSize,
+ implode(',', $productIdsInBatch),
+ $this->apiTimeout
+ ),
+ $store
+ );
+ $op->upsert();
+ }
+ $this->logBenchmarkSummary(self::BENCHMARK_SYNC_NAME, $store, $this);
+ }
+}
diff --git a/Model/Service/Sync/Partial/PriceBulkConsumer.php b/Model/Service/Sync/Partial/PriceBulkConsumer.php
new file mode 100644
index 000000000..14035a54d
--- /dev/null
+++ b/Model/Service/Sync/Partial/PriceBulkConsumer.php
@@ -0,0 +1,109 @@
+
+ * @copyright 2020 Nosto Solutions Ltd
+ * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause
+ *
+ */
+
+namespace Nosto\Tagging\Model\Service\Sync\Partial;
+
+use Magento\Framework\EntityManager\EntityManager;
+use Magento\Framework\Json\Helper\Data as JsonHelper;
+use Magento\Store\Model\App\Emulation;
+use Nosto\Exception\MemoryOutOfBoundsException;
+use Nosto\NostoException;
+use Nosto\Tagging\Helper\Scope as NostoScopeHelper;
+use Nosto\Tagging\Logger\Logger;
+use Nosto\Tagging\Model\ResourceModel\Magento\Product\CollectionFactory;
+use Nosto\Tagging\Model\Service\Sync\AbstractBulkConsumer;
+use Nosto\Tagging\Model\Service\Sync\Partial\PriceService as SyncService;
+
+/**
+ * Price Bulk Consumer
+ *
+ * Used for bulk operations when doing partial price updates
+ */
+class PriceBulkConsumer extends AbstractBulkConsumer
+{
+ /** @var SyncService */
+ private SyncService $syncService;
+
+ /** @var NostoScopeHelper */
+ private NostoScopeHelper $nostoScopeHelper;
+
+ /** @var CollectionFactory */
+ private CollectionFactory $collectionFactory;
+
+ /**
+ * AsyncBulkConsumer constructor.
+ * @param SyncService $syncService
+ * @param NostoScopeHelper $nostoScopeHelper
+ * @param CollectionFactory $collectionFactory
+ * @param JsonHelper $jsonHelper
+ * @param EntityManager $entityManager
+ * @param Emulation $storeEmulation
+ * @param Logger $logger
+ */
+ public function __construct(
+ SyncService $syncService,
+ NostoScopeHelper $nostoScopeHelper,
+ CollectionFactory $collectionFactory,
+ JsonHelper $jsonHelper,
+ EntityManager $entityManager,
+ Emulation $storeEmulation,
+ Logger $logger
+ ) {
+ $this->syncService = $syncService;
+ $this->nostoScopeHelper = $nostoScopeHelper;
+ $this->collectionFactory = $collectionFactory;
+ parent::__construct(
+ $logger,
+ $jsonHelper,
+ $entityManager,
+ $storeEmulation
+ );
+ }
+
+ /**
+ * @inheritDoc
+ * @throws MemoryOutOfBoundsException
+ * @throws NostoException
+ */
+ public function doOperation(array $productIds, string $storeId)
+ {
+ $store = $this->nostoScopeHelper->getStore($storeId);
+ $productCollection = $this->collectionFactory->create()
+ ->addIdsToFilter($productIds)
+ ->addStoreFilter($storeId);
+ $this->syncService->syncProducts($productCollection, $store);
+ }
+}
diff --git a/Model/Service/Sync/Partial/PriceBulkPublisher.php b/Model/Service/Sync/Partial/PriceBulkPublisher.php
new file mode 100644
index 000000000..e4f56eb30
--- /dev/null
+++ b/Model/Service/Sync/Partial/PriceBulkPublisher.php
@@ -0,0 +1,78 @@
+
+ * @copyright 2020 Nosto Solutions Ltd
+ * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause
+ *
+ */
+
+namespace Nosto\Tagging\Model\Service\Sync\Partial;
+
+use Nosto\Tagging\Model\Service\Sync\AbstractBulkPublisher;
+
+// @codingStandardsIgnoreFile
+class PriceBulkPublisher extends AbstractBulkPublisher
+{
+ public const NOSTO_SYNC_MESSAGE_QUEUE = 'nosto_product_sync.partial_price_update';
+ public const BULK_SIZE = 100;
+
+ /**
+ * @inheritDoc
+ */
+ public function getTopicName(): string
+ {
+ return self::NOSTO_SYNC_MESSAGE_QUEUE;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getBulkSize(): int
+ {
+ return self::BULK_SIZE;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getBulkDescription(): string
+ {
+ return sprintf('Sync inventory for %d Nosto products', 2);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getMetaData(): string
+ {
+ return 'Sync inventory for Nosto products';
+ }
+}
diff --git a/Model/Service/Sync/Partial/PriceService.php b/Model/Service/Sync/Partial/PriceService.php
new file mode 100644
index 000000000..5e391c53b
--- /dev/null
+++ b/Model/Service/Sync/Partial/PriceService.php
@@ -0,0 +1,181 @@
+
+ * @copyright 2020 Nosto Solutions Ltd
+ * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause
+ *
+ */
+
+namespace Nosto\Tagging\Model\Service\Sync\Partial;
+
+use Exception;
+use Magento\Catalog\Model\Product;
+use Magento\Store\Model\Store;
+use Nosto\Exception\MemoryOutOfBoundsException;
+use Nosto\NostoException;
+use Nosto\Operation\Product\PriceUpdate;
+use Nosto\Request\Http\Exception\AbstractHttpException;
+use Nosto\Tagging\Helper\Account as NostoHelperAccount;
+use Nosto\Tagging\Helper\Data as NostoDataHelper;
+use Nosto\Tagging\Helper\Url as NostoHelperUrl;
+use Nosto\Tagging\Logger\Logger as NostoLogger;
+use Nosto\Tagging\Model\ResourceModel\Magento\Product\Collection as ProductCollection;
+use Nosto\Tagging\Model\Service\AbstractService;
+use Nosto\Tagging\Model\Service\Cache\CacheService;
+use Nosto\Tagging\Model\Service\Product\Partial\InventoryProductService as ProductService;
+use Nosto\Tagging\Util\PagingIterator;
+use Nosto\Tagging\Model\Product\Repository as ProductRepository;
+
+class PriceService extends AbstractService
+{
+ public const BENCHMARK_SYNC_NAME = 'nosto_product_partial_upsert_inventory';
+ public const BENCHMARK_SYNC_BREAKPOINT = 1;
+
+ /** @var NostoHelperAccount */
+ private NostoHelperAccount $nostoHelperAccount;
+
+ /** @var NostoHelperUrl */
+ private NostoHelperUrl $nostoHelperUrl;
+
+ /** @var NostoDataHelper */
+ private NostoDataHelper $nostoDataHelper;
+
+ /** @var ProductService */
+ private ProductService $productService;
+
+ /** @var CacheService */
+ private CacheService $cacheService;
+
+ /** @var int */
+ private int $apiBatchSize;
+
+ /** @var int */
+ private int $apiTimeout;
+
+ /** @var ProductRepository */
+ private ProductRepository $productRepository;
+
+ /**
+ * Sync constructor.
+ * @param NostoHelperAccount $nostoHelperAccount
+ * @param NostoHelperUrl $nostoHelperUrl
+ * @param NostoLogger $logger
+ * @param NostoDataHelper $nostoDataHelper
+ * @param ProductService $productService
+ * @param CacheService $cacheService
+ * @param ProductRepository $productRepository
+ * @param $apiBatchSize
+ * @param $apiTimeout
+ */
+ public function __construct(
+ NostoHelperAccount $nostoHelperAccount,
+ NostoHelperUrl $nostoHelperUrl,
+ NostoLogger $logger,
+ NostoDataHelper $nostoDataHelper,
+ ProductService $productService,
+ CacheService $cacheService,
+ ProductRepository $productRepository,
+ $apiBatchSize,
+ $apiTimeout
+ ) {
+ parent::__construct($nostoDataHelper, $nostoHelperAccount, $logger);
+ $this->productService = $productService;
+ $this->nostoHelperAccount = $nostoHelperAccount;
+ $this->nostoHelperUrl = $nostoHelperUrl;
+ $this->nostoDataHelper = $nostoDataHelper;
+ $this->cacheService = $cacheService;
+ $this->productRepository = $productRepository;
+ $this->apiBatchSize = $apiBatchSize;
+ $this->apiTimeout = $apiTimeout;
+ }
+
+ /**
+ * @param ProductCollection $collection
+ * @param Store $store
+ * @throws MemoryOutOfBoundsException
+ * @throws NostoException
+ * @throws AbstractHttpException
+ * @throws Exception
+ */
+ public function syncProducts(ProductCollection $collection, Store $store)
+ {
+ if (!$this->nostoDataHelper->isProductUpdatesEnabled($store)) {
+ $this->logDebugWithStore(
+ 'Nosto product sync is disabled - skipping upserting products to Nosto',
+ $store
+ );
+ return;
+ }
+ $account = $this->nostoHelperAccount->findAccount($store);
+ $this->startBenchmark(self::BENCHMARK_SYNC_NAME, self::BENCHMARK_SYNC_BREAKPOINT);
+
+ $collection->setPageSize($this->apiBatchSize);
+ $iterator = new PagingIterator($collection);
+
+ /** @var ProductCollection $page */
+ foreach ($iterator as $page) {
+ $productIdsInBatch = [];
+ $this->checkMemoryConsumption('product sync');
+ $op = new PriceUpdate($account, $this->nostoHelperUrl->getActiveDomain($store));
+ $op->setResponseTimeout($this->apiTimeout);
+ $products = $this->productRepository->getByIds(
+ $page->getAllIds(
+ $this->apiBatchSize,
+ ($iterator->getCurrentPageNumber() - 1) * $this->apiBatchSize
+ )
+ );
+ /** @var Product $product */
+ foreach ($products->getItems() as $product) {
+ $productIdsInBatch[] = $product->getId();
+ $nostoProduct = $this->productService->getProduct($product, $store);
+ if ($nostoProduct === null) {
+ throw new NostoException('Could not get product from the product service.');
+ }
+ $op->addProduct($nostoProduct);
+ // phpcs:ignore
+ $this->cacheService->save($nostoProduct, $store);
+ $this->tickBenchmark(self::BENCHMARK_SYNC_NAME);
+ }
+
+ $this->logDebugWithStore(
+ sprintf(
+ 'Upserting batch of %d (%s) - API timeout is set to %d seconds',
+ $this->apiBatchSize,
+ implode(',', $productIdsInBatch),
+ $this->apiTimeout
+ ),
+ $store
+ );
+ $op->upsert();
+ }
+ $this->logBenchmarkSummary(self::BENCHMARK_SYNC_NAME, $store, $this);
+ }
+}
diff --git a/etc/communication.xml b/etc/communication.xml
index bbbd74187..841f413cb 100644
--- a/etc/communication.xml
+++ b/etc/communication.xml
@@ -41,4 +41,10 @@
+
+
+
+
+
+
diff --git a/etc/di.xml b/etc/di.xml
index cccf6dccd..ea45f1753 100644
--- a/etc/di.xml
+++ b/etc/di.xml
@@ -199,6 +199,20 @@
+
+
+
+ Nosto\Tagging\Model\Indexer\Dimensions\StoreDimensionProvider
+
+
+
+
+
+
+ Nosto\Tagging\Model\Indexer\Dimensions\StoreDimensionProvider
+
+
+
diff --git a/etc/indexer.xml b/etc/indexer.xml
index 00183f6ee..3029e6fcd 100644
--- a/etc/indexer.xml
+++ b/etc/indexer.xml
@@ -38,4 +38,12 @@
Nosto Product Indexer
Populates message queue with product ids to be sent to Nosto
+
+ Nosto Product Price Partial Indexer
+ Populates message queue with product ids that have changed price to be sent to Nosto
+
+
+ Nosto Product Inventory Partial Indexer
+ Populates message queue with product ids that have changed inventory to be sent to Nosto
+
diff --git a/etc/mview.xml b/etc/mview.xml
index 232767d84..f2be25496 100644
--- a/etc/mview.xml
+++ b/etc/mview.xml
@@ -50,13 +50,6 @@
-
-
-
-
-
-
-
@@ -64,4 +57,15 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/etc/queue.xml b/etc/queue.xml
index f8ad78b09..65a0268fd 100644
--- a/etc/queue.xml
+++ b/etc/queue.xml
@@ -47,4 +47,16 @@
consumerInstance="Magento\Framework\MessageQueue\Consumer"
handler="Nosto\Tagging\Model\Service\Sync\Delete\AsyncBulkConsumer::process"/>
+
+
+
+
+
+
diff --git a/etc/queue_publisher.xml b/etc/queue_publisher.xml
index 1194a0372..433aa489a 100644
--- a/etc/queue_publisher.xml
+++ b/etc/queue_publisher.xml
@@ -41,4 +41,10 @@
+
+
+
+
+
+
diff --git a/etc/queue_topology.xml b/etc/queue_topology.xml
index 2b929aa7b..9925e5076 100644
--- a/etc/queue_topology.xml
+++ b/etc/queue_topology.xml
@@ -38,5 +38,7 @@
+
+