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