Licence Transfer Hooks

3 views Markdown

The seven hooks moving a software licence to another account and resetting an installation.

Overview

A licence transfer is a separate flow from a service transfer. It keeps its own record, charges its own fee, and moves along on e-mail confirmation from both sides.

Reissuing has nothing to do with transfers: the installation details a licence is tied to are cleared, so the customer can set the software up on another server.

Reference

Stopping a licence transfer

gateservice.license_transfer
LicenseTransfer the product config arrives

Runs before a licence transfer starts.

Parameters 2
$servicearrayThe service record about to move, with its options decoded.
$lt_configarrayThe product's licence transfer settings: the fee type, which side is invoiced, the limits. Read this when writing your own rule; the operator may have set each product differently.
Return 1
stringA non-empty string stops the transfer; the text is thrown as the error.
Listener
Hook::add('gate:service.license_transfer', 10, function ($service, $lt_config) {
    // A developer licence given free of charge does not move.
    if (!empty($service['options']['developer']))
        return 'A developer licence cannot move to another account.';

    return null;
});

Changing the transfer fee

filterservice.license_transfer_fee
Hook::runRefs by reference

Runs after the transfer fee was worked out, before the invoice is raised.

Parameters 3
$feearrayrefThe fee as worked out: type, amount, currency_id, basis, base_amount, base_currency_id. For the percentage type the base is the product's current one-time price (basis = product_price); it falls back to the service amount only when the product has no active one-time price. Zeroing the amount makes the transfer free.
$servicearrayThe service moving, with its amount and currency.
$lt_configarrayThe product's fee settings.
Return 1
voidThe value changes by reference; the return is not read.
Listener
Hook::add('filter:service.license_transfer_fee', 10,
    function (&$fee, $service, $lt_config) {
        // Charge nothing where both sides are the same company.
        if (Acme::sameCompany($service)) $fee['amount'] = 0.0;
    });

Learning that a transfer started

actionservice.license_transfer_started
LicenseTransfer confirmation is pending

Runs after the transfer record was created. The licence has not moved yet.

Parameters 4
$transfer_idintThe id of the created transfer record.
$service_idintThe id of the service moving.
$transferor_idintThe current owner.
$transferee_idintThe target owner.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:service.license_transfer_started', 10,
    function ($transfer_id, $service_id, $transferor_id, $transferee_id) {
        // The licence has NOT moved: it waits on both sides confirming.
        Audit::licenseTransfer('started', $transfer_id, $service_id);
    });

Following one side confirming

actionlicense.transfer.verified
LicenseTransfer it runs twice

Runs where one side confirmed by e-mail. It runs twice per transfer.

Parameters 2
$transferarrayThe transfer record as it stands after the confirmation.
$partystringThe side that confirmed: transferor (the sender) or transferee (the recipient). Read which side from here, since the hook runs twice.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:license.transfer.verified', 10, function ($transfer, $party) {
    // The hook runs TWICE: react to the recipient's confirmation alone.
    if ($party === 'transferee') Crm::licenseAccepted($transfer['id'] ?? 0);
});

Learning that a transfer completed

actionservice.license_transfer.completed
LicenseTransfer a freshly read record

Runs after the licence moved to its new owner.

Parameters 2
$transfer_idintThe id of the transfer record.
$transferarrayThe freshly read transfer row: its status is complete and the completion time is filled in. The cache was bypassed, so the values are current.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:service.license_transfer.completed', 10,
    function ($transfer_id, $transfer) {
        // The record was read afresh: status and time are current.
        Acme::licenseMoved((int) ($transfer['owner_id'] ?? 0), $transfer);
    });

Following a licence reissue

actionservice.license_reissued
ClientServices the installation was cleared

Runs after the licence's installation binding was cleared, so the customer can set the software up on another server.

Parameters 2
$servicearrayThe service record from before the reset. Its installation and address fields still hold the old values, which is how you learn the previous server.
$uidintAlways the service owner. Not the person who acted: a sub-user or a staff member may have triggered it.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:service.license_reissued', 10, function ($service, $uid) {
    // The old installation is STILL on the record: you see the previous one.
    Acme::forgetInstall($service['options']['install'] ?? '');
});

Following a licence update

actionservice.license_updated
AdminServices the licence fields were saved

Runs after an operator saved the service's Management tab and something about the licence changed. Anything holding that licence elsewhere is now stale. Bring your own record up to date here.

Watched fields: the key, the domain, the IP, the version and the licence parameters. Nothing fires when none of them moved. The save already happened, so nothing you return is read.

