# Invoice Amount Hooks

https://dev.wisecp.com/es/invoice-amount-hooks

The eight hooks touching the numbers on an invoice: the items, the totals, the late fee, the renewal price and formalising.

## Overview

These hooks decide **what the customer pays**. They all work by reference and the value you write goes straight onto the invoice; a mistake in arithmetic here **becomes money**.

The last two differ: formalising turns an invoice into a **legal record**. After that the amount cannot change, the invoice cannot be deleted and its number is fixed.

## Reference

### Changing the invoice totals

filterinvoice.totals

`Invoices` before they are saved

Runs after the totals were worked out, before they are written to the invoice.

Parameters 3

$totalsarrayrefThe totals about to be written: `subtotal`, `tax`, `additional_tax`, `pmethod_commission`, `total`, `discounts`. Changing one alone leaves the figures **out of step**; touching the subtotal means fixing the grand total too.

$invoicearrayThe invoice row: the tax rate, whether it is formalised, the currency, the commission rate. Context you cannot change.

$itemsarrayThe items used in the arithmetic. Context you cannot change.

Return 1

voidThe value changes **by reference**; the return is not read.

Listener PHP

```php
Hook::add('filter:invoice.totals', 10, function (&$totals, $invoice, $items) {
    // Having changed one figure, go over the GRAND TOTAL as well.
    if (Acme::roundUp($invoice)) {
        $totals['total'] = ceil((float) $totals['total']);
    }
});
```

### Changing the invoice discounts

filterinvoice.items

`AdminInvoices` the discount payload

Runs while an invoice is edited, before the discounts are saved.

Parameters 4

$idintThe id of the invoice being edited.

$invDiscountsarrayrefThe discounts about to be saved, including the per-item custom ones. Despite the name this is the **discount structure**, not a list of items.

$pendingCustomDiscountsarrayThe custom discounts keyed by item id.

$invoicearrayThe current invoice row.

Return 1

voidThe value changes **by reference**; the return is not read.

Listener PHP

```php
Hook::add('filter:invoice.items', 10,
    function ($id, &$invDiscounts, $pendingCustomDiscounts, $invoice) {
        // The name misleads: this is the discount structure, not the items.
        Acme::capDiscounts($invDiscounts, (float) ($invoice['subtotal'] ?? 0));
    });
```

### Changing the late fee

filterinvoice.late_fee_amount

`cronjobs/InvoiceLateFee` rounded afterwards

Runs after the late fee was worked out, before it is added to the invoice as an item.

Parameters 3

$feefloatrefThe raw late fee in the invoice's currency. It is **rounded again** after the filter, so no need to fuss over the last decimal here.

$invoicearrayThe invoice row: the subtotal, currency and owner.

$cyclestringThe fee cycle: once, or daily. On a daily cycle this hook runs **every day**.

Return 1

voidThe value changes **by reference**; the return is not read.

Listener PHP

```php
Hook::add('filter:invoice.late_fee_amount', 10, function (&$fee, $invoice, $cycle) {
    // On a daily cycle this runs every day: hold it under a ceiling.
    $cap = (float) ($invoice['subtotal'] ?? 0) * 0.2;
    if ($fee > $cap) $fee = $cap;
});
```

### Following a late fee added

actioninvoice.late_fee_applied

`cronjobs/InvoiceLateFee` an item was created

Runs after the late fee was added to the invoice as an item.

Parameters 4

$invoice_idintThe invoice the fee went on.

$fee_amountfloatThe amount added — the **final** value after the filter.

$fee_typestring`percentage` or `fixed`.

$item_idintThe id of the created invoice item.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:invoice.late_fee_applied', 10,
    function ($invoice_id, $fee_amount, $fee_type, $item_id) {
        // Tell the customer the invoice grew; a silent rise becomes a complaint.
        Notify::lateFee($invoice_id, (float) $fee_amount);
    });
```

### Changing the renewal price

filterinvoice.renewal_amount

`Invoices` the unit price

Runs after a service's renewal price was resolved.

Parameters 1

$resultarrayrefThe price result: `amount` (the **unit** price), `quantity`, `currency`, `taxexempt`, `additional_taxes`, `discounts`, `pricing_source`, `period_time`. The amount is **per unit**; the total is multiplied afterwards.

Return 1

voidThe value changes **by reference**; the return is not read.

Listener PHP

```php
Hook::add('filter:invoice.renewal_amount', 10, function (&$result) {
    // amount is the UNIT price: the core multiplies by quantity.
    if (($result['pricing_source'] ?? '') === 'locked') return;

    $result['amount'] = Acme::loyaltyPrice((float) $result['amount']);
});
```

### Changing the renewal description

filterinvoice.renewal_description

`Invoices` it shows on the invoice

Runs after the renewal item's description was built.

Parameters 1

$descriptionstringrefThe item description. The text the customer reads on the invoice, so write it in their language.

Return 1

voidThe value changes **by reference**; the return is not read.

Listener PHP

```php
Hook::add('filter:invoice.renewal_description', 10, function (&$description) {
    $description .= ' — ' . Acme::periodNote();
});
```

### Stopping an invoice being formalised

gateinvoice.formalize

`AdminInvoices` no way back

Runs before an invoice is formalised. After this step it can be **neither changed nor deleted**.

Parameters 2

$invoicearrayThe invoice about to be formalised.

$user_idintThe id of the person acting.

Return 1

stringA non-empty string **stops** the formalising; the text is thrown as the error.

Listener PHP

```php
Hook::add('gate:invoice.formalize', 10, function ($invoice, $user_id) {
    // Formalising cannot be undone: not with tax details missing.
    if (!Acme::taxDetailsComplete($invoice))
        return 'Complete the tax details before formalising.';

    return null;
});
```

### Following a formalised invoice

actioninvoice.formalized

`AdminInvoices` read afresh

Runs after the invoice was formalised.

Parameters 2

$invoicearrayThe invoice **read afresh** with its formalised mark, including the document file where one was produced.

$user_idintThe id of the person who acted.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:invoice.formalized', 10, function ($invoice, $user_id) {
    // A legal record now: send it to the books here, not earlier.
    Accounting::submit($invoice);
});
```

