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