How to create a notification by yourself - LinkNacional/WHMCS-WhatsApp-API-Notifications-open-source GitHub Wiki

This documentation matches the version v4.0+ of the module.

For v3, tutorial, access here.

Summary

What is this tutorial about?

This tutorial will help you to create your own notifications.

To make your own notification you need to have programming knowledge and follow the module standards.

Creating your own notification

  • Custom notification must be placed in this path: /modules/addons/lknhooknotification/src/Notifications/Custom/
  • Custom notification must have unique codes. Example: HelloWorldInvoiceCreated, NewServiceInvoice

Basic example:

Path: /modules/addons/lknhooknotification/src/Notifications/Custom/HelloWorldInvoiceCreatedNotification.php

<?php

namespace Lkn\HookNotification\Notifications\Custom;

use Lkn\HookNotification\Core\NotificationReport\Domain\NotificationReportCategory;
use Lkn\HookNotification\Core\Notification\Domain\AbstractNotification;
use Lkn\HookNotification\Core\Notification\Domain\NotificationParameter;
use Lkn\HookNotification\Core\Notification\Domain\NotificationParameterCollection;
use Lkn\HookNotification\Core\Shared\Infrastructure\Hooks;

/**
 * @see https://developers.whmcs.com/hooks-reference/invoices-and-quotes/#invoicecreated
 */
final class HelloWorldInvoiceCreatedNotification extends AbstractNotification
{
    public function __construct()
    {
        parent::__construct(
            'HelloWorldInvoiceCreated',
            NotificationReportCategory::INVOICE,
            Hooks::INVOICE_CREATED,
            new NotificationParameterCollection([
                new NotificationParameter(
                    'invoice_id',
                    lkn_hn_lang('Invoice ID'),
                    fn (): int => $this->whmcsHookParams['invoiceid']
                ),
            ]),
            fn() => getClientIdByInvoiceId($this->whmcsHookParams['invoiceid']),
            fn() => $this->whmcsHookParams['invoiceid'],
        );
    }
}

Then, the module will load this class in the Notifications page:

Screenshot 2025-04-30 at 2 42 23 PM

When, adding a new template, you will be able to use the parameters defined inside the class:

Screenshot 2025-04-30 at 2 42 58 PM

Note that $this->whmcsHookParams is automatically filled by the module and is the content that WHMCS passes to the hook as parameters when the notification is fired.

For this example, the hook used is Hooks::INVOICE_CREATE hook, which has the following parameters:

Screenshot 2025-04-30 at 2 46 38 PM

Source: WHMCS Hooks Reference

Adding conditional logic to decide if the notification should be run

In this example, we can see that the notification NewServiceInvoice still use the Hooks::INVOICE_CREATED, but has implemented the method shouldRun(): bool.

In this method, you have access to the WHMCS hook params $this->whmcsHookParams and can write custom business rules to decided if the notification should be sent or not.

In this case, we will fire the notification only if it is associated to a client service.

<?php

/**
 * Code: InvoiceReminderPdf
 */

namespace Lkn\HookNotification\Notifications;

use Lkn\HookNotification\Core\Notification\Domain\AbstractNotification;
use Lkn\HookNotification\Core\Notification\Domain\NotificationParameter;
use Lkn\HookNotification\Core\Notification\Domain\NotificationParameterCollection;
use Lkn\HookNotification\Core\NotificationReport\Domain\NotificationReportCategory;
use Lkn\HookNotification\Core\Shared\Infrastructure\Hooks;

