WooCommerce order stock status not reducing on completed COD orders

Root Cause Diagnosis

WooCommerce reduces stock through wc_maybe_reduce_stock_levels(), hooked to woocommerce_payment_complete, woocommerce_order_status_processing, woocommerce_order_status_on-hold and woocommerce_order_status_completed. The function exits silently if global stock management is off, the product has manage_stock disabled, the order’s _order_stock_reduced flag is already set, or a filter returns false. COD orders never call payment_complete(), so reduction depends entirely on the status-transition hooks firing and passing those guards. For a COD order that moved processing → completed, stock was normally already reduced at processing, so no second reduction occurs at completed.

Quick Fix

Verify the global setting and the product setting, then reduce stock manually for an affected order (replace 123 with the order ID and 456 with the product ID):

wp option get woocommerce_manage_stock          # must return: yes
wp post meta get 456 _manage_stock              # must return: yes

wp eval '
$id = 123;
$order = wc_get_order( $id );
foreach ( $order->get_items() as $item ) {
    $item->delete_meta_data( "_reduced_stock" );
    $item->save();
}
$order->get_data_store()->set_stock_reduced( $id, false );
wc_reduce_stock_levels( $id );
'

Step-by-Step Resolution

1. Check the order notes. Open WooCommerce → Orders → [order] and read the notes panel. A line such as Stock levels reduced: Product (SKU) 10→9 means reduction already ran, likely at processing. If no such note exists at any status change, continue.

2. Enable global stock management. Go to WooCommerce → Settings → Products → Inventory and check Enable stock management. When this is off, wc_reduce_stock_levels() returns immediately.

3. Enable product-level stock management. Go to Products → [product] → Edit → Inventory and check Manage stock?. For variable products, either enable it on the parent (variations set to “Parent” inherit it) or enable it per variation under Variations → [variation] → Manage stock?.

See also  Fixing WordPress media upload error: "HTTP error" without debug logs in wp-config

4. Inspect the order’s reduction flags. If an order was cloned, imported, or created by a script that copied meta, the flag may be set although stock never changed.

wp eval '
$o = wc_get_order( 123 );
var_dump( $o->get_data_store()->get_stock_reduced( 123 ) );
foreach ( $o->get_items() as $i ) { var_dump( $i->get_meta( "_reduced_stock" ) ); }
'

true or a non-empty item value with no matching order note confirms a stale flag. Clear it using the Quick Fix snippet.

5. Detect filters blocking reduction.

wp eval '
var_dump( has_filter( "woocommerce_can_reduce_order_stock" ) );
var_dump( has_filter( "woocommerce_payment_complete_reduce_order_stock" ) );
'

A non-false result means a theme or plugin is hooked. Isolate it by deactivating plugins one by one:

wp plugin deactivate --all --skip-plugins
wp plugin activate woocommerce

Retest, then reactivate plugins individually. Inventory sync, ERP, multi-warehouse, and order auto-complete plugins are the usual cause.

6. Audit custom code that sets order status. Status changes must go through the CRUD API so the transition hooks fire:

$order = wc_get_order( $order_id );
$order->update_status( 'completed' ); // fires woocommerce_order_status_completed

These bypass the hooks and will not reduce stock:

wp_update_post( [ 'ID' => $order_id, 'post_status' => 'wc-completed' ] );
$wpdb->update( $wpdb->posts, [ 'post_status' => 'wc-completed' ], [ 'ID' => $order_id ] );

7. Flush stale stock displays. If the _stock value is correct in the database but the storefront or admin shows the old quantity:

wp wc tool run clear_transients --user=1
wp cache flush

Then purge the page cache (Cloudflare, Nginx FastCGI cache, LiteSpeed, etc.).

8. Confirm the fix. Place a test COD order for a stock-managed product and check that the order note appears once at processing. Then verify the value:

wp post meta get 456 _stock

Why This Fails for Some Users

  • Reduction occurs at processing, not completed. COD orders usually enter processing at checkout, so stock drops at checkout and nothing changes at completed. The _order_stock_reduced flag prevents a double reduction.
  • COD gateway status override. The woocommerce_cod_process_payment_order_status filter or a custom checkout plugin can leave orders in pending. Stock is then only held, not reduced, until a reducing status transition occurs.
  • HPOS (High-Performance Order Storage). Stock flags live in order meta tables. If HPOS sync is on and wp_postmeta and wc_orders_meta diverge, a stale _order_stock_reduced value can persist. Check WooCommerce → Settings → Advanced → Features and resolve sync discrepancies before editing flags.
  • Direct SQL or imported orders. CSV importers and REST clients that write status directly do not trigger transition hooks, and orders created through them have no _reduced_stock item meta.
  • Persistent object cache (Redis/Memcached). A misconfigured key prefix or a shared cache across sites can serve stale _stock post meta even though the database row is correct. Verify with wp db query "SELECT meta_value FROM wp_postmeta WHERE post_id=456 AND meta_key='_stock'" and compare it with wp post meta get 456 _stock.
  • Variations inheriting from the parent. If the parent has Manage stock? disabled and the variation’s stock setting is “Parent”, managing_stock() returns false and the item is skipped.
  • Table prefix mismatch. Replace wp_ in the SQL above with your actual prefix.
See also  Fixing Elementor Infinite Loading Wheel Caused by admin-ajax.php 403 Forbidden

Leave a Comment