Skip to content

Commit 562f103

Browse files
authored
LYNX-377: Configure the response to the shopper if the requested quantity is not available
1 parent 5404b9b commit 562f103

File tree

10 files changed

+281
-43
lines changed

10 files changed

+281
-43
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
/**
3+
* Copyright 2024 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
/**
9+
* Catalog Inventory Config Backend Model
10+
*/
11+
namespace Magento\CatalogInventory\Model\Config\Source;
12+
13+
use Magento\Framework\Data\OptionSourceInterface;
14+
15+
class NotAvailableMessage implements OptionSourceInterface
16+
{
17+
/**
18+
* Options getter
19+
*
20+
* @return array
21+
*/
22+
public function toOptionArray(): array
23+
{
24+
$options = [];
25+
$options[] = [
26+
'value' => 1,
27+
'label' => __('Only X available for sale. Please adjust the quantity to continue'),
28+
];
29+
$options[] = [
30+
'value' => 2,
31+
'label' => __('Not enough items for sale. Please adjust the quantity to continue'),
32+
];
33+
return $options;
34+
}
35+
36+
/**
37+
* Get options in "key-value" format
38+
*
39+
* @return array
40+
*/
41+
public function toArray(): array
42+
{
43+
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')
46+
];
47+
}
48+
}