Parameters 3
$serviceIdintThe service that was saved.
$changesarrayOnly what moved, as field => ['old' => …, 'new' => …]. Keys: key, domain, ip, version, parameters.
$servicearrayThe service record from before the save. Read it afresh if you need the stored row.
Reading $changes['key'] 3
old emptycreationThe key was issued for the first time. Create your record rather than moving one.
both filledmoveFind your record by the old key and put it on the new one.
new emptyclearedThe operator emptied the field. The key was already handed out, so deleting the remote record is usually wrong.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:service.license_updated', 10,
    function ($serviceId, $changes, $service) {
        // The key did not move: something else did, so just republish.
        if (!isset($changes['key'])) return Acme::republish((int) $serviceId);

        $old = (string) $changes['key']['old'];
        $new = (string) $changes['key']['new'];

        if ($new === '') return;                                 // cleared, not deleted

        $old === ''
            ? Acme::register((int) $serviceId, $new)             // issued for the first time
            : Acme::rekey($old, $new);                           // moved onto a new key
    });

Following a licence report

actionservice.license_report_submitted
ClientServices a customer report

Runs where a customer submitted a report about their licence.

Parameters 2
$servicearrayThe service the report is about.
$uidintThe service owner's id.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:service.license_report_submitted', 10, function ($service, $uid) {
    Ops::queue('license-report', (int) ($service['id'] ?? 0));
});

Following a transfer ending

actionservice.license_transfer_cancelled
LicenseTransfer three endings

Runs when a licence transfer comes to an end. Three separate endings land here: cancelled, rejected and timed out.

Parameters 3
$transfer_idintThe id of the transfer that ended.
$statusstringWhich ending: cancelled, rejected or expired.
$reasonstringThe reason code, such as an admin cancelling or verification running out of time.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:service.license_transfer_cancelled', 10,
    function ($transfer_id, $status, $reason) {
        // Lift the transfer lock on the licence server.
        Acme::unlockTransfer($transfer_id);
    });

Following verification being sent again

actionservice.license_transfer.verification_resent
ClientLicenseTransfer to waiting parties

Runs when the transfer verification emails go out again. They reach only the parties who have not verified yet; nobody who confirmed is written to twice.

Parameters 3
$servicearrayThe record of the service being transferred. The transfer is not done: the service still sits with its current owner.
$activearrayThe live transfer record, with its id and status.
$pendingarrayThe list of parties the mail went to again.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:service.license_transfer.verification_resent', 10,
    function ($service, $active, $pending) {
        // Nudge over a second channel, only the parties still waiting.
        foreach ($pending as $party) Acme::remind($party, (int) ($active['id'] ?? 0));
    });

Stopping a software domain change

gateservice.software_domain_change
ClientServices before the write

Runs while a customer changes the domain their licence is bound to, after the core checks pass and before anything is written.

Parameters 4
$servicearrayThe service record as it was before the change.
$oldstringThe domain currently bound; empty when none was ever set.
$domainstringThe requested domain, already normalised: lower case, leading www dropped. This is not the raw form value; write your check against this.
$uidintThe account that owns the service.
Return 1
string|nullA non-empty text blocks the change and is shown to the customer. When blocked, neither the domain is written nor the change hook fires.
Listener
Hook::add('gate:service.software_domain_change', 10,
    function ($service, $old, $domain, $uid) {
        // Apply your own block list.
        if (Acme::blockedDomain($domain)) return 'This domain cannot hold a licence.';

        return null;
    });

Following a software domain change

actionservice.software_domain_changed
ClientServices after the write

Runs once the bound domain of a licence is saved. This is where you tell the licence server.

Parameters 4
$servicearrayThe service record.
$oldstringThe previous domain; empty on a first binding.
$domainstringThe newly bound domain.
$uidintThe owning account.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:service.software_domain_changed', 10,
    function ($service, $old, $domain, $uid) {
        Acme::rebindLicence((int) ($service['id'] ?? 0), $domain);
    });

Pitfalls

The confirmation hook runs twice per transfer

The sender and the recipient confirm separately, and the hook runs on both. A listener reacting without reading the side does its work twice. Check the party value in your first line.

On the reissue hook the installation is the OLD one

The hook hands you the service record from before the reset: the installation and address fields point at the old server. That is not a fault but your only chance: do whatever you need with the old installation here, because the value is about to go.

The owner and the person acting are not the same

The id on the reissue hook is always the service owner. A sub-user or a staff member may have triggered it, and that id never reaches this hook. "Who did this" cannot be answered from here.

A licence transfer is not a service transfer

The two flows look alike and use separate hooks: a licence transfer has a record of its own, a fee of its own and e-mail confirmation from both sides. A system listening to the service transfer hooks never sees a licence one.

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.