Skip to content

Commit f489d0b

Browse files
authored
LYNX-620 - GQL - itemsV2 > Original row total, price range prices is returned as $0.00 for downloadable product with file options which has separate prices
1 parent 5c6d83d commit f489d0b

File tree

4 files changed

+514
-32
lines changed

4 files changed

+514
-32
lines changed

app/code/Magento/CatalogGraphQl/Model/PriceRangeDataProvider.php

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
11
<?php
22
/**
3-
* Copyright © Magento, Inc. All rights reserved.
4-
* See COPYING.txt for license details.
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
55
*/
66
declare(strict_types=1);
77

88
namespace Magento\CatalogGraphQl\Model;
99

10-
use Exception;
1110
use Magento\Catalog\Api\Data\ProductInterface;
1211
use Magento\Catalog\Model\Product;
1312
use Magento\CatalogGraphQl\Model\Resolver\Product\Price\Discount;
1413
use Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderPool as PriceProviderPool;
1514
use Magento\Framework\Exception\LocalizedException;
16-
use Magento\Framework\GraphQl\Query\Resolver\Value;
1715
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
1816
use Magento\Framework\Pricing\PriceCurrencyInterface;
1917
use Magento\Framework\Pricing\SaleableInterface;
@@ -27,6 +25,8 @@ class PriceRangeDataProvider
2725
{
2826
private const STORE_FILTER_CACHE_KEY = '_cache_instance_store_filter';
2927

28+
private const TYPE_DOWNLOADABLE = 'downloadable';
29+
3030
/**
3131
* @param PriceProviderPool $priceProviderPool
3232
* @param Discount $discount
@@ -45,18 +45,48 @@ public function __construct(
4545
* @param ContextInterface $context
4646
* @param ResolveInfo $info
4747
* @param array $value
48-
* @throws Exception
49-
* @return mixed|Value
48+
* @return array
49+
* @throws LocalizedException
5050
*/
5151
public function prepare(ContextInterface $context, ResolveInfo $info, array $value): array
52+
{
53+
$store = $context->getExtensionAttributes()->getStore();
54+
$product = $this->getProduct($value, $context, $store);
55+
56+
$requestedFields = $info->getFieldSelection(10);
57+
$returnArray = [];
58+
59+
$returnArray['minimum_price'] = ($requestedFields['minimum_price'] ?? 0) ? ($this->canShowPrice($product) ?
60+
$this->getMinimumProductPrice($product, $store) : $this->formatEmptyResult()) : $this->formatEmptyResult();
61+
$returnArray['maximum_price'] = ($requestedFields['maximum_price'] ?? 0) ? ($this->canShowPrice($product) ?
62+
$this->getMaximumProductPrice($product, $store) : $this->formatEmptyResult()) : $this->formatEmptyResult();
63+
64+
if ($product->getTypeId() === self::TYPE_DOWNLOADABLE &&
65+
$product->getData('links_purchased_separately')) {
66+
$downloadableLinkPrice = (float)$this->getDownloadableLinkPrice($product);
67+
if ($downloadableLinkPrice > 0) {
68+
$returnArray['maximum_price']['regular_price']['value'] += $downloadableLinkPrice;
69+
$returnArray['maximum_price']['final_price']['value'] += $downloadableLinkPrice;
70+
}
71+
}
72+
73+
return $returnArray;
74+
}
75+
76+
/**
77+
* Validate and return product
78+
*
79+
* @param array $value
80+
* @param ContextInterface $context
81+
* @param StoreInterface $store
82+
* @return Product
83+
* @throws LocalizedException
84+
*/
85+
private function getProduct(array $value, ContextInterface $context, StoreInterface $store): Product
5286
{
5387
if (!isset($value['model'])) {
5488
throw new LocalizedException(__('"model" value should be specified'));
5589
}
56-
/** @var StoreInterface $store */
57-
$store = $context->getExtensionAttributes()->getStore();
58-
59-
/** @var Product $product */
6090
$product = $value['model'];
6191
$product->unsetData('minimal_price');
6292
// add store filter for the product
@@ -69,15 +99,28 @@ public function prepare(ContextInterface $context, ResolveInfo $info, array $val
6999
}
70100
}
71101

72-
$requestedFields = $info->getFieldSelection(10);
73-
$returnArray = [];
102+
return $product;
103+
}
74104

