agentsclimarketplace

Wc order lifecycle and items

Skill Lonsdale201/wp-agent-skills/woocommerce/wc-order-lifecycle-and-items

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.From its SKILL.md

Install
npx -y skills add Lonsdale201/wp-agent-skills --skill wc-order-lifecycle-and-items

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 21 stars21 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

11.2 KB, ~2.4k tokens by cl100k_base, as published. Nobody here has run it

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:

HookArgsNotes
woocommerce_order_status_{$to}$order_id, $order, $status_transitionFires first.
status transition noteinternalSkipped for draft/new/checkout-draft origins.
woocommerce_order_status_{$from}_to_{$to}$order_id, $orderOnly when there is a previous status.
woocommerce_order_status_changed$order_id, $from, $to, $orderGeneral transition hook.
woocommerce_order_payment_status_changed$order_id, $orderOnly 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

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.