CluPilotCloud/app/Services/Billing/InvoiceRenderer.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 : '';
}
}