Creating and changing orders, HPOS
Orders are created not only by a customer on the site: they are taken by phone, imported from a marketplace, extended with a tracking number. The order object has setter methods for that. The rule is the same as for products: set_... and save(), no direct database queries.
Creating an order
// wp-content/plugins/acme-shop/scripts/create-order.php
$order = wc_create_order(['customer_id' => 0]);
$order->add_product(wc_get_product(12), 2);
$order->set_address([
'first_name' => 'Anna',
'last_name' => 'Schmidt',
'email' => 'anna@example.com',
'address_1' => 'Hauptstrasse 5',
'city' => 'Berlin',
'postcode' => '10115',
'country' => 'DE',
], 'billing');
$order->set_payment_method('cod');
$order->calculate_totals();
$order->update_status('processing', 'Order taken by phone.');
echo 'Created order ' . $order->get_id() . PHP_EOL;wc_create_order() creates an empty order, add_product() adds an item, calculate_totals() computes totals and taxes. Run the file with wp eval-file: an order for two mugs appears in WooCommerce → Orders with the Processing status.
Changing
// wp-content/plugins/acme-shop/scripts/update-order.php
$order = wc_get_order(1024);
$order->set_shipping_city('Hamburg');
$order->set_customer_note('Call an hour before delivery.');
$order->save();As with a product, changes reach the database only after save(). If items or shipping were changed, call calculate_totals() before saving.
Order meta
Your own data — a tracking number, an ID in an external system — is kept in meta data:
// wp-content/plugins/acme-shop/includes/orders.php
add_action('woocommerce_order_status_completed', function (int $orderId, WC_Order $order) {
if ($order->get_meta('_acme_tracking') === '') {
$order->update_meta_data('_acme_tracking', 'AB' . $orderId);
$order->save();
}
}, 10, 2);When an order moves to Completed, a tracking number is written into it. The check before writing protects against the hook firing twice.
Notes
$order->add_order_note('Tracking number: AB1024', true);The second argument, true, makes the note visible to the customer: they get an email and see it in My account → Orders. Without it the note is internal and visible only in the admin panel.
Finding by meta data
$orders = wc_get_orders([
'limit' => 1,
'meta_query' => [
['key' => '_acme_tracking', 'value' => 'AB1024'],
],
]);
$order = $orders[0] ?? null;This is how an order is found by a number from an external system — for example, when the delivery service reports a handover.
HPOS rules
HPOS keeps orders in its own tables, not in wp_posts. Hence the rules:
| Instead of | Use |
|---|---|
get_post($orderId) | wc_get_order($orderId) |
get_post_meta($orderId, $key, true) | $order->get_meta($key) |
update_post_meta($orderId, $key, $value) | $order->update_meta_data($key, $value) and save() |
new WP_Query(['post_type' => 'shop_order']) | wc_get_orders([...]) |
get_post_type($id) === 'shop_order' | $order instanceof WC_Order |
Code that follows these rules works with any storage. That is exactly what the custom_order_tables compatibility declaration from the plugin lesson promises.
A refund
wc_create_refund([
'order_id' => 1024,
'amount' => 9.90,
'reason' => 'The product was damaged in delivery.',
]);The function creates a refund for the given amount. When the whole order amount is refunded, the order moves to the Refunded status.
Common mistake
save() is forgotten after update_meta_data(). The value is in the object until the end of the request, so everything looks right while debugging, but it never reaches the database. The exception is hooks like woocommerce_checkout_create_order, which are called right before saving: there WooCommerce runs save() itself.