Skip to main content

Backend

The Receipts module handles upload, preview, download, and deletion of receipts associated with costs. Unlike the Invoice module, it does not generate PDFs: it only manages manually uploaded files.

File Structure​

app/
├── Http/
│ ├── Controllers/Receipts/
│ │ └── ReceiptController.php
│ └── Requests/Receipts/
│ └── UploadReceiptRequest.php
└── Services/Receipts/
├── ReceiptService.php
└── Storage/
└── ReceiptStorageManager.php

Routes​

All under auth, verified, 2fa middleware. Routes are nested under project and cost:

MethodURIActionDescription
POST/projects/{project}/costs/{cost}/receiptuploadUpload receipt
GET/projects/{project}/costs/{cost}/receipt/downloaddownloadDownload file
GET/projects/{project}/costs/{cost}/receipt/previewpreviewInline preview
DELETE/projects/{project}/costs/{cost}/receiptdestroyDelete receipt

Controller​

ReceiptController delegates all operations to ReceiptService:

  • upload(UploadReceiptRequest $request, Project $project, Cost $cost) - Saves uploaded file
  • download(Project $project, Cost $cost) - Downloads file
  • preview(Project $project, Cost $cost) - Opens file inline in browser
  • destroy(Project $project, Cost $cost) - Deletes file and resets receipt_path

ReceiptService​

Module orchestrator. Manages the receipt file lifecycle.

Methods​

  • upload(Cost $cost, UploadedFile $file) - Generates filename, saves via ReceiptStorageManager, updates Cost.receipt_path
  • download(Cost $cost) - Retrieves and downloads file
  • preview(Cost $cost) - Renders file inline
  • delete(Cost $cost) - Removes file and resets receipt_path
  • ensureReceiptExists(Cost $cost) - Validates that the receipt exists, otherwise abort 404

Filename Generation​

The generateFilename() method creates structured filenames:

  • Format: {prefix}-{type}-{date}-{timestamp}.{extension}
  • Prefix: Localized string __('receipts.filename_prefix')
  • Type: From the cost type (hosting, api, tool, license, ads, service, travel)
  • Date: paid_at formatted as YYYY-MM-DD
  • Timestamp: Unix timestamp for uniqueness
  • Example: receipt-hosting-2025-01-15-1736942400.pdf

ReceiptStorageManager​

Manages the physical storage of receipt files.

  • Storage path: storage/app/receipts/
  • Disk: Laravel local

Methods​

  • saveUploadedFile(UploadedFile $file, string $filename) - Saves with storeAs, 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​

UploadReceiptRequest:

  • File required
  • Formats: pdf, jpg, jpeg, png
  • Maximum size: 5MB (half of invoices)
  • Localized error messages (__('receipts.*'))

Cost Model​

Receipt-related fields:

  • receipt_path - Path to saved file (nullable)
  • hasReceipt() - Checks if receipt_path has a value
  • getReceiptUploadUrl() - Returns upload route URL
  • getReceiptDownloadUrl() - Returns download route URL
  • getReceiptPreviewUrl() - Returns preview route URL
  • getReceiptDeleteUrl() - Returns delete route URL

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

Differences from the Invoice Module​

AspectInvoiceReceipt
PDF generationYes (DomPDF)No
Manual upload onlyNo (also generates)Yes
Max size10MB5MB
Storage pathstorage/app/invoices/storage/app/receipts/
Associated withPaymentCost
Filenameinvoice-{id}-{year}.ext{prefix}-{type}-{date}-{ts}.ext
Route nestingFlat (/invoices/payments/{payment})Nested (/projects/{project}/costs/{cost}/receipt)

Technical Notes​

  • The module has no automatic generation: it only manages files uploaded by the user.
  • The filename includes the cost type, making files identifiable even outside the application.
  • Routes are nested under project/cost because receipts are always in the context of a specific project's cost.
  • As with invoices, actions are integrated into the cost module views, not in their own views.