diff --git a/app/code/Magento/SalesGraphQl/Model/OrderItem/DataProvider.php b/app/code/Magento/SalesGraphQl/Model/OrderItem/DataProvider.php index 6dc44c794d70..26b9fb133d9d 100644 --- a/app/code/Magento/SalesGraphQl/Model/OrderItem/DataProvider.php +++ b/app/code/Magento/SalesGraphQl/Model/OrderItem/DataProvider.php @@ -64,6 +64,13 @@ class DataProvider */ private $orderItemList = []; + /** + * Orders already loaded upstream, keyed by entity id, so the item load does not reload them. + * + * @var OrderInterface[] + */ + private $providedOrders = []; + /** * @param OrderItemRepositoryInterface $orderItemRepository * @param ProductRepositoryInterface $productRepository @@ -101,6 +108,16 @@ public function addOrderItemId(int $orderItemId): void } } + /** + * Provide an already-loaded order so the item fetch can reuse it instead of reloading it from the DB + * + * @param OrderInterface $order + */ + public function addOrder(OrderInterface $order): void + { + $this->providedOrders[(int)$order->getEntityId()] = $order; + } + /** * Get order item by item id * @@ -207,22 +224,27 @@ function ($orderItem) { */ private function fetchOrders(array $orderItems): array { - $orderIds = array_map( - function ($orderItem) { - return $orderItem->getOrderId(); - }, - $orderItems - ); - - $searchCriteria = $this->searchCriteriaBuilder - ->addFilter('entity_id', $orderIds, 'in') - ->create(); - $orders = $this->orderRepository->getList($searchCriteria)->getItems(); + // Start from orders the caller already loaded (e.g. the customer's order list resolver) so they + // are not queried again, and only load the ids that were not provided. + $orderList = $this->providedOrders; + $missingOrderIds = []; + foreach ($orderItems as $orderItem) { + $orderId = (int)$orderItem->getOrderId(); + if (!isset($orderList[$orderId])) { + $missingOrderIds[$orderId] = $orderId; + } + } - $orderList = []; - foreach ($orders as $order) { - $orderList[$order->getEntityId()] = $order; + if (!empty($missingOrderIds)) { + $searchCriteria = $this->searchCriteriaBuilder + ->addFilter('entity_id', $missingOrderIds, 'in') + ->create(); + $orders = $this->orderRepository->getList($searchCriteria)->getItems(); + foreach ($orders as $order) { + $orderList[(int)$order->getEntityId()] = $order; + } } + return $orderList; } diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/CreditMemo/CreditMemoItems.php b/app/code/Magento/SalesGraphQl/Model/Resolver/CreditMemo/CreditMemoItems.php index 2f3b59b9832f..1c5f6213af09 100644 --- a/app/code/Magento/SalesGraphQl/Model/Resolver/CreditMemo/CreditMemoItems.php +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/CreditMemo/CreditMemoItems.php @@ -67,6 +67,7 @@ public function resolve( $creditMemoModel = $value['model']; /** @var OrderInterface $parentOrderModel */ $parentOrderModel = $value['order']; + $this->orderItemProvider->addOrder($parentOrderModel); return $this->valueFactory->create( $this->getCreditMemoItems($parentOrderModel, $creditMemoModel->getItems()) diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/Invoice/InvoiceItems.php b/app/code/Magento/SalesGraphQl/Model/Resolver/Invoice/InvoiceItems.php index d602c44bb4de..e65f98d2d0f7 100644 --- a/app/code/Magento/SalesGraphQl/Model/Resolver/Invoice/InvoiceItems.php +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/Invoice/InvoiceItems.php @@ -67,6 +67,7 @@ public function resolve( $invoiceModel = $value['model']; /** @var OrderInterface $parentOrderModel */ $parentOrderModel = $value['order']; + $this->orderItemProvider->addOrder($parentOrderModel); return $this->valueFactory->create( $this->getInvoiceItems($parentOrderModel, $invoiceModel->getItems()) diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/OrderItems.php b/app/code/Magento/SalesGraphQl/Model/Resolver/OrderItems.php index dd38f04245f9..9fac595fd09c 100644 --- a/app/code/Magento/SalesGraphQl/Model/Resolver/OrderItems.php +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/OrderItems.php @@ -54,6 +54,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, ?array $value } /** @var OrderInterface $parentOrder */ $parentOrder = $value['model']; + $this->orderItemProvider->addOrder($parentOrder); $orderItemIds = []; foreach ($parentOrder->getItems() as $item) { if (!$item->getParentItemId()) { diff --git a/app/code/Magento/SalesGraphQl/Test/Unit/Model/OrderItem/DataProviderTest.php b/app/code/Magento/SalesGraphQl/Test/Unit/Model/OrderItem/DataProviderTest.php new file mode 100644 index 000000000000..dd610c33f136 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Test/Unit/Model/OrderItem/DataProviderTest.php @@ -0,0 +1,183 @@ +orderItemRepository = $this->createMock(OrderItemRepositoryInterface::class); + $this->productRepository = $this->createMock(ProductRepositoryInterface::class); + $this->orderRepository = $this->createMock(OrderRepositoryInterface::class); + $this->searchCriteriaBuilder = $this->createMock(SearchCriteriaBuilder::class); + $this->optionsProcessor = $this->createMock(OptionsProcessor::class); + $this->taxHelper = $this->createMock(TaxHelper::class); + + $this->searchCriteriaBuilder->method('addFilter')->willReturnSelf(); + $this->searchCriteriaBuilder->method('create')->willReturn($this->createMock(SearchCriteria::class)); + $this->optionsProcessor->method('getItemOptions') + ->willReturn(['selected_options' => [], 'entered_options' => []]); + + $this->dataProvider = new DataProvider( + $this->orderItemRepository, + $this->productRepository, + $this->orderRepository, + $this->searchCriteriaBuilder, + $this->optionsProcessor, + $this->taxHelper + ); + } + + public function testProvidedOrderIsNotReloadedFromRepository(): void + { + $orderId = 5; + $orderItemId = 42; + $orderItem = $this->createOrderItemMock($orderItemId, $orderId, 7); + $order = $this->createOrderMock($orderId, 'USD'); + + $this->stubItemAndProductLists([$orderItem]); + // The order is already loaded upstream, so it must not be queried again. + $this->orderRepository->expects($this->never())->method('getList'); + + $this->dataProvider->addOrderItemId($orderItemId); + $this->dataProvider->addOrder($order); + $result = $this->dataProvider->getOrderItemById($orderItemId); + + $this->assertSame('USD', $result['product_sale_price']['currency']); + } + + public function testMissingOrderFallsBackToRepository(): void + { + $orderId = 9; + $orderItemId = 77; + $orderItem = $this->createOrderItemMock($orderItemId, $orderId, 3); + $order = $this->createOrderMock($orderId, 'EUR'); + + $this->stubItemAndProductLists([$orderItem]); + // No order was provided, so the repository must be queried for the missing id. + $orderSearchResult = $this->createMock(OrderSearchResultInterface::class); + $orderSearchResult->method('getItems')->willReturn([$order]); + $this->orderRepository->expects($this->once())->method('getList')->willReturn($orderSearchResult); + + $this->dataProvider->addOrderItemId($orderItemId); + $result = $this->dataProvider->getOrderItemById($orderItemId); + + $this->assertSame('EUR', $result['product_sale_price']['currency']); + } + + /** + * @param OrderItemModel[] $orderItems + */ + private function stubItemAndProductLists(array $orderItems): void + { + $itemSearchResult = $this->createMock(OrderItemSearchResultInterface::class); + $itemSearchResult->method('getItems')->willReturn($orderItems); + $this->orderItemRepository->method('getList')->willReturn($itemSearchResult); + + $productSearchResult = $this->createMock(ProductSearchResultsInterface::class); + $productSearchResult->method('getItems')->willReturn([]); + $this->productRepository->method('getList')->willReturn($productSearchResult); + } + + /** + * @param int $itemId + * @param int $orderId + * @param int $productId + * @return OrderItemModel|MockObject + */ + private function createOrderItemMock(int $itemId, int $orderId, int $productId) + { + $orderItem = $this->createMock(OrderItemModel::class); + // getItemId() is a DB value surfaced as a string; the provider base64_encodes it. + $orderItem->method('getItemId')->willReturn((string)$itemId); + $orderItem->method('getOrderId')->willReturn($orderId); + $orderItem->method('getProductId')->willReturn($productId); + $orderItem->method('getName')->willReturn('Item name'); + $orderItem->method('getSku')->willReturn('sku-' . $itemId); + $orderItem->method('getProductType')->willReturn('simple'); + $orderItem->method('getChildrenItems')->willReturn([]); + $orderItem->method('getStatus')->willReturn('Ordered'); + $orderItem->method('getDiscountAmount')->willReturn(0); + $orderItem->method('getPrice')->willReturn(10.0); + $orderItem->method('getPriceInclTax')->willReturn(12.0); + return $orderItem; + } + + /** + * @param int $orderId + * @param string $currency + * @return OrderInterface|MockObject + */ + private function createOrderMock(int $orderId, string $currency) + { + $order = $this->createMock(OrderInterface::class); + $order->method('getEntityId')->willReturn($orderId); + $order->method('getStoreId')->willReturn(1); + $order->method('getOrderCurrencyCode')->willReturn($currency); + $order->method('getDiscountDescription')->willReturn(null); + $order->method('getDiscountAmount')->willReturn(0); + return $order; + } +}