85 lines
2.9 KiB
PHP
85 lines
2.9 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Billing;
|
|
|
|
use App\Models\Invoice;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Facades\View;
|
|
|
|
/**
|
|
* An invoice as a PDF, built only from what was frozen into the document.
|
|
*
|
|
* Nothing here reads today's settings. Every value — the issuer's address, its
|
|
* VAT number, the rate, the customer's address, the lines — comes from the
|
|
* snapshot taken when the number was assigned. That is what makes it safe to
|
|
* store no PDF at all: rendering the same invoice in 2033 produces the same
|
|
* page it produced on the day it was issued, whatever has changed in between.
|
|
*
|
|
* The one exception is the logo file, which is referenced by path rather than
|
|
* copied. A replaced logo therefore appears on old invoices too — recorded here
|
|
* as a known limitation rather than hidden: keeping every superseded logo
|
|
* forever is a real cost, and a changed mark on a reprint misleads nobody about
|
|
* what was charged.
|
|
*/
|
|
final class InvoiceRenderer
|
|
{
|
|
public function forInvoice(Invoice $invoice): string
|
|
{
|
|
return $this->render((array) $invoice->snapshot);
|
|
}
|
|
|
|
/** @param array<string, mixed> $snapshot */
|
|
public function render(array $snapshot): string
|
|
{
|
|
$issuer = (array) ($snapshot['issuer'] ?? []);
|
|
|
|
$pdf = new InvoiceDocument('P', 'mm', 'A4', true, 'UTF-8');
|
|
$pdf->setIssuer($issuer, $this->logoFile($issuer), __('invoice.page'));
|
|
|
|
$pdf->SetCreator('CluPilot');
|
|
$pdf->SetAuthor((string) ($issuer['name'] ?? ''));
|
|
$pdf->SetTitle((string) ($snapshot['meta']['number'] ?? ''));
|
|
|
|
// Room at the bottom for the four-column footer, which is fixed height
|
|
// and would otherwise be written over the last line of the table.
|
|
$pdf->SetMargins(20, 34, 20);
|
|
$pdf->SetAutoPageBreak(true, 34);
|
|
$pdf->SetFont('dejavusans', '', 9);
|
|
|
|
$pdf->AddPage();
|
|
|
|
// Rendered from a Blade template: a table of line items expressed as
|
|
// Cell() calls with millimetre coordinates is a table nobody will ever
|
|
// adjust, and this one has to be adjusted the first time somebody sees
|
|
// it on paper.
|
|
$pdf->writeHTML(
|
|
View::make('pdf.invoice', ['s' => $snapshot])->render(),
|
|
true, false, true, false, ''
|
|
);
|
|
|
|
return $pdf->Output('', 'S');
|
|
}
|
|
|
|
/**
|
|
* The logo on disk, or nothing.
|
|
*
|
|
* Nothing is a perfectly good answer: an invoice without a logo is still a
|
|
* valid invoice, and refusing to render one because an image is missing
|
|
* would block the only document that actually has to exist.
|
|
*
|
|
* @param array<string, mixed> $issuer
|
|
*/
|
|
private function logoFile(array $issuer): string
|
|
{
|
|
$path = trim((string) ($issuer['logo_path'] ?? ''));
|
|
|
|
if ($path === '') {
|
|
return '';
|
|
}
|
|
|
|
$full = Storage::disk('public')->path($path);
|
|
|
|
return is_file($full) ? $full : '';
|
|
}
|
|
}
|