Skip to content
Devix Open Source

Guide

Laravel, ZATCA and the amount in words

composer require devix-labs/invoice-pdf
php artisan vendor:publish --tag=invoice-config

The service provider registers itself. config/invoice.php holds the defaults: language, font, accent, paper.

A controller

use Devix\Invoice\{Invoice, Line, Party};
use Devix\Invoice\Laravel\Facades\Invoice as Renderer;

public function download(Order $order)
{
    $invoice = new Invoice(
        number: $order->invoice_number,
        seller: new Party(
            config('company.name'),
            config('company.trn'),
            config('company.address'),
            nameAr: config('company.name_ar'),
        ),
        buyer: new Party($order->client->name, $order->client->trn, $order->client->address),
        issuedAt: $order->invoiced_at,
        currency: $order->currency,
        country: config('company.country'),
    );

    foreach ($order->items as $item) {
        $invoice->add(new Line(
            description: $item->name,
            unitPrice: $item->unit_price,      // a decimal column, not a float
            quantity: $item->quantity,
            taxBasisPoints: $item->vat_basis_points,
            descriptionAr: $item->name_ar,
        ));
    }

    abort_if(! $invoice->isCompliant(), 422, implode(', ', $invoice->problems()));

    return response($this->renderer->pdf($invoice), 200, [
        'Content-Type' => 'application/pdf',
        'Content-Disposition' => 'attachment; filename="'.$invoice->number.'.pdf"',
    ]);
}

Store amounts as decimal or as an integer of minor units. Not double, and not float. That is the same argument as the rest of this package, one layer down.

Catch a non-compliant invoice in a test, not in an audit

public function test_every_invoice_we_issue_would_pass(): void
{
    foreach (Order::invoiced()->cursor() as $order) {
        $this->assertSame([], $this->invoiceFor($order)->problems(), "order {$order->id}");
    }
}

That test is the reason problems() returns codes rather than a boolean.

The ZATCA QR, for Saudi Arabia

Devix Laravel ZATCA produces the payload; put it on the invoice as a data URI:

use Devix\Zatca\Laravel\Facades\Zatca;

$payload = Zatca::encode(
    seller: config('company.name_ar'),
    vatNumber: config('company.vat'),
    timestamp: $invoice->issuedAt,
    total: $invoice->format($invoice->grossMinor()),
    vat: $invoice->format($invoice->taxMinor()),
);

$renderer = new Renderer([
    'language' => 'ar',
    'qr' => 'data:image/png;base64,'.base64_encode($qrPng),
]);

A Saudi simplified tax invoice is not valid without it.

The amount in words

Every invoice in the region carries it, and getting Arabic grammar right is not something to attempt inline — the number agrees in gender with the noun, and eleven to ninety-nine take the accusative singular. Devix Laravel Number Words does it:

use Devix\NumberWords\Laravel\Facades\NumberWords;

new Renderer([
    'language' => 'ar',
    'amountInWords' => NumberWords::currency($invoice->format($invoice->grossMinor()), 'AED', 'ar'),
]);

Queueing it

PDF rendering is not something to do inside a request if the invoice is long:

class RenderInvoice implements ShouldQueue
{
    public function handle(Renderer $renderer): void
    {
        Storage::put("invoices/{$this->order->invoice_number}.pdf", $renderer->pdf($this->invoice()));
    }
}

Renderer is bound in the container with your config already applied, so type-hinting it is all that is needed.

Another engine

html() gives you the finished, shaped, self-contained document. Anything that turns HTML into a PDF takes it:

$html = $renderer->html($invoice);

Browsershot::html($html)->pdf();              // spatie/laravel-pdf
$mpdf->WriteHTML($html);                      // mPDF

With mPDF, turn its own shaping off for this HTML — autoScriptToLang and autoArabic would shape the already-shaped text a second time.

Updated 15 Sep 2026