Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 80 additions & 23 deletions app/code/Magento/QuoteGraphQl/Model/Resolver/CartPrices.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?php
/**
* Copyright 2019 Adobe
* All Rights Reserved.
* Copyright 2025 Adobe
* * All Rights Reserved.
*/
declare(strict_types=1);

Expand All @@ -13,13 +13,20 @@
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Magento\Framework\Api\DataObjectHelper;
use Magento\Framework\Api\ExtensibleDataInterface;
use Magento\Quote\Api\Data\TotalsInterface as QuoteTotalsInterface;
use Magento\Quote\Api\Data\TotalsInterfaceFactory;
use Magento\Quote\Model\Quote;
use Magento\Quote\Model\Quote\Address\Total;
use Magento\Quote\Model\Cart\Totals as CartTotals;
use Magento\QuoteGraphQl\Model\Cart\TotalsCollector;
use Magento\Store\Model\ScopeInterface;

/**
* @inheritdoc
*
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class CartPrices implements ResolverInterface
{
Expand All @@ -28,17 +35,26 @@ class CartPrices implements ResolverInterface
*/
private $totalsCollector;

/**
* @var string
*/
private const QUERY_TYPE = 'query';

/**
* @var ScopeConfigInterface
*/
private ScopeConfigInterface $scopeConfig;

/**
* @param TotalsCollector $totalsCollector
* @param TotalsInterfaceFactory $totalsFactory
* @param DataObjectHelper $dataObjectHelper
* @param ScopeConfigInterface|null $scopeConfig
*/
public function __construct(
TotalsCollector $totalsCollector,
private TotalsInterfaceFactory $totalsFactory,
private DataObjectHelper $dataObjectHelper,
?ScopeConfigInterface $scopeConfig = null
) {
$this->totalsCollector = $totalsCollector;
Expand All @@ -56,16 +72,30 @@ 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
);

$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;
Expand All @@ -92,14 +122,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;
Expand Down Expand Up @@ -133,37 +168,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 $cartTotals->getSubtotal() + $discountExclTax;
return $totals->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));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?php
/**
* Copyright 2021 Adobe
* All Rights Reserved.
* Copyright 2025 Adobe
* * All Rights Reserved.
*/
declare(strict_types=1);

Expand All @@ -11,16 +11,23 @@
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Magento\Framework\Api\DataObjectHelper;
use Magento\GraphQl\Model\Query\Context;
use Magento\Quote\Api\Data\TotalsInterface;
use Magento\Quote\Api\Data\TotalsInterfaceFactory;
use Magento\Quote\Model\Quote;
use Magento\Quote\Model\Quote\Address;
use Magento\Quote\Model\Quote\Address\Total;
use Magento\QuoteGraphQl\Model\Cart\TotalsCollector;
use Magento\QuoteGraphQl\Model\Resolver\CartPrices;
use GraphQL\Language\AST\OperationDefinitionNode;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\MockObject\MockObject;

/**
* @see CartPrices
*
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class CartPricesTest extends TestCase
{
Expand Down Expand Up @@ -49,6 +56,11 @@ class CartPricesTest extends TestCase
*/
private ResolveInfo $resolveInfoMock;

/**
* @var DataObjectHelper|MockObject
*/
private DataObjectHelper $dataObjectHelperMock;

/**
* @var Context|MockObject
*/
Expand All @@ -64,6 +76,16 @@ class CartPricesTest extends TestCase
*/
private Total $totalMock;

/**
* @var TotalsInterfaceFactory|MockObject
*/
private $totalsFactoryMock;

/**
* @var Address|MockObject
*/
private $shippingAddressMock;

/**
* @var array
*/
Expand All @@ -72,13 +94,31 @@ class CartPricesTest extends TestCase
protected function setUp(): void
{
$this->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()
Expand All @@ -96,6 +136,8 @@ protected function setUp(): void
->getMock();
$this->cartPrices = new CartPrices(
$this->totalsCollectorMock,
$this->totalsFactoryMock,
$this->dataObjectHelperMock,
$this->scopeConfigMock
);
}
Expand All @@ -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
Expand All @@ -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')
Expand Down