woocommerce / woocommerce

A customizable, open-source ecommerce platform built on WordPress. Build any commerce solution you can imagine.
https://woocommerce.com
9.4k stars 10.76k forks source link

Cart and stock problem #44231

Closed Hellotoyou11 closed 3 months ago

Hellotoyou11 commented 8 months ago

Prerequisites

Describe the bug

Hi. Here is the situation. Imagine there is one product left in stock. User A adds to cart. All good. User B also adds the product to cart. However he goes to cart page and sees the following warning: "There is not enough “...” in stock. Please reduce the quantity in your cart." However the product is not sold. Is just in someone cart page.

Also noticed this: (may be useless info or not)

Expected behavior

It should only remove the product from stock after someone actually buys.

Actual behavior

It allows user B to add to his cart... however in the cart page it shows him an error "There is not enough “...” in stock. Please reduce the quantity in your cart."

Steps to reproduce

Warning cart page

WordPress Environment ``` ### WordPress Environment ### WordPress address (URL): https://tubarao.pt Site address (URL): https://tubarao.pt WC Version: 8.5.2 REST API Version: ✔ 8.5.2 WC Blocks Version: ✔ 11.8.0-dev Action Scheduler Version: ✔ 3.7.1 Log Directory Writable: ✔ WP Version: 6.4.3 WP Multisite: – WP Memory Limit: 256 MB WP Debug Mode: – WP Cron: – Language: pt_PT External object cache: – ### Server Environment ### Server Info: LiteSpeed PHP Version: 8.1.27 PHP Post Max Size: 256 MB PHP Time Limit: 30 PHP Max Input Vars: 2500 cURL Version: 7.87.0 OpenSSL/1.1.1w SUHOSIN Installed: – MySQL Version: 10.6.16-MariaDB-cll-lve Max Upload Size: 256 MB Default Timezone is UTC: ✔ fsockopen/cURL: ✔ SoapClient: ✔ DOMDocument: ✔ GZip: ✔ Multibyte String: ✔ Remote Post: ✔ Remote Get: ✔ ### Database ### WC Database Version: 8.5.2 WC Database Prefix: L3E_j1 Tamanho total da base de dados: 5.24MB Tamanho dos dados da base de dados: 3.54MB Tamanho do índice da base de dados: 1.70MB L3E_j1woocommerce_sessions: Dados: 0.02MB + Índice: 0.02MB + Engine InnoDB L3E_j1woocommerce_api_keys: Dados: 0.02MB + Índice: 0.03MB + Engine InnoDB L3E_j1woocommerce_attribute_taxonomies: Dados: 0.02MB + Índice: 0.02MB + Engine InnoDB L3E_j1woocommerce_downloadable_product_permissions: Dados: 0.02MB + Índice: 0.06MB + Engine InnoDB L3E_j1woocommerce_order_items: Dados: 0.02MB + Índice: 0.02MB + Engine InnoDB L3E_j1woocommerce_order_itemmeta: Dados: 0.02MB + Índice: 0.03MB + Engine InnoDB L3E_j1woocommerce_tax_rates: Dados: 0.02MB + Índice: 0.06MB + Engine InnoDB L3E_j1woocommerce_tax_rate_locations: Dados: 0.02MB + Índice: 0.03MB + Engine InnoDB L3E_j1woocommerce_shipping_zones: Dados: 0.02MB + Índice: 0.00MB + Engine InnoDB L3E_j1woocommerce_shipping_zone_locations: Dados: 0.02MB + Índice: 0.03MB + Engine InnoDB L3E_j1woocommerce_shipping_zone_methods: Dados: 0.02MB + Índice: 0.00MB + Engine InnoDB L3E_j1woocommerce_payment_tokens: Dados: 0.02MB + Índice: 0.02MB + Engine InnoDB L3E_j1woocommerce_payment_tokenmeta: Dados: 0.02MB + Índice: 0.03MB + Engine InnoDB L3E_j1woocommerce_log: Dados: 0.02MB + Índice: 0.02MB + Engine InnoDB L3E_j1actionscheduler_actions: Dados: 0.02MB + Índice: 0.11MB + Engine InnoDB L3E_j1actionscheduler_claims: Dados: 0.02MB + Índice: 0.02MB + Engine InnoDB L3E_j1actionscheduler_groups: Dados: 0.02MB + Índice: 0.02MB + Engine InnoDB L3E_j1actionscheduler_logs: Dados: 0.02MB + Índice: 0.03MB + Engine InnoDB L3E_j1commentmeta: Dados: 0.02MB + Índice: 0.03MB + Engine InnoDB L3E_j1comments: Dados: 0.02MB + Índice: 0.09MB + Engine InnoDB L3E_j1links: Dados: 0.02MB + Índice: 0.02MB + Engine InnoDB L3E_j1options: Dados: 2.45MB + Índice: 0.06MB + Engine InnoDB L3E_j1postmeta: Dados: 0.02MB + Índice: 0.03MB + Engine InnoDB L3E_j1posts: Dados: 0.06MB + Índice: 0.06MB + Engine InnoDB L3E_j1termmeta: Dados: 0.02MB + Índice: 0.03MB + Engine InnoDB L3E_j1terms: Dados: 0.02MB + Índice: 0.03MB + Engine InnoDB L3E_j1term_relationships: Dados: 0.02MB + Índice: 0.02MB + Engine InnoDB L3E_j1term_taxonomy: Dados: 0.02MB + Índice: 0.03MB + Engine InnoDB L3E_j1usermeta: Dados: 0.02MB + Índice: 0.03MB + Engine InnoDB L3E_j1users: Dados: 0.02MB + Índice: 0.05MB + Engine InnoDB L3E_j1wc_admin_notes: Dados: 0.06MB + Índice: 0.00MB + Engine InnoDB L3E_j1wc_admin_note_actions: Dados: 0.05MB + Índice: 0.02MB + Engine InnoDB L3E_j1wc_category_lookup: Dados: 0.02MB + Índice: 0.00MB + Engine InnoDB L3E_j1wc_customer_lookup: Dados: 0.02MB + Índice: 0.03MB + Engine InnoDB L3E_j1wc_download_log: Dados: 0.02MB + Índice: 0.03MB + Engine InnoDB L3E_j1wc_orders: Dados: 0.02MB + Índice: 0.11MB + Engine InnoDB L3E_j1wc_orders_meta: Dados: 0.02MB + Índice: 0.03MB + Engine InnoDB L3E_j1wc_order_addresses: Dados: 0.02MB + Índice: 0.06MB + Engine InnoDB L3E_j1wc_order_coupon_lookup: Dados: 0.02MB + Índice: 0.03MB + Engine InnoDB L3E_j1wc_order_operational_data: Dados: 0.02MB + Índice: 0.03MB + Engine InnoDB L3E_j1wc_order_product_lookup: Dados: 0.02MB + Índice: 0.06MB + Engine InnoDB L3E_j1wc_order_stats: Dados: 0.02MB + Índice: 0.05MB + Engine InnoDB L3E_j1wc_order_tax_lookup: Dados: 0.02MB + Índice: 0.03MB + Engine InnoDB L3E_j1wc_product_attributes_lookup: Dados: 0.02MB + Índice: 0.02MB + Engine InnoDB L3E_j1wc_product_download_directories: Dados: 0.02MB + Índice: 0.02MB + Engine InnoDB L3E_j1wc_product_meta_lookup: Dados: 0.02MB + Índice: 0.09MB + Engine InnoDB L3E_j1wc_rate_limits: Dados: 0.02MB + Índice: 0.02MB + Engine InnoDB L3E_j1wc_reserved_stock: Dados: 0.02MB + Índice: 0.00MB + Engine InnoDB L3E_j1wc_tax_rate_classes: Dados: 0.02MB + Índice: 0.02MB + Engine InnoDB L3E_j1wc_webhooks: Dados: 0.02MB + Índice: 0.02MB + Engine InnoDB ### Post Type Counts ### attachment: 1 page: 7 post: 2 product: 1 shop_order_placehold: 2 wp_navigation: 1 wp_template: 2 ### Security ### Secure connection (HTTPS): ✔ Hide errors from visitors: ✔ ### Active Plugins (1) ### WooCommerce: por Automattic – 8.5.2 ### Inactive Plugins (0) ### ### Settings ### API Enabled: – Force SSL: – Currency: EUR (€) Currency Position: right_space Thousand Separator: Decimal Separator: , Number of Decimals: 2 Taxonomies: Product Types: external (external) grouped (grouped) simple (simple) variable (variable) Taxonomies: Product Visibility: exclude-from-catalog (exclude-from-catalog) exclude-from-search (exclude-from-search) featured (featured) outofstock (outofstock) rated-1 (rated-1) rated-2 (rated-2) rated-3 (rated-3) rated-4 (rated-4) rated-5 (rated-5) Connected to Woo.com: – Enforce Approved Product Download Directories: ✔ HPOS feature screen enabled: ✔ HPOS feature enabled: ✔ Order datastore: Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore HPOS data sync enabled: – ### WC Pages ### Página da Loja: #7 - /loja/ Carrinho: #8 - /carrinho/ Finalizar compras: #9 - /finalizar-compra/ A minha conta: #10 - /minha-conta/ Termos e condições: ❌ Página não definida ### Theme ### Name: Twenty Twenty-Four Version: 1.0 Author URL: https://pt.wordpress.org Child Theme: ❌ – Se estiver a modificar o tema do WooCommerce ou um tema principal que não construiu pessoalmente recomendamos o uso de um tema dependente. Veja: Como criar um tema dependente WooCommerce Support: ❌ Não declarado ### Templates ### Overrides: – ### Admin ### Enabled Features: activity-panels analytics product-block-editor coupons core-profiler customer-effort-score-tracks import-products-task experimental-fashion-sample-products shipping-smart-defaults shipping-setting-tour homescreen marketing mobile-app-banner navigation onboarding onboarding-tasks product-variation-management product-virtual-downloadable product-external-affiliate product-grouped remote-inbox-notifications remote-free-extensions payment-gateway-suggestions shipping-label-banner subscriptions store-alerts transient-notices woo-mobile-welcome wc-pay-promotion wc-pay-welcome-page Disabled Features: customize-store minified-js new-product-management-experience product-linked settings async-product-editor-category-field Daily Cron: ✔ Next scheduled: 2024-02-01 15:19:53 +00:00 Options: ✔ Notes: 64 Onboarding: skipped ### Action Scheduler ### Concluída: 5 Oldest: 2024-01-31 15:20:56 +0000 Newest: 2024-01-31 15:24:46 +0000 Falhada: 1 Oldest: 2024-01-31 15:22:06 +0000 Newest: 2024-01-31 15:22:06 +0000 Pendente: 1 Oldest: 2024-02-01 15:20:56 +0000 Newest: 2024-02-01 15:20:56 +0000 ### Status report information ### Generated at: 2024-01-31 15:55:42 +00:00 ```
### Isolating the problem - [X] I have deactivated other plugins and confirmed this bug occurs when only WooCommerce plugin is active. - [X] This bug happens with a default WordPress theme active, or [Storefront](https://woo.com/products/storefront/). - [X] I can reproduce this bug consistently using the steps above.
Hellotoyou11 commented 8 months ago

