Domain Lifecycle End Hooks

1 views Markdown

The eight hooks along an unpaid domain's road to the end: the grace period, redemption, the drop, and importing.

Overview

A domain past its date does not vanish in one step. First comes the grace period, where the name still renews at the normal price. Then redemption: the name stops working and getting it back is far more expensive. Finally the drop, where the name opens to everyone.

Each of the three has its own hook and all of them run from a scheduled task. Nobody presses a button and no one is watching a screen.

Reference

Catching the start of the grace period

actiondomain.grace_started
cronjobs/DomainLifecycle a scheduled task

Runs the moment the domain entered its grace period; the name still works and renews at the normal price.

Parameters 2
$service_idintThe id of the domain service.
$statearrayThe lifecycle state: stage, days_past_due, days_in_stage. This stage also carries grace_days, grace_fee and grace_fee_active.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:domain.grace_started', 10, function ($service_id, $state) {
    // Reaching the customer at this stage is the cheapest way back.
    Crm::nudge($service_id, 'domain-grace', (int) ($state['grace_days'] ?? 0));
});

Catching the start of redemption

actiondomain.redemption_started
cronjobs/DomainLifecycle the name stopped working

Runs where the grace period ended. The name has stopped working here and a redemption fee applies.

Parameters 2
$service_idintThe id of the domain service.
$statearrayThe lifecycle state: stage, days_past_due, days_in_stage. This stage carries redemption_days, redemption_fee, amount_cid and can_restore.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:domain.redemption_started', 10, function ($service_id, $state) {
    // The fee and its currency arrive in the state; do not work them out.
    if (!empty($state['can_restore']))
        Crm::urgent($service_id, (float) ($state['redemption_fee'] ?? 0),
            (int) ($state['amount_cid'] ?? 0));
});

Stopping the drop request

gatedomain.delete_request
cronjobs/DomainPurge in a cron context

Runs before the cancellation request reaches the provider. It sits inside a scheduled task, so stopping it means nobody sees an error.

Parameters 3
$servicearrayThe domain service about to be dropped.
$statearrayThe lifecycle state: stage, days_past_due, days_in_stage.
$modulestringThe provider module's name. It arrives empty where no module is assigned.
Return 1
stringA non-empty string stops the drop. The task tries again next round, so keep your own mark for a lasting hold.
Listener
Hook::add('gate:domain.delete_request', 10, function ($service, $state, $module) {
    // Hold the drop a round where the customer promised to pay.
    if (Acme::promiseToPay((int) ($service['id'] ?? 0)))
        return 'A payment was promised; the drop was held.';

    return null;
});

Following the drop request

actiondomain.delete_requested
cronjobs/DomainPurge it reached the provider

Runs after the cancellation request was sent to the provider.

Parameters 3
$servicearrayThe domain service being dropped.
$statearrayThe lifecycle state: stage, days_past_due, days_in_stage.
$modulestringThe provider module's name; it can be empty.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:domain.delete_requested', 10,
    function ($service, $state, $module) {
        Audit::note('domain-drop', $service['name'] ?? '', $module ?: 'no module');
    });

Catching the finished drop

actiondomain.purged
cronjobs/DomainPurge no way back

Runs after the service was cancelled and the domain dropped. This is the last step.

Parameters 3
$service_idintThe id of the domain service.
$statearrayThe lifecycle state: stage, days_past_due, days_in_stage. At this point stage is the drop stage.
$modulestringThe provider module's name; it can be empty.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:domain.purged', 10, function ($service_id, $state, $module) {
    // The name is no longer ours: clear what hangs off it.
    DnsMonitor::forget($service_id);
});

Following an expiry notice

actiondomain.expired_notice_sent
cronjobs/DomainExpired once per milestone

Runs after a notice was sent for a domain past its date.

Parameters 2
$service_idintThe id of the domain service.
$delayed_dayintThe days past the date. It is the notice milestone: the hook runs more than once for one name, with different day values.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:domain.expired_notice_sent', 10,
    function ($service_id, $delayed_day) {
        // It runs per milestone: day 1, day 7 and day 15 arrive separately.
        if ($delayed_day >= 15) Ops::escalate('domain-expiry', $service_id);
    });

Following an import

actiondomain.imported
Imports a bulk job

Runs after domains were imported from a provider. The hook runs once for the whole batch, not per name.

Parameters 2
$importedarrayThe imported service labels: strings shaped like "example.com (#123)". Labels arrive, not service records, so the id wants picking out of the text.
$module_namestringThe provider module the import ran against.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:domain.imported', 10, function ($imported, $module_name) {
    // Runs ONCE for the whole batch, not per name.
    Ops::note('domain-import', $module_name . ': ' . count($imported));
});

Changing the grace and redemption fee

filterdomain.grace_redemption_save
AdminProductsDomain passed by link

Runs before a grace or redemption fee is saved. This is how you hold one fee policy in one place instead of typing it extension by extension.

Parameters 3
$set_dataarrayby linkThe price record about to be written: amount, cid (currency), owner_id, status and the rest.
$typestringgrace or redemption.
$idintThe extension id. A zero means the record is not for one extension but the default for all of them.
Return 1
voidThe return is ignored; you write over the record. The hook fires once per currency and per type, so expect several calls for a single save.
Listener
Hook::add('filter:domain.grace_redemption_save', 10, function (&$set_data, $type, $id) {
    // Make the grace period free on every extension.
    if ($type === 'grace') $set_data['amount'] = 0;
});

Pitfalls

No screen is watching these hooks

Every hook here runs from a scheduled task. Stopping a gate means nobody sees an error: not the customer, not the operator. Record your decision on your own side, or no one can tell why the domain never dropped.

Stopping is not permanent

The task tries again next round. Thinking "I stopped it once" at the gate is wrong; your condition is asked afresh every round. For a lasting hold, keep your own mark and read it at the gate.

The notice hook repeats for one name

Expiry notices go out milestone by milestone and the hook runs again at each. A listener reacting without reading the day value produces a run of alerts for a single name.

The import hands you labels, not records

The import hook hands you strings shaped like "example.com (#123)". Expecting service records is wrong; where you need an id, pick it out of the text and read the record yourself.

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.