Customer Service and Message Hooks

1 views Markdown

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
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
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
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
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
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
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|nullA non-empty text blocks the operation and is shown to the customer as the error. An empty return lets it carry on.
Listener
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
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|nullA non-empty text blocks the operation and is shown to the customer as the error. An empty return lets it carry on.
Listener
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
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
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
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
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.

Was this helpful?

Thanks for your feedback!

Still Need Help?

Our support team is here around the clock for anything you can't find above.