# Customer Service and Message Hooks

https://dev.wisecp.com/es/client-service-and-message-hooks

The thirteen hooks over the service detail and bulk messaging: the module panel, add-ons, package changes, the usage chart and sending.

## Overview

What a customer sees on the service detail, and bulk message sending, live here.

The service filters share a pattern around **visibility flags**: hiding a section and emptying it give different outcomes. On the message side everything is counted in **parts**; the recipient count does not give you the cost.

## Reference

### Changing the module panel

filterclient.service_management_content

`ClientServices` raw markup

Runs before the panel produced by the server module is shown to the customer. It is the only place to touch the output of the remote panel.

Parameters 2

$contentstringby linkThe **raw markup** the module produced. ? That output can carry values from the remote server and is shown **unescaped**. If you add a value from outside, clean it yourself.

$ctxarrayContext: the service record and the panel page requested.

Return 1

voidThe return is ignored; you write over the data.

Listener PHP

```php
Hook::add('filter:client.service_management_content', 10, function (&$content, $ctx) {
    // The output is shown UNESCAPED.
    if (($ctx['page'] ?? '') !== 'dashboard') return;

    $content .= '<div class="alert alert-info">Acme</div>';
});
```

### Changing the add-on lists

filterclient.service_detail.addons

`ClientServices` two lists

Runs once the add-on lists on the service detail are prepared. **Two separate lists** arrive: the ones owned and the ones on offer.

Parameters 3

$addonsOwnedarrayby linkThe add-ons the service holds, active and pending together.

$addonsAvailablearrayby linkThe add-on offers that can be bought. Removing one from the offers also blocks the purchase: the customer never sees it.

$ctxarrayContext: the service record and the account id.

Return 1

voidThe return is ignored; you write over the data.

Listener PHP

```php
Hook::add('filter:client.service_detail.addons', 10,
    function (&$addonsOwned, &$addonsAvailable, $ctx) {
        // Removing an offer also blocks the purchase.
        $addonsAvailable = array_values(array_filter($addonsAvailable,
            fn ($a) => Acme::offerAllowed($a, $ctx['service'] ?? [])));
    });
```

### Changing the package change catalogue

filterclient.service_detail.upgrade_plans

`ClientServices` a visibility flag

Runs once the package upgrade and downgrade options are prepared.

Parameters 2

$updownarrayby linkThe plan catalogue and its state flags: visibility, the plan list and prices. Closing the visibility flag hides the section entirely; emptying the plan list shows an empty section. The two are different outcomes.

$ctxarrayContext: the service record.

Return 1

voidThe return is ignored; you write over the data.

Listener PHP

```php
Hook::add('filter:client.service_detail.upgrade_plans', 10, function (&$updown, $ctx) {
    // Hiding and emptying are different outcomes.
    if (Acme::locked($ctx['service'] ?? [])) $updown['visible'] = false;
});
```

### Changing the licence transfer section

filterclient.service_detail.license_transfer

`ClientServices` the fee note included

Runs once the transfer section of a software licence is prepared.

Parameters 2

$ltarrayby linkThe transfer data: visibility, mode, whether a transfer is pending, who pays the fee and the fee note.

$ctxarrayContext: the service record and its type.

Return 1

voidThe return is ignored; you write over the data.

Listener PHP

```php
Hook::add('filter:client.service_detail.license_transfer', 10, function (&$lt, $ctx) {
    // Do not show the section at all for a licence that cannot move.
    if (Acme::nonTransferable($ctx['service'] ?? [])) $lt['visible'] = false;
});
```

### Changing the service timeline

filterclient.service_detail.activity

`ClientServices` the timeline

Runs before the history rows of a service reach the screen. The place to put your own events among the core ones.

Parameters 2

$activityarrayby linkThe timeline rows.

$ctxarrayContext: the service id.

Return 1

voidThe return is ignored; you write over the data.

Listener PHP

```php
Hook::add('filter:client.service_detail.activity', 10, function (&$activity, $ctx) {
    foreach (Acme::events((int) ($ctx['service_id'] ?? 0)) as $e) $activity[] = $e;
});
```

### Changing the usage chart

filterclient.service_metric_chart

`ClientServices` null marks a gap

Runs once the data of the usage chart is prepared.

Parameters 2

$chartarrayby linkThe chart data: day labels and per-day values. ? A day with no data arrives as **empty**, not zero: turning those into zeros draws a drop that never happened.

$ctxarrayContext: the service, the metric key and the month being charted.

Return 1

voidThe return is ignored; you write over the data.

Listener PHP

```php
Hook::add('filter:client.service_metric_chart', 10, function (&$chart, $ctx) {
    // A day with no data arrives EMPTY: do not turn it into a zero.
    $chart['acme_limit'] = Acme::planLimit($ctx['metric'] ?? '');
});
```

### Stopping a message being sent

gateclient.sms_send

`ClientSms` before sending

Runs before a customer sends messages in bulk. The cost is worked out but **not yet charged**.

Parameters 4

$uidintThe account sending.

$originstringThe sender identity.

