Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
dfc2a18
#40209 - Conditional TotalCollect Trigger for mutation and Virtual Carts
Oct 11, 2025
170d7bc
Merge branch '2.4-develop' into Fix-QuoteGraphQl-CartPrices-Performance
senthilengg Oct 11, 2025
82f2786
#40209 - Fixing Implicit Null of ScopeConfig
Oct 11, 2025
43760cc
#40209 - Fixing Static Tests
Oct 11, 2025
3fd68ba
#40209 - Removing Extenstion Attribute after Shipping getData()
Oct 13, 2025
a7ca4ed
Merge branch '2.4-develop' into Fix-QuoteGraphQl-CartPrices-Performance
senthilengg Oct 13, 2025
64c6b34
Merge branch '2.4-develop' into Fix-QuoteGraphQl-CartPrices-Performance
senthilengg Oct 15, 2025
1cbabde
Merge branch '2.4-develop' into Fix-QuoteGraphQl-CartPrices-Performance
senthilengg Oct 17, 2025
5474f24
Merge branch '2.4-develop' into Fix-QuoteGraphQl-CartPrices-Performance
senthilengg Oct 21, 2025
2c13e99
Fix copyright comment formatting in CartPrices.php
senthilengg Oct 21, 2025
723bb3d
Fix copyright notice formatting in CartPricesTest.php
senthilengg Oct 21, 2025
11b684c
Merge branch '2.4-develop' into Fix-QuoteGraphQl-CartPrices-Performance
senthilengg Oct 24, 2025
62f16f9
Merge branch '2.4-develop' into Fix-QuoteGraphQl-CartPrices-Performance
engcom-Hotel Oct 27, 2025
4b41a49
Merge branch '2.4-develop' into Fix-QuoteGraphQl-CartPrices-Performance
senthilengg Oct 29, 2025
602829c
#40209 - Fixing discount lable null and WebAPI Test Failures
Oct 29, 2025
0495e41
#40209 - Adding Discounts to graphql response retriving from extensio…
Oct 30, 2025
f4ee24a
#40209 - Made new params in constructor as optional and updated unit …
Oct 30, 2025
7dc3e80
#40209 - Removed Extension Attributes and skip optimisation if discou…
Oct 31, 2025
e2d9c23
Merge branch '2.4-develop' into Fix-QuoteGraphQl-CartPrices-Performance
senthilengg Oct 31, 2025
0fd2f0a
#40209 - Removed unused variable
Nov 1, 2025
e1843a8
#40209 - Adding Trigger Recollect as a condition to fire collectTotals
Nov 1, 2025
2d74943
Merge branch '2.4-develop' into Fix-QuoteGraphQl-CartPrices-Performance
senthilengg Nov 1, 2025
5e0e3fe
Remove unused TotalTestHelper import from CartPricesTest
senthilengg Nov 1, 2025
39be9e8
#40209 - Bringing back the helper removed from last merge
Nov 2, 2025
cdf6d63
Merge branch '2.4-develop' into Fix-QuoteGraphQl-CartPrices-Performance
engcom-Hotel Nov 3, 2025
1daad58
#40209 - Addressing review comments
Nov 3, 2025
86400ac
Merge branch '2.4-develop' into Fix-QuoteGraphQl-CartPrices-Performance
senthilengg Nov 3, 2025
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