PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.4.1
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.4.1
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.4.1, at api/Resource/FrontendResource/CartResource.php

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