0

I am currently trying to build an eCommerce site. The functionalities are quite basic; there are products with limited quantities and customers can purchase those products. I've decided to use Drupal 10 as my backend CMS headless to handle the products and orders. The frontend communicates with Drupal through REST APIs. This includes order creation. When a user checks out, an order gets created in the backend, and the quantity of the item purchased is decreased.

I have a potential issue arising with the above order creation. The order creation can be seen below... The code is a bit simplified but the point is there:

public function post($data) {
  if (!$this->loggedUser->hasPermission('access content')) {
    throw new AccessDeniedHttpException();
  }

  $productVariationStorage = \Drupal::entityTypeManager()->getStorage('commerce_product_variation');
  $productVariation = $productVariationStorage->load($data['id']);

  // potential check to make sure the product variation is available (quantity > 0)
  // Throw error message if not available
  $orderItems = array();
  $orderItem = OrderItem::create([
    'type' => 'example_order_item',
    'purchased_entity' => $data['id'],
    'quantity' => 1,
  ]);

  $orderItem->save();
  array_push($orderItems, $orderItem);

  $order = Order::create([
    'type' => 'example_order',
    'uid' => $this->loggedUser->id(),
    'store_id' => 1,
    'order_items' => $orderItems,
    'placed' => \Drupal::time()->getCurrentTime(),
    'state' => 'draft'
  ]);
  $order->save();

  $productVariation->field_quantity->value -= 1;
  $productVariation->save();

  $resultArray = array();
  $resultArray['order_id'] = $order->uuid();
  return new ResourceResponse($resultArray);
}

The purchased product ID is sent from the frontend in the request body. Drupal then loads the product variation and checks if the quantity is greater than zero (if it is available, this can only be seen written in comments). If it is available, it proceeds to create an order item type and an order, etc. The quantity is then reduced by one, and the product variation is saved.

Let's say customer one clicks checkout on the frontend and the order creation API is called resulting in the above PHP code running. However, very shortly after customer one clicks checkout, customer two also clicks checkout with the same product (quantity is one for the product). A problem arises if the order of execution is the following for the two requests:

  1. Customer one check if available->it's available, continue
  2. Customer two check if available->it's available, continue
  3. Customer one create order, decrease quantity, save, etc.
  4. Customer two create order, decrease quantity, save, etc.

Both customers are able to purchase a single product, and I need to refund someone. Can this scenario happen? If so, the solution would be to somehow lock the product variation entity until it is up to date. While it is locked nobody can read/write. Can this be done from PHP?

I appreciate the help in advance, and if anything is unclear, let me know, and I'll edit the post!

5
  • 2
    Are you using a stock management module like drupal.org/project/commerce_stock?
    – mona lisa
    Commented Sep 30, 2023 at 14:32
  • 1
    This is common in most commerce systems. You can't sell what you don't have - so you should check before processing a payment if the item is still in-stock. If there is stock, proceed. Otherwise it should throw an error and let the user know that the item is now sold out.
    – Kevin
    Commented Sep 30, 2023 at 14:41
  • @cilefen I am not. Can the module help in a headless Drupal setup as well? Commented Sep 30, 2023 at 17:36
  • @Kevin Exactly. But my problem is the checking and the updating is not atomic. So really the only solution is to lock the resource I am reading and intending to update. This is what I do not know how to do in Drupal. Commented Sep 30, 2023 at 17:38
  • 3
    Drupal has a lock API. api.drupal.org/api/drupal/…
    – mona lisa
    Commented Sep 30, 2023 at 22:14

1 Answer 1

3

There are many ways to handle stock preventing overselling ... from a pattern standpoint, not necessarily from a contrib standpoint. However, our least favorite version is using a field on the product variation itself, as that results in a product update on every purchase. In a high-volume scenario, that's going to really hurt your database... and as you've seen, it's not great for concurrency issues.

Rather than using a field on the product, we'd typically recommend a custom table tracking the product variation ID and stock level. When a customer starts a purchase, you might use a separate table to place a hold and then only decrement the stock level once that order is placed. Thus, when you want to determine if a product is available for purchase, you're actually looking up how many holds are active against how many remaining stock for a given product.

Then, to solve for concurrency, you'll need an additional check in the checkout flow. Prior to payment, or at least between payment authorization and capture, you need to confirm the stock you thought was there is still there. If it isn't, you need to error out (and void any authorization if you went that route) ... if it is, you capture and carry on. This will almost always entail custom code, as we've yet to encounter the same set of rules twice, to be honest.

2
  • I see what you're saying here, but I do have a few questions. Firstly, if I have a separate table tracking stock, why is the confirmation of stock before payment less painful for the database than checking the quantity field belonging to the variation (the same goes for updating stock after payment)? Secondly, in a low-volume scenario, does my solution along with using the lock API work? Commented Oct 3, 2023 at 10:19
  • 1
    A simple direct query against the field data table wouldn’t be too taxing, but typically what happens is a full entity load. The real gains will be updates to the stock level not requiring a full variation entity load and update. Commented Oct 4, 2023 at 13:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.