diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartPrices.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartPrices.php index 282137e930d8c..5bafcc83dd09b 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartPrices.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartPrices.php @@ -1,6 +1,6 @@ totalsCollector = $totalsCollector; @@ -56,16 +72,34 @@ public function resolve(Field $field, $context, ResolveInfo $info, ?array $value /** @var Quote $quote */ $quote = $value['model']; - /** - * To calculate a right discount value - * before calculate totals - * need to reset Cart Fixed Rules in the quote - */ - $quote->setCartFixedRules([]); - $cartTotals = $this->totalsCollector->collectQuoteTotals($quote); $currency = $quote->getQuoteCurrencyCode(); - $appliedTaxes = $this->getAppliedTaxes($cartTotals, $currency); + if (!$quote->isVirtual() && $info->operation->operation == self::QUERY_TYPE) { + $addressTotalsData = $quote->getShippingAddress()->getData(); + unset($addressTotalsData[ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY]); + $cartTotals = $this->totalsFactory->create(); + $this->dataObjectHelper->populateWithArray( + $cartTotals, + $addressTotalsData, + QuoteTotalsInterface::class + ); + + if (isset($addressTotalsData['discount_description'])) { + $cartTotals->setDiscountDescription($addressTotalsData['discount_description']); + } + + $appliedTaxes = $this->getAppliedTaxes($quote->getShippingAddress(), $currency); + } else { + /** + * To calculate a right discount value + * before calculate totals + * need to reset Cart Fixed Rules in the quote + */ + $quote->setCartFixedRules([]); + $cartTotals = $this->totalsCollector->collectQuoteTotals($quote); + $appliedTaxes = $this->getAppliedTaxes($cartTotals, $currency); + } + $grandTotal = $cartTotals->getGrandTotal(); $totalAppliedTaxes = 0; @@ -92,14 +126,19 @@ public function resolve(Field $field, $context, ResolveInfo $info, ?array $value /** * Returns taxes applied to the current quote * - * @param Total $total + * @param \Magento\Quote\Model\Quote\Address|Total $addressOrTotals * @param string $currency * @return array + * @throws \InvalidArgumentException */ - private function getAppliedTaxes(Total $total, string $currency): array + private function getAppliedTaxes($addressOrTotals, string $currency): array { + if (!$addressOrTotals instanceof Total && !$addressOrTotals instanceof \Magento\Quote\Model\Quote\Address) { + throw new \InvalidArgumentException('Unsupported totals type: ' . get_class($addressOrTotals)); + } + $appliedTaxesData = []; - $appliedTaxes = $total->getAppliedTaxes(); + $appliedTaxes = $addressOrTotals->getAppliedTaxes(); if (empty($appliedTaxes)) { return $appliedTaxesData; @@ -133,37 +172,59 @@ private function getAppliedTaxes(Total $total, string $currency): array /** * Returns information about an applied discount * - * @param Total $total + * @param Total|CartTotals $totals * @param string $currency * @return array|null + * @throws \InvalidArgumentException */ - private function getDiscount(Total $total, string $currency) + private function getDiscount($totals, string $currency) { - if ($total->getDiscountAmount() === 0) { + $this->validateTotalsInstance($totals); + + if ($totals->getDiscountAmount() == 0) { return null; } return [ - 'label' => $total->getDiscountDescription() !== null ? explode(', ', $total->getDiscountDescription()) : [], - 'amount' => ['value' => $total->getDiscountAmount(), 'currency' => $currency] + 'label' => $totals->getDiscountDescription() !== null ? + explode(', ', $totals->getDiscountDescription()) : [], + 'amount' => ['value' => $totals->getDiscountAmount(), 'currency' => $currency] ]; } /** * Get Subtotal with discount excluding tax. * - * @param Total $cartTotals + * @param Total|CartTotals $totals * @return float + * @throws \InvalidArgumentException */ - private function getSubtotalWithDiscountExcludingTax(Total $cartTotals): float + private function getSubtotalWithDiscountExcludingTax($totals): float { + $this->validateTotalsInstance($totals); + $discountIncludeTax = $this->scopeConfig->getValue( 'tax/calculation/discount_tax', ScopeInterface::SCOPE_STORE ) ?? 0; $discountExclTax = $discountIncludeTax ? - $cartTotals->getDiscountAmount() + $cartTotals->getDiscountTaxCompensationAmount() : - $cartTotals->getDiscountAmount(); + $totals->getDiscountAmount() + $totals->getDiscountTaxCompensationAmount() : + $totals->getDiscountAmount(); + + return $totals->getSubtotal() + $discountExclTax; + } - return $cartTotals->getSubtotal() + $discountExclTax; + /** + * Validates the provided totals instance to ensure it is of a supported type. + * + * @param Total|CartTotals $totals + * @return void + * @throws \InvalidArgumentException If the provided totals instance is of an unsupported type. + */ + private function validateTotalsInstance($totals) + { + + if (!$totals instanceof Total && !$totals instanceof CartTotals) { + throw new \InvalidArgumentException('Unsupported totals type: ' . get_class($totals)); + } } } diff --git a/app/code/Magento/QuoteGraphQl/Test/Unit/Model/Resolver/CartPricesTest.php b/app/code/Magento/QuoteGraphQl/Test/Unit/Model/Resolver/CartPricesTest.php index 544c7554622ac..4792da6745b73 100644 --- a/app/code/Magento/QuoteGraphQl/Test/Unit/Model/Resolver/CartPricesTest.php +++ b/app/code/Magento/QuoteGraphQl/Test/Unit/Model/Resolver/CartPricesTest.php @@ -1,6 +1,6 @@ totalsCollectorMock = $this->createMock(TotalsCollector::class); + $this->dataObjectHelperMock = $this->createMock(DataObjectHelper::class); + $this->totalsFactoryMock = $this->getMockBuilder(TotalsInterfaceFactory::class) + ->disableOriginalConstructor() + ->onlyMethods(['create']) + ->addMethods( + [ + 'getSubtotal', + 'getSubtotalInclTax', + 'getGrandTotal', + 'getDiscountTaxCompensationAmount', + 'getDiscountAmount', + 'getDiscountDescription', + 'getAppliedTaxes' + ] + ) + ->getMock(); $this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class); $this->fieldMock = $this->createMock(Field::class); $this->resolveInfoMock = $this->createMock(ResolveInfo::class); + $this->resolveInfoMock->operation = new OperationDefinitionNode([]); $this->contextMock = $this->createMock(Context::class); $this->quoteMock = $this->getMockBuilder(Quote::class) ->disableOriginalConstructor() ->addMethods(['getQuoteCurrencyCode']) + ->onlyMethods(['isVirtual', 'getShippingAddress']) ->getMock(); $this->totalMock = $this->getMockBuilder(Total::class) ->disableOriginalConstructor() @@ -96,6 +136,8 @@ protected function setUp(): void ->getMock(); $this->cartPrices = new CartPrices( $this->totalsCollectorMock, + $this->totalsFactoryMock, + $this->dataObjectHelperMock, $this->scopeConfigMock ); } @@ -107,7 +149,70 @@ public function testResolveWithoutModelInValueParameter(): void $this->cartPrices->resolve($this->fieldMock, $this->contextMock, $this->resolveInfoMock, $this->valueMock); } - public function testResolve(): void + public function testResolveQuery(): void + { + $this->resolveInfoMock->operation->operation = 'query'; + + $this->shippingAddressMock = $this->getMockBuilder(Address::class) + ->disableOriginalConstructor() + ->onlyMethods(['getData']) + ->getMock(); + + $this->shippingAddressMock->expects($this->any()) + ->method('getData') + ->willReturn([]); + + $this->quoteMock + ->expects($this->once()) + ->method('isVirtual') + ->willReturn(0); + + $this->quoteMock + ->expects($this->any()) + ->method('getShippingAddress') + ->willReturn($this->shippingAddressMock); + + $this->dataObjectHelperMock->expects($this->once()) + ->method('populateWithArray') + ->with( + $this->identicalTo($this->totalMock), + [], + TotalsInterface::class + ); + + $this->totalsFactoryMock + ->expects($this->once()) + ->method('create') + ->willReturn($this->totalMock); + + $this->resolve(); + } + + public function testResolveQueryVirtual(): void + { + $this->quoteMock + ->expects($this->once()) + ->method('isVirtual') + ->willReturn(1); + + $this->totalMock + ->expects($this->once()) + ->method('getAppliedTaxes'); + + $this->resolve(); + } + public function testResolveMutation(): void + { + $this->resolveInfoMock->operation->operation = 'mutation'; + + $this->totalMock + ->expects($this->once()) + ->method('getAppliedTaxes'); + + $this->resolve(); + } + + private function resolve(): void { $this->valueMock = ['model' => $this->quoteMock]; $this->quoteMock @@ -126,9 +231,6 @@ public function testResolve(): void $this->totalMock ->method('getDiscountDescription') ->willReturn('Discount Description'); - $this->totalMock - ->expects($this->once()) - ->method('getAppliedTaxes'); $this->scopeConfigMock ->expects($this->once()) ->method('getValue')