diff --git a/dev/MagentoTests/Integration/Cancel/CancelOrderWithDeletedProductTest.php b/dev/MagentoTests/Integration/Cancel/CancelOrderWithDeletedProductTest.php new file mode 100644 index 0000000..b64dfc5 --- /dev/null +++ b/dev/MagentoTests/Integration/Cancel/CancelOrderWithDeletedProductTest.php @@ -0,0 +1,240 @@ +objectManager = Bootstrap::getObjectManager(); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->productRepository->cleanCache(); + $this->getSourceItemsBySku = $this->objectManager->get(GetSourceItemsBySkuInterface::class); + $this->consumerFactory = $this->objectManager->get(ConsumerFactory::class); + $this->registry = $this->objectManager->get(Registry::class); + $this->consumerConfig = $this->objectManager->get(ConsumerConfig::class); + $this->queueRepository = $this->objectManager->get(QueueRepository::class); + } + + /** + * @magentoConfigFixture default/cataloginventory/options/synchronize_with_catalog 0 + */ + public function testCancelOrderWithDeletedProductAndNotSyncWithCatalog() + { + $sku = uniqid('product_will_be_deleted'); + + /** + * Create a product with 5 qty + */ + $this->productFixture = new ProductFixture( + ProductBuilder::aSimpleProduct() + ->withSku($sku) + ->withPrice(10) + ->withStockQty(5) + ->withIsInStock(true) + ->build() + ); + + /** + * Create a customer and login + */ + $this->customerFixture = new CustomerFixture(CustomerBuilder::aCustomer()->withAddresses( + AddressBuilder::anAddress()->asDefaultBilling(), + AddressBuilder::anAddress()->asDefaultShipping() + )->build()); + $this->customerFixture->login(); + + /** + * Order 1 qty + */ + $checkout = CustomerCheckout::fromCart( + CartBuilder::forCurrentSession() + ->withSimpleProduct( + $this->productFixture->getSku(), + 1 + ) + ->build() + ); + + $order = $checkout + ->withPaymentMethodCode('checkmo') + ->placeOrder(); + $this->assertGreaterThan(0, strlen($order->getIncrementId()), 'the order does not have a valid increment_id'); + $this->assertIsNumeric($order->getId(), 'the order does not have an entity_id'); + + /** + * Delete the product + */ + $origVal = $this->registry->registry('isSecureArea'); + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', true); + $product = $this->productRepository->deleteById($sku); + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', $origVal); + + try { + $this->productRepository->getById($sku); + $this->fail('This product should not be loadable: ' . $sku); + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + // This exception is expected to happen + } + + /** + * cataloginventory/options/synchronize_with_catalog=0 + * + * Verify the source items still exist after the delete + */ + $sourceItems = $this->getSourceItemsBySku->execute($sku); + self::assertNotEmpty($sourceItems); + + /** + * Cancel the order + */ + $this->assertTrue($order->canCancel(), 'Cannot cancel the order'); + $order->cancel(); + $this->assertEquals('canceled', $order->getStatus(), 'The order was not cancelled'); + } + + /** + * @magentoConfigFixture default/cataloginventory/options/synchronize_with_catalog 1 + */ + public function testCancelOrderWithDeletedProductAndSyncWithCatalog() + { + $sku = uniqid('product_will_be_deleted'); + + /** + * Create a product with 5 qty + */ + $this->productFixture = new ProductFixture( + ProductBuilder::aSimpleProduct() + ->withSku($sku) + ->withPrice(10) + ->withStockQty(5) + ->withIsInStock(true) + ->build() + ); + + /** + * Create a customer and login + */ + $this->customerFixture = new CustomerFixture(CustomerBuilder::aCustomer()->withAddresses( + AddressBuilder::anAddress()->asDefaultBilling(), + AddressBuilder::anAddress()->asDefaultShipping() + )->build()); + $this->customerFixture->login(); + + /** + * Order 1 qty + */ + $checkout = CustomerCheckout::fromCart( + CartBuilder::forCurrentSession() + ->withSimpleProduct( + $this->productFixture->getSku(), + 1 + ) + ->build() + ); + + $order = $checkout + ->withPaymentMethodCode('checkmo') + ->placeOrder(); + $this->assertGreaterThan(0, strlen($order->getIncrementId()), 'the order does not have a valid increment_id'); + $this->assertIsNumeric($order->getId(), 'the order does not have an entity_id'); + + /** + * Ensure consumers are cleared before next steps + * + * Backported logic from \Magento\TestFramework\MessageQueue\ClearQueueProcessor + */ + /** @var ConsumerConfigItemInterface $consumerConfig */ + $consumerConfig = $this->consumerConfig->getConsumer('inventory.source.items.cleanup'); + $queue = $this->queueRepository->get($consumerConfig->getConnection(), $consumerConfig->getQueue()); + while ($message = $queue->dequeue()) { + $queue->acknowledge($message); + } + + /** + * Delete the product + */ + $origVal = $this->registry->registry('isSecureArea'); + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', true); + $product = $this->productRepository->deleteById($sku); + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', $origVal); + + try { + $this->productRepository->getById($sku); + $this->fail('This product should not be loadable: ' . $sku); + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + // This exception is expected to happen + } + + /** + * Process the source items cleanup consumers and verify the deletions worked, because + * cataloginventory/options/synchronize_with_catalog=1 + */ + $consumer = $this->consumerFactory->get('inventory.source.items.cleanup'); + $consumer->process(1); + + $sourceItems = $this->getSourceItemsBySku->execute($sku); + self::assertEmpty($sourceItems); + + /** + * Cancel the order + */ + $this->assertTrue($order->canCancel(), 'Cannot cancel the order'); + $order->cancel(); + $this->assertEquals('canceled', $order->getStatus(), 'The order was not cancelled'); + } +} diff --git a/src/Observer/CancelOrderItemObserver.php b/src/Observer/CancelOrderItemObserver.php index 7e96f1d..77f4c14 100644 --- a/src/Observer/CancelOrderItemObserver.php +++ b/src/Observer/CancelOrderItemObserver.php @@ -52,6 +52,6 @@ public function execute(EventObserver $observer): void return; } - $this->executeSourceDeductionForItems->executeSourceDeductionForItems($orderItem, $itemsToCancel); + $this->executeSourceDeductionForItems->executeSourceDeductionForItems($orderItem, $itemsToCancel, true); } } diff --git a/src/Service/ExecuteSourceDeductionForItems.php b/src/Service/ExecuteSourceDeductionForItems.php index dec4d8f..071202c 100644 --- a/src/Service/ExecuteSourceDeductionForItems.php +++ b/src/Service/ExecuteSourceDeductionForItems.php @@ -113,9 +113,10 @@ public function __construct( /** * @param OrderItem $orderItem * @param array $itemsToCancel + * @param bool $graceful * @throws NoSuchEntityException */ - public function executeSourceDeductionForItems(OrderItem $orderItem, array $itemsToCancel) + public function executeSourceDeductionForItems(OrderItem $orderItem, array $itemsToCancel, bool $graceful = false) { $order = $orderItem->getOrder(); @@ -163,12 +164,20 @@ public function executeSourceDeductionForItems(OrderItem $orderItem, array $item 'salesEvent' => $salesEvent ]); - $this->sourceDeductionService->execute($sourceDeductionRequest); + try { + $this->sourceDeductionService->execute($sourceDeductionRequest); + } catch (NoSuchEntityException $noSuchEntityException) { + if (!$graceful) { + throw $noSuchEntityException; + } + } } } $itemsIds = $this->product->getProductsIdsBySkus($itemsSkus); $itemsIds = array_values(array_map('intval', $itemsIds)); - $this->priceIndexer->reindexList($itemsIds); + if (!empty($itemsIds)) { + $this->priceIndexer->reindexList($itemsIds); + } } }