$quotearrayThe send summary: recipient count, total and countries.

$sendCtxarrayExtra context: the list, the group and the currency.

Return 1

string|null**A non-empty text blocks the operation** and is shown to the customer as the error. An empty return lets it carry on.

Listener PHP

```php
Hook::add('gate:client.sms_send', 10, function ($uid, $origin, $quote, $sendCtx) {
    // The cost is worked out but NOT yet charged.
    if ((int) ($quote['recipients'] ?? 0) > Acme::dailyCap($uid))
        return 'This exceeds your daily sending limit.';

    return null;
});
```

### Following messages being sent

actionclient.sms_sent

`ClientSms` what was actually billed

Runs after messages go out in bulk.

Parameters 4

$uidintThe account sending.

$originstringThe sender identity.

$quotearrayThe summary of the send. This is **what was actually billed**: recipient count, part count, length and encoding. A long message takes more parts than recipients, and the charge follows the parts.

$sentCtxarrayContext: the currency, the sender record, the module and the batch reference.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:client.sms_sent', 10, function ($uid, $origin, $quote, $sentCtx) {
    // The charge follows the PARTS, not the recipient count.
    Acme::recordUsage($uid, (int) ($quote['total_parts'] ?? 0));
});
```

### Stopping a sender identity being added

gateclient.sms_sender_add

`ClientSms` format already checked

Runs before a customer adds a new sender identity.

Parameters 2

$uidintThe account asking.

$namestringThe sender name requested. Its format is already checked: at most eleven letters or digits, no spaces. Your check belongs on the **content**, not the format.

Return 1

string|null**A non-empty text blocks the operation** and is shown to the customer as the error. An empty return lets it carry on.

Listener PHP

```php
Hook::add('gate:client.sms_sender_add', 10, function ($uid, $name) {
    // The format is already checked: look at the content.
    if (Acme::reservedBrand($name)) return 'This name cannot be used.';

    return null;
});
```

### Following a sender identity being added

actionclient.sms_sender_created

`ClientSms` added, not approved

Runs after a new sender identity is added. **Being added does not mean it can be used**: some countries need a separate application.

Parameters 2

$uidintThe account that added it.

$originarrayA summary of the record created: its id and name.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:client.sms_sender_created', 10, function ($uid, $origin) {
    // Being added does not mean it can be used.
    Acme::noteSender($uid, $origin['name'] ?? '');
});
```

### Following a sender application

actionclient.sms_sender_requested

`ClientSms` a country list

Runs after an application is made to pre-register a sender identity.

Parameters 3

$uidintThe account applying.

$originarrayA summary of the sender identity.

$codesarrayThe country codes applied for. A first application can cover several countries; a resubmission is usually one. It always arrives as a **list**.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:client.sms_sender_requested', 10, function ($uid, $origin, $codes) {
    // It always arrives as a list.
    foreach ($codes as $iso) Acme::trackApplication($uid, $origin['name'] ?? '', $iso);
});
```

### Following contacts being imported

actionclient.sms_contacts_imported

`ClientSms` skipped counted separately

Runs after a customer imports a contact list.

Parameters 4

$uidintThe account importing.

$importedintHow many contacts were actually written; **always at least one**.

$skippedintHow many rows were skipped for an invalid name or number. The header row skipped automatically is **not** in this number: do not conflate the two when reporting.

$group_idintThe target group; a **zero** means the contacts were saved without one.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:client.sms_contacts_imported', 10,
    function ($uid, $imported, $skipped, $group_id) {
        // The header row is not counted among the skipped.
        if ($skipped > 0) Acme::warnImportQuality($uid, $imported, $skipped);
    });
```

### Changing the country price table

filterclient.sms_rate

`ClientSms` per part

Runs once the country prices shown to the customer are prepared.

Parameters 2

$outarrayby linkCountry against price, with the fee and whether pre-registration is needed. ? The fee is **per part**, not per message: a long message splits into several parts and is charged for each.

$ctxarrayContext: the currency the prices were resolved in.

Return 1

voidThe return is ignored; you write over the data.

Listener PHP

```php
Hook::add('filter:client.sms_rate', 10, function (&$out, $ctx) {
    // The fee is PER PART.
    foreach ($out as $iso => $row)
        $out[$iso]['rate'] = Acme::applyMargin((float) ($row['rate'] ?? 0));
});
```

## Pitfalls

> **An empty day on the chart is not a zero**
> 
> A day with no data collected arrives **empty** in the usage chart. Turning it into a zero draws a drop that never happened and shows the customer a false picture of their usage. Leave the gap as a gap.

> **Message cost is counted per part**
> 
> A long message splits into several parts and **each part is charged**. A calculation treating the recipient count as the cost falls far short on long messages. Use the part count from the send summary.

## Related Articles

- [Service Hooks](https://dev.wisecp.com/en/service-lifecycle-hooks)
- [Customer Panel Data Hooks](https://dev.wisecp.com/en/client-panel-data-hooks)
- [Customer Site Hooks](https://dev.wisecp.com/en/hooks-on-the-customer-site)
