15
15
use Magento \Quote \Api \CartItemRepositoryInterface ;
16
16
use Magento \Quote \Api \Data \CartInterface ;
17
17
use 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 ;
18
22
19
23
class CartQuantityValidator implements CartQuantityValidatorInterface
20
24
{
21
25
/**
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
28
29
*/
29
- private $ stockRegistry ;
30
+ private array $ cumulativeQty = [] ;
30
31
31
32
/**
33
+ * CartQuantityValidator Constructor
34
+ *
32
35
* @param CartItemRepositoryInterface $cartItemRepository
33
36
* @param StockRegistryInterface $stockRegistry
37
+ * @param Config $config
38
+ * @param LoggerInterface $logger
34
39
*/
35
40
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
38
45
) {
39
- $ this ->cartItemRepository = $ cartItemRepository ;
40
- $ this ->stockRegistry = $ stockRegistry ;
41
46
}
42
47
43
48
/**
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
45
50
*
46
51
* @param CartInterface $customerCart
47
52
* @param CartInterface $guestCart
@@ -50,33 +55,174 @@ public function __construct(
50
55
public function validateFinalCartQuantities (CartInterface $ customerCart , CartInterface $ guestCart ): bool
51
56
{
52
57
$ 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 */
54
63
foreach ($ guestCart ->getAllVisibleItems () as $ guestCartItem ) {
55
64
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 ;
74
75
}
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 ;
75
102
}
76
103
}
104
+
105
+ $ this ->cumulativeQty = [];
106
+
77
107
return $ modified ;
78
108
}
79
109
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
+
80
226
/**
81
227
* Check if backorders are enabled for the stock item
82
228
*
0 commit comments