Settings User A - All good User B - Checkout User B - Warning User B - warning trying to add to cart on shop page Translation to last notice - You can not add this product because quantity stock left is not enough 0

Hellotoyou11 commented 8 months ago

More information to help to solve on the wordpress forum https://wordpress.org/support/topic/quantity-in-stock-to-0-before-user-pays/#post-17394191

This is a major bug, the store does not work without a proper working cart.

opr commented 8 months ago

Hi @Hellotoyou11 thank you for reporting this issue.

I can reproduce this. I see you've disabled the stock-holding feature too, which is useful to know. I would expect this to be reflected when using the blocks as well.

I've added this issue to our backlog to be worked on.

hagedigital commented 7 months ago

Facing the same issue, it's marking all of the products as sold out and not reversing the stock levels from the wp_wc_reserved_stock table

Aotricx commented 6 months ago

This could have something to do with cache plugins. I saw someone else saying they deactivated hummingbird which is their default theme cache plugin and it fixed it, so I tried deactivating my default theme plugin which is speedy cache, and it works just perfectly now. I still have litespeed cache enabled but it doesn't seem to affect anything.

hagedigital commented 6 months ago

I don't use a cache plugin on my site, only Cloudflare

Aotricx commented 6 months ago

I don't use a cache plugin on my site, only Cloudflare

