Order Status Hooks

2 vues Markdown

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

gateorder.create
Orders::create() before the record

Runs before the order record is written.

Parameters 1
$dataarrayThe order data about to be written: owner, amount, currency, items, taxes, discounts, status, payment method.
Return 1
stringA non-empty string stops the action; the text is thrown as the error.
Listener
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

actionorder.created
Orders::create():215 the status can be waiting

Runs after the order row was written. The services behind it are not built yet.

Parameters 1
$orderarrayThe order data joined with its new id: owner, amount, currency, items, taxes, discounts, status, payment method, address, notes. The status is usually waiting.
Return 1
voidThe return is ignored.
Listener
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

gateorder.status_change
Orders::change_status() whether it reaches the module

Runs before an order status changes.

Parameters 3
$order_idintThe order id. The record does not arrive; read it yourself where you need it.
$statusstringThe target status.
$apply_on_moduleboolWhether the change reaches the provider module. Where it is false no service is built and only the record moves.
Return 1
stringA non-empty string stops the action; the text is thrown as the error.
Listener
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

actionorder.status_changed
Orders::change_status() the old status can be empty

Runs after an order status changed.

Parameters 3
$idintThe order id.
$statusstringThe new status: waiting, inprocess, active, cancelled.
$old_statusstringThe previous status. On the first write it arrives as an empty string; allow for that when comparing.
Return 1
voidThe return is ignored.
Listener
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

actionorder.updated
AdminOrders an operator edited it

Runs after an order record was updated.

Parameters 2
$idintThe order id.
$dataarrayThe update data written.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:order.updated', 10, function ($id, $data) {
    Audit::orderEdited((int) $id, array_keys($data));
});

Catching an abandoned cart

actionorder.expired
cronjobs/OrderCleanup a scheduled task

Runs after an unpaid order timed out and was cancelled.

Parameters 3
$order_idintThe id of the cancelled order.
$ordernumstringThe order number the customer sees.
$user_idintThe cart's owner.
Return 1
voidThe return is ignored.
Listener
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

gateorder.delete
AdminOrders a list arrives, not one record

Runs before an order is deleted.

Parameters 1
$idsarrayThe ids about to be deleted. An array arrives even for one order; do not expect a single id.
Return 1
stringA non-empty string stops the action; the text is thrown as the error.
Listener
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

actionorder.deleted
Orders::delete() a last snapshot

Runs after an order was deleted.

Parameters 2
$idintThe id of the deleted order.
$orderarrayA snapshot from before the delete, with items, taxes, discounts and details decoded.
Return 1
voidThe return is ignored.
Listener
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

filterorder.total
Orders the amount on the invoice

Runs after the order totals were worked out.

Parameters 4
$tax_calcarrayrefThe totals: total, tax_amount, the extra tax details, the display subtotal. total is the amount written permanently onto the order.
$subtotalfloatThe subtotal before discounts and tax.
$total_discountfloatThe reseller and coupon discounts together.
$itemsarrayThe final cart lines.
Return 1
voidThe value changes by reference; the return is not read.
Listener
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

An order record is not a service

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.

On the first write the old status is empty

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.

The delete gate takes a list

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.

A change that skips the module builds nothing

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.

Cet article vous a-t-il été utile ?

Merci pour votre retour !

Besoin d'aide supplémentaire ?

Notre équipe d'assistance est disponible 24h/24 pour tout ce que vous ne trouvez pas ci-dessus.