final class NewServiceInvoiceNotification extends AbstractNotification
{
    public function __construct()
    {
        parent::__construct(
            'NewServiceInvoice',
            NotificationReportCategory::INVOICE,
            Hooks::INVOICE_CREATED,
            new NotificationParameterCollection([
                new NotificationParameter(
                    'invoice_id',
                    lkn_hn_lang('Invoice ID'),
                    fn (): int => $this->whmcsHookParams['invoiceid']
                ),
                new NotificationParameter(
                    'invoice_items',
                    lkn_hn_lang('Invoice items'),
                    fn (): string => getItemsRelatedToInvoice($this->whmcsHookParams['invoiceid'])
                ),
                new NotificationParameter(
                    'invoice_due_date',
                    lkn_hn_lang('Invoice due date'),
                    fn (): string => getInvoiceDueDateByInvoiceId($this->whmcsHookParams['invoiceid'])
                ),
                new NotificationParameter(
                    'invoice_pdf_url',
                    lkn_hn_lang('Invoice PDF URL'),
                    fn (): string => getInvoicePdfUrlByInvocieId($this->whmcsHookParams['invoiceid'])
                ),
                new NotificationParameter(
                    'invoice_balance',
                    lkn_hn_lang('Invoice balance'),
                    fn (): string => getInvoiceBalance($this->whmcsHookParams['invoiceid'])
                ),
                new NotificationParameter(
                    'invoice_total',
                    lkn_hn_lang('Invoice total'),
                    fn (): string => getInvoiceTotal($this->whmcsHookParams['invoiceid'])
                ),
                new NotificationParameter(
                    'invoice_subtotal',
                    lkn_hn_lang('Invoice subtotal'),
                    fn (): string => getInvoiceSubtotal($this->whmcsHookParams['invoiceid'])
                ),
                new NotificationParameter(
                    'client_id',
                    lkn_hn_lang('Client ID'),
                    fn (): int => $this->client->id
                ),
                new NotificationParameter(
                    'client_email',
                    lkn_hn_lang('Client email'),
                    fn (): string => getClientEmailByClientId($this->client->id)
                ),
                new NotificationParameter(
                    'client_first_name',
                    lkn_hn_lang('Client first name'),
                    fn (): string => getClientFirstNameByClientId($this->client->id)
                ),
                new NotificationParameter(
                    'client_full_name',
                    lkn_hn_lang('Client full name'),
                    fn (): string => getClientFullNameByClientId($this->client->id)
                ),
            ]),
            fn() => getClientIdByInvoiceId($this->whmcsHookParams['invoiceid']),
            fn() => $this->whmcsHookParams['invoiceid']
        );
    }

    public function shouldRun(): bool
    {
        $invoiceId = $this->whmcsHookParams['invoiceid'];

        // Checks if the invoice was generated by the CRON.
        if (
            $this->whmcsHookParams['source'] !== 'autogen' ||
            $this->whmcsHookParams['user'] !== 'system'
        ) {
            return false;
        }

        // Invoices of orders does not count.
        if (is_int(getOrderIdByInvoiceId($invoiceId))) {
            return false;
        }

        $invoiceItems = getInvoiceItems($invoiceId);

        $invoiceItemRelatedToProduct = array_filter(
            $invoiceItems,
            function (array $item): bool {
                return !empty($item['product_id']);
            }
        );

        if (count($invoiceItemRelatedToProduct) > 0) {
            return true;
        }

        return false;
    }
}

The shouldRun(): bool method is optional to be implemented and must return true, meaning the notification should be sent, or false otherwise.

Cron-based notifications

Cron-based notifications are notifications that use one of the cron hooks:

Screenshot 2025-04-30 at 3 40 43 PM

Example:

<?php

namespace Lkn\HookNotification\Notifications\Custom;

use DateTime;
use Exception;
use Lkn\HookNotification\Core\NotificationReport\Domain\NotificationReportCategory;
use Lkn\HookNotification\Core\Notification\Domain\AbstractCronNotification;
use Lkn\HookNotification\Core\Notification\Domain\NotificationParameter;
use Lkn\HookNotification\Core\Notification\Domain\NotificationParameterCollection;
use Lkn\HookNotification\Core\Shared\Infrastructure\Hooks;
use WHMCS\Database\Capsule;