75-
$returnArray['minimum_price'] = ($requestedFields['minimum_price'] ?? 0) ? ($this->canShowPrice($product) ?
76-
$this->getMinimumProductPrice($product, $store) : $this->formatEmptyResult()) : $this->formatEmptyResult();
77-
$returnArray['maximum_price'] = ($requestedFields['maximum_price'] ?? 0) ? ($this->canShowPrice($product) ?
78-
$this->getMaximumProductPrice($product, $store) : $this->formatEmptyResult()) : $this->formatEmptyResult();
105+
/**
106+
* Get the downloadable link price
107+
*
108+
* @param Product $product
109+
* @return float
110+
*/
111+
private function getDownloadableLinkPrice(Product $product): float
112+
{
113+
$downloadableLinks = $product->getTypeInstance()->getLinks($product);
114+
if (empty($downloadableLinks)) {
115+
return 0.0;
116+
}
79117

80-
return $returnArray;
118+
$price = 0.0;
119+
foreach ($downloadableLinks as $link) {
120+
$price += (float)$link->getPrice();
121+
}
122+
123+
return $price;
81124
}
82125

83126
/**
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Downloadable\Test\Fixture;
9+
10+
use Magento\Catalog\Api\ProductRepositoryInterface;
11+
use Magento\Catalog\Test\Fixture\Product;
12+
use Magento\Downloadable\Model\Link;
13+
use Magento\Downloadable\Model\Product\Type;
14+
use Magento\Framework\App\Filesystem\DirectoryList;
15+
use Magento\Framework\DataObject;
16+
use Magento\Framework\Exception\FileSystemException;
17+
use Magento\Framework\Exception\LocalizedException;
18+
use Magento\Framework\Filesystem\Io\File;
19+
use Magento\TestFramework\Fixture\Api\DataMerger;
20+
use Magento\TestFramework\Fixture\Api\ServiceFactory;
21+
use Magento\TestFramework\Fixture\Data\ProcessorInterface;
22+
23+
class DownloadableProduct extends Product
24+
{
25+
private const DEFAULT_DATA = [
26+
'type_id' => Type::TYPE_DOWNLOADABLE,
27+
'name' => 'DownloadableProduct%uniqid%',
28+
'sku' => 'downloadable-product%uniqid%',
29+
'price' => 0.00,
30+
'links_purchased_separately' => 1,
31+
'links_title' => 'Downloadable Links%uniqid%',
32+
'links_exist' => 0,
33+
'extension_attributes' => [
34+
'website_ids' => [1],
35+
'stock_item' => [
36+
'use_config_manage_stock' => true,
37+
'qty' => 100,
38+
'is_qty_decimal' => false,
39+
'is_in_stock' => true,
40+
],
41+
'downloadable_product_links' => [],
42+
'downloadable_product_samples' => null
43+
],
44+
];
45+
46+
/**
47+
* DownloadableProduct constructor
48+
*
49+
* @param ServiceFactory $serviceFactory
50+
* @param ProcessorInterface $dataProcessor
51+
* @param DataMerger $dataMerger
52+
* @param ProductRepositoryInterface $productRepository
53+
* @param DirectoryList $directoryList
54+
* @param Link $link
55+
* @param File $file
56+
*/
57+
public function __construct(
58+
private readonly ServiceFactory $serviceFactory,
59+
private readonly ProcessorInterface $dataProcessor,
60+
private readonly DataMerger $dataMerger,
61+
private readonly ProductRepositoryInterface $productRepository,
62+
private readonly DirectoryList $directoryList,
63+
private readonly Link $link,
64+
private readonly File $file
65+
) {
66+
parent::__construct($serviceFactory, $dataProcessor, $dataMerger, $productRepository);
67+
}
68+
69+
/**
70+
* @inheritdoc
71+
*
72+
* @throws FileSystemException
73+
* @throws LocalizedException
74+
*/
75+
public function apply(array $data = []): ?DataObject
76+
{
77+
return parent::apply($this->prepareData($data));
78+
}
79+
80+
/**
81+
* Prepare product data
82+
*
83+
* @param array $data
84+
* @return array
85+
* @throws FileSystemException
86+
* @throws LocalizedException
87+
*/
88+
private function prepareData(array $data): array
89+
{
90+
$data = $this->dataMerger->merge(self::DEFAULT_DATA, $data);
91+
92+
// Remove common properties not needed for downloadable products
93+
unset($data['weight']);
94+
95+
// Prepare downloadable links
96+
$links = $this->prepareLinksData($data);
97+
$data['extension_attributes']['downloadable_product_links'] = $links;
98+
$data['links_exist'] = count($links);
99+
100+
return $this->dataProcessor->process($this, $data);
101+
}
102+
103+
/**
104+
* Prepare links data
105+
*
106+
* @param array $data
107+
* @return array
108+
* @throws FileSystemException
109+
* @throws LocalizedException
110+
*/
111+
private function prepareLinksData(array $data): array
112+
{
113+
$links = [];
114+
foreach ($data['extension_attributes']['downloadable_product_links'] as $link) {
115+
$links[] = [
116+
'id' => null,
117+
'title' => $link['title'] ?? 'Test Link%uniqid%',
118+
'price' => $link['price'] ?? 0,
119+
'link_type' => $link['link_type'] ?? 'file',
120+
'link_url' => null,
121+
'link_file' => $this->generateDownloadableLink($link['link_file'] ?? 'test-' . uniqid() . '.txt'),
122+
'is_shareable' => $link['is_shareable'] ?? 0,
123+
'number_of_downloads' => $link['number_of_downloads'] ?? 5,
124+
'sort_order' => $link['sort_order'] ?? 10,
125+
];
126+
}
127+
128+
return $links;
129+
}
130+
131+
/**
132+
* Generate downloadable link file
133+
*
134+
* @param string $fileName
135+
* @return string
136+
* @throws FileSystemException|LocalizedException
137+
*/
138+
public function generateDownloadableLink(string $fileName): string
139+
{
140+
try {
141+
$subDir = sprintf('%s/%s', $fileName[0], $fileName[1]);
142+
$mediaPath = sprintf(
143+
'%s/%s/%s',
144+
$this->directoryList->getPath(DirectoryList::MEDIA),
145+
$this->link->getBasePath(),
146+
$subDir
147+
);
148+
$this->file->checkAndCreateFolder($mediaPath);
149+
$this->file->write(sprintf('%s/%s', $mediaPath, $fileName), "This is a temporary text file.");
150+
151+
return sprintf('/%s/%s', $subDir, $fileName);
152+
} catch (FileSystemException $e) {
153+
throw new FileSystemException(__($e->getMessage()));
154+
} catch (LocalizedException $e) {
155+
throw new LocalizedException(__($e->getMessage()));
156+
}
157+
}
158+
}

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

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, ?array $value
127127
*/
128128
private function getOriginalItemPrice(Item $cartItem): float
129129
{
130-
$originalItemPrice = $cartItem->getOriginalPrice() + $this->getOptionsPrice($cartItem);
130+
$originalItemPrice = $cartItem->getOriginalPrice() + $this->getCustomOptionPrice($cartItem);
131131

132132
// To add downloadable product link price to the original item price
133133
if ($cartItem->getProductType() === Type::TYPE_DOWNLOADABLE &&
@@ -147,7 +147,7 @@ private function getOriginalItemPrice(Item $cartItem): float
147147
private function getOriginalRowTotal(Item $cartItem): float
148148
{
149149
// Round unit price before multiplying to prevent losing 1 cent on subtotal
150-
return $this->priceCurrency->round($this->getOriginalItemPrice($cartItem) * $cartItem->getTotalQty());
150+
return $this->priceCurrency->round($this->getOriginalItemPrice($cartItem)) * $cartItem->getTotalQty();
151151
}
152152

153153
/**
@@ -156,14 +156,13 @@ private function getOriginalRowTotal(Item $cartItem): float
156156
* @param Item $cartItem
157157
* @return float
158158
*/
159-
private function getOptionsPrice(Item $cartItem): float
159+
private function getCustomOptionPrice(Item $cartItem): float
160160
{
161161
$price = 0.0;
162162
$optionIds = $cartItem->getProduct()->getCustomOption('option_ids');
163163
if (!$optionIds) {
164164
return $price;
165165
}
166-
167166
foreach (explode(',', $optionIds->getValue() ?? '') as $optionId) {
168167
$option = $cartItem->getProduct()->getOptionById($optionId);
169168
$optionValueIds = $cartItem->getOptionByCode('option_' . $optionId);
@@ -189,18 +188,19 @@ private function getOptionsPrice(Item $cartItem): float
189188
*/
190189
private function getDownloadableLinkPrice(Item $cartItem): float
191190
{
192-
$price = 0.0;
193-
$links = $cartItem->getProduct()->getCustomOption('downloadable_link_ids');
194-
if (!$links || empty($links->getValue())) {
195-
return $price;
191+
$linksOption = $cartItem->getProduct()->getCustomOption('downloadable_link_ids');
192+
if (!$linksOption || !$linksOption->getValue()) {
193+
return 0.0;
196194
}
197-
$selectedLinks = explode(',', $links->getValue());
195+
196+
$selectedLinks = array_flip(explode(',', $linksOption->getValue()));
198197
$downloadableLinks = $cartItem->getProduct()->getTypeInstance()->getLinks($cartItem->getProduct());
199-
foreach ($downloadableLinks as $link) {
200-
if (in_array($link->getId(), $selectedLinks)) {
201-
$price += (float)$link->getPrice();
202-
}
203-
}
204-
return $price;
198+
199+
return array_reduce(
200+
$downloadableLinks,
201+
fn(float $total, $link) => isset($selectedLinks[$link->getId()]) ?
202+
$total + (float) $link->getPrice() : $total,
203+
0.0
204+
);
205205
}
206206
}

0 commit comments

Comments
 (0)