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

754 lines 22.3 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
571 if ($productVariation instanceof ProductVariation) {
572 $item = CartHelper::generateCartItemFromVariation($productVariation, $quantity);
573 } else {
574 $item = CartHelper::generateCartItemCustomItem($productVariation, $quantity);
575 }
576
577
578 $existingItemsArray[] = static::getCartSingleItemPreparedArray(
579 [
580 'variation' => $productVariation,
581 'quantity' => $quantity
582 ]
583 );
584
585
586 return [
587 'message' => __('Item added in cart!', 'fluent-cart'),
588 'cart_data' => $existingItemsArray,
589 ];
590 }
591
592 private static function updateCartItemsQuantity($params = []): array
593 {
594 $itemId = Arr::get($params, 'item_id');
595
596 $incrementBy = Arr::get($params, 'increment_by');
597 $existingItemsArray = Arr::get($params, 'existing_items', []);
598 if (!is_array($existingItemsArray)) {
599 $existingItemsArray = [];
600 }
601
602
603 $index = -1;
604
605 foreach ($existingItemsArray as $itemIndex => $existingItem) {
606 if (Arr::get($existingItem, 'object_id') == $itemId) {
607 $index = $itemIndex;
608 break;
609 }
610 }
611
612
613 /** @var $productVariation ProductVariation */
614 if ($incrementBy == 0) {
615 return static::removeItemFromCart($existingItemsArray, $index);
616 }
617
618 $productVariation = ProductVariation::query()->where('id', $itemId)->with([
619 'product',
620 'product.detail',
621 'product.licensesMeta',
622 'product_detail',
623 'media',
624 'shippingClass'
625 ])->first();
626
627
628 $isFilteredItem = false;
629 if (empty($productVariation)) {
630 $isFilteredItem = true;
631 $productVariation = apply_filters('fluent_cart/cart_item_product_variation', $productVariation, $itemId, $incrementBy, $existingItemsArray);
632 }
633
634
635 if (empty($productVariation)) {
636 return [
637 'code' => 'failed',
638 'message' => __('Item is not available.', 'fluent-cart')
639 ];
640 }
641
642 //($index === 0 || !empty($index)) && isset($existingItemsArray[$index])
643 //inline check will not work
644
645 $isValidIndex = false;
646 if ($index != -1) {
647 $isValidIndex = true;
648 }
649
650 if ($isValidIndex && isset($existingItemsArray[$index])) {
651
652 return static::updateItemQuantityInCart(
653 $productVariation,
654 $existingItemsArray,
655 $index,
656 $incrementBy,
657 $isFilteredItem,
658 );
659 }
660
661 return static::addItemInCart(
662 $productVariation,
663 $existingItemsArray,
664 $index,
665 $incrementBy,
666 $isFilteredItem,
667 );
668 }
669
670
671 private static function getCartSingleItemPreparedArray($params = []): array
672 {
673
674 $variation = Arr::get($params, 'variation');
675 $quantity = Arr::get($params, 'quantity');
676 return CartHelper::generateCartItemFromVariation($variation, $quantity);
677
678 }
679
680 /**
681 * Check if cart exists
682 */
683
684 public static function getOrSetCartForThisDevice($autoCreate = false)
685 {
686
687 $cartHash = Cookie::getCartHash();
688
689 if ($cartHash) {
690 $cart = static::getQuery()
691 ->where('stage', '!=', 'completed')
692 ->where('cart_hash', $cartHash)
693 ->where('cart_group', 'global')
694 ->first();
695
696 if ($cart) {
697 return $cart;
698 }
699 }
700
701 $userId = get_current_user_id();
702 if ($userId) {
703 $cart = static::getQuery()
704 ->where('user_id', $userId)
705 ->where('stage', '!=', 'completed')
706 ->where('cart_group', 'global')
707 ->first();
708
709 if ($cart) {
710 return $cart;
711 }
712 }
713
714 if (!$autoCreate) {
715 return null;
716 }
717
718 $cart = new Cart();
719 $cart->cart_data = [];
720
721 $cart = CartHelper::addCommonCartData($cart);
722
723 $cart->save();
724
725 Cookie::setCartHash($cart->cart_hash);
726
727 return $cart;
728 }
729
730 /**
731 * This method should be called only if no cart is found in a current device
732 */
733 private static function setupNewCart()
734 {
735 $cartArray['cart_data'] = [];
736 if ($userId = get_current_user_id()) {
737 $cartArray['user_id'] = $userId;
738 }
739 $cartArray['cart_group'] = 'global';
740 return Cart::query()->create($cartArray);
741
742 }
743
744 public static function resetCartData()
745 {
746 $cart = CartResource::get();
747
748 if (is_array($cart->cart_data) && !empty($cart->cart_data)) {
749 $cart->cart_data = [];
750 $cart->save();
751 }
752 }
753 }
754