1515use Magento \Quote \Api \CartItemRepositoryInterface ;
1616use Magento \Quote \Api \Data \CartInterface ;
1717use Magento \Quote \Api \Data \CartItemInterface ;
18+ use Magento \Quote \Model \Quote \Item ;
19+ use Magento \Checkout \Model \Config ;
20+ use Psr \Log \LoggerInterface ;
21+ use Magento \Catalog \Api \Data \ProductInterface ;
1822
1923class CartQuantityValidator implements CartQuantityValidatorInterface
2024{
2125 /**
22- * @var CartItemRepositoryInterface
23- */
24- private $ cartItemRepository ;
25-
26- /**
27- * @var StockRegistryInterface
26+ * Array to hold cumulative quantities for each SKU
27+ *
28+ * @var array
2829 */
29- private $ stockRegistry ;
30+ private array $ cumulativeQty = [] ;
3031
3132 /**
33+ * CartQuantityValidator Constructor
34+ *
3235 * @param CartItemRepositoryInterface $cartItemRepository
3336 * @param StockRegistryInterface $stockRegistry
37+ * @param Config $config
38+ * @param LoggerInterface $logger
3439 */
3540 public function __construct (
36- CartItemRepositoryInterface $ cartItemRepository ,
37- StockRegistryInterface $ stockRegistry
41+ private readonly CartItemRepositoryInterface $ cartItemRepository ,
42+ private readonly StockRegistryInterface $ stockRegistry ,
43+ private readonly Config $ config ,
44+ private readonly LoggerInterface $ logger
3845 ) {
39- $ this ->cartItemRepository = $ cartItemRepository ;
40- $ this ->stockRegistry = $ stockRegistry ;
4146 }
4247
4348 /**
44- * Validate combined cart quantities to make sure they are within available stock
49+ * Validate combined cart quantities to ensure they are within available stock
4550 *
4651 * @param CartInterface $customerCart
4752 * @param CartInterface $guestCart
@@ -50,33 +55,174 @@ public function __construct(
5055 public function validateFinalCartQuantities (CartInterface $ customerCart , CartInterface $ guestCart ): bool
5156 {
5257 $ modified = false ;
53- /** @var CartItemInterface $guestCartItem */
58+ $ this ->cumulativeQty = [];
59+
60+ /** @var \Magento\Quote\Model\Quote $guestCart */
61+ /** @var \Magento\Quote\Model\Quote $customerCart */
62+ /** @var \Magento\Quote\Model\Quote\Item $guestCartItem */
5463 foreach ($ guestCart ->getAllVisibleItems () as $ guestCartItem ) {
5564 foreach ($ customerCart ->getAllItems () as $ customerCartItem ) {
56- if ($ customerCartItem ->compare ($ guestCartItem )) {
57- $ product = $ customerCartItem ->getProduct ();
58- $ stockCurrentQty = $ this ->stockRegistry ->getStockStatus (
59- $ product ->getId (),
60- $ product ->getStore ()->getWebsiteId ()
61- )->getQty ();
62-
63- if (($ stockCurrentQty < $ guestCartItem ->getQty () + $ customerCartItem ->getQty ())
64- && !$ this ->isBackordersEnabled ($ product )) {
65- try {
66- $ this ->cartItemRepository ->deleteById ($ guestCart ->getId (), $ guestCartItem ->getItemId ());
67- $ modified = true ;
68- } catch (NoSuchEntityException $ e ) {
69- continue ;
70- } catch (CouldNotSaveException $ e ) {
71- continue ;
72- }
73- }
65+ if (!$ customerCartItem ->compare ($ guestCartItem )) {
66+ continue ;
67+ }
68+
69+ $ mergePreference = $ this ->config ->getCartMergePreference ();
70+
71+ if ($ mergePreference === Config::CART_PREFERENCE_CUSTOMER ) {
72+ $ this ->safeDeleteCartItem ((int )$ guestCart ->getId (), (int )$ guestCartItem ->getItemId ());
73+ $ modified = true ;
74+ break ;
7475 }
76+
77+ $ product = $ customerCartItem ->getProduct ();
78+ $ sku = $ product ->getSku ();
79+ $ websiteId = (int ) $ product ->getStore ()->getWebsiteId ();
80+
81+ $ isQtyValid = $ customerCartItem ->getChildren ()
82+ ? $ this ->validateCompositeProductQty ($ guestCartItem , $ customerCartItem )
83+ : $ this ->validateProductQty (
84+ $ product ,
85+ $ sku ,
86+ $ guestCartItem ->getQty (),
87+ $ customerCartItem ->getQty (),
88+ $ websiteId
89+ );
90+
91+ if ($ mergePreference === Config::CART_PREFERENCE_GUEST ) {
92+ $ this ->safeDeleteCartItem ((int )$ customerCart ->getId (), (int )$ customerCartItem ->getItemId ());
93+ $ modified = true ;
94+ }
95+
96+ if (!$ isQtyValid ) {
97+ $ this ->safeDeleteCartItem ((int )$ guestCart ->getId (), (int )$ guestCartItem ->getItemId ());
98+ $ modified = true ;
99+ }
100+
101+ break ;
75102 }
76103 }
104+
105+ $ this ->cumulativeQty = [];
106+
77107 return $ modified ;
78108 }
79109
110+ /**
111+ * Validate product quantity against available stock
112+ *
113+ * @param ProductInterface $product
114+ * @param string $sku
115+ * @param float $guestItemQty
116+ * @param float $customerItemQty
117+ * @param int $websiteId
118+ * @return bool
119+ */
120+ private function validateProductQty (
121+ ProductInterface $ product ,
122+ string $ sku ,
123+ float $ guestItemQty ,
124+ float $ customerItemQty ,
125+ int $ websiteId
126+ ): bool {
127+ $ salableQty = $ this ->stockRegistry ->getStockStatus ($ product ->getId (), $ websiteId )->getQty ();
128+
129+ $ this ->cumulativeQty [$ sku ] ??= 0 ;
130+ $ this ->cumulativeQty [$ sku ] += $ this ->getCurrentCartItemQty ($ guestItemQty , $ customerItemQty );
131+
132+ // If backorders are enabled, allow quantities beyond available stock
133+ if ($ this ->isBackordersEnabled ($ product )) {
134+ return true ;
135+ }
136+
137+ return $ salableQty >= $ this ->cumulativeQty [$ sku ];
138+ }
139+
140+ /**
141+ * Validate composite product quantities against available stock
142+ *
143+ * @param Item $guestItem
144+ * @param Item $customerItem
145+ * @return bool
146+ */
147+ private function validateCompositeProductQty (Item $ guestItem , Item $ customerItem ): bool
148+ {
149+ $ guestChildren = $ guestItem ->getChildren ();
150+ $ customerChildren = $ customerItem ->getChildren ();
151+
152+ foreach ($ customerChildren as $ customerChild ) {
153+ $ sku = $ customerChild ->getProduct ()->getSku ();
154+ $ guestChild = $ this ->retrieveChildItem ($ guestChildren , $ sku );
155+
156+ $ guestQty = $ guestChild ? $ guestItem ->getQty () * $ guestChild ->getQty () : 0 ;
157+ $ customerQty = $ customerItem ->getQty () * $ customerChild ->getQty ();
158+
159+ $ product = $ customerChild ->getProduct ();
160+ $ websiteId = (int ) $ product ->getStore ()->getWebsiteId ();
161+
162+ // If backorders are enabled for this product, skip quantity validation
163+ if ($ this ->isBackordersEnabled ($ product )) {
164+ continue ;
165+ }
166+
167+ if (!$ this ->validateProductQty ($ product , $ sku , $ guestQty , $ customerQty , $ websiteId )) {
168+ return false ;
169+ }
170+ }
171+
172+ return true ;
173+ }
174+
175+ /**
176+ * Find a child item by SKU in the list of children
177+ *
178+ * @param CartItemInterface[] $children
179+ * @param string $sku
180+ * @return CartItemInterface|null
181+ */
182+ private function retrieveChildItem (array $ children , string $ sku ): ?CartItemInterface
183+ {
184+ foreach ($ children as $ child ) {
185+ /** @var \Magento\Quote\Model\Quote\Item $child */
186+ if ($ child ->getProduct ()->getSku () === $ sku ) {
187+ return $ child ;
188+ }
189+ }
190+
191+ return null ;
192+ }
193+
194+ /**
195+ * Get the current cart item quantity based on the merge preference
196+ *
197+ * @param float $guestCartItemQty
198+ * @param float $customerCartItemQty
199+ * @return float
200+ */
201+ private function getCurrentCartItemQty (float $ guestCartItemQty , float $ customerCartItemQty ): float
202+ {
203+ return match ($ this ->config ->getCartMergePreference ()) {
204+ Config::CART_PREFERENCE_CUSTOMER => $ customerCartItemQty ,
205+ Config::CART_PREFERENCE_GUEST => $ guestCartItemQty ,
206+ default => $ guestCartItemQty + $ customerCartItemQty
207+ };
208+ }
209+
210+ /**
211+ * Safely delete a cart item by ID, logging any exceptions
212+ *
213+ * @param int $cartId
214+ * @param int $itemId
215+ * @return void
216+ */
217+ private function safeDeleteCartItem (int $ cartId , int $ itemId ): void
218+ {
219+ try {
220+ $ this ->cartItemRepository ->deleteById ($ cartId , $ itemId );
221+ } catch (NoSuchEntityException | CouldNotSaveException $ e ) {
222+ $ this ->logger ->error ($ e ->getMessage ());
223+ }
224+ }
225+
80226 /**
81227 * Check if backorders are enabled for the stock item
82228 *
0 commit comments