PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.5.0
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.5.0
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 1.3.20 1.3.19 trunk All 48 releases
fluent-cart / api / Resource / FrontendResource / CartResource.php

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

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