turns out it didn't fix it. I'm still having the issue. This is really annoying. I only ever add products with 1 stock as they're unique and they are released on drops. Ive seen some people change when the stock is reduced by changing the code to only reduce the stock once an order hits processing status. I am not very involved in site coding so Im not sure how to do that

hagedigital commented 6 months ago

Reducing stock levels once it hits processing or on hold is the default behaviour, I believe this is primarily todo with the reversed stock table using the blocks checkout

The standard woocommerce checkout shortcode does not have this issue

Jonasdir commented 6 months ago

Hello, I have the same problem - after the user puts the product in the cart, when he goes to the cart, it shows that the quantity is insufficient. How to solve it?

bekarice commented 5 months ago

just came across this bug, going to chime in with expectations. No user wants to hold stock for someone who might check out over someone who will pay right now. Seems like the process is:

  1. A draft checkout is created immediately upon visiting checkout. No personal information is required to create the draft, and thus reserve the stock.
  2. The stock is held for 10 minutes (upped to 60m or the default stock setting if the person attempts to pay, okay that part makes sense)
  3. Anyone who subsequently tries to purchase during that lock sees that the stock is reserved

Imagine if you sent an email for a new product with limited quantity; now the first N people who get to checkout have just locked it all up, whether they have strong purchase intent or not. Now all of the people who could be purchasing bounce because they think the stock is gone and they missed out, even though they didn't. So your conversion rate has definitely tanked, because there's a 0% chance that everyone who made it checkout first was 100% going to buy.

