PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.7.1
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.7.1
1.7.1 1.7.0 1.6.6 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 All 51 releases
fluent-cart / api / Resource / FrontendResource / CartResource.php

CartResource.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.7.1, at api/Resource/FrontendResource/CartResource.php

861 lines 27.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\Api\Resource\FrontendResource;
4
5 use FluentCart\Api\Cookie\Cookie;
6 use FluentCart\Api\Resource\BaseResourceApi;
7 use FluentCart\App\App;
8 use FluentCart\App\Helpers\AddressHelper;
9 use FluentCart\App\Helpers\CartHelper;
10 use FluentCart\App\Helpers\Helper;
11 use FluentCart\App\Models\Cart;
12 use FluentCart\App\Models\Customer;
13 use FluentCart\App\Models\ProductVariation;
14 use FluentCart\App\Services\RateLimiter;
15 use FluentCart\Framework\Database\Orm\Builder;
16 use FluentCart\Framework\Support\Arr;
17 use FluentCart\Framework\Support\Collection;
18 use WP_Error;
19
20 class CartResource extends BaseResourceApi
21 {
22
23 /**
24 * Per-request memo for get(). In production every HTTP request runs in a
25 * fresh PHP process, so this lives exactly one request. Long-running
26 * processes that simulate multiple requests (test suites, CLI) must clear
27 * it between simulated requests via resetCartCache() — as a
28 * function-static it was unreachable and leaked the first request's cart
29 * into every subsequent one.
30 *
31 * Only a resolved Cart is memoized; a null ("no cart") result is
32 * deliberately re-queried on the next call — matching the original
33 * function-static behavior, where isset(null) === false.
34 *
35 * @var Cart|null|false false = not resolved yet
36 */
37 private static $cartCache = false;
38
39 public static function resetCartCache(): void
40 {
41 static::$cartCache = false;
42 }
43
44 public static function getQuery(): Builder
45 {
46 return Cart::query();
47 }
48
49 public static function generateCartForInstantCheckout($variationId, $quantity = 1, $params = [])
50 {
51 RateLimiter::isSpamming('generate_instant_checkout_cart_attempt', 10, 60, true);
52
53 $isCustom = Arr::get($params, 'is_custom', false);
54
55 if($isCustom) {
56 $variation = Arr::get($params, 'variation');
57 $variation = CartHelper::normalizeCustomFields($variation);
58 }
59 else {
60 // product_detail must be eager-loaded — generateCartItemFromVariation
61 // reads $variation['product_detail']['variation_type'] to stamp
62 // the cart item's variation_type. Without it the field is null on
63 // instant-checkout carts and CartRenderer can't tell whether to
64 // hide the variant-title line for simple products.
65 $variation = ProductVariation::query()
66 ->with(['product', 'product_detail', 'media', 'shippingClass'])
67 ->where('id', $variationId)->first();
68
69 $variation = apply_filters('fluent_cart/cart/item_modify', $variation, [
70 'item_id' => $variationId,
71 'quantity' => $quantity,
72 ]);
73 }
74
75 if (!$variation) {
76 return new WP_Error(__('Invalid Product', 'fluent-cart'));
77 }
78
79 $quantity = apply_filters('fluent_cart/item_max_quantity', $quantity, [
80 'variation' => $variation,
81 'product' => !$isCustom ? $variation->product : []
82 ]);
83
84 // After the filter, not before: this path takes its quantity straight from a
85 // public URL param, and the filter above can replace it with anything.
86 $error = CartHelper::validateQuantity($quantity);
87 if ($error) {
88 return $error;
89 }
90 $quantity = (int)$quantity;
91
92 if ($variation->payment_type === 'subscription') {
93 $quantity = 1;
94 }
95
96 if(!$isCustom) {
97 $canPurchase = $variation->canPurchase($quantity);
98 if (is_wp_error($canPurchase)) {
99 return $canPurchase;
100 }
101 }
102
103 $cartQuery = Cart::query()
104 ->whereJsonLength('cart_data', 1)
105 ->whereRaw("JSON_UNQUOTE(JSON_EXTRACT(cart_data, '$[0].id')) = ?", [(string)$variationId])
106 ->whereRaw("JSON_UNQUOTE(JSON_EXTRACT(cart_data, '$[0].quantity')) = ?", [(string)$quantity])
107 ->where('cart_group', 'instant')
108 ->where('stage', '==', 'draft');
109
110 if (is_user_logged_in()) {
111 $cartQuery->where('user_id', get_current_user_id());
112 } else {
113 $cartQuery->whereNull('user_id');
114 }
115
116 $cart = $cartQuery->first();
117
118 if (!$cart) {
119 if(!$isCustom) {
120 $cart = CartHelper::generateCartFromVariation($variation, $quantity);
121 }
122 else {
123 // Legacy object-to-array conversion. Kept for backward compatibility.
124 $cart = CartHelper::generateCartFromCustomVariation(json_decode(json_encode($variation), true), $quantity);
125 }
126 } else {
127 // Refresh cart_data from the current variation on every instant hit.
128 // The inputs (variationId + quantity) are deterministic URL params,
129 // so regenerating is idempotent — and it picks up any fields that
130 // were missing on a previously-created draft (variation_type for
131 // advanced-variation rows, refreshed pricing, updated featured
132 // media). Without this, a stale draft from before a code change
133 // keeps rendering with its old shape.
134 if (!$isCustom) {
135 $cart->cart_data = [
136 CartHelper::generateCartItemFromVariation($variation, $quantity)
137 ];
138 } else {
139 $cart->cart_data = [
140 CartHelper::generateCartItemCustomItem(json_decode(json_encode($variation), true), $quantity)
141 ];
142 }
143 }
144
145 if (is_user_logged_in()) {
146 $cart->user_id = get_current_user_id();
147 }
148
149 $cart->cart_group = 'instant';
150
151 $cart->cart_hash = md5(time() . wp_generate_uuid4());
152
153 $cart->save();
154 return $cart;
155
156 }
157
158 /**
159 * Retrieve cart based on the provided parameters.
160 *
161 * This function looks for a cart associated with the provided cart hash. If no cart is found,
162 * it creates a new cart for anonymous users. If the user is logged in, it associates the cart
163 * with the logged-in user.
164 *
165 * @param array $params Optional. Additional parameters for cart retrieval.
166 * [
167 * // Include optional parameters, if any.
168 * ]
169 *
170 */
171 public static function get(array $params = [])
172 {
173 if (static::$cartCache !== false && static::$cartCache !== null) {
174 return static::$cartCache;
175 }
176
177 $autoCreate = Arr::get($params, 'create', false);
178
179 $cartHash = Arr::get($params, 'hash');
180
181 //$cartHash = App::request()->get('fct_cart_hash');
182
183 if ($cartHash) {
184 $cartQuery = static::getQuery()
185 ->where('cart_hash', $cartHash)
186 ->where('stage', '!=', 'completed')
187 ->where('cart_group', 'instant');
188
189 $tempCart = $cartQuery->first();
190
191 static::$cartCache = $tempCart;
192
193 if (!$autoCreate) {
194 return $tempCart;
195 }
196 }
197
198 static::$cartCache = static::getOrSetCartForThisDevice($autoCreate);
199
200 return static::$cartCache;
201 }
202
203 public static function find($id, $params = [])
204 {
205
206 }
207
208 /**
209 * Create cart with the provided item data.
210 *
211 * @param array $data Required. Array containing the necessary parameters
212 * [
213 * 'id' => (int) Required.The ID of the item,
214 * 'quantity' => (int) Optional.The quantity of the item
215 * ]
216 * @param array $params Optional. Additional parameters for cart creation or update.
217 * [
218 * // Include optional parameters, if any.
219 * ]
220 *
221 */
222 public static function create($data, $params = [])
223 {
224 $itemId = Arr::get($data, 'id');
225 $quantity = Arr::get($data, 'quantity', 1);
226
227 // This path writes cart_data directly instead of going through Cart::addItem().
228 $error = CartHelper::validateQuantity($quantity);
229 if ($error) {
230 return static::makeErrorResponse([
231 ['code' => 403, 'message' => $error->get_error_message()]
232 ]);
233 }
234
235 $cart = CartResource::get([
236 'create' => true
237 ]);
238 $cartArray = $cart->cart_data;
239
240 $cartArray = self::updateCartItemsQuantity(
241 [
242 'item_id' => $itemId,
243 'increment_by' => $quantity,
244 'existing_items' => $cartArray
245 ]
246 );
247
248 if (Arr::get($cartArray, 'code', '') === 'failed') {
249 return static::makeErrorResponse([
250 ['code' => 423, 'message' => Arr::get($cartArray, 'message', __('Cart validation error!', 'fluent-cart'))]
251 ]);
252 }
253
254 $cart->cart_data = Arr::get($cartArray, 'cart_data');
255 $message = Arr::get($cartArray, 'message');
256
257 $isCreated = $cart->save();
258
259 if ($isCreated) {
260 return static::makeSuccessResponse(
261 $isCreated,
262 __('Successfully added!', 'fluent-cart')
263 );
264 }
265
266 return static::makeErrorResponse([
267 ['code' => 400, 'message' => __('Could not add', 'fluent-cart')]
268 ]);
269 }
270
271 /**
272 * Update the quantity of an item in the cart.
273 *
274 * @param array $data Required. Array containing the necessary parameters for item quantity
275 * [
276 * 'item_id' => (int) Required.The ID of the product_variation,
277 * 'quantity'=> (int) Optional.The quantity of the item
278 * ]
279 * @param int $id Required. The ID of the cart.
280 * @param array $params Optional. Additional parameters for updating cart
281 * [
282 * // Include optional parameters, if any.
283 * ]
284 *
285 */
286 public static function update($data, $id = '', $params = [])
287 {
288 $cart = self::get([
289 'create' => true,
290 'hash' => Arr::get($params, 'hash'),
291 ]);
292
293 $itemId = (int)Arr::get($data, 'item_id');
294 $quantity = Arr::get($data, 'quantity', 0);
295 $byInput = (bool)Arr::get($data, 'by_input', false);
296
297 if (!$itemId) {
298 return new WP_Error(
299 'invalid_item',
300 __('Invalid item.', 'fluent-cart')
301 );
302 }
303
304 $existingItem = $cart->findExistingItemAndIndex($itemId);
305 $existingItem = Arr::get($existingItem, 1);
306
307 $rawIsCustom = $existingItem !== null
308 ? Arr::get($existingItem, 'is_custom', false)
309 : Arr::get($data, 'is_custom', false);
310
311 $isCustom = in_array(
312 strtolower((string) $rawIsCustom),
313 ['1', 'true'],
314 true
315 );
316
317 if($isCustom){
318 // Detect item and quantity change, let external modify item
319 if ($existingItem) {
320 $changedVariation = apply_filters('fluent_cart/cart/custom_item_quantity_changed', $existingItem,
321 [
322 'old_quantity' => (int) Arr::get($existingItem, 'quantity', 0),
323 'new_quantity' => $quantity,
324 'by_input' => $byInput,
325 'is_changed' => true,
326 'is_custom' => $isCustom,
327 ]);
328
329 if (!is_object($changedVariation)) {
330 $changedVariation = (object) $changedVariation;
331 }
332
333 $variation = $changedVariation;
334
335 $quantity = isset($changedVariation->quantity)
336 ? (int) $changedVariation->quantity
337 : 0;
338 }
339 else {
340 $variation = apply_filters('fluent_cart/cart/validate_custom_item', $existingItem, [
341 'item_id' => $itemId,
342 'quantity' => $quantity,
343 'is_custom' => $isCustom,
344 ]);
345
346 if (!is_object($variation)) {
347 $variation = (object) $variation;
348 }
349 }
350
351 if (!$variation || !is_object($variation)) {
352 return new WP_Error('invalid_custom_item',
353 __('Invalid custom item data.', 'fluent-cart'));
354 }
355
356 }else{
357 $variation = ProductVariation::query()->where('id', $itemId)->with('product')->first();
358 $variation = apply_filters('fluent_cart/cart/item_modify', $variation, [
359 'item_id' => $itemId,
360 'quantity' => $quantity,
361 ]);
362 }
363
364 if (!$variation) {
365 // An item already in the cart whose variation row has since
366 // disappeared (product/variation deleted) is dropped gracefully.
367 // An id that was never in the cart and resolves to nothing is a
368 // client error — silently answering "Cart updated successfully"
369 // hid typos and probing as a 200 no-op.
370 if ($existingItem !== null) {
371 return $cart->removeItem($itemId);
372 }
373
374 return new WP_Error(
375 'invalid_item',
376 __('Invalid item.', 'fluent-cart')
377 );
378 }
379
380 $soldIndividually = $isCustom
381 ? !empty($variation->sold_individually)
382 : (bool) $variation->soldIndividually();
383
384 if ($soldIndividually) {
385 if ($quantity >= 1) {
386 $quantity = 1;
387 }
388 $byInput = true;
389 }
390
391 if($isCustom) {
392 $cart = $cart->addByCustom(
393 is_array($variation) ? $variation : (array) $variation,
394 [
395 'quantity' => $quantity,
396 'is_custom' => $isCustom,
397 ]
398 );
399
400 }
401 else {
402 $cart = $cart->addByVariation($variation, [
403 'quantity' => $quantity,
404 'by_input' => $byInput,
405 'will_validate' => true,
406 'replace' => false,
407 'is_custom' => $isCustom,
408 ]);
409 }
410
411
412 if (is_wp_error($cart)) {
413 return $cart;
414 }
415
416 $utmData = static::prepareUtmData($data);
417 if ($utmData) {
418 // Replaced, not merged. A cart row is reused across visits, so merging
419 // key by key accumulated a union of every touch that ever reached it and
420 // the column stopped describing any single one. The browser has already
421 // resolved which touch this is, so its block is the answer.
422 $cart->utm_data = $utmData;
423 $cart->save();
424 }
425
426 return $cart;
427 }
428
429 public static function prepareUtmData(array $params): array
430 {
431 $data = [];
432 $allowedUtmParams = [
433 'utm_campaign',
434 'utm_content',
435 'utm_term',
436 'utm_source',
437 'utm_medium',
438 'utm_id',
439 'refer_url',
440 'fbclid',
441 'gclid'
442 ];
443
444 foreach ($allowedUtmParams as $utmParam) {
445 if (isset($params[$utmParam])) {
446 $data[$utmParam] = $params[$utmParam];
447 }
448 }
449 return $data;
450 }
451
452 /**
453 * Delete cart based on the provided user ID or cart hash.
454 *
455 * @param int $id Required. The user ID associated with the cart.
456 * @param array $params Optional. Additional parameters for cart deletion.
457 * [
458 * 'cart_hash' => (string) Optional. The cart hash for additional identification.
459 * ]
460 *
461 */
462 public static function delete($id, $params = [])
463 {
464 $cart = static::get();
465
466 if (empty($cart)) {
467 return null;
468 }
469
470 $deleted = $cart->delete();
471
472 if ($deleted) {
473 Cookie::deleteCartHash();
474 }
475
476 return $deleted;
477 }
478
479 public static function getStatus(): array
480 {
481 $cart = static::get(
482 [
483 'create' => false // Do not create a new cart if it doesn't exist
484 ]
485 );
486
487 if (!$cart) {
488 return [];
489 }
490
491 return [
492 'cart_hash' => $cart->cart_hash,
493 'cart_data' => $cart->cart_data,
494 'cart_user' => $cart->user_id,
495 ];
496 }
497
498 public static function isLicensedProduct($productVariation): bool
499 {
500 return Helper::hasLicense(Arr::get($productVariation, 'product'));
501 }
502
503
504 private static function validateShouldAddProduct($productVariation, $existingItemsArray)
505 {
506 if (Arr::get($productVariation, 'product.post_status') !== 'publish') {
507 return new WP_Error(
508 'item_not_available',
509 __('Item is not available.', 'fluent-cart')
510 );
511 }
512
513 $variationIds = (new Collection($existingItemsArray))->pluck('object_id');
514 $paymentType = Arr::get($productVariation, 'other_info.payment_type', false);
515
516 $hasInstantCheckoutParam = !empty(App::request()->get(Helper::INSTANT_CHECKOUT_URL_PARAM));
517
518 //early return as don't allow subscription item to add in cart
519 if ($paymentType !== 'onetime' && !$hasInstantCheckoutParam) {
520 return new WP_Error(
521 'item_not_available',
522 __('Item is not available.', 'fluent-cart')
523 );
524 }
525 $hasSubscription = static::hasSubscriptionProduct($existingItemsArray);
526
527 if (!$variationIds->contains(Arr::get($productVariation, 'id')) || empty($existingItemsArray)) {
528 return new WP_Error(
529 'item_not_available',
530 __('Item is not available.', 'fluent-cart')
531 );
532
533 }
534
535 if ($paymentType === 'onetime' && !$hasSubscription) {
536 return true;
537 }
538
539 if ($paymentType === 'onetime' && $hasSubscription) {
540 return new WP_Error(
541 'subscription_items_can_not_combined',
542 __('Subscription items can\'t be combined with other products in the cart.', 'fluent-cart')
543 );
544 }
545
546 return new WP_Error(
547 'item_not_available',
548 __('Item is not available.', 'fluent-cart')
549 );
550 }
551
552 public static function hasSubscriptionProduct($existingItemsArray = []): bool
553 {
554 $subscriptionProduct = (new Collection($existingItemsArray))->pluck('other_info')->filter(function ($info) {
555 $otherInfo = (array)$info;
556 $type = Arr::get($otherInfo, 'payment_type', false);
557 return $type === 'subscription';
558 });
559 return $subscriptionProduct->count() > 0;
560 }
561
562 private static function removeItemFromCart($existingItemsArray, $index): array
563 {
564 unset($existingItemsArray[$index]);
565 $message = __('Item removed from cart', 'fluent-cart');
566 return [
567 'message' => $message,
568 'cart_data' => $existingItemsArray,
569 ];
570 }
571
572 public static function updateItemQuantityInCart($productVariation, $existingItemsArray, $index, $quantity = 1, $isFilteredItem = false): array
573 {
574 $canBeAdded = true;
575
576
577 if (!$isFilteredItem) {
578 $canBeAdded = static::validateShouldAddProduct($productVariation, $existingItemsArray);
579 }
580
581
582 if (is_wp_error($canBeAdded)) {
583 return [
584 'code' => 'failed',
585 'message' => $canBeAdded->get_error_message()
586 ];
587 }
588
589 $updatedQuantity = $existingItemsArray[$index]['quantity'] + $quantity;
590
591 if ($updatedQuantity < 0) {
592 $updatedQuantity = 0;
593 }
594
595 if ($updatedQuantity > 0 && ($error = CartHelper::validateQuantity($updatedQuantity))) {
596 return [
597 'code' => 'failed',
598 'message' => $error->get_error_message()
599 ];
600 }
601
602 if (!$isFilteredItem) {
603
604 if (!CartHelper::shouldAddItemToCart($productVariation, $updatedQuantity)) {
605 return [
606 'code' => 'failed',
607 'message' => __("You've reached the maximum quantity for this product.", 'fluent-cart')
608 ];
609 }
610 }
611
612 if ($productVariation instanceof ProductVariation) {
613 $item = CartHelper::generateCartItemFromVariation($productVariation, $updatedQuantity);
614 } else {
615 $item = CartHelper::generateCartItemCustomItem($productVariation, $updatedQuantity);
616 }
617
618
619 $existingItemsArray[$index] = $item;
620 return [
621 'message' => __('Quantity updated!', 'fluent-cart'),
622 'cart_data' => $existingItemsArray,
623 ];
624 }
625
626 public static function addItemInCart($productVariation, $existingItemsArray, $index, $quantity = 1, $isFilteredItem = false): array
627 {
628
629 if ($quantity < 1) {
630 $quantity = 1;
631 }
632
633 if ($error = CartHelper::validateQuantity($quantity)) {
634 return [
635 'code' => 'failed',
636 'message' => $error->get_error_message()
637 ];
638 }
639
640 if (!$isFilteredItem) {
641 if (!CartHelper::shouldAddItemToCart($productVariation, $quantity)) {
642 return [
643 'code' => 'failed',
644 'message' => sprintf(
645 /* translators: %s is the product title */
646 __('%s is out of stock', 'fluent-cart'),
647 Arr::get($productVariation, 'variation_title')
648 ),
649 ];
650 }
651
652 $paymentType = $productVariation instanceof ProductVariation
653 ? $productVariation->payment_type
654 : Arr::get($productVariation, 'payment_type');
655
656 if ($paymentType === 'subscription' && $quantity > 1) {
657 return [
658 'code' => 'failed',
659 'message' => __('You cannot purchase more than one subscription at a time.', 'fluent-cart'),
660 ];
661 }
662
663 if (!empty($existingItemsArray)) {
664 $hasSubscription = static::hasSubscriptionProduct($existingItemsArray);
665
666 if ($paymentType === 'subscription' || $hasSubscription) {
667 return [
668 'code' => 'failed',
669 'message' => __("Subscription items can't be combined with other products in the cart.", 'fluent-cart'),
670 ];
671 }
672 }
673 }
674
675 if ($productVariation instanceof ProductVariation) {
676 $item = CartHelper::generateCartItemFromVariation($productVariation, $quantity);
677 } else {
678 $item = CartHelper::generateCartItemCustomItem($productVariation, $quantity);
679 }
680
681
682 $existingItemsArray[] = static::getCartSingleItemPreparedArray(
683 [
684 'variation' => $productVariation,
685 'quantity' => $quantity
686 ]
687 );
688
689
690 return [
691 'message' => __('Item added in cart!', 'fluent-cart'),
692 'cart_data' => $existingItemsArray,
693 ];
694 }
695
696 private static function updateCartItemsQuantity($params = []): array
697 {
698 $itemId = Arr::get($params, 'item_id');
699
700 $incrementBy = Arr::get($params, 'increment_by');
701 $existingItemsArray = Arr::get($params, 'existing_items', []);
702 if (!is_array($existingItemsArray)) {
703 $existingItemsArray = [];
704 }
705
706
707 $index = -1;
708
709 foreach ($existingItemsArray as $itemIndex => $existingItem) {
710 if (Arr::get($existingItem, 'object_id') == $itemId) {
711 $index = $itemIndex;
712 break;
713 }
714 }
715
716
717 /** @var $productVariation ProductVariation */
718 if ($incrementBy == 0) {
719 return static::removeItemFromCart($existingItemsArray, $index);
720 }
721
722 $productVariation = ProductVariation::query()->where('id', $itemId)->with([
723 'product',
724 'product.detail',
725 'product.licensesMeta',
726 'product_detail',
727 'media',
728 'shippingClass'
729 ])->first();
730
731
732 $isFilteredItem = false;
733 if (empty($productVariation)) {
734 $isFilteredItem = true;
735 $productVariation = apply_filters('fluent_cart/cart_item_product_variation', $productVariation, $itemId, $incrementBy, $existingItemsArray);
736 }
737
738
739 if (empty($productVariation)) {
740 return [
741 'code' => 'failed',
742 'message' => __('Item is not available.', 'fluent-cart')
743 ];
744 }
745
746 //($index === 0 || !empty($index)) && isset($existingItemsArray[$index])
747 //inline check will not work
748
749 $isValidIndex = false;
750 if ($index != -1) {
751 $isValidIndex = true;
752 }
753
754 if ($isValidIndex && isset($existingItemsArray[$index])) {
755
756 return static::updateItemQuantityInCart(
757 $productVariation,
758 $existingItemsArray,
759 $index,
760 $incrementBy,
761 $isFilteredItem,
762 );
763 }
764
765 return static::addItemInCart(
766 $productVariation,
767 $existingItemsArray,
768 $index,
769 $incrementBy,
770 $isFilteredItem,
771 );
772 }
773
774
775 private static function getCartSingleItemPreparedArray($params = []): array
776 {
777
778 $variation = Arr::get($params, 'variation');
779 $quantity = Arr::get($params, 'quantity');
780 return CartHelper::generateCartItemFromVariation($variation, $quantity);
781
782 }
783
784 /**
785 * Check if cart exists
786 */
787
788 public static function getOrSetCartForThisDevice($autoCreate = false)
789 {
790
791 $cartHash = Cookie::getCartHash();
792
793 if ($cartHash) {
794 $cart = static::getQuery()
795 ->where('stage', '!=', 'completed')
796 ->where('cart_hash', $cartHash)
797 ->where('cart_group', 'global')
798 ->first();
799
800 if ($cart) {
801 return $cart;
802 }
803 }
804
805 $userId = get_current_user_id();
806 if ($userId) {
807 // Latest cart first — without an order, first() picks by primary key
808 // (cart_hash), which resurrects an arbitrary old cart for the user.
809 $cart = static::getQuery()
810 ->where('user_id', $userId)
811 ->where('stage', '!=', 'completed')
812 ->where('cart_group', 'global')
813 ->orderBy('updated_at', 'DESC')
814 ->first();
815
816 if ($cart) {
817 return $cart;
818 }
819 }
820
821 if (!$autoCreate) {
822 return null;
823 }
824
825 $cart = new Cart();
826 $cart->cart_data = [];
827
828 $cart = CartHelper::addCommonCartData($cart);
829
830 $cart->save();
831
832 Cookie::setCartHash($cart->cart_hash);
833
834 return $cart;
835 }
836
837 /**
838 * This method should be called only if no cart is found in a current device
839 */
840 private static function setupNewCart()
841 {
842 $cartArray['cart_data'] = [];
843 if ($userId = get_current_user_id()) {
844 $cartArray['user_id'] = $userId;
845 }
846 $cartArray['cart_group'] = 'global';
847 return Cart::query()->create($cartArray);
848
849 }
850
851 public static function resetCartData()
852 {
853 $cart = CartResource::get();
854
855 if (is_array($cart->cart_data) && !empty($cart->cart_data)) {
856 $cart->cart_data = [];
857 $cart->save();
858 }
859 }
860 }
861