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 */
66declare (strict_types=1 );
77
88namespace Magento \QuoteGraphQl \Model \Cart \MergeCarts ;
99
10-
1110use Magento \CatalogInventory \Api \StockRegistryInterface ;
1211use Magento \Framework \Exception \CouldNotSaveException ;
1312use Magento \Framework \Exception \NoSuchEntityException ;
1413use Magento \Quote \Api \CartItemRepositoryInterface ;
1514use Magento \Quote \Api \Data \CartInterface ;
1615use Magento \Quote \Api \Data \CartItemInterface ;
16+ use Magento \Quote \Model \Quote \Item ;
17+ use Magento \Checkout \Model \Config ;
18+ use Psr \Log \LoggerInterface ;
19+ use Magento \Catalog \Api \Data \ProductInterface ;
1720
1821class CartQuantityValidator implements CartQuantityValidatorInterface
1922{
2023 /**
21- * @var CartItemRepositoryInterface
22- */
23- private $ cartItemRepository ;
24-
25- /**
26- * @var StockRegistryInterface
24+ * Array to hold cumulative quantities for each SKU
25+ *
26+ * @var array
2727 */
28- private $ stockRegistry ;
28+ private array $ cumulativeQty = [] ;
2929
3030 /**
31+ * CartQuantityValidator Constructor
32+ *
3133 * @param CartItemRepositoryInterface $cartItemRepository
3234 * @param StockRegistryInterface $stockRegistry
35+ * @param Config $config
36+ * @param LoggerInterface $logger
3337 */
3438 public function __construct (
35- CartItemRepositoryInterface $ cartItemRepository ,
36- StockRegistryInterface $ stockRegistry
39+ private readonly CartItemRepositoryInterface $ cartItemRepository ,
40+ private readonly StockRegistryInterface $ stockRegistry ,
41+ private readonly Config $ config ,
42+ private readonly LoggerInterface $ logger
3743 ) {
38- $ this ->cartItemRepository = $ cartItemRepository ;
39- $ this ->stockRegistry = $ stockRegistry ;
4044 }
4145
4246 /**
43- * Validate combined cart quantities to make sure they are within available stock
47+ * Validate combined cart quantities to ensure they are within available stock
4448 *
4549 * @param CartInterface $customerCart
4650 * @param CartInterface $guestCart
@@ -49,28 +53,157 @@ public function __construct(
4953 public function validateFinalCartQuantities (CartInterface $ customerCart , CartInterface $ guestCart ): bool
5054 {
5155 $ modified = false ;
52- /** @var CartItemInterface $guestCartItem */
53- foreach ($ guestCart ->getAllVisibleItems () as $ guestCartItem ) {
54- foreach ($ customerCart ->getAllItems () as $ customerCartItem ) {
55- if ($ customerCartItem ->compare ($ guestCartItem )) {
56- $ product = $ customerCartItem ->getProduct ();
57- $ stockCurrentQty = $ this ->stockRegistry ->getStockStatus (
58- $ product ->getId (),
59- $ product ->getStore ()->getWebsiteId ()
60- )->getQty ();
61- if ($ stockCurrentQty < $ guestCartItem ->getQty () + $ customerCartItem ->getQty ()) {
62- try {
63- $ this ->cartItemRepository ->deleteById ($ guestCart ->getId (), $ guestCartItem ->getItemId ());
64- $ modified = true ;
65- } catch (NoSuchEntityException $ e ) {
66- continue ;
67- } catch (CouldNotSaveException $ e ) {
68- continue ;
69- }
70- }
56+ $ this ->cumulativeQty = [];
57+
58+ foreach ($ guestCart ->getAllVisibleItems () as $ guestItem ) {
59+ foreach ($ customerCart ->getAllItems () as $ customerItem ) {
60+ if (!$ customerItem ->compare ($ guestItem )) {
61+ continue ;
62+ }
63+
64+ $ mergePreference = $ this ->config ->getCartMergePreference ();
65+
66+ if ($ mergePreference === Config::CART_PREFERENCE_CUSTOMER ) {
67+ $ this ->safeDeleteCartItem ((int )$ guestCart ->getId (), (int )$ guestItem ->getItemId ());
68+ $ modified = true ;
69+ break ;
70+ }
71+
72+ $ product = $ customerItem ->getProduct ();
73+ $ sku = $ product ->getSku ();
74+ $ websiteId = (int ) $ product ->getStore ()->getWebsiteId ();
75+
76+ $ isQtyValid = $ customerItem ->getChildren ()
77+ ? $ this ->validateCompositeProductQty ($ guestItem , $ customerItem )
78+ : $ this ->validateProductQty (
79+ $ product ,
80+ $ sku ,
81+ $ guestItem ->getQty (),
82+ $ customerItem ->getQty (),
83+ $ websiteId
84+ );
85+
86+ if ($ mergePreference === Config::CART_PREFERENCE_GUEST ) {
87+ $ this ->safeDeleteCartItem ((int )$ customerCart ->getId (), (int )$ customerItem ->getItemId ());
88+ $ modified = true ;
89+ }
90+
91+ if (!$ isQtyValid ) {
92+ $ this ->safeDeleteCartItem ((int )$ guestCart ->getId (), (int )$ guestItem ->getItemId ());
93+ $ modified = true ;
7194 }
95+
96+ break ;
7297 }
7398 }
99+
100+ $ this ->cumulativeQty = [];
101+
74102 return $ modified ;
75103 }
104+
105+ /**
106+ * Validate product quantity against available stock
107+ *
108+ * @param ProductInterface $product
109+ * @param string $sku
110+ * @param float $guestItemQty
111+ * @param float $customerItemQty
112+ * @param int $websiteId
113+ * @return bool
114+ */
115+ private function validateProductQty (
116+ ProductInterface $ product ,
117+ string $ sku ,
118+ float $ guestItemQty ,
119+ float $ customerItemQty ,
120+ int $ websiteId
121+ ): bool {
122+ $ salableQty = $ this ->stockRegistry ->getStockStatus ($ product ->getId (), $ websiteId )->getQty ();
123+
124+ $ this ->cumulativeQty [$ sku ] ??= 0 ;
125+ $ this ->cumulativeQty [$ sku ] += $ this ->getCurrentCartItemQty ($ guestItemQty , $ customerItemQty );
126+
127+ return $ salableQty >= $ this ->cumulativeQty [$ sku ];
128+ }
129+
130+ /**
131+ * Validate composite product quantities against available stock
132+ *
133+ * @param Item $guestItem
134+ * @param Item $customerItem
135+ * @return bool
136+ */
137+ private function validateCompositeProductQty (Item $ guestItem , Item $ customerItem ): bool
138+ {
139+ $ guestChildren = $ guestItem ->getChildren ();
140+ $ customerChildren = $ customerItem ->getChildren ();
141+
142+ foreach ($ customerChildren as $ customerChild ) {
143+ $ sku = $ customerChild ->getProduct ()->getSku ();
144+ $ guestChild = $ this ->retrieveChildItem ($ guestChildren , $ sku );
145+
146+ $ guestQty = $ guestChild ? $ guestItem ->getQty () * $ guestChild ->getQty () : 0 ;
147+ $ customerQty = $ customerItem ->getQty () * $ customerChild ->getQty ();
148+
149+ $ product = $ customerChild ->getProduct ();
150+ $ websiteId = (int ) $ product ->getStore ()->getWebsiteId ();
151+
152+ if (!$ this ->validateProductQty ($ product , $ sku , $ guestQty , $ customerQty , $ websiteId )) {
153+ return false ;
154+ }
155+ }
156+
157+ return true ;
158+ }
159+
160+ /**
161+ * Find a child item by SKU in the list of children
162+ *
163+ * @param CartItemInterface[] $children
164+ * @param string $sku
165+ * @return CartItemInterface|null
166+ */
167+ private function retrieveChildItem (array $ children , string $ sku ): ?CartItemInterface
168+ {
169+ foreach ($ children as $ child ) {
170+ if ($ child ->getProduct ()->getSku () === $ sku ) {
171+ return $ child ;
172+ }
173+ }
174+
175+ return null ;
176+ }
177+
178+ /**
179+ * Get the current cart item quantity based on the merge preference
180+ *
181+ * @param float $guestCartItemQty
182+ * @param float $customerCartItemQty
183+ * @return float
184+ */
185+ private function getCurrentCartItemQty (float $ guestCartItemQty , float $ customerCartItemQty ): float
186+ {
187+ return match ($ this ->config ->getCartMergePreference ()) {
188+ Config::CART_PREFERENCE_CUSTOMER => $ customerCartItemQty ,
189+ Config::CART_PREFERENCE_GUEST => $ guestCartItemQty ,
190+ default => $ guestCartItemQty + $ customerCartItemQty
191+ };
192+ }
193+
194+ /**
195+ * Safely delete a cart item by ID, logging any exceptions
196+ *
197+ * @param int $cartId
198+ * @param int $itemId
199+ * @return void
200+ */
201+ private function safeDeleteCartItem (int $ cartId , int $ itemId ): void
202+ {
203+ try {
204+ $ this ->cartItemRepository ->deleteById ($ cartId , $ itemId );
205+ } catch (NoSuchEntityException | CouldNotSaveException $ e ) {
206+ $ this ->logger ->error ($ e ->getMessage ());
207+ }
208+ }
76209}
0 commit comments