Seems like a better default is to not hold stock, but keep filters so people can opt to hold stock for draft orders if needed? It's unclear to me why this 10 minute hold is even useful as written (comments would help to justify if there's a very concrete reason).

For those of you struggling here, use

add_filter( 'woocommerce_hold_stock_for_checkout', '__return_false' );

where you keep custom code (e.g. Code Snippets plugin).

nerrad commented 5 months ago

@ralucaStan and @pmcpinto I think this needs a closer look and appreciate that @opr has (correctly) prioritized this as high recently.

@bekarice you've made some good points but I think we also have to consider the impact changing the behaviour can have on high-traffic stores (with high-intent shoppers) where the stock hold allows folks who have landed in the checkout to finish the purchase.

A few thoughts on this:

ralucaStan commented 5 months ago

Thank you Darren for the ping and to everybody offering feedback on this thread:

To wrap up the behaviour for people catching up:

Bugs surfaced from this ticket: (I'll document them and share the links)

Improvements needed surfaced by this ticket: I'll create tickets and share them

Things that need discussion and fixing soon: I'll start a discussion for this and share the link

bekarice commented 5 months ago

thanks @nerrad and @ralucaStan ! going to clarify a couple points:

So what I'm saying here is, the fundamental idea of the 10 minute hold is what should change, not that we need to add better handling / information around the 10 minute hold. Is there a merchant-based rationale for why that 10 minutes is needed? Or a technical rationale that I'm simply not seeing?

hagedigital commented 5 months ago

I agree, being able to disable this feature for high volume clients would be essential. Cart abandonment can be huge in fast fashion. Draft orders are abandoned frequently, if stock deduction causes the item to appear out of stock for 10 minutes it would lead to a loss of sale for another high intent customer who may have been ready to purchase within the window

senadir commented 5 months ago

While I can see the side of a customer coming in to buy and not seeing stock, I think it's 10 times more frustrating to actually add an item to cart, reach Checkout, start filling out the form, hitting place order just to be shown that the item is actually out of stock.

I think we can look into making this modifiable at least, but I'd vote against removing stock holding or reducing it to 2 minutes.

hagedigital commented 5 months ago

Having an option to alter the amount of time or disable would be ideal. Caching needs to be considered, are high volume stores purging the cache for a product page / possible tethered category pages which displays stock availability? If stock deductions are frequently requiring cache purging, it would increase the overall server load

senadir commented 5 months ago

You can for now, modify the whole stock reserve via woocommerce_order_hold_stock_minutes, this will apply to both reserves that happens when you start checkout (the 10mn), or the one that happens when you place the order (the 60mn that can be changed via settings).

You can for now do this for example to disable the initial one:

add_filter(
    'woocommerce_order_hold_stock_minutes',
    function ( $duration ) {
        if ( 10 === $duration ) {
            // 10mn is the duration for draft orders.
            return 0;
        }
    },
    10,
    1
);
bekarice commented 5 months ago

Hey @senadir , so that was the behavior before blocks: customers didn't see anything about "out of stock items" until submitting orders.

Stock reservations were introduced to fix race conditions for checkout — so if I went to check out at the same time as someone else, by the time both orders submitted, I might have gotten both inserted, and now the stock is negative even though backorders aren't allowed (fixed in WC 4.4 iirc). There were no reservations before submitting an order until blocks introduced draft orders, which were not excluded then from the reservation query (as I'm contesting they should be excluded).

