# Service Status Hooks

https://dev.wisecp.com/es/service-status-hooks

Nine hooks from a sold service opening to its record going: setting up, status changes, suspending, terminating and deleting.

## Overview

A service lives in **two places at once**: as a row in our database and as a real account at the end of a provider module. Most hooks here speak about the first.

Knowing that split matters: when a status changes, **our record** changed. Whether the account on the server truly closed is a separate question, and usually the outcome of a job that was **queued**.

## Reference

### Changing the service about to be created

filterservice.save_data

`Hook::runRefs` by reference

Runs before the service row is written to the database.

Parameters 1

$dataarrayrefThe row about to be written: `user_id`, `product_id`, `options`, `status`, `duedate`. What you change here goes straight to the database.

Return 1

voidThe value changes **by reference**; the return is not read.

Listener PHP

```php
Hook::add('filter:service.save_data', 10, function (&$data) {
    // Put your own tracking key inside options; do not add a column.
    $data['options']['acme_batch'] = Acme::currentBatch();
});
```

### Learning that a service opened

actionservice.created

`Services::create()` the row landed

Runs after the service row was written. The account at the provider may **not exist yet** at this point.

Parameters 2

$idintThe new service id.

$dataarrayThe data that was written. It has passed the filter, so your changes show here.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:service.created', 10, function ($id, $data) {
    // The row exists, the account may not: do not wait on provisioning here.
    Crm::opened((int) $id, (int) ($data['user_id'] ?? 0));
});
```

### Stopping a status change

gateservice.status_change

`Services::change_status()` both states

Runs before the service status changes, handing you both the old state and the target.

Parameters 3

$servicearrayThe service record with its **current** status.

$statusstringThe target: `active`, `suspended`, `cancelled`, `inprocess`.

$oldStatusstringThe current status. Read both together to target one particular transition.

Return 1

stringA non-empty string **stops** the transition; the text is thrown as the error.

Listener PHP

```php
Hook::add('gate:service.status_change', 10,
    function ($service, $status, $oldStatus) {
        // Check only the reopening of a cancelled service.
        if ($oldStatus === 'cancelled' && $status === 'active'
            && !Acme::reactivationAllowed($service))
            return 'A cancelled service reopens with an operator\'s approval.';

        return null;
    });
```

### Following a status change

actionservice.status_changed

`Services::change_status()` the module may have overridden

Runs after the status changed. The value you get is the **final** one, after any override by the module.

Parameters 4

$serviceIdintThe service id.

$statusstringThe new status. It can **differ** from the one asked for where the module stepped in.

$oldStatusstringThe previous status.

$servicearrayA snapshot of the service **before** the change.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:service.status_changed', 10,
    function ($serviceId, $status, $oldStatus, $service) {
        // Do no work again where it landed back on the same status.
        if ($status === $oldStatus) return;

        Crm::statusMoved($serviceId, $oldStatus, $status);
    });
```

### Stopping a suspend

gateservice.suspend

`cronjobs/ServiceSuspend` a veto turns into a cancel

Runs before a service or add-on is suspended. **Stopping it has a cost**: the job turns into a cancel signal.

Parameters 4

$target_typestring`service` or `addon`. One hook carries both.

$target_idintThe id of the record being suspended.

$servicearrayThe live row: `status`, `duedate`, `module`, `owner_id`.

$user_idintThe service owner.

Return 1

stringA non-empty string **vetoes** the suspend — and it does not end there: the signal turns into a **cancel** and your reason is recorded. So "do not suspend" is not the same as "do nothing".

Listener PHP

```php
Hook::add('gate:service.suspend', 10,
    function ($target_type, $target_id, $service, $user_id) {
        if ($target_type !== 'service') return null;      // leave add-ons alone

        // MIND: a veto stops the suspend but turns the job into a CANCEL signal.
        if (Acme::vipAccount($user_id)) return 'VIP account: review by hand.';

        return null;
    });
```

### Following a suspend

actionservice.suspended

`cronjobs/ServiceSuspend` the module job is queued

Runs after the service was suspended. The actual closing on the server was **queued**.

Parameters 5

$target_typestring`service` or `addon`.

$target_idintThe id of the suspended record.

$user_idintThe service owner.

$reasonstringThe reason, **translated into the customer's language**.

$module_queue_idintThe queue id of the module job. `0` means it was **never queued**: on a service without a module nothing happens on the server side.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:service.suspended', 10,
    function ($target_type, $target_id, $user_id, $reason, $module_queue_id) {
        // A queue id of 0 means nothing was done on the server.
        if ($module_queue_id === 0) Ops::note('suspend-no-module', $target_id);
    });
```

### Following a termination

actionservice.terminated

`cronjobs/ServiceTerminate` the server side

Runs after the service was terminated. This hook carries the **server side**: which module, which server.

Parameters 5

$target_idintThe id of the terminated service.

$service_typestring`hosting` or `server`.

$modulestringThe server module's name.

$server_idintThe server the service sat on.

$module_queue_idintThe queue id of the termination job.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:service.terminated', 10,
    function ($target_id, $service_type, $module, $server_id, $module_queue_id) {
        // Capacity came free on that server: update your own counter.
        Capacity::released((int) $server_id, $service_type);
    });
```

### Stopping a service delete

gateservice.delete

`Services::delete()` the record goes

Runs before the service record is deleted. Deleting is **not the same** as closing the account on the server: it removes our record alone.

Parameters 1

$servicearrayThe service record about to be deleted.

Return 1

stringA non-empty string **stops** the delete; the text is thrown as the error.

Listener PHP

```php
Hook::add('gate:service.delete', 10, function ($service) {
    // Deleting a live service leaves an orphaned account on the server.
    if (($service['status'] ?? '') === 'active')
        return 'A live service cannot be deleted; terminate it first.';

    return null;
});
```

### Following a delete

actionservice.deleted

`Services::delete()` a last snapshot

Runs after the service record was deleted.

Parameters 2

$idintThe id of the deleted service.

$servicearrayA snapshot from **before** the delete. Take what you need from here; the record is gone.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:service.deleted', 10, function ($id, $service) {
    Acme::forgetService((int) $id, $service['name'] ?? '');
});
```

## Pitfalls

> **A suspend veto becomes a cancel**
> 
> Returning a non-empty text at the suspend gate does not stop the work, it **turns it into a cancel**. A rule written as "do not suspend this one" can end with the service **cancelled outright**. Write here knowing what follows.

> **A queue id of zero means nothing happened on the server**
> 
> The suspend and terminate hooks hand you a **queue id**. Zero means the module job was **never created**: the service has no module, or the server is unknown. Our record changed and nothing happened on the server.

> **The final status may not be the one asked for**
> 
> The status event hands you the value **after any override by the module**. The target you saw at the gate and the value in the event can **differ**. Reacting to a transition, read the value from the event rather than the intent at the gate.

> **Deleting is not closing**
> 
> Deleting a service removes **our record** and nothing else. The account on the server stays where it is, now visible from nowhere. Where the server side should close too, terminate first and delete after.

## Related Articles

- [Service Lifecycle Hooks](https://dev.wisecp.com/en/service-lifecycle-hooks)
- Order Flow Hooks
- Scheduled Task Hooks