final class Invoice6DaysLateNotification extends AbstractCronNotification
{
    public function __construct()
    {
        $parameters = [
            new NotificationParameter(
                'invoice_id',
                lkn_hn_lang('invoice_id'),
                fn (): int => $this->whmcsHookParams['invoice_id'],
            ),
            new NotificationParameter(
                'invoice_balance',
                lkn_hn_lang('invoice_balance'),
                fn (): string => getInvoiceBalance($this->whmcsHookParams['invoice_id'])
            ),
            new NotificationParameter(
                'invoice_total',
                lkn_hn_lang('invoice_total'),
                fn (): string => getInvoiceTotal($this->whmcsHookParams['invoice_id'])
            ),
            new NotificationParameter(
                'invoice_subtotal',
                lkn_hn_lang('invoice_subtotal'),
                fn (): string => getInvoiceSubtotal($this->whmcsHookParams['invoice_id'])
            ),
            new NotificationParameter(
                'invoice_due_date',
                lkn_hn_lang('invoice_due_date'),
                fn (): string => getInvoiceDueDateByInvoiceId($this->whmcsHookParams['invoice_id'])
            ),
            new NotificationParameter(
                'invoice_items',
                lkn_hn_lang('invoice_items'),
                fn (): string => $this->getInvoiceIdAndFirstItsFirstItem(),
            ),
            new NotificationParameter(
                'invoice_id_and_first_item',
                lkn_hn_lang('invoice_id_and_first_item'),
                fn (): string => getInvoicePdfUrlByInvocieId($this->whmcsHookParams['invoice_id'])
            ),
            new NotificationParameter(
                'client_id',
                lkn_hn_lang('Client ID'),
                fn (): int => $this->client->id
            ),
            new NotificationParameter(
                'client_email',
                lkn_hn_lang('Client email'),
                fn (): string => getClientEmailByClientId($this->client->id)
            ),
            new NotificationParameter(
                'client_first_name',
                lkn_hn_lang('Client first name'),
                fn (): string => getClientFirstNameByClientId($this->client->id)
            ),
            new NotificationParameter(
                'client_full_name',
                lkn_hn_lang('Client full name'),
                fn (): string => getClientFullNameByClientId($this->client->id)
            ),
        ];

        $asaasTable = Capsule::schema()->hasTable('mod_cobrancaasaasmpay');

        if ($asaasTable) {
            $parameters['invoice_pdf_url_asaas_pay'] = new NotificationParameter(
                'invoice_pdf_url_asaas_pay',
                lkn_hn_lang('invoice_pdf_url_asaas_pay'),
                fn () => $this->getAsaasPayUrl()
            );
        }

        parent::__construct(
            'Invoice6DaysLate',
            NotificationReportCategory::INVOICE,
            Hooks::DAILY_CRON_JOB,
            new NotificationParameterCollection($parameters),
            fn() => $this->whmcsHookParams['client_id'],
            fn() => $this->whmcsHookParams['report_category_id'],
        );
    }

    public function getPayload(): array
    {
        $invoices = localAPI('GetInvoices', [
            'limitnum' => 1000,
            'status' => 'Overdue',
        ]);

        $payloads = [];

        foreach ($invoices['invoices']['invoice'] as $invoice) {
            $givenDateTime   = new DateTime($invoice['duedate']);
            $currentDateTime = new DateTime();
            $interval        = $currentDateTime->diff($givenDateTime);

            if (
                $interval->days !== 6
                || $invoice['paymentmethod'] === 'freeproducts'
                || $invoice['total'] === '0.00'
            ) {
                continue;
            }

            $invoiceId = $invoice['id'];
            $clientId  = $invoice['userid'];

            $payloads[] = [
                'client_id' => $clientId,
                'report_category_id' => $invoiceId,
                'invoice_id' => $invoiceId,
            ];
        }

        return $payloads;
    }

    private function getAsaasPayUrl()
    {
        $invoicePayMethod = Capsule::table('tblinvoices')->where('id', $this->whmcsHookParams['invoice_id'])->first('paymentmethod')->paymentmethod;

        if ($invoicePayMethod !== 'cobrancaasaasmpay') {
            throw new Exception('Invoice does not belong to cobrancaasaasmpay gateway.');
        }

        $asaasPayBoletoUrl = Capsule::table('mod_cobrancaasaasmpay')->where('fatura_id', $this->whmcsHookParams['invoice_id'])->first('url_boleto')->url_boleto;

        if (empty($asaasPayBoletoUrl)) {
            throw new Exception('Could not get Asaas URL.');
        }

        return str_replace('/b/pdf/', '/i/', $asaasPayBoletoUrl);
    }

    private function getInvoiceIdAndFirstItsFirstItem(): string
    {
        $invoiceId = $this->whmcsHookParams['invoice_id'];

        return "$invoiceId " . getInvoiceItemsDescriptionsByInvoiceId($invoiceId)[0];
    }
}

Cron notifications must inherit the abstract class AbstractCronNotification and implement the getPayload(): array method.

The Invoice6DaysLate notification above has implemented the method getPayload(): array that will return a list of WHMCS hook params. Each sublist in the list will be passed to the notification as if it were $this->whmcsHookParams. So you can use getPayload(): array to generate the parameters that will feed the notification.

