Skip to main content

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:

MethodURIActionDescription
POST/invoices/payments/{payment}/generategenerateGenerate PDF and download
POST/invoices/payments/{payment}/uploaduploadUpload manual invoice
GET/invoices/payments/{payment}/previewpreviewInline preview
GET/invoices/payments/{payment}/downloaddownloadFile download
DELETE/invoices/payments/{payment}destroyDelete 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, updates invoice_path on the Payment
  • preview(Payment $payment) - Opens the file inline in the browser
  • download(Payment $payment) - Downloads the saved file
  • destroy(Payment $payment) - Deletes file from storage and resets invoice_path

InvoiceService

Main orchestrator. Coordinates the sub-services:

  • generateAndDownload(Payment $payment) - Delegates to InvoicePdfGenerator
  • upload(Payment $payment, UploadedFile $file) - Saves via InvoiceStorageManager, updates the model
  • download(Payment $payment) - Retrieves and downloads via InvoiceStorageManager
  • preview(Payment $payment) - Renders inline via InvoiceStorageManager
  • delete(Payment $payment) - Removes file and resets invoice_path

InvoicePdfGenerator

Generates PDF using Barryvdh/DomPDF.

Data Prepared for the Template

DataSource
paymentPayment model
projectpayment->project relationship
clientproject->client relationship
businessBusinessSettings::current()
invoice_numberFrom InvoiceNumberGenerator
invoice_datepaid_at or current date
due_dateFrom the Payment

PDF Template

The invoices/pdf.blade.php template includes 5 partials:

PartialContent
partials/stylesCSS (DejaVu Sans font, table-based layout, professional colors)
partials/headerBusiness logo, name, invoice number, date
partials/partiesLeft column: business data (name, address, tax ID, VAT number). Right column: client data (name, address, VAT number) or project name fallback
partials/amountFormatted amount and due date
partials/descriptionPayment notes, amount, payment instructions with IBAN (if configured)
partials/footerCurrency 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 path
  • download(string $path, string $downloadName) - HTTP download response (attachment)
  • preview(string $path, string $displayName) - HTTP inline response with correct MIME type
  • delete(?string $path) - Removes file
  • exists(?string $path) - Checks existence
  • getMimeType(string $path) - Returns MIME type (application/pdf, image/jpeg, image/png, fallback application/octet-stream)
  • ensureDirectoryExists() - Creates directory if it does not exist

Response Headers

Preview (inline):

  • Content-Disposition: inline
  • X-Content-Type-Options: nosniff
  • Cache-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 if invoice_path has a value
  • getInvoiceDownloadUrl() - Returns download route URL
  • getInvoicePreviewUrl() - Returns preview route URL

File deletion happens automatically when the Payment is deleted (via model boot).

Generation vs Upload Flow

PDF Generation

  1. User clicks "Generate"
  2. InvoicePdfGenerator creates PDF with DomPDF
  3. StreamedResponse downloads the file directly
  4. The file is NOT saved to disk

Manual Upload

  1. User uploads file via modal
  2. File validated by UploadInvoiceRequest
  3. InvoiceStorageManager saves to storage/app/invoices/
  4. Payment.invoice_path updated
  5. 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.