# Order Status Hooks

https://dev.wisecp.com/es/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

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 PHP

```php
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 PHP

```php
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 PHP

```php
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 PHP

```php
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 PHP

```php
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 PHP

```php
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 PHP

```php
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 PHP

```php
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 PHP

```php
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.

## Related Articles

- [Cart Hooks](https://dev.wisecp.com/en/cart-hooks)
- [Service Status Hooks](https://dev.wisecp.com/en/service-status-hooks)
- [Invoice Lifecycle Hooks](https://dev.wisecp.com/en/invoice-lifecycle-hooks)
