| name | wc-order-lifecycle-and-items |
| description | Work safely with WooCommerce order statuses, payment completion, status hooks, order items, line-item meta, totals, stock side effects, and paid analytics/conversion idempotency. Covers `payment_complete()` vs `update_status()`, status-hook ordering, `woocommerce_thankyou` vs paid events, replay-safe external side effects, concrete order-item classes, totals, HPOS-safe CRUD, and stock handling. Use when reacting to orders, changing statuses/items, provisioning, fulfillment, stock logic, external conversion events, or debugging paid orders that skipped lifecycle side effects. |
| metadata | {"wp-skills-author":"Soczó Kristóf","wp-skills-contact":"mailto:lonsdale201@hotmail.com","wp-skills-plugin":"woocommerce","wp-skills-plugin-version-tested":"10.9.4","wp-skills-php-min":"7.4","wp-skills-last-updated":"2026-07-15"} |
WooCommerce order lifecycle and items
Use this when plugin code reacts to orders, changes statuses, creates or edits order items, changes totals, or depends on stock/payment side effects.
Misconception this skill corrects
"The payment succeeded, so I can just call $order->update_status( 'completed' )."
For a real payment success path, gateways should call $order->payment_complete( $transaction_id ). That clears the awaiting-payment session flag, sets transaction/date-paid data, chooses processing vs completed via woocommerce_payment_complete_order_status, saves, and fires woocommerce_payment_complete. update_status() still fires status hooks, emails, and stock handlers, but it does not perform the full paid-order contract.
When to use this skill
Trigger when ANY of the following is true:
- A plugin changes order status.
- A plugin reacts to
processing, completed, cancelled, failed, refunded, or custom statuses.
- A gateway, webhook, fulfillment integration, ERP sync, license grant, stock adjustment, or provisioning flow touches orders.
- Code adds, removes, or edits order items.
- Code modifies item meta or order totals.
- The diff contains
payment_complete, update_status, set_status, woocommerce_order_status_, woocommerce_order_status_changed, woocommerce_order_payment_status_changed, WC_Order_Item_Product, add_item, or calculate_totals.
Status APIs
Use unprefixed statuses in order object APIs:
$order = wc_get_order( $order_id );
if ( $order instanceof WC_Order && $order->has_status( 'processing' ) ) {
$order->update_status(
'completed',
__( 'Marked complete by MyPlugin.', 'myplugin' ),
true
);
}
WC_Order::set_status() stages the transition on the object. WC_Order::update_status() calls set_status() and saves immediately. The method docblock confirms no internal wc- prefix is required.
Gateway or webhook payment success:
$order = wc_get_order( $order_id );
if ( $order instanceof WC_Order && $provider_status === 'captured' ) {
$order->payment_complete( $transaction_id );
}
Use update_status( 'on-hold' ), update_status( 'failed' ), or update_status( 'cancelled' ) for non-success states.
Status hook ordering
When a saved order status transition is processed, WooCommerce fires hooks in this order:
| Hook | Args | Notes |
|---|
woocommerce_order_status_{$to} | $order_id, $order, $status_transition | Fires first. |
| status transition note | internal | Skipped for draft/new/checkout-draft origins. |
woocommerce_order_status_{$from}_to_{$to} | $order_id, $order | Only when there is a previous status. |
woocommerce_order_status_changed | $order_id, $from, $to, $order | General transition hook. |
woocommerce_order_payment_status_changed | $order_id, $order | Only pending/failed to paid status. |
Use narrow hooks for narrow logic, and woocommerce_order_status_changed for transition-aware logic:
add_action(
'woocommerce_order_status_changed',
static function ( int $order_id, string $from, string $to, WC_Order $order ): void {
if ( 'processing' !== $to || 'processing' === $from ) {
return;
}
as_enqueue_async_action(
'myplugin_sync_paid_order',
array( 'order_id' => $order_id ),
'myplugin'
);
},
10,
4
);
Do not perform slow API calls directly inside status hooks. Enqueue a job and make the job idempotent.
Model paid business events separately from page and status events
woocommerce_thankyou proves that a receipt/order-received page rendered. It
does not prove capture or payment: the order can be pending, on-hold, failed
later, or the page can be refreshed. Likewise, woocommerce_new_order is not a
stable Purchase event and can run before every downstream integration has the
final item snapshot it expects.
For a paid conversion, start from WooCommerce's payment lifecycle:
- prefer
woocommerce_payment_complete when the gateway follows the canonical
API, or woocommerce_order_payment_status_changed for the pending/failed to
paid transition;
- load the order fresh and verify
is_paid()/date paid plus the product's
accepted status policy;
- claim a durable logical event such as
order:{id}:purchase:v1 before enqueueing
the remote job; status hooks can replay after manual transitions and retries;
- pass the same operation key to a provider idempotency field/header and retain
reconciliation state when the provider cannot enforce it.
A unique insert or INSERT IGNORE is only useful if downstream actions are
emitted only when this invocation created the event. Calling do_action()
or a remote API unconditionally after a duplicate/no-op insert bypasses local
deduplication. Keep local claim, queue creation, remote delivery, and completion
states explicit.
Model refund, cancellation, and Subscriptions renewal payments as separate
versioned business events. Do not infer them by replaying the initial Purchase.
Test pending/on-hold, processing, completed, failed, thank-you refresh, manual
status reversal, webhook retry, partial/full refund, and renewal orders.
Order creation hooks
woocommerce_new_order is not a universal "checkout just started" hook. Since WC 10.8 the CPT and HPOS stores skip normal new-order behavior for draft/new/checkout-draft transitions and fire it when the order becomes non-draft. For checkout-specific behavior, use checkout hooks such as woocommerce_checkout_order_created or woocommerce_checkout_order_processed.
Add a product line item
Use concrete item classes. Do not instantiate base WC_Order_Item; WC 9.9+ warns against direct base-item instantiation.
$order = wc_get_order( $order_id );
$product = wc_get_product( $product_id );
if ( $order instanceof WC_Order && $product instanceof WC_Product ) {
$price = (float) $product->get_price( 'edit' );
$item = new WC_Order_Item_Product();
$item->set_product( $product );
$item->set_quantity( 1 );
$item->set_subtotal( $price );
$item->set_total( $price );
$item->add_meta_data( '_myplugin_source', 'manual-adjustment', true );
$order->add_item( $item );
$order->calculate_totals();
$order->save();
}
add_item() attaches the item to the order object and assigns a temporary item key until save. Recalculate totals after changing items, fees, shipping, discounts, or taxes.
Edit existing line items
$order = wc_get_order( $order_id );
if ( $order instanceof WC_Order ) {
foreach ( $order->get_items( 'line_item' ) as $item ) {
if ( ! $item instanceof WC_Order_Item_Product ) {
continue;
}
$item->add_meta_data( '_myplugin_exported', current_time( 'mysql', true ), true );
$item->save();
}
}
$item->save() persists an item-only change; an additional $order->save() is needed only when order properties also changed. For machine data, use private meta keys. For customer/admin-visible item meta, use stable keys and translate only the display label.
In WooCommerce 10.9+, $item->get_order() returns the item's already-associated order instance when available. It is not guaranteed to be an independent snapshot: mutating that object mutates the same in-memory order used by surrounding code.
Stock side effects
WooCommerce already wires stock reduction and restoration to order lifecycle hooks:
wc_maybe_reduce_stock_levels() runs on woocommerce_payment_complete, woocommerce_order_status_completed, woocommerce_order_status_processing, and woocommerce_order_status_on-hold.
wc_maybe_increase_stock_levels() runs on woocommerce_order_status_cancelled and woocommerce_order_status_pending.
- Each line item stores
_reduced_stock to avoid reducing stock twice.
woocommerce_order_item_quantity filters the quantity used for stock reduction.
woocommerce_reduce_order_item_stock, woocommerce_reduce_order_stock, and woocommerce_restore_order_stock let integrations observe changes.
Do not call wc_reduce_stock_levels() blindly in payment/webhook code. In the normal paid flow, payment_complete() and the status hooks already cover it. If you implement custom stock behavior, respect _reduced_stock and make the operation idempotent.
HPOS-safe order data
Orders are not posts in HPOS mode. Use WooCommerce CRUD:
$order = wc_get_order( $order_id );
if ( $order instanceof WC_Order ) {
$order->update_meta_data( '_myplugin_external_id', $external_id );
$order->save();
}
Do not use get_post_meta(), update_post_meta(), WP_Query over shop_order, or direct wp_postmeta SQL for order state.
Common mistakes
- Using
update_status( 'completed' ) as a payment success replacement for payment_complete().
- Passing
wc-processing to object methods that expect unprefixed statuses.
- Running slow fulfillment/API calls directly inside order status hooks.
- Treating
woocommerce_thankyou, checkout completion, or order creation as
proof of successful payment/Purchase.
- Deduplicating a local row but firing the conversion action even when the
insert was a duplicate/no-op.
- Instantiating
WC_Order_Item instead of WC_Order_Item_Product, WC_Order_Item_Fee, WC_Order_Item_Shipping, WC_Order_Item_Coupon, or WC_Order_Item_Tax.
- Editing items and forgetting
calculate_totals() and save().
- Calling stock reduction manually after WooCommerce already did it.
- Saving order data through post meta instead of CRUD APIs.
Cross-skill routing
- Payment gateway process and webhook success:
wc-payment-gateway
- HPOS storage/query compatibility:
wc-hpos-compatibility
- Background work from order hooks:
wc-action-scheduler-jobs
- Cart/checkout line-item meta before order creation:
wc-cart-checkout-classic
References
- Official documentation: https://woocommerce.github.io/code-reference/classes/WC-Order.html
- Verified source paths:
wp-content/plugins/woocommerce/includes/class-wc-order.php
wp-content/plugins/woocommerce/includes/abstracts/abstract-wc-order.php
wp-content/plugins/woocommerce/includes/class-wc-order-item.php
wp-content/plugins/woocommerce/includes/class-wc-order-item-product.php
wp-content/plugins/woocommerce/includes/wc-order-functions.php
wp-content/plugins/woocommerce/includes/wc-stock-functions.php
wp-content/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php