Just encountered this today. So here is an explanation what happens.

If you want to programatically add something to the cart, you can use the addEntity method of the cart manager. Doing so, will save a new order item. And saving a new order item will trigger a save of the order. And saving the order will trigger order refresh. And triggering the order refresh will process all order processors. For example commerce.availability_checker services, called by by the manager, required by commerce_order.availability_order_processor which is an order processor. phew.

So, to reproduce this issue, consider this simple code (replace those magic numbers with real IDs):

<?php

// Magic numbers that works for me right now.
$store_id = 1;
$variation_id = 551634;

/** @var \Drupal\commerce_cart\CartProviderInterface $cart_manager */
$cart_provider = \Drupal::service('commerce_cart.cart_provider');
/** @var \Drupal\commerce_cart\CartManagerInterface $cart_manager */
$cart_manager = \Drupal::service('commerce_cart.cart_manager');
$variation = \Drupal\commerce_product\Entity\ProductVariation::load($variation_id);
$store = \Drupal\commerce_store\Entity\Store::load($store_id);
$user = \Drupal\user\Entity\User::load(1);
$cart = $cart_provider->getCart('default', $store, $user);
if (!$cart) {
  $cart = $cart_provider->createCart('default', $store, $user);
}
$cart_manager->emptyCart($cart);
$cart_manager->addEntity($cart, $variation, 1);

And consider a super simple availability checker like so:

  /**
   * {@inheritdoc}
   */
  public function applies(PurchasableEntityInterface $entity) {
    return TRUE;
  }

  /**
   * {@inheritdoc}
   */
  public function check(PurchasableEntityInterface $entity, $quantity, Context $context) {
    return FALSE;
  }

Now try to run the php code for adding the entity, for example by saving it as test.php and invoking it with drush scr test.php

This will now create a cart for user 1, with one item in the cart, which is deleted while adding it. This gives a range of problems on different checkout steps, but ignoring that, it also gives a completely empty cart page, which looks weird :)

$order->hasItems also returns true for this order.

Could be several fixes for this, but that is the gist of it at least.

Comments

eiriksm created an issue. See original summary.

eiriksm’s picture

Issue summary: View changes
eiriksm’s picture

Issue summary: View changes
eiriksm’s picture

For now, I solved this by injecting the availability manager, and doing a ::check before I try to add something. I still feel like this is a bug though :p

starlight-sparkle’s picture

#3088597: Add a constraint to purchased_entity which checks the availability manager makes it possible to call ::validate() on the order item entity after it is created, but there are two ways where this is bypassed and still causes the problem stated in IS to occur.

  1. In the UI, when the variation select field is hidden by form display config, clicking "add to cart" on the product page (default variation) doesn't trigger the validation as the PurchasedEntityAvailableConstraint violation is filtered out by ContentEntityForm::validateForm()
  2. When variations are added to cart programmatically thru CartManager::addEntity(), it will internally call CartManager::createOrderItem() followed by CartManager::addOrderItem(), which doesn't validate the newly created order item before it is saved.