Screenshot 2025-04-30 at 3 51 14 PM

Manual Notifications

Manual notifications are the ones that use output hooks:

Screenshot 2025-04-30 at 4 58 59 PM

These notifications are only fired manually by an admin.

These notifications must extend AbstractManualNotification.

<?php

namespace Lkn\HookNotification\Notifications;

use Lkn\HookNotification\Core\Notification\Domain\AbstractManualNotification;
use Lkn\HookNotification\Core\Notification\Domain\NotificationParameter;
use Lkn\HookNotification\Core\Notification\Domain\NotificationParameterCollection;
use Lkn\HookNotification\Core\NotificationReport\Domain\NotificationReportCategory;
use Lkn\HookNotification\Core\Shared\Infrastructure\Hooks;

final class InvoiceReminderNotification extends AbstractManualNotification
{
    public function __construct()
    {
        parent::__construct(
            'InvoiceReminder',
            NotificationReportCategory::INVOICE,
            Hooks::ADMIN_INVOICES_CONTROLS_OUTPUT,
            new NotificationParameterCollection([
                new NotificationParameter(
                    'invoice_id',
                    lkn_hn_lang('Invoice ID'),
                    fn (): int => $this->whmcsHookParams['invoiceid']
                ),
                new NotificationParameter(
                    'invoice_items',
                    lkn_hn_lang('Invoice items'),
                    fn (): string => getItemsRelatedToInvoice($this->whmcsHookParams['invoiceid'])
                ),
                new NotificationParameter(
                    'invoice_due_date',
                    lkn_hn_lang('Invoice due date'),
                    fn (): string => getInvoiceDueDateByInvoiceId($this->whmcsHookParams['invoiceid'])
                ),
                new NotificationParameter(
                    'invoice_pdf_url',
                    lkn_hn_lang('Invoice PDF URL'),
                    fn (): string => getInvoicePdfUrlByInvocieId($this->whmcsHookParams['invoiceid'])
                ),
                new NotificationParameter(
                    'invoice_balance',
                    lkn_hn_lang('Invoice balance'),
                    fn (): string => getInvoiceBalance($this->whmcsHookParams['invoiceid'])
                ),
                new NotificationParameter(
                    'invoice_total',
                    lkn_hn_lang('Invoice total'),
                    fn (): string => getInvoiceTotal($this->whmcsHookParams['invoiceid'])
                ),
                new NotificationParameter(
                    'invoice_subtotal',
                    lkn_hn_lang('Invoice subtotal'),
                    fn (): string => getInvoiceSubtotal($this->whmcsHookParams['invoiceid'])
                ),
                new NotificationParameter(
                    'client_id',
                    lkn_hn_lang('Client ID'),
                    fn (): int => $this->client->id
                ),
                new NotificationParameter(
                    'client_email',
                    lkn_hn_lang('Client email'),
                    fn (): string => getClientEmailByClientId($this->client->id)
                ),
                new NotificationParameter(
                    'client_first_name',
                    lkn_hn_lang('Client first name'),
                    fn (): string => getClientFirstNameByClientId($this->client->id)
                ),
                new NotificationParameter(
                    'client_full_name',
                    lkn_hn_lang('Client full name'),
                    fn (): string => getClientFullNameByClientId($this->client->id)
                ),
            ]),
            fn() => getClientIdByInvoiceId($this->whmcsHookParams['invoiceid']),
            fn() => $this->whmcsHookParams['invoiceid']
        );
    }
}

In the example above, the InvoiceReminder notification uses the output hook Hooks::ADMIN_INVOICES_CONTROLS_OUTPUT.

If enabled, the notification will be show at the admin invoice page:

Screenshot 2025-04-30 at 5 01 23 PM

Screenshot 2025-04-30 at 5 01 49 PM

All kinds of notifications can implement the method shouldRun(): bool.

Notification title and parameter translation

Custom translation must be placed at: lknhooknotification/src/Notifications/Custom/lang/

  • To translate a notification title, you should do:

Custom/lang/english.php

Screenshot 2025-05-26 at 9 10 42 AM

Custom/lang/portugues-br.php

Screenshot 2025-05-26 at 9 10 26 AM

  • To translate a parameter, you can call the lkn_hn_lang function:

Screenshot 2025-05-26 at 9 14 42 AM