| @@ -301,8 +301,38 @@ | ||
| 301 | 301 | { |
| 302 | 302 | return Arr::get($this->checkout_data, 'is_locked') === 'yes' && $this->order_id; |
| 303 | 303 | } |
| 304 | 304 | |
| 305 | + /** | |
| 306 | + * Whether this cart can still take an additional item, such as an order bump. | |
| 307 | + * | |
| 308 | + * False when the cart is locked to an existing payment (custom payment link, | |
| 309 | + * renewal invoice, early installment) or already carries an upgrade. | |
| 310 | + * | |
| 311 | + * `is_locked` is a 'yes'/'no' string, so it must be compared explicitly — | |
| 312 | + * `!empty()` treats the string 'no' as locked. | |
| 313 | + * | |
| 314 | + * Deliberately distinct from isLocked(), which additionally requires order_id | |
| 315 | + * and is therefore false for renewal and early-installment carts, which never | |
| 316 | + * set that column. | |
| 317 | + * | |
| 318 | + * Filterable so an integration that locks its cart only to pin its own item | |
| 319 | + * (e.g. a booking) can still take order bumps. The filter only decides the | |
| 320 | + * lock; a cart carrying an upgrade is refused after it either way. The upgrade | |
| 321 | + * swap in WebCheckoutHandler::handleOrderBumpRequest() ignores this filter and | |
| 322 | + * keeps refusing any `is_locked` cart. | |
| 323 | + */ | |
| 324 | + public function acceptsAdditionalItems() | |
| 325 | + { | |
| 326 | + $accepts = (bool) apply_filters( | |
| 327 | + 'fluent_cart/cart/accepts_additional_items', | |
| 328 | + Arr::get($this->checkout_data, 'is_locked') !== 'yes', | |
| 329 | + ['cart' => $this] | |
| 330 | + ); | |
| 331 | + | |
| 332 | + return $accepts && empty(Arr::get($this->checkout_data, 'upgrade_data')); | |
| 333 | + } | |
| 334 | + | |
| 305 | 335 | public function addItem($item = [], $replacingIndex = null) |
| 306 | 336 | { |
| 307 | 337 | if ($this->isLocked()) { |
| 308 | 338 | return new \WP_Error('cart_locked', __('This cart is locked and cannot be modified.', 'fluent-cart')); |
| @@ -381,9 +411,14 @@ | ||
| 381 | 411 | } |
| 382 | 412 | |
| 383 | 413 | public function addByVariation(ProductVariation $variation, $config = []) |
| 384 | 414 | { |
| 385 | - $quantity = (int)Arr::get($config, 'quantity', 1); | |
| 415 | + $quantity = Arr::get($config, 'quantity', 1); | |
| 416 | + $error = CartHelper::validateQuantity($quantity, true); | |
| 417 | + if ($error) { | |
| 418 | + return $error; | |
| 419 | + } | |
| 420 | + $quantity = (int)$quantity; | |
| 386 | 421 | $byInput = Arr::get($config, 'by_input', false); |
| 387 | 422 | |
| 388 | 423 | if ($quantity == 0) { |
| 389 | 424 | // that means we have to remove it |
| @@ -389,8 +424,12 @@ | ||
| 389 | 424 | // that means we have to remove it |
| 390 | 425 | return $this->removeItem($variation->id, Arr::get($config, 'remove_args', []), true); |
| 391 | 426 | } |
| 392 | 427 | |
| 428 | + if (!$variation->product) { | |
| 429 | + return new \WP_Error('product_not_found', __('This product is no longer available.', 'fluent-cart')); | |
| 430 | + } | |
| 431 | + | |
| 393 | 432 | $validate = Arr::get($config, 'will_validate', false); |
| 394 | 433 | |
| 395 | 434 | $replacingIndex = null; |
| 396 | 435 | |
| @@ -423,8 +462,14 @@ | ||
| 423 | 462 | // remove the item if quantity is zero or negative after adjustment |
| 424 | 463 | return $this->removeItem($variation->id); |
| 425 | 464 | } |
| 426 | 465 | |
| 466 | + // Repeated increments accumulate onto the stored quantity, so re-check the sum. | |
| 467 | + $error = CartHelper::validateQuantity($quantity); | |
| 468 | + if ($error) { | |
| 469 | + return $error; | |
| 470 | + } | |
| 471 | + | |
| 427 | 472 | if ($validate) { |
| 428 | 473 | $canPurchase = $variation->canPurchase($quantity); |
| 429 | 474 | $canPurchase = apply_filters('fluent_cart/cart/can_purchase', $canPurchase, [ |
| 430 | 475 | 'cart' => $this, |
| @@ -472,9 +517,14 @@ | ||
| 472 | 517 | __('Invalid custom item data.', 'fluent-cart') |
| 473 | 518 | ); |
| 474 | 519 | } |
| 475 | 520 | |
| 476 | - $quantity = (int)Arr::get($config, 'quantity', 1); | |
| 521 | + $quantity = Arr::get($config, 'quantity', 1); | |
| 522 | + $error = CartHelper::validateQuantity($quantity, true); | |
| 523 | + if ($error) { | |
| 524 | + return $error; | |
| 525 | + } | |
| 526 | + $quantity = (int)$quantity; | |
| 477 | 527 | $variationId = Arr::get($variation, 'id'); |
| 478 | 528 | |
| 479 | 529 | if ($quantity == 0) { |
| 480 | 530 | // that means we have to remove it |