Backend
The Invoice module handles PDF generation, manual upload, preview, download, and deletion of invoices associated with payments. It uses a service layer with separate responsibilities: PDF generation, storage management, filename construction, and numbering.
File Structure
app/
├── Http/
│ ├── Controllers/Invoices/
│ │ └── InvoiceController.php
│ └── Requests/Invoices/
│ └── UploadInvoiceRequest.php
└── Services/Invoices/
├── InvoiceService.php
├── Generators/
│ ├── InvoicePdfGenerator.php
│ ├── InvoiceNumberGenerator.php
│ └── InvoiceFilenameBuilder.php
└── Storage/
└── InvoiceStorageManager.php
Routes
All under auth, verified, 2fa middleware:
| Method | URI | Action | Description |
|---|---|---|---|
| POST | /invoices/payments/{payment}/generate | generate | Generate PDF and download |
| POST | /invoices/payments/{payment}/upload | upload | Upload manual invoice |
| GET | /invoices/payments/{payment}/preview | preview | Inline preview |
| GET | /invoices/payments/{payment}/download | download | File download |
| DELETE | /invoices/payments/{payment} | destroy | Delete invoice |
Controller
InvoiceController delegates all operations to InvoiceService:
generate(Payment $payment)- Generates PDF and downloads it directly (StreamedResponse, not saved to disk)upload(UploadInvoiceRequest $request, Payment $payment)- Saves uploaded file, updatesinvoice_pathon the Paymentpreview(Payment $payment)- Opens the file inline in the browserdownload(Payment $payment)- Downloads the saved filedestroy(Payment $payment)- Deletes file from storage and resetsinvoice_path
InvoiceService
Main orchestrator. Coordinates the sub-services:
generateAndDownload(Payment $payment)- Delegates toInvoicePdfGeneratorupload(Payment $payment, UploadedFile $file)- Saves viaInvoiceStorageManager, updates the modeldownload(Payment $payment)- Retrieves and downloads viaInvoiceStorageManagerpreview(Payment $payment)- Renders inline viaInvoiceStorageManagerdelete(Payment $payment)- Removes file and resetsinvoice_path
InvoicePdfGenerator
Generates PDF using Barryvdh/DomPDF.
Data Prepared for the Template
| Data | Source |
|---|---|
payment | Payment model |
project | payment->project relationship |
client | project->client relationship |
business | BusinessSettings::current() |
invoice_number | From InvoiceNumberGenerator |
invoice_date | paid_at or current date |
due_date | From the Payment |
PDF Template
The invoices/pdf.blade.php template includes 5 partials:
| Partial | Content |
|---|---|
partials/styles | CSS (DejaVu Sans font, table-based layout, professional colors) |
partials/header | Business logo, name, invoice number, date |
partials/parties | Left column: business data (name, address, tax ID, VAT number). Right column: client data (name, address, VAT number) or project name fallback |
partials/amount | Formatted amount and due date |
partials/description | Payment notes, amount, payment instructions with IBAN (if configured) |
partials/footer | Currency information |
Paper format: A4.
InvoiceNumberGenerator
Generates unique invoice numbers.
- Format:
{payment_id}/{year}(e.g.,5/2025) - Guarantees uniqueness: payment_id is unique, the year adds temporal context
InvoiceFilenameBuilder
Builds readable filenames for download.
- Format:
invoice-{id_zero_padded}-{year}.{extension} - Example:
invoice-005-2025.pdf - Supported extensions: pdf, jpg, png
InvoiceStorageManager
Manages the physical storage of invoice files.
- Storage path:
storage/app/invoices/ - Disk: Laravel
local
Methods
saveUploadedFile(UploadedFile $file)- Saves with timestamp prefix, returns pathdownload(string $path, string $downloadName)- HTTP download response (attachment)preview(string $path, string $displayName)- HTTP inline response with correct MIME typedelete(?string $path)- Removes fileexists(?string $path)- Checks existencegetMimeType(string $path)- Returns MIME type (application/pdf,image/jpeg,image/png, fallbackapplication/octet-stream)ensureDirectoryExists()- Creates directory if it does not exist
Response Headers
Preview (inline):
Content-Disposition: inlineX-Content-Type-Options: nosniffCache-Control: private, no-store
Download (attachment):
Content-Disposition: attachment
Upload Validation
UploadInvoiceRequest:
- File required
- Formats: pdf, jpg, jpeg, png
- Maximum size: 10MB
- Localized error messages (
__('invoices.*'))
Payment Model
Invoice-related fields:
invoice_path- Path to saved file (nullable)hasInvoice()- Checks ifinvoice_pathhas a valuegetInvoiceDownloadUrl()- Returns download route URLgetInvoicePreviewUrl()- Returns preview route URL
File deletion happens automatically when the Payment is deleted (via model boot).
Generation vs Upload Flow
PDF Generation
- User clicks "Generate"
InvoicePdfGeneratorcreates PDF with DomPDF- StreamedResponse downloads the file directly
- The file is NOT saved to disk
Manual Upload
- User uploads file via modal
- File validated by
UploadInvoiceRequest InvoiceStorageManagersaves tostorage/app/invoices/Payment.invoice_pathupdated- Redirect to the project's payments tab
Technical Notes
- PDF generation is on-demand: nothing is saved to disk, it generates and streams.
- Manual upload is the only way to have a persistent file associated with the payment.
- The module does not have its own views for the invoice list: actions are integrated into the payment module views.