Order Status Hooks
Nine hooks across an order record's life: creation, status changes, configuration, expiry and deletion.
Overview
An order is the cart's lasting record. Its status decides when services open: a waiting order opens nothing, an active one builds the services.
Carts left unpaid drop on their own after a while. That flow has a hook of its own and runs from a scheduled task.
Reference
Stopping an order record
Runs before the order record is written.
Hook::add('gate:order.create', 10, function ($data) {
if (Acme::blocked((int) ($data['user_id'] ?? 0)))
return 'This account cannot place new orders.';
return null;
});Following an order record
Runs after the order row was written. The services behind it are not built yet.
Hook::add('action:order.created', 10, function ($order) {
// The services are NOT built yet: they open when the status turns active.
Crm::orderRecord((int) ($order['id'] ?? 0), $order);
});Stopping an order status change
Runs before an order status changes.
Hook::add('gate:order.status_change', 10,
function ($order_id, $status, $apply_on_module) {
// A change that skips the module builds nothing: shape the rule for that.
if ($status === 'active' && $apply_on_module && !Acme::capacityFree())
return 'No new service can open now; capacity is full.';
return null;
});Following an order status
Runs after an order status changed.
waiting, inprocess, active, cancelled.Hook::add('action:order.status_changed', 10, function ($id, $status, $old_status) {
// On the first write the old status is EMPTY: a plain comparison misleads.
if ($old_status === '' || $status === $old_status) return;
if ($status === 'active') Crm::orderActivated($id);
});Following an order update
Runs after an order record was updated.
Hook::add('action:order.updated', 10, function ($id, $data) {
Audit::orderEdited((int) $id, array_keys($data));
});Catching an abandoned cart
Runs after an unpaid order timed out and was cancelled.
Hook::add('action:order.expired', 10, function ($order_id, $ordernum, $user_id) {
// An abandoned cart: the best moment for a win-back.
Retention::abandoned((int) $user_id, $ordernum);
});Stopping an order delete
Runs before an order is deleted.
Hook::add('gate:order.delete', 10, function ($ids) {
// An ARRAY arrives even for a single delete.
foreach ($ids as $id)
if (Acme::hasPaidInvoice((int) $id))
return 'An order with a paid invoice cannot be deleted.';
return null;
});Following an order delete
Runs after an order was deleted.
Hook::add('action:order.deleted', 10, function ($id, $order) {
// The items arrive decoded: no JSON parsing needed.
Accounting::orderRemoved((int) $id, $order['items'] ?? []);
});Changing the order total
Runs after the order totals were worked out.
total, tax_amount, the extra tax details, the display subtotal. total is the amount written permanently onto the order.Hook::add('filter:order.total', 10,
function (&$tax_calc, $subtotal, $total_discount, $items) {
// total is written PERMANENTLY; touching the tax keys shapes the
// invoice lines as well.
if (Acme::roundUp()) $tax_calc['total'] = ceil((float) $tax_calc['total']);
});Pitfalls
On the order-created hook the status is usually waiting and the services behind it are not built. A listener doing service work finds nothing here; the right moment is the status turning active.
The previous status on the status event can be an empty string: the order is being written for the first time. A listener comparing "it was this, now it is that" gets it wrong there. Handle the empty value separately.
Even for a single order the gate takes an array. A listener expecting one id tries to read the array as a number and the rule never holds. Always walk it with a loop.
The third parameter of the status gate says whether the change reaches the provider module. Where it is false only the record moves and nothing opens on a server. A rule about capacity, servers or providers has to read it.
Related Articles
¡Gracias por sus comentarios!
Nuestro equipo de soporte está disponible las 24 horas para ayudarle con lo que no encuentre aquí.