Skip to content

Commit a782aba

Browse files
authored
LYNX-540: Add quantity to ProductInterface
1 parent 130412f commit a782aba

File tree

4 files changed

+326
-4
lines changed

4 files changed

+326
-4
lines changed

app/code/Magento/CatalogInventory/Model/Config/Source/NotAvailableMessage.php

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@
1414

1515
class NotAvailableMessage implements OptionSourceInterface
1616
{
17+
/**
18+
* Message config values
19+
*/
20+
public const VALUE_ONLY_X_OF_Y = 1;
21+
public const VALUE_NOT_ENOUGH_ITEMS = 2;
22+
1723
/**
1824
* Options getter
1925
*
@@ -23,11 +29,11 @@ public function toOptionArray(): array
2329
{
2430
$options = [];
2531
$options[] = [
26-
'value' => 1,
32+
'value' => self::VALUE_ONLY_X_OF_Y,
2733
'label' => __('Only X available for sale. Please adjust the quantity to continue'),
2834
];
2935
$options[] = [
30-
'value' => 2,
36+
'value' => self::VALUE_NOT_ENOUGH_ITEMS,
3137
'label' => __('Not enough items for sale. Please adjust the quantity to continue'),
3238
];
3339
return $options;
@@ -41,8 +47,8 @@ public function toOptionArray(): array
4147
public function toArray(): array
4248
{
4349
return [
44-
1 => __('Only X available for sale. Please adjust the quantity to continue'),
45-
2 => __('Not enough items for sale. Please adjust the quantity to continue')
50+
self::VALUE_ONLY_X_OF_Y => __('Only X available for sale. Please adjust the quantity to continue'),
51+
self::VALUE_NOT_ENOUGH_ITEMS => __('Not enough items for sale. Please adjust the quantity to continue')
4652
];
4753
}
4854
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\CatalogInventoryGraphQl\Model\Resolver;
9+
10+
use Magento\Catalog\Api\ProductRepositoryInterface;
11+
use Magento\Catalog\Model\Product;
12+
use Magento\CatalogInventory\Model\StockState;
13+
use Magento\CatalogInventory\Model\Config\Source\NotAvailableMessage;
14+
use Magento\Framework\App\Config\ScopeConfigInterface;
15+
use Magento\Framework\Exception\LocalizedException;
16+
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
17+
use Magento\Framework\GraphQl\Config\Element\Field;
18+
use Magento\Framework\GraphQl\Query\ResolverInterface;
19+
use Magento\Quote\Model\Quote\Item;
20+
use Magento\QuoteGraphQl\Model\CartItem\ProductStock;
21+
22+
/**
23+
* Resolver for ProductInterface quantity
24+
* Returns the available stock quantity based on cataloginventory/options/not_available_message
25+
*/
26+
class QuantityResolver implements ResolverInterface
27+
{
28+
/**
29+
* Configurable product type code
30+
*/
31+
private const PRODUCT_TYPE_CONFIGURABLE = "configurable";
32+
33+
/**
34+
* Scope config path for not_available_message
35+
*/
36+
private const CONFIG_PATH_NOT_AVAILABLE_MESSAGE = "cataloginventory/options/not_available_message";
37+
38+
/**
39+
* @param ProductRepositoryInterface $productRepositoryInterface
40+
* @param ScopeConfigInterface $scopeConfig
41+
* @param StockState $stockState
42+
* @param ProductStock $productStock
43+
*/
44+
public function __construct(
45+
private readonly ProductRepositoryInterface $productRepositoryInterface,
46+
private readonly ScopeConfigInterface $scopeConfig,
47+
private readonly StockState $stockState,
48+
private readonly ProductStock $productStock,
49+
) {
50+
}
51+
52+
/**
53+
* @inheritdoc
54+
*
55+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
56+
*/
57+
public function resolve(
58+
Field $field,
59+
$context,
60+
ResolveInfo $info,
61+
array $value = null,
62+
array $args = null
63+
): ?float {
64+
65+
if ((int) $this->scopeConfig->getValue(
66+
self::CONFIG_PATH_NOT_AVAILABLE_MESSAGE
67+
) === NotAvailableMessage::VALUE_NOT_ENOUGH_ITEMS) {
68+
return null;
69+
}
70+
71+
if (isset($value['cart_item']) && $value['cart_item'] instanceof Item) {
72+
return $this->productStock->getProductAvailableStock($value['cart_item']);
73+
}
74+
75+
if (!isset($value['model'])) {
76+
throw new LocalizedException(__('"model" value should be specified'));
77+
}
78+
79+
/** @var Product $product */
80+
$product = $value['model'];
81+
82+
if ($product->getTypeId() === self::PRODUCT_TYPE_CONFIGURABLE) {
83+
$product = $this->productRepositoryInterface->get($product->getSku());
84+
}
85+
return $this->stockState->getStockQty($product->getId());
86+
}
87+
}

app/code/Magento/CatalogInventoryGraphQl/etc/schema.graphqls

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
interface ProductInterface {
55
only_x_left_in_stock: Float @doc(description: "Remaining stock if it is below the value assigned to the Only X Left Threshold option in the Admin.") @resolver(class: "Magento\\CatalogInventoryGraphQl\\Model\\Resolver\\OnlyXLeftInStockResolver")
66
stock_status: ProductStockStatus @doc(description: "The stock status of the product.") @resolver(class: "Magento\\CatalogInventoryGraphQl\\Model\\Resolver\\StockStatusProvider")
7+
quantity: Float @doc(description: "Amount of available stock") @resolver(class: "Magento\\CatalogInventoryGraphQl\\Model\\Resolver\\QuantityResolver")
78
}
89

910
enum ProductStockStatus @doc(description: "States whether a product stock status is in stock or out of stock.") {
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\GraphQl\CatalogInventory;
9+
10+
use Magento\Bundle\Test\Fixture\AddProductToCart as AddBundleProductToCart;
11+
use Magento\Bundle\Test\Fixture\Link as BundleSelectionFixture;
12+
use Magento\Bundle\Test\Fixture\Option as BundleOptionFixture;
13+
use Magento\Bundle\Test\Fixture\Product as BundleProductFixture;
14+
use Magento\Catalog\Test\Fixture\Product as ProductFixture;
15+
use Magento\Catalog\Test\Fixture\ProductStock as ProductStockFixture;
16+
use Magento\ConfigurableProduct\Test\Fixture\AddProductToCart as AddConfigurableProductToCartFixture;
17+
use Magento\ConfigurableProduct\Test\Fixture\Attribute as AttributeFixture;
18+
use Magento\ConfigurableProduct\Test\Fixture\Product as ConfigurableProductFixture;
19+
use Magento\Framework\DataObject;
20+
use Magento\Quote\Test\Fixture\AddProductToCart;
21+
use Magento\Quote\Test\Fixture\GuestCart as GuestCartFixture;
22+
use Magento\Quote\Test\Fixture\QuoteIdMask as QuoteMaskFixture;
23+
use Magento\TestFramework\Fixture\Config;
24+
use Magento\TestFramework\Fixture\DataFixture;
25+
use Magento\TestFramework\Fixture\DataFixtureStorage;
26+
use Magento\TestFramework\Fixture\DataFixtureStorageManager;
27+
use Magento\TestFramework\TestCase\GraphQlAbstract;
28+
29+
/**
30+
* Product quantity test model
31+
*/
32+
class StockQuantityTest extends GraphQlAbstract
33+
{
34+
/**
35+
* @var DataFixtureStorage
36+
*/
37+
private $fixtures;
38+
39+
/**
40+
* @inheritDoc
41+
*/
42+
protected function setUp(): void
43+
{
44+
parent::setUp();
45+
$this->fixtures = DataFixtureStorageManager::getStorage();
46+
}
47+
48+
#[
49+
Config('cataloginventory/options/not_available_message', 1),
50+
DataFixture(ProductFixture::class, as: 'product'),
51+
DataFixture(GuestCartFixture::class, as: 'cart'),
52+
DataFixture(AddProductToCart::class, ['cart_id' => '$cart.id$', 'product_id' => '$product.id$']),
53+
DataFixture(QuoteMaskFixture::class, ['cart_id' => '$cart.id$'], 'quoteIdMask'),
54+
DataFixture(ProductStockFixture::class, ['prod_id' => '$product.id$', 'prod_qty' => 10])
55+
]
56+
public function testStockQuantitySimpleProduct(): void
57+
{
58+
$this->assertProductStockQuantity(10);
59+
}
60+
61+
#[
62+
Config('cataloginventory/options/not_available_message', 1),
63+
DataFixture(ProductFixture::class, as: 'product'),
64+
DataFixture(
65+
BundleSelectionFixture::class,
66+
['sku' => '$product.sku$'],
67+
'link'
68+
),
69+
DataFixture(
70+
BundleOptionFixture::class,
71+
[
72+
'title' => 'Checkbox Options',
73+
'type' => 'checkbox',
74+
'required' => 1,
75+
'product_links' => ['$link$']
76+
],
77+
'option'
78+
),
79+
DataFixture(
80+
BundleProductFixture::class,
81+
['_options' => ['$option$']],
82+
'bundleProduct'
83+
),
84+
DataFixture(GuestCartFixture::class, as: 'cart'),
85+
DataFixture(
86+
AddBundleProductToCart::class,
87+
[
88+
'cart_id' => '$cart.id$',
89+
'product_id' => '$bundleProduct.id$',
90+
'selections' => [['$product.id$']],
91+
],
92+
),
93+
DataFixture(QuoteMaskFixture::class, ['cart_id' => '$cart.id$'], 'quoteIdMask'),
94+
DataFixture(ProductStockFixture::class, ['prod_id' => '$product.id$', 'prod_qty' => 10])
95+
]
96+
public function testStockQuantityBundleProduct(): void
97+
{
98+
$this->assertProductStockQuantity(10);
99+
$this->assertNoStockQuantity('bundleProduct');
100+
}
101+
102+
#[
103+
Config('cataloginventory/options/not_available_message', 1),
104+
DataFixture(ProductFixture::class, as: 'product'),
105+
DataFixture(AttributeFixture::class, as: 'attribute'),
106+
DataFixture(
107+
ConfigurableProductFixture::class,
108+
[
109+
'_options' => ['$attribute$'],
110+
'_links' => ['$product$']
111+
],
112+
'configurableProduct'
113+
),
114+
DataFixture(GuestCartFixture::class, as: 'cart'),
115+
DataFixture(QuoteMaskFixture::class, ['cart_id' => '$cart.id$'], 'quoteIdMask'),
116+
DataFixture(
117+
AddConfigurableProductToCartFixture::class,
118+
[
119+
'cart_id' => '$cart.id$',
120+
'product_id' => '$configurableProduct.id$',
121+
'child_product_id' => '$product.id$',
122+
],
123+
),
124+
DataFixture(ProductStockFixture::class, ['prod_id' => '$product.id$', 'prod_qty' => 10]),
125+
]
126+
public function testStockQuantityConfigurableProduct(): void
127+
{
128+
$this->assertProductStockQuantity(10);
129+
$this->assertNoStockQuantity('configurableProduct');
130+
}
131+
132+
#[
133+
Config('cataloginventory/options/not_available_message', 2),
134+
DataFixture(ProductFixture::class, as: 'product'),
135+
DataFixture(GuestCartFixture::class, as: 'cart'),
136+
DataFixture(AddProductToCart::class, ['cart_id' => '$cart.id$', 'product_id' => '$product.id$']),
137+
DataFixture(QuoteMaskFixture::class, ['cart_id' => '$cart.id$'], 'quoteIdMask'),
138+
]
139+
public function testStockQuantityEmpty(): void
140+
{
141+
$this->assertProductStockQuantity(null);
142+
}
143+
144+
/**
145+
* Asserts products stock quantity from cart & product query
146+
*
147+
* @param float|null $stockQuantity
148+
* @return void
149+
*/
150+
private function assertProductStockQuantity(?float $stockQuantity): void
151+
{
152+
$maskedQuoteId = $this->fixtures->get('quoteIdMask')->getMaskedId();
153+
$cartQuery = $this->getCartQuery($maskedQuoteId);
154+
$cartResponse = $this->graphQlMutation($cartQuery);
155+
$cartResponseDataObject = new DataObject($cartResponse);
156+
self::assertEquals(
157+
$stockQuantity,
158+
$cartResponseDataObject->getData('cart/itemsV2/items/0/product/quantity')
159+
);
160+
161+
$productQuery = $this->getProductQuery($this->fixtures->get('product')->getSku());
162+
$productResponse = $this->graphQlMutation($productQuery);
163+
$productResponseDataObject = new DataObject($productResponse);
164+
self::assertEquals(
165+
$stockQuantity,
166+
$productResponseDataObject->getData('products/items/0/quantity')
167+
);
168+
}
169+
170+
/**
171+
* Asserts bundle & conf product stock quantity from product query
172+
*
173+
* @param string $productFixture
174+
* @return void
175+
*/
176+
private function assertNoStockQuantity(string $productFixture): void
177+
{
178+
$productQuery = $this->getProductQuery($this->fixtures->get($productFixture)->getSku());
179+
$productResponse = $this->graphQlMutation($productQuery);
180+
$productResponseDataObject = new DataObject($productResponse);
181+
self::assertEquals(
182+
0,
183+
$productResponseDataObject->getData('products/items/0/quantity')
184+
);
185+
}
186+
187+
/**
188+
* Return cart query with product.quantity field
189+
*
190+
* @param string $cartId
191+
* @return string
192+
*/
193+
private function getCartQuery(string $cartId): string
194+
{
195+
return <<<QUERY
196+
{
197+
cart(cart_id:"{$cartId}") {
198+
itemsV2 {
199+
items {
200+
product {
201+
quantity
202+
}
203+
}
204+
}
205+
}
206+
}
207+
QUERY;
208+
}
209+
210+
/**
211+
* Return product query with product.quantity field
212+
*
213+
* @param string $sku
214+
* @return string
215+
*/
216+
private function getProductQuery(string $sku): string
217+
{
218+
return <<<QUERY
219+
{
220+
products(filter: { sku: { eq: "{$sku}" } }) {
221+
items {
222+
quantity
223+
}
224+
}
225+
}
226+
QUERY;
227+
}
228+
}

0 commit comments

Comments
 (0)