Skip to content

Commit dcaa30e

Browse files
committed
Merge remote-tracking branch 'origin/ACP2E-4248' into PR_2025_11_04_flowers
2 parents a4113c6 + 60a90c5 commit dcaa30e

File tree

4 files changed

+456
-4
lines changed

4 files changed

+456
-4
lines changed

app/code/Magento/QuoteGraphQl/Model/GetDiscounts.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ class GetDiscounts
2222
*/
2323
public function execute(Quote $quote, array $discounts): ?array
2424
{
25-
$discounts = $discounts ?: $quote->getBillingAddress()->getExtensionAttributes()->getDiscounts() ?? [];
26-
2725
if (empty($discounts)) {
2826
return null;
2927
}

app/code/Magento/QuoteGraphQl/Model/Resolver/Discounts.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use Magento\Framework\GraphQl\Query\ResolverInterface;
1313
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
1414
use Magento\QuoteGraphQl\Model\GetDiscounts;
15+
use Magento\Quote\Model\Quote;
1516

1617
/**
1718
* @inheritdoc
@@ -37,11 +38,13 @@ public function resolve(Field $field, $context, ResolveInfo $info, ?array $value
3738
if (!isset($value['model'])) {
3839
throw new LocalizedException(__('"model" value should be specified'));
3940
}
41+
/** @var Quote $quote */
4042
$quote = $value['model'];
41-
43+
$address = $quote->getIsVirtual() ? $quote->getBillingAddress() : $quote->getShippingAddress();
44+
$discounts = $address->getExtensionAttributes()?->getDiscounts() ?? [];
4245
return $this->getDiscounts->execute(
4346
$quote,
44-
$quote->getShippingAddress()->getExtensionAttributes()->getDiscounts() ?? []
47+
$discounts
4548
);
4649
}
4750
}
Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\QuoteGraphQl\Model;
9+
10+
use Exception;
11+
use Magento\Catalog\Test\Fixture\Product as ProductFixture;
12+
use Magento\Catalog\Test\Fixture\Virtual as VirtualProductFixture;
13+
use Magento\Customer\Test\Fixture\Customer;
14+
use Magento\Framework\Exception\LocalizedException;
15+
use Magento\Integration\Api\CustomerTokenServiceInterface;
16+
use Magento\Quote\Test\Fixture\AddProductToCart as AddProductToCartFixture;
17+
use Magento\Quote\Test\Fixture\CustomerCart;
18+
use Magento\Quote\Test\Fixture\QuoteIdMask;
19+
use Magento\SalesRule\Model\Rule;
20+
use Magento\SalesRule\Model\Rule\Condition\Product;
21+
use Magento\SalesRule\Model\Rule\Condition\Product\Combine;
22+
use Magento\SalesRule\Test\Fixture\Rule as SalesRuleFixture;
23+
use Magento\TestFramework\Fixture\DataFixture;
24+
use Magento\TestFramework\Fixture\DataFixtureStorageManager;
25+
use Magento\TestFramework\Helper\Bootstrap;
26+
use Magento\TestFramework\TestCase\GraphQlAbstract;
27+
28+
class DiscountsTest extends GraphQlAbstract
29+
{
30+
/** @var CustomerTokenServiceInterface */
31+
private CustomerTokenServiceInterface $customerTokenService;
32+
33+
/** @var Rule|null */
34+
protected ?Rule $createdRule = null;
35+
36+
/** @inheritdoc */
37+
protected function setUp(): void
38+
{
39+
$objectManager = Bootstrap::getObjectManager();
40+
$this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class);
41+
parent::setUp();
42+
}
43+
44+
#[
45+
DataFixture(ProductFixture::class, as: 'product'),
46+
DataFixture(Customer::class, ['email' => 'customer@example.com'], as: 'customer'),
47+
DataFixture(CustomerCart::class, ['customer_id' => '$customer.id$'], as: 'cart'),
48+
DataFixture(AddProductToCartFixture::class, ['cart_id' => '$cart.id$', 'product_id' => '$product.id$']),
49+
DataFixture(QuoteIdMask::class, ['cart_id' => '$cart.id$'], as: 'quoteIdMask'),
50+
DataFixture(SalesRuleFixture::class, ['discount_amount' => 10, 'simple_action' => 'by_percent'], as: 'rule')
51+
]
52+
/**
53+
* Test discounts resolver for a non-virtual quote
54+
* @throws LocalizedException
55+
*/
56+
public function testDiscountsNonVirtualQuote()
57+
{
58+
$maskedQuoteId = DataFixtureStorageManager::getStorage()->get('quoteIdMask')->getMaskedId();
59+
$query = $this->getCartDiscountsQuery($maskedQuoteId);
60+
61+
$response = $this->graphQlQuery(
62+
$query,
63+
[],
64+
'',
65+
$this->getHeaderMap()
66+
);
67+
$cartData = $response['cart'];
68+
$discounts = $cartData['prices']['discounts'] ?? [];
69+
$this->assertNotEmpty($discounts);
70+
$this->assertCount(1, $discounts);
71+
$discount = $discounts[0];
72+
$this->assertArrayHasKey('label', $discount);
73+
$this->assertArrayHasKey('amount', $discount);
74+
$this->assertArrayHasKey('value', $discount['amount']);
75+
$this->assertArrayHasKey('currency', $discount['amount']);
76+
$this->assertArrayHasKey('applied_to', $discount);
77+
$this->assertEquals('Discount', $discount['label']);
78+
$this->assertGreaterThan(0, $discount['amount']['value']);
79+
$this->assertEquals('USD', $discount['amount']['currency']);
80+
$this->assertEquals('ITEM', $discount['applied_to']);
81+
}
82+
83+
#[
84+
DataFixture(
85+
VirtualProductFixture::class,
86+
['sku' => 'virtual111', 'price' => 100, 'category_ids' => [2]],
87+
as: 'product1'
88+
),
89+
DataFixture(
90+
VirtualProductFixture::class,
91+
['sku' => 'virtual222', 'price' => 100, 'category_ids' => [2]],
92+
as: 'product2'
93+
),
94+
DataFixture(Customer::class, ['email' => 'customer@example.com'], as: 'customer'),
95+
DataFixture(CustomerCart::class, ['customer_id' => '$customer.id$'], as: 'cart'),
96+
DataFixture(AddProductToCartFixture::class, ['cart_id' => '$cart.id$', 'product_id' => '$product1.id$']),
97+
DataFixture(AddProductToCartFixture::class, ['cart_id' => '$cart.id$', 'product_id' => '$product2.id$']),
98+
DataFixture(QuoteIdMask::class, ['cart_id' => '$cart.id$'], as: 'quoteIdMask')
99+
]
100+
/**
101+
* Test discounts resolver for a virtual quote with conditional discount on specific SKU
102+
*
103+
* @throws Exception
104+
*/
105+
public function testDiscountsVirtualQuote()
106+
{
107+
$this->createSalesRuleForSku("virtual222");
108+
$quoteIdMask = DataFixtureStorageManager::getStorage()->get('quoteIdMask');
109+
$maskedQuoteId = $quoteIdMask->getMaskedId();
110+
$query = $this->getCartDiscountsQueryWithItems($maskedQuoteId);
111+
$response = $this->graphQlQuery(
112+
$query,
113+
[],
114+
'',
115+
$this->getHeaderMap()
116+
);
117+
$cartData = $response['cart'];
118+
$this->assertCount(2, $cartData['items']);
119+
$virtual1Item = null;
120+
$virtual2Item = null;
121+
foreach ($cartData['items'] as $item) {
122+
if ($item['product']['sku'] === 'virtual111') {
123+
$virtual1Item = $item;
124+
} elseif ($item['product']['sku'] === 'virtual222') {
125+
$virtual2Item = $item;
126+
}
127+
}
128+
129+
$this->assertNotNull($virtual1Item, 'virtual1 item not found in cart');
130+
$this->assertNotNull($virtual2Item, 'virtual2 item not found in cart');
131+
$this->assertEmpty($virtual1Item['prices']['discounts'], 'virtual111 should not have any discounts');
132+
$this->assertNotEmpty($virtual2Item['prices']['discounts'], 'virtual222 should have discounts');
133+
$this->assertCount(1, $virtual2Item['prices']['discounts']);
134+
$discount = $virtual2Item['prices']['discounts'][0];
135+
$this->assertArrayHasKey('label', $discount);
136+
$this->assertArrayHasKey('amount', $discount);
137+
$this->assertArrayHasKey('value', $discount['amount']);
138+
$this->assertEquals('Discount', $discount['label']);
139+
$this->assertEquals(10, $discount['amount']['value']);
140+
$cartDiscounts = $cartData['prices']['discounts'] ?? [];
141+
$this->assertNotEmpty($cartDiscounts, 'Cart should have discounts');
142+
$this->assertCount(1, $cartDiscounts);
143+
$this->assertEquals('Discount', $cartDiscounts[0]['label']);
144+
$this->assertEquals('ITEM', $cartDiscounts[0]['applied_to']);
145+
$this->assertEquals(10, $cartDiscounts[0]['amount']['value']);
146+
}
147+
148+
#[
149+
DataFixture(ProductFixture::class, as: 'product'),
150+
DataFixture(Customer::class, ['email' => 'customer@example.com'], as: 'customer'),
151+
DataFixture(CustomerCart::class, ['customer_id' => '$customer.id$'], as: 'cart'),
152+
DataFixture(AddProductToCartFixture::class, ['cart_id' => '$cart.id$', 'product_id' => '$product.id$']),
153+
DataFixture(QuoteIdMask::class, ['cart_id' => '$cart.id$'], as: 'quoteIdMask')
154+
]
155+
/**
156+
* Test discounts resolver when no discounts are applied
157+
*/
158+
public function testDiscountsNoDiscounts()
159+
{
160+
$maskedQuoteId = DataFixtureStorageManager::getStorage()->get('quoteIdMask')->getMaskedId();
161+
$query = $this->getCartDiscountsQuery($maskedQuoteId);
162+
$response = $this->graphQlQuery(
163+
$query,
164+
[],
165+
'',
166+
$this->getHeaderMap()
167+
);
168+
$cartData = $response['cart'];
169+
$discounts = $cartData['prices']['discounts'] ?? [];
170+
$this->assertEmpty($discounts);
171+
}
172+
173+
/**
174+
* Get cart discounts query
175+
*
176+
* @param string $maskedQuoteId
177+
* @return string
178+
*/
179+
private function getCartDiscountsQuery(string $maskedQuoteId): string
180+
{
181+
return <<<QUERY
182+
query {
183+
cart(cart_id: "{$maskedQuoteId}") {
184+
prices {
185+
discounts {
186+
label
187+
amount {
188+
value
189+
currency
190+
}
191+
applied_to
192+
}
193+
}
194+
}
195+
}
196+
QUERY;
197+
}
198+
199+
/**
200+
* Get cart discounts query with items
201+
*
202+
* @param string $maskedQuoteId
203+
* @return string
204+
*/
205+
private function getCartDiscountsQueryWithItems(string $maskedQuoteId): string
206+
{
207+
return <<<QUERY
208+
query {
209+
cart(cart_id: "{$maskedQuoteId}") {
210+
items {
211+
product {
212+
sku
213+
name
214+
}
215+
prices {
216+
price {
217+
value
218+
}
219+
row_total {
220+
value
221+
}
222+
discounts {
223+
label
224+
amount {
225+
value
226+
currency
227+
}
228+
}
229+
}
230+
}
231+
prices {
232+
discounts {
233+
label
234+
amount {
235+
value
236+
currency
237+
}
238+
applied_to
239+
}
240+
}
241+
}
242+
}
243+
QUERY;
244+
}
245+
246+
/**
247+
* Get bearer authorization header
248+
*
249+
* @param string $username
250+
* @param string $password
251+
* @return array
252+
* @throws \Magento\Framework\Exception\AuthenticationException
253+
*/
254+
private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array
255+
{
256+
$customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password);
257+
return ['Authorization' => 'Bearer ' . $customerToken];
258+
}
259+
260+
/**
261+
* Create SalesRule with specific sku condition
262+
*
263+
* @param string $sku
264+
* @param int $discountPercent
265+
* @return Rule
266+
* @throws Exception
267+
*/
268+
protected function createSalesRuleForSku(string $sku, int $discountPercent = 10): Rule
269+
{
270+
$objectManager = Bootstrap::getObjectManager();
271+
/** @var Rule $rule */
272+
$rule = $objectManager->create(Rule::class);
273+
$rule->setName("{$discountPercent}% off for {$sku}")
274+
->setIsActive(1)
275+
->setSimpleAction('by_percent');
276+
$rule->loadPost([
277+
'name' => "{$discountPercent}% " . "off for virtual222",
278+
'is_active' => 1,
279+
'simple_action' => 'by_percent',
280+
'discount_amount' => $discountPercent,
281+
'website_ids' => [1],
282+
'customer_group_ids' => [0, 1, 2, 3],
283+
'actions' => [
284+
1 => [
285+
'type' => Combine::class,
286+
'attribute' => null,
287+
'operator' => null,
288+
'value' => '1',
289+
'is_value_processed' => null,
290+
'aggregator' => 'all',
291+
'actions' => [
292+
1 => [
293+
'type' => Product::class,
294+
'attribute' => 'sku',
295+
'operator' => '==',
296+
'value' => 'virtual222',
297+
'is_value_processed' => false,
298+
]
299+
]
300+
]
301+
],
302+
]);
303+
$rule->save();
304+
$this->createdRule = $rule;
305+
return $rule;
306+
}
307+
308+
protected function tearDown(): void
309+
{
310+
if ($this->createdRule && $this->createdRule->getId()) {
311+
$this->createdRule->delete();
312+
}
313+
parent::tearDown();
314+
}
315+
}

0 commit comments

Comments
 (0)