Totally fair point to ask, "what behavior do we want now?", as you are. Here's the breakdown:

Scenario Pros Cons
Do not reserve stock for draft orders
  • highest intent buyers get the goods
  • maximizes conversion rate / order throughput for merchants
  • behavior is understandable for customers (even if they may want a different one)
  • customers may be disappointed when placing order if stock is gone
Reserve stock for draft orders
  • customers starting checkout are "guaranteed" they can buy
  • conversion rate lower for merchants (0% chance all checkouts convert)
  • customers with items in cart may still be disappointed at checkout

(We don't seem to have any technical rationale supporting one or the other for draft orders?)

Note that high volume sellers likely benefit from only holding stock for folks who actually submit purchases. They are most likely to encounter simultaneous shoppers. Low volume sellers are unlikely to have this influence their checkout process either way, as they won't have enough shoppers trying to check out at the same time.

So we could:

  1. remove reservations for draft orders (do scenario 1)
  2. keep reservations for draft orders and do nothing (don't think this is a good path)
  3. keep reservations for draft orders, and consider handling to avoid letting folks add reserved products to the cart in the first place (don't love this either, given it means that customers can still be disappointed in the shopping process, and requires adding a lot more handling to the code for this scenario)

I'm saying, Woo's onus should be to make the merchant experience best, then after that, facilitate the merchant making their own customer experience best. When we can do both, great! But right now the current behavior optimizes for customer experience (very arguably, and only in some cases), without providing the best merchant experience and negatively affecting merchant sales.

We can improve merchant experience, likely do no harm to customer experience (esp for most merchants), AND keep simpler code by excluding draft orders from stock reservations (in comparison to adding more handling for add to cart / quantity change scenarios).

pmcpinto commented 5 months ago

Hey everyone, thanks for sharing your feedback on this issue. After reviewing all the comments, I believe that not reserving stock for draft orders is the solution that offers the best tradeoffs. This approach benefits high-volume stores by preventing inventory from being tied up by potential shoppers with low purchase intent. Additionally, it simplifies our implementation and maintenance efforts.

Draft orders have the potential to be helpful in activities like abandoned cart recovery, which we want to explore in the future. Once we have a clearer plan for enhancing this feature to create more value, we can revisit this decision and consider adding control over stock reservation for draft orders.

cc @elizaan36 @nikkivias in case you have some thoughts about this topic.

elizaan36 commented 5 months ago

After reviewing all the comments, I believe that not reserving stock for draft orders is the solution that offers the best tradeoffs.

+1 for all the reasons @pmcpinto shared. This would be the best approach for merchants. It's quite a common pattern that if the checkout process takes too long, the shopper receives a notification that the item is no longer in stock. I think merchants and shoppers would expect this as default behavior.

ralucaStan commented 4 months ago

The team targets to explore the implications of draft orders that don't reserve stock in the upcoming weeks