app/code/Magento/CatalogInventory/Model/StockStateProvider.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Magento\Catalog\Model\ProductFactory;
1010
use Magento\CatalogInventory\Api\Data\StockItemInterface;
1111
use Magento\CatalogInventory\Model\Spi\StockStateProviderInterface;
12+
use Magento\Framework\App\Config\ScopeConfigInterface;
1213
use Magento\Framework\DataObject\Factory as ObjectFactory;
1314
use Magento\Framework\Locale\FormatInterface;
1415
use Magento\Framework\Math\Division as MathDivision;
@@ -48,13 +49,15 @@ class StockStateProvider implements StockStateProviderInterface
4849
* @param FormatInterface $localeFormat
4950
* @param ObjectFactory $objectFactory
5051
* @param ProductFactory $productFactory
52+
* @param ScopeConfigInterface $scopeConfig
5153
* @param bool $qtyCheckApplicable
5254
*/
5355
public function __construct(
5456
MathDivision $mathDivision,
5557
FormatInterface $localeFormat,
5658
ObjectFactory $objectFactory,
5759
ProductFactory $productFactory,
60+
private readonly ScopeConfigInterface $scopeConfig,
5861
$qtyCheckApplicable = true
5962
) {
6063
$this->mathDivision = $mathDivision;
@@ -165,9 +168,17 @@ public function checkQuoteItemQty(StockItemInterface $stockItem, $qty, $summaryQ
165168

166169
if (!$this->checkQty($stockItem, $summaryQty) || !$this->checkQty($stockItem, $qty)) {
167170
$message = __('The requested qty is not available');
171+
if ((int) $this->scopeConfig->getValue('cataloginventory/options/not_available_message') === 1) {
172+
$itemMessage = (__(sprintf(
173+
'Only %s available for sale. Please adjust the quantity to continue',
174+
$stockItem->getQty() - $stockItem->getMinQty()
175+
)));
176+
} else {
177+
$itemMessage = (__('Not enough items for sale. Please adjust the quantity to continue'));
178+
}
168179
$result->setHasError(true)
169180
->setErrorCode('qty_available')
170-
->setMessage($message)
181+
->setMessage($itemMessage)
171182
->setQuoteMessage($message)
172183
->setQuoteMessageIndex('qty');
173184
return $result;

app/code/Magento/CatalogInventory/etc/adminhtml/system.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@
3535
<label>Display Products Availability in Stock on Storefront</label>
3636
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
3737
</field>
38+
<field id="not_available_message" translate="label" type="select" sortOrder="5" showInDefault="1" showInWebsite="1" canRestore="1">
39+
<label>Not Available Message</label>
40+
<source_model>Magento\CatalogInventory\Model\Config\Source\NotAvailableMessage</source_model>
41+
</field>
3842
</group>
3943
<group id="item_options" translate="label comment" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
4044
<comment>

app/code/Magento/CatalogInventory/etc/config.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
<show_out_of_stock>0</show_out_of_stock>
1515
<stock_threshold_qty>0</stock_threshold_qty>
1616
<display_product_stock_status>1</display_product_stock_status>
17+
<not_available_message>2</not_available_message>
1718
</options>
1819
<item_options>
1920
<manage_stock>1</manage_stock>

app/code/Magento/CatalogInventory/i18n/en_US.csv

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,6 @@ Stock,Stock
7373
"Done","Done"
7474
"The requested qty exceeds the maximum qty allowed in shopping cart","The requested qty exceeds the maximum qty allowed in shopping cart"
7575
"You cannot use decimal quantity for this product.","You cannot use decimal quantity for this product."
76+
"Not enough items for sale. Please adjust the quantity to continue","Not enough items for sale. Please adjust the quantity to continue"
77+
"Only X available for sale. Please adjust the quantity to continue","Only X available for sale. Please adjust the quantity to continue"
78+
"Only %s available for sale. Please adjust the quantity to continue","Only %s available for sale. Please adjust the quantity to continue"
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
/**
3+
* Copyright 2024 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\CatalogInventoryGraphQl\Model\Resolver;
9+
10+
use Magento\Framework\App\Config\ScopeConfigInterface;
11+
use Magento\Framework\Exception\LocalizedException;
12+
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
13+
use Magento\Framework\GraphQl\Config\Element\Field;
14+
use Magento\Framework\GraphQl\Query\ResolverInterface;
15+
use Magento\Quote\Model\Quote\Item;
16+
use Magento\QuoteGraphQl\Model\CartItem\ProductStock;
17+
18+
/**
19+
* Resolver for not_available_message
20+
* Returns the configured response to the shopper if the requested quantity is not available
21+
*/
22+
class NotAvailableMessageResolver implements ResolverInterface
23+
{
24+
/**
25+
* @param ScopeConfigInterface $scopeConfig
26+
* @param ProductStock $productStock
27+
*/
28+
public function __construct(
29+
private readonly ScopeConfigInterface $scopeConfig,
30+
private readonly ProductStock $productStock
31+
) {
32+
}
33+
34+
/**
35+
* @inheritdoc
36+
*/
37+
public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null)
38+
{
39+
if (!isset($value['model'])) {
40+
throw new LocalizedException(__('"model" value should be specified'));
41+
}
42+
/** @var Item $cartItem */
43+
$cartItem = $value['model'];
44+
45+
if ($this->productStock->isProductAvailable($cartItem)) {
46+
return null;
47+
}
48+
49+
if ((int) $this->scopeConfig->getValue('cataloginventory/options/not_available_message') === 1) {
50+
return sprintf(
51+
'Only %s available for sale. Please adjust the quantity to continue',
52+
(string) $this->productStock->getProductAvailableStock($cartItem)
53+
);
54+
}
55+
56+
return 'Not enough items for sale. Please adjust the quantity to continue';
57+
}
58+
}

app/code/Magento/CatalogInventoryGraphQl/composer.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
"magento/module-store": "*",
99
"magento/module-catalog": "*",
1010
"magento/module-catalog-inventory": "*",
11+
"magento/module-quote": "*",
12+
"magento/module-quote-graph-ql": "*",
1113
"magento/module-graph-ql": "*"
1214
},
1315
"license": [

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,7 @@ enum ProductStockStatus @doc(description: "States whether a product stock status
1010
IN_STOCK
1111
OUT_OF_STOCK
1212
}
13+
14+
interface CartItemInterface @typeResolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\CartItemTypeResolver") @doc(description: "An interface for products in a cart.") {
15+
not_available_message: String @doc(description: "Message to display when the product is not available with this selected option.") @resolver(class: "Magento\\CatalogInventoryGraphQl\\Model\\Resolver\\NotAvailableMessageResolver")
16+
}

app/code/Magento/QuoteGraphQl/Model/CartItem/ProductStock.php

Lines changed: 73 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -52,25 +52,14 @@ public function __construct(
5252
*/
5353
public function isProductAvailable(Item $cartItem): bool
5454
{
55-
/**
56-
* @var ProductInterface $variantProduct
57-
* Configurable products cannot have stock, only its variants can. If the user adds a configurable product
58-
* using its SKU and the selected options, we need to get the variant it refers to from the quote.
59-
*/
60-
$variantProduct = null;
61-
62-
if ($cartItem->getProductType() === self::PRODUCT_TYPE_CONFIGURABLE) {
63-
if ($cartItem->getChildren()[0] !== null) {
64-
$variantProduct = $this->productRepositoryInterface->get($cartItem->getSku());
65-
}
66-
}
6755
$requestedQty = $cartItem->getQtyToAdd() ?? $cartItem->getQty();
6856
$previousQty = $cartItem->getPreviousQty() ?? 0;
6957

7058
if ($cartItem->getProductType() === self::PRODUCT_TYPE_BUNDLE) {
7159
return $this->isStockAvailableBundle($cartItem, $previousQty, $requestedQty);
7260
}
7361

62+
$variantProduct = $this->getVariantProduct($cartItem);
7463
$requiredItemQty = $requestedQty + $previousQty;
7564
if ($variantProduct !== null) {
7665
return $this->isStockQtyAvailable($variantProduct, $requestedQty, $requiredItemQty, $previousQty);
@@ -102,6 +91,50 @@ public function isStockAvailableBundle(Item $cartItem, int $previousQty, $reques
10291
return true;
10392
}
10493

94+
/**
95+
* Returns the cart item's available stock value
96+
*
97+
* @param Item $cartItem
98+
* @return float
99+
* @throws NoSuchEntityException
100+
*/
101+
public function getProductAvailableStock(Item $cartItem): float
102+
{
103+
if ($cartItem->getProductType() === self::PRODUCT_TYPE_BUNDLE) {
104+
return $this->getLowestStockValueOfBundleProduct($cartItem);
105+
}
106+
107+
$variantProduct = $this->getVariantProduct($cartItem);
108+
if ($variantProduct !== null) {
109+
return $this->getAvailableStock($variantProduct);
110+
}
111+
return $this->getAvailableStock($cartItem->getProduct());
112+
}
113+
114+
/**
115+
* Returns variant product if available
116+
*
117+
* @param Item $cartItem
118+
* @return ProductInterface|null
119+
* @throws NoSuchEntityException
120+
*/
121+
private function getVariantProduct(Item $cartItem): ?ProductInterface
122+
{
123+
/**
124+
* @var ProductInterface $variantProduct
125+
* Configurable products cannot have stock, only its variants can. If the user adds a configurable product
126+
* using its SKU and the selected options, we need to get the variant it refers to from the quote.
127+
*/
128+
$variantProduct = null;
129+
130+
if ($cartItem->getProductType() === self::PRODUCT_TYPE_CONFIGURABLE) {
131+
if ($cartItem->getChildren()[0] !== null) {
132+
$variantProduct = $this->productRepositoryInterface->get($cartItem->getSku());
133+
}
134+
}
135+
return $variantProduct;
136+
}
137+
105138
/**
106139
* Check if product is available in stock
107140
*
@@ -127,4 +160,32 @@ private function isStockQtyAvailable(
127160

128161
return ((bool) $stockStatus->getHasError()) === false;
129162
}
163+
164+
/**
165+
* Returns the product's available stock value
166+
*
167+
* @param ProductInterface $product
168+
* @return float
169+
*/
170+
private function getAvailableStock(ProductInterface $product): float
171+
{
172+
return $this->stockState->getStockQty($product->getId());
173+
}
174+
175+
/**
176+
* Returns the lowest stock value of bundle product
177+
*
178+
* @param Item $cartItem
179+
* @return float
180+
*/
181+
private function getLowestStockValueOfBundleProduct(Item $cartItem): float
182+
{
183+
$bundleStock = [];
184+
$qtyOptions = $cartItem->getQtyOptions();
185+
foreach ($qtyOptions as $qtyOption) {
186+
$bundleStock[] = $this->getAvailableStock($qtyOption->getProduct());
187+
}
188+
189+
return min($bundleStock);
190+
}
130191
}

0 commit comments

Comments
 (0)