Skip to content

Commit

Permalink
Merge pull request #118 from AmpersandHQ/test-cancel-order-with-delet…
Browse files Browse the repository at this point in the history
…ed-product

Fix edge case canceling an order with a deleted product
  • Loading branch information
cvdberg1 authored Aug 1, 2023
2 parents 4263e3c + 9cbfbb7 commit d26ec20
Show file tree
Hide file tree
Showing 3 changed files with 253 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
<?php
declare(strict_types=1);
namespace Ampersand\DisableStockReservation\Test\Integration\Cancel;

use Magento\Framework\MessageQueue\QueueRepository;
use Magento\Framework\MessageQueue\Consumer\ConfigInterface as ConsumerConfig;
use Magento\Framework\MessageQueue\Consumer\Config\ConsumerConfigItemInterface;
use Magento\Framework\Registry;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Framework\ObjectManagerInterface;
use Magento\InventoryApi\Api\GetSourceItemsBySkuInterface;
use Magento\Framework\MessageQueue\ConsumerFactory;
use Magento\InventorySales\Model\GetStockBySalesChannelCache;
use Magento\TestFramework\Helper\Bootstrap;
use TddWizard\Fixtures\Catalog\ProductBuilder;
use TddWizard\Fixtures\Catalog\ProductFixture;
use TddWizard\Fixtures\Sales\ShipmentBuilder;
use TddWizard\Fixtures\Sales\InvoiceBuilder;
use TddWizard\Fixtures\Checkout\CustomerCheckout;
use TddWizard\Fixtures\Checkout\CartBuilder;
use TddWizard\Fixtures\Customer\CustomerFixture;
use TddWizard\Fixtures\Customer\CustomerBuilder;
use TddWizard\Fixtures\Customer\AddressBuilder;
use PHPUnit\Framework\TestCase;

class CancelOrderWithDeletedProduct extends TestCase
{
/** @var ObjectManagerInterface */
private $objectManager;

/** @var GetSourceItemsBySkuInterface */
private $getSourceItemsBySku;

/** @var ConsumerFactory */
private $consumerFactory;

/** @var ProductRepositoryInterface */
private $productRepository;

/** @var ConsumerConfig*/
private $consumerConfig;

/** @var QueueRepository */
private $queueRepository;

/** @var Registry */
private $registry;

/** @var ProductFixture */
private $productFixture;

/** @var CustomerFixture */
private $customerFixture;

public function setUp(): void
{
parent::setUp();

$this->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');
}
}
2 changes: 1 addition & 1 deletion src/Observer/CancelOrderItemObserver.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,6 @@ public function execute(EventObserver $observer): void
return;
}

$this->executeSourceDeductionForItems->executeSourceDeductionForItems($orderItem, $itemsToCancel);
$this->executeSourceDeductionForItems->executeSourceDeductionForItems($orderItem, $itemsToCancel, true);
}
}
15 changes: 12 additions & 3 deletions src/Service/ExecuteSourceDeductionForItems.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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);
}
}
}

0 comments on commit d26ec20

Please sign in to comment.