### Changing the list summary cards

filterinvoice.list_stats

`admin/invoices` passed by link

Runs after the summary cards above the invoice list are worked out. What you change reaches both the cards and the visual hook that adds to that area.

Parameters 2

$initial_statsarrayby linkThe card data: unpaid, paid and overdue, each with a formatted total and a count.

$stats_cardsarrayThe card setup: type and period.

Return 1

voidThe return is ignored; you write over the array.

Listener PHP

```php
Hook::add('filter:invoice.list_stats', 10, function (&$initial_stats, $stats_cards) {
    // Keep reseller invoices out of the summary.
    $initial_stats['unpaid']['formatted'] = Acme::excludeResellers($initial_stats['unpaid']);
});
```

### Changing the invoice document font

filterinvoice.pdf_font

`Invoices::create_pdf` through the object

Runs while the invoice document is built. The default font cannot draw letters outside the Latin alphabet, so this is where you supply the right one.

Parameters 2

$pdfobjectThe document builder. Being an object it changes without a by-link mark: calling a method on it is enough.

$invoicearrayThe invoice record. It carries the customer’s stored language; it is a copy, so changing it has no effect.

Return 1

voidThe return is ignored. You make the change by calling a method on the object.

Listener PHP

```php
Hook::add('filter:invoice.pdf_font', 10, function ($pdf, $invoice) {
    // The default font cannot draw letters outside the Latin alphabet.
    if (($invoice['user_data']['lang'] ?? '') === 'ru') $pdf->setDefaultFont('dejavusans');
});
```

### Adding a payment method logo

registerinvoice.module_logos

`templates/admin` returns one URL

Runs while the payment method badges are drawn on the invoice screen. Add the logo of your own method here.

Parameters 0

—It takes no parameters.

Return 1

string|nullYou return **a single address**, not markup: the template places it in an image. Return `null` when you have no logo to show. Empty text, zero and false are filtered out anyway.

Listener PHP

```php
Hook::add('register:invoice.module_logos', 10, function () {
    // Return a single address, not markup.
    return Acme::assetUrl('acme-pay.svg');
});
```

### Following a customer applying a coupon

actioninvoice.coupon_applied_by_client

`ClientInvoices` applied by the customer

Runs after a customer applies a coupon to their own invoice. The discount is already worked in.

Parameters 4

$uidintThe customer who applied it.

$idintThe invoice id. The invoice itself is left out on purpose: by the time the hook runs the totals have moved, so a copy would be stale. Read it fresh if you need it.

$couponarrayThe coupon record. Its amount field may have shifted during the application.

$couponCtxarrayWhat the application produced: the discount, the currency and the lines it touched.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:invoice.coupon_applied_by_client', 10,
    function ($uid, $id, $coupon, $couponCtx) {
        // The invoice row is left out on purpose: read it fresh if you need it.
        Acme::trackDiscount($uid, $coupon['code'] ?? '', (float) ($couponCtx['discount'] ?? 0));
    });
```

## Pitfalls

> **Changing one figure does not fix the rest**
> 
> Changing the subtotal on the totals filter and leaving the grand total as it was makes the invoice **disagree with itself**: the customer reads one figure and the payment asks for another. Touching one field means going over **the ones tied to it**.

> **The discount filter is mistaken for an item list**
> 
> The name suggests items, and yet this filter hands you the **discount structure**. A listener expecting a list of items finds an array it does not know and quietly writes in the wrong place. Check what the second parameter is **from its entry**.

> **A daily late fee runs every day**
> 
> Where the cycle is daily, the filter and the event run **again each day**. A rule with no ceiling turns an unpaid invoice **unpayable** within weeks. Sending a notification, mind that it does not go out daily either.

> **Formalising is a point of no return**
> 
> A formalised invoice **cannot be deleted and its amount cannot change**. Sending to the books, reserving a number and archiving belong on the **formalised event**; work written earlier treats an invoice that is later cancelled as a legal record.

## Related Articles

- [Invoice Lifecycle Hooks](https://dev.wisecp.com/en/invoice-lifecycle-hooks)
- [Payment Hooks](https://dev.wisecp.com/en/payment-hooks)
- [Service Renewal Hooks](https://dev.wisecp.com/en/service-renewal-hooks)
