Add invoice PDF generation with dompdf
Install barryvdh/laravel-dompdf, create professional invoice Blade template with EZSCALE branding, add download endpoints for customer billing and admin invoice pages. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ namespace App\Http\Controllers\Account;
|
|||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Invoice;
|
use App\Models\Invoice;
|
||||||
use App\Services\Billing\BillingServiceFactory;
|
use App\Services\Billing\BillingServiceFactory;
|
||||||
|
use Barryvdh\DomPDF\Facade\Pdf;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@@ -93,13 +94,11 @@ class BillingController extends Controller
|
|||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($invoice->invoice_pdf) {
|
$invoice->load(['user', 'items']);
|
||||||
return redirect($invoice->invoice_pdf);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a basic invoice download as JSON for now
|
$pdf = Pdf::loadView('pdf.invoice', ['invoice' => $invoice]);
|
||||||
// PDF generation will be added with a dedicated package
|
|
||||||
return response()->json($invoice->load('items'));
|
return $pdf->download("invoice-{$invoice->number}.pdf");
|
||||||
}
|
}
|
||||||
|
|
||||||
public function transactions(Request $request): Response
|
public function transactions(Request $request): Response
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ namespace App\Http\Controllers\Admin;
|
|||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\AuditLog;
|
use App\Models\AuditLog;
|
||||||
use App\Models\Invoice;
|
use App\Models\Invoice;
|
||||||
|
use Barryvdh\DomPDF\Facade\Pdf;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
@@ -59,6 +60,15 @@ class InvoiceController extends Controller
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function download(Invoice $invoice): \Symfony\Component\HttpFoundation\Response
|
||||||
|
{
|
||||||
|
$invoice->load(['user', 'items']);
|
||||||
|
|
||||||
|
$pdf = Pdf::loadView('pdf.invoice', ['invoice' => $invoice]);
|
||||||
|
|
||||||
|
return $pdf->download("invoice-{$invoice->number}.pdf");
|
||||||
|
}
|
||||||
|
|
||||||
public function void(Invoice $invoice): RedirectResponse
|
public function void(Invoice $invoice): RedirectResponse
|
||||||
{
|
{
|
||||||
$invoice->update(['status' => 'void']);
|
$invoice->update(['status' => 'void']);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.2",
|
"php": "^8.2",
|
||||||
|
"barryvdh/laravel-dompdf": "^3.1",
|
||||||
"inertiajs/inertia-laravel": "^2.0",
|
"inertiajs/inertia-laravel": "^2.0",
|
||||||
"laravel/cashier": "^16.2",
|
"laravel/cashier": "^16.2",
|
||||||
"laravel/fortify": "^1.34",
|
"laravel/fortify": "^1.34",
|
||||||
|
|||||||
514
website/composer.lock
generated
514
website/composer.lock
generated
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "7ed919792a4edfaad8af686d9fdf0522",
|
"content-hash": "5c74851b1987089bba21c4645c42a0f3",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "bacon/bacon-qr-code",
|
"name": "bacon/bacon-qr-code",
|
||||||
@@ -61,6 +61,83 @@
|
|||||||
},
|
},
|
||||||
"time": "2025-11-19T17:15:36+00:00"
|
"time": "2025-11-19T17:15:36+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "barryvdh/laravel-dompdf",
|
||||||
|
"version": "v3.1.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/barryvdh/laravel-dompdf.git",
|
||||||
|
"reference": "8e71b99fc53bb8eb77f316c3c452dd74ab7cb25d"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/barryvdh/laravel-dompdf/zipball/8e71b99fc53bb8eb77f316c3c452dd74ab7cb25d",
|
||||||
|
"reference": "8e71b99fc53bb8eb77f316c3c452dd74ab7cb25d",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"dompdf/dompdf": "^3.0",
|
||||||
|
"illuminate/support": "^9|^10|^11|^12",
|
||||||
|
"php": "^8.1"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"larastan/larastan": "^2.7|^3.0",
|
||||||
|
"orchestra/testbench": "^7|^8|^9|^10",
|
||||||
|
"phpro/grumphp": "^2.5",
|
||||||
|
"squizlabs/php_codesniffer": "^3.5"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"aliases": {
|
||||||
|
"PDF": "Barryvdh\\DomPDF\\Facade\\Pdf",
|
||||||
|
"Pdf": "Barryvdh\\DomPDF\\Facade\\Pdf"
|
||||||
|
},
|
||||||
|
"providers": [
|
||||||
|
"Barryvdh\\DomPDF\\ServiceProvider"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "3.0-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Barryvdh\\DomPDF\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Barry vd. Heuvel",
|
||||||
|
"email": "barryvdh@gmail.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "A DOMPDF Wrapper for Laravel",
|
||||||
|
"keywords": [
|
||||||
|
"dompdf",
|
||||||
|
"laravel",
|
||||||
|
"pdf"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/barryvdh/laravel-dompdf/issues",
|
||||||
|
"source": "https://github.com/barryvdh/laravel-dompdf/tree/v3.1.1"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://fruitcake.nl",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/barryvdh",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-02-13T15:07:54+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "brick/math",
|
"name": "brick/math",
|
||||||
"version": "0.14.7",
|
"version": "0.14.7",
|
||||||
@@ -549,6 +626,161 @@
|
|||||||
],
|
],
|
||||||
"time": "2024-02-05T11:56:58+00:00"
|
"time": "2024-02-05T11:56:58+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "dompdf/dompdf",
|
||||||
|
"version": "v3.1.4",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/dompdf/dompdf.git",
|
||||||
|
"reference": "db712c90c5b9868df3600e64e68da62e78a34623"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/dompdf/dompdf/zipball/db712c90c5b9868df3600e64e68da62e78a34623",
|
||||||
|
"reference": "db712c90c5b9868df3600e64e68da62e78a34623",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"dompdf/php-font-lib": "^1.0.0",
|
||||||
|
"dompdf/php-svg-lib": "^1.0.0",
|
||||||
|
"ext-dom": "*",
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"masterminds/html5": "^2.0",
|
||||||
|
"php": "^7.1 || ^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"ext-gd": "*",
|
||||||
|
"ext-json": "*",
|
||||||
|
"ext-zip": "*",
|
||||||
|
"mockery/mockery": "^1.3",
|
||||||
|
"phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11",
|
||||||
|
"squizlabs/php_codesniffer": "^3.5",
|
||||||
|
"symfony/process": "^4.4 || ^5.4 || ^6.2 || ^7.0"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-gd": "Needed to process images",
|
||||||
|
"ext-gmagick": "Improves image processing performance",
|
||||||
|
"ext-imagick": "Improves image processing performance",
|
||||||
|
"ext-zlib": "Needed for pdf stream compression"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Dompdf\\": "src/"
|
||||||
|
},
|
||||||
|
"classmap": [
|
||||||
|
"lib/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"LGPL-2.1"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "The Dompdf Community",
|
||||||
|
"homepage": "https://github.com/dompdf/dompdf/blob/master/AUTHORS.md"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter",
|
||||||
|
"homepage": "https://github.com/dompdf/dompdf",
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/dompdf/dompdf/issues",
|
||||||
|
"source": "https://github.com/dompdf/dompdf/tree/v3.1.4"
|
||||||
|
},
|
||||||
|
"time": "2025-10-29T12:43:30+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dompdf/php-font-lib",
|
||||||
|
"version": "1.0.2",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/dompdf/php-font-lib.git",
|
||||||
|
"reference": "a6e9a688a2a80016ac080b97be73d3e10c444c9a"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/a6e9a688a2a80016ac080b97be73d3e10c444c9a",
|
||||||
|
"reference": "a6e9a688a2a80016ac080b97be73d3e10c444c9a",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"php": "^7.1 || ^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11 || ^12"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"FontLib\\": "src/FontLib"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"LGPL-2.1-or-later"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "The FontLib Community",
|
||||||
|
"homepage": "https://github.com/dompdf/php-font-lib/blob/master/AUTHORS.md"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "A library to read, parse, export and make subsets of different types of font files.",
|
||||||
|
"homepage": "https://github.com/dompdf/php-font-lib",
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/dompdf/php-font-lib/issues",
|
||||||
|
"source": "https://github.com/dompdf/php-font-lib/tree/1.0.2"
|
||||||
|
},
|
||||||
|
"time": "2026-01-20T14:10:26+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dompdf/php-svg-lib",
|
||||||
|
"version": "1.0.2",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/dompdf/php-svg-lib.git",
|
||||||
|
"reference": "8259ffb930817e72b1ff1caef5d226501f3dfeb1"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/8259ffb930817e72b1ff1caef5d226501f3dfeb1",
|
||||||
|
"reference": "8259ffb930817e72b1ff1caef5d226501f3dfeb1",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"php": "^7.1 || ^8.0",
|
||||||
|
"sabberworm/php-css-parser": "^8.4 || ^9.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Svg\\": "src/Svg"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"LGPL-3.0-or-later"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "The SvgLib Community",
|
||||||
|
"homepage": "https://github.com/dompdf/php-svg-lib/blob/master/AUTHORS.md"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "A library to read, parse and export to PDF SVG files.",
|
||||||
|
"homepage": "https://github.com/dompdf/php-svg-lib",
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/dompdf/php-svg-lib/issues",
|
||||||
|
"source": "https://github.com/dompdf/php-svg-lib/tree/1.0.2"
|
||||||
|
},
|
||||||
|
"time": "2026-01-02T16:01:13+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "dragonmantank/cron-expression",
|
"name": "dragonmantank/cron-expression",
|
||||||
"version": "v3.6.0",
|
"version": "v3.6.0",
|
||||||
@@ -2842,6 +3074,73 @@
|
|||||||
],
|
],
|
||||||
"time": "2026-01-15T06:54:53+00:00"
|
"time": "2026-01-15T06:54:53+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "masterminds/html5",
|
||||||
|
"version": "2.10.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/Masterminds/html5-php.git",
|
||||||
|
"reference": "fcf91eb64359852f00d921887b219479b4f21251"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fcf91eb64359852f00d921887b219479b4f21251",
|
||||||
|
"reference": "fcf91eb64359852f00d921887b219479b4f21251",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-dom": "*",
|
||||||
|
"php": ">=5.3.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "2.7-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Masterminds\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Matt Butcher",
|
||||||
|
"email": "technosophos@gmail.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Matt Farina",
|
||||||
|
"email": "matt@mattfarina.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Asmir Mustafic",
|
||||||
|
"email": "goetas@gmail.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "An HTML5 parser and serializer.",
|
||||||
|
"homepage": "http://masterminds.github.io/html5-php",
|
||||||
|
"keywords": [
|
||||||
|
"HTML5",
|
||||||
|
"dom",
|
||||||
|
"html",
|
||||||
|
"parser",
|
||||||
|
"querypath",
|
||||||
|
"serializer",
|
||||||
|
"xml"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/Masterminds/html5-php/issues",
|
||||||
|
"source": "https://github.com/Masterminds/html5-php/tree/2.10.0"
|
||||||
|
},
|
||||||
|
"time": "2025-07-25T09:04:22+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "moneyphp/money",
|
"name": "moneyphp/money",
|
||||||
"version": "v4.8.0",
|
"version": "v4.8.0",
|
||||||
@@ -4676,6 +4975,80 @@
|
|||||||
},
|
},
|
||||||
"time": "2025-12-14T04:43:48+00:00"
|
"time": "2025-12-14T04:43:48+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "sabberworm/php-css-parser",
|
||||||
|
"version": "v9.1.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/MyIntervals/PHP-CSS-Parser.git",
|
||||||
|
"reference": "1b363fdbdc6dd0ca0f4bf98d3a4d7f388133f1fb"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/1b363fdbdc6dd0ca0f4bf98d3a4d7f388133f1fb",
|
||||||
|
"reference": "1b363fdbdc6dd0ca0f4bf98d3a4d7f388133f1fb",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-iconv": "*",
|
||||||
|
"php": "^7.2.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
|
||||||
|
"thecodingmachine/safe": "^1.3 || ^2.5 || ^3.3"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"php-parallel-lint/php-parallel-lint": "1.4.0",
|
||||||
|
"phpstan/extension-installer": "1.4.3",
|
||||||
|
"phpstan/phpstan": "1.12.28 || 2.1.25",
|
||||||
|
"phpstan/phpstan-phpunit": "1.4.2 || 2.0.7",
|
||||||
|
"phpstan/phpstan-strict-rules": "1.6.2 || 2.0.6",
|
||||||
|
"phpunit/phpunit": "8.5.46",
|
||||||
|
"rawr/phpunit-data-provider": "3.3.1",
|
||||||
|
"rector/rector": "1.2.10 || 2.1.7",
|
||||||
|
"rector/type-perfect": "1.0.0 || 2.1.0"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-mbstring": "for parsing UTF-8 CSS"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-main": "9.2.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Sabberworm\\CSS\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Raphael Schweikert"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Oliver Klee",
|
||||||
|
"email": "github@oliverklee.de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Jake Hotson",
|
||||||
|
"email": "jake.github@qzdesign.co.uk"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Parser for CSS Files written in PHP",
|
||||||
|
"homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser",
|
||||||
|
"keywords": [
|
||||||
|
"css",
|
||||||
|
"parser",
|
||||||
|
"stylesheet"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues",
|
||||||
|
"source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v9.1.0"
|
||||||
|
},
|
||||||
|
"time": "2025-09-14T07:37:21+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "spatie/laravel-permission",
|
"name": "spatie/laravel-permission",
|
||||||
"version": "6.24.0",
|
"version": "6.24.0",
|
||||||
@@ -7561,6 +7934,145 @@
|
|||||||
],
|
],
|
||||||
"time": "2026-01-01T22:13:48+00:00"
|
"time": "2026-01-01T22:13:48+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "thecodingmachine/safe",
|
||||||
|
"version": "v3.3.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/thecodingmachine/safe.git",
|
||||||
|
"reference": "2cdd579eeaa2e78e51c7509b50cc9fb89a956236"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/thecodingmachine/safe/zipball/2cdd579eeaa2e78e51c7509b50cc9fb89a956236",
|
||||||
|
"reference": "2cdd579eeaa2e78e51c7509b50cc9fb89a956236",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^8.1"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"php-parallel-lint/php-parallel-lint": "^1.4",
|
||||||
|
"phpstan/phpstan": "^2",
|
||||||
|
"phpunit/phpunit": "^10",
|
||||||
|
"squizlabs/php_codesniffer": "^3.2"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"lib/special_cases.php",
|
||||||
|
"generated/apache.php",
|
||||||
|
"generated/apcu.php",
|
||||||
|
"generated/array.php",
|
||||||
|
"generated/bzip2.php",
|
||||||
|
"generated/calendar.php",
|
||||||
|
"generated/classobj.php",
|
||||||
|
"generated/com.php",
|
||||||
|
"generated/cubrid.php",
|
||||||
|
"generated/curl.php",
|
||||||
|
"generated/datetime.php",
|
||||||
|
"generated/dir.php",
|
||||||
|
"generated/eio.php",
|
||||||
|
"generated/errorfunc.php",
|
||||||
|
"generated/exec.php",
|
||||||
|
"generated/fileinfo.php",
|
||||||
|
"generated/filesystem.php",
|
||||||
|
"generated/filter.php",
|
||||||
|
"generated/fpm.php",
|
||||||
|
"generated/ftp.php",
|
||||||
|
"generated/funchand.php",
|
||||||
|
"generated/gettext.php",
|
||||||
|
"generated/gmp.php",
|
||||||
|
"generated/gnupg.php",
|
||||||
|
"generated/hash.php",
|
||||||
|
"generated/ibase.php",
|
||||||
|
"generated/ibmDb2.php",
|
||||||
|
"generated/iconv.php",
|
||||||
|
"generated/image.php",
|
||||||
|
"generated/imap.php",
|
||||||
|
"generated/info.php",
|
||||||
|
"generated/inotify.php",
|
||||||
|
"generated/json.php",
|
||||||
|
"generated/ldap.php",
|
||||||
|
"generated/libxml.php",
|
||||||
|
"generated/lzf.php",
|
||||||
|
"generated/mailparse.php",
|
||||||
|
"generated/mbstring.php",
|
||||||
|
"generated/misc.php",
|
||||||
|
"generated/mysql.php",
|
||||||
|
"generated/mysqli.php",
|
||||||
|
"generated/network.php",
|
||||||
|
"generated/oci8.php",
|
||||||
|
"generated/opcache.php",
|
||||||
|
"generated/openssl.php",
|
||||||
|
"generated/outcontrol.php",
|
||||||
|
"generated/pcntl.php",
|
||||||
|
"generated/pcre.php",
|
||||||
|
"generated/pgsql.php",
|
||||||
|
"generated/posix.php",
|
||||||
|
"generated/ps.php",
|
||||||
|
"generated/pspell.php",
|
||||||
|
"generated/readline.php",
|
||||||
|
"generated/rnp.php",
|
||||||
|
"generated/rpminfo.php",
|
||||||
|
"generated/rrd.php",
|
||||||
|
"generated/sem.php",
|
||||||
|
"generated/session.php",
|
||||||
|
"generated/shmop.php",
|
||||||
|
"generated/sockets.php",
|
||||||
|
"generated/sodium.php",
|
||||||
|
"generated/solr.php",
|
||||||
|
"generated/spl.php",
|
||||||
|
"generated/sqlsrv.php",
|
||||||
|
"generated/ssdeep.php",
|
||||||
|
"generated/ssh2.php",
|
||||||
|
"generated/stream.php",
|
||||||
|
"generated/strings.php",
|
||||||
|
"generated/swoole.php",
|
||||||
|
"generated/uodbc.php",
|
||||||
|
"generated/uopz.php",
|
||||||
|
"generated/url.php",
|
||||||
|
"generated/var.php",
|
||||||
|
"generated/xdiff.php",
|
||||||
|
"generated/xml.php",
|
||||||
|
"generated/xmlrpc.php",
|
||||||
|
"generated/yaml.php",
|
||||||
|
"generated/yaz.php",
|
||||||
|
"generated/zip.php",
|
||||||
|
"generated/zlib.php"
|
||||||
|
],
|
||||||
|
"classmap": [
|
||||||
|
"lib/DateTime.php",
|
||||||
|
"lib/DateTimeImmutable.php",
|
||||||
|
"lib/Exceptions/",
|
||||||
|
"generated/Exceptions/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"description": "PHP core functions that throw exceptions instead of returning FALSE on error",
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/thecodingmachine/safe/issues",
|
||||||
|
"source": "https://github.com/thecodingmachine/safe/tree/v3.3.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/OskarStark",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/shish",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/staabm",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-05-14T06:15:44+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "tijsverkoyen/css-to-inline-styles",
|
"name": "tijsverkoyen/css-to-inline-styles",
|
||||||
"version": "v2.4.0",
|
"version": "v2.4.0",
|
||||||
|
|||||||
23
website/database/factories/InvoiceItemFactory.php
Normal file
23
website/database/factories/InvoiceItemFactory.php
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Invoice;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
/** @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\InvoiceItem> */
|
||||||
|
class InvoiceItemFactory extends Factory
|
||||||
|
{
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'invoice_id' => Invoice::factory(),
|
||||||
|
'description' => fake()->sentence(4),
|
||||||
|
'amount' => fake()->randomFloat(2, 5, 200),
|
||||||
|
'quantity' => fake()->numberBetween(1, 3),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -120,11 +120,9 @@ function formatDateTime(dateStr: string | null): string {
|
|||||||
|
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<VBtn
|
<VBtn
|
||||||
v-if="invoice.invoice_pdf"
|
|
||||||
color="info"
|
color="info"
|
||||||
variant="tonal"
|
variant="tonal"
|
||||||
:href="invoice.invoice_pdf"
|
:href="`/invoices/${invoice.id}/download`"
|
||||||
target="_blank"
|
|
||||||
>
|
>
|
||||||
<VIcon icon="tabler-download" start />
|
<VIcon icon="tabler-download" start />
|
||||||
Download PDF
|
Download PDF
|
||||||
|
|||||||
@@ -54,7 +54,15 @@ defineProps<Props>()
|
|||||||
</td>
|
</td>
|
||||||
<td class="text-end">${{ parseFloat(invoice.total).toFixed(2) }}</td>
|
<td class="text-end">${{ parseFloat(invoice.total).toFixed(2) }}</td>
|
||||||
<td class="text-end">
|
<td class="text-end">
|
||||||
<a :href="`/billing/invoices/${invoice.id}/download`" class="text-primary text-decoration-none">Download</a>
|
<VBtn
|
||||||
|
:href="`/billing/invoices/${invoice.id}/download`"
|
||||||
|
color="primary"
|
||||||
|
variant="tonal"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<VIcon icon="tabler-download" start />
|
||||||
|
PDF
|
||||||
|
</VBtn>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
219
website/resources/views/pdf/invoice.blade.php
Normal file
219
website/resources/views/pdf/invoice.blade.php
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Invoice {{ $invoice->number }}</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Helvetica', Arial, sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #333;
|
||||||
|
margin: 40px;
|
||||||
|
}
|
||||||
|
.company-name {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #7367F0;
|
||||||
|
}
|
||||||
|
.invoice-title {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: right;
|
||||||
|
color: #7367F0;
|
||||||
|
}
|
||||||
|
.invoice-meta {
|
||||||
|
text-align: right;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.meta-label {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.address-label {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 11px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
table.items {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
table.items th {
|
||||||
|
background-color: #7367F0;
|
||||||
|
color: white;
|
||||||
|
padding: 8px 12px;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 11px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
table.items th.text-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
table.items td {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
table.items tr:nth-child(even) {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
.totals {
|
||||||
|
width: 300px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
.totals table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.totals td {
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
.totals .total-row td {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
border-top: 2px solid #7367F0;
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
|
.status-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.status-paid {
|
||||||
|
background-color: #28c76f;
|
||||||
|
}
|
||||||
|
.status-pending, .status-sent {
|
||||||
|
background-color: #ff9f43;
|
||||||
|
}
|
||||||
|
.status-overdue {
|
||||||
|
background-color: #ea5455;
|
||||||
|
}
|
||||||
|
.status-draft {
|
||||||
|
background-color: #82868b;
|
||||||
|
}
|
||||||
|
.status-void {
|
||||||
|
background-color: #82868b;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
margin-top: 40px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #999;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
padding-top: 15px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{{-- Header --}}
|
||||||
|
<table width="100%" style="margin-bottom: 30px;">
|
||||||
|
<tr>
|
||||||
|
<td width="50%" style="vertical-align: top;">
|
||||||
|
<div class="company-name">EZSCALE</div>
|
||||||
|
<div style="margin-top: 5px; color: #666;">Cloud Infrastructure Services</div>
|
||||||
|
</td>
|
||||||
|
<td width="50%" style="vertical-align: top; text-align: right;">
|
||||||
|
<div class="invoice-title">INVOICE</div>
|
||||||
|
<div class="invoice-meta">
|
||||||
|
<div><span class="meta-label">Invoice #:</span> {{ $invoice->number }}</div>
|
||||||
|
<div><span class="meta-label">Date:</span> {{ $invoice->created_at->format('M d, Y') }}</div>
|
||||||
|
<div><span class="meta-label">Due Date:</span> {{ $invoice->due_date ? $invoice->due_date->format('M d, Y') : 'N/A' }}</div>
|
||||||
|
<div style="margin-top: 5px;">
|
||||||
|
<span class="status-badge status-{{ strtolower($invoice->status) }}">{{ ucfirst($invoice->status) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{{-- Bill To / From --}}
|
||||||
|
<table width="100%" style="margin-bottom: 30px;">
|
||||||
|
<tr>
|
||||||
|
<td width="50%" style="vertical-align: top;">
|
||||||
|
<div class="address-label">Bill To</div>
|
||||||
|
<div><strong>{{ $invoice->user->name }}</strong></div>
|
||||||
|
<div>{{ $invoice->user->email }}</div>
|
||||||
|
@if($invoice->user->company)<div>{{ $invoice->user->company }}</div>@endif
|
||||||
|
@if($invoice->user->phone)<div>{{ $invoice->user->phone }}</div>@endif
|
||||||
|
</td>
|
||||||
|
<td width="50%" style="vertical-align: top;">
|
||||||
|
<div class="address-label">From</div>
|
||||||
|
<div><strong>EZSCALE</strong></div>
|
||||||
|
<div>support@ezscale.cloud</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{{-- Line Items --}}
|
||||||
|
<table class="items">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Description</th>
|
||||||
|
<th class="text-right" style="text-align: center;">Qty</th>
|
||||||
|
<th class="text-right" style="text-align: right;">Unit Price</th>
|
||||||
|
<th class="text-right" style="text-align: right;">Total</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@forelse($invoice->items as $item)
|
||||||
|
<tr>
|
||||||
|
<td>{{ $item->description }}</td>
|
||||||
|
<td style="text-align: center;">{{ $item->quantity }}</td>
|
||||||
|
<td style="text-align: right;">${{ number_format((float) $item->amount, 2) }}</td>
|
||||||
|
<td style="text-align: right;">${{ number_format((float) $item->amount * $item->quantity, 2) }}</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td>Service charges</td>
|
||||||
|
<td style="text-align: center;">1</td>
|
||||||
|
<td style="text-align: right;">${{ number_format((float) $invoice->total - (float) $invoice->tax, 2) }}</td>
|
||||||
|
<td style="text-align: right;">${{ number_format((float) $invoice->total - (float) $invoice->tax, 2) }}</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{{-- Totals --}}
|
||||||
|
<div class="totals">
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td>Subtotal</td>
|
||||||
|
<td style="text-align: right;">
|
||||||
|
@if($invoice->items->count() > 0)
|
||||||
|
${{ number_format($invoice->items->sum(fn ($item) => (float) $item->amount * $item->quantity), 2) }}
|
||||||
|
@else
|
||||||
|
${{ number_format((float) $invoice->total - (float) $invoice->tax, 2) }}
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@if((float) $invoice->tax > 0)
|
||||||
|
<tr>
|
||||||
|
<td>Tax</td>
|
||||||
|
<td style="text-align: right;">${{ number_format((float) $invoice->tax, 2) }}</td>
|
||||||
|
</tr>
|
||||||
|
@endif
|
||||||
|
<tr class="total-row">
|
||||||
|
<td>Total</td>
|
||||||
|
<td style="text-align: right;">${{ number_format((float) $invoice->total, 2) }}</td>
|
||||||
|
</tr>
|
||||||
|
@if($invoice->status === 'paid' && $invoice->paid_at)
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" style="text-align: right; font-size: 10px; color: #28c76f; padding-top: 4px;">
|
||||||
|
Paid on {{ $invoice->paid_at->format('M d, Y') }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endif
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Footer --}}
|
||||||
|
<div class="footer">
|
||||||
|
<p>Thank you for your business!</p>
|
||||||
|
<p>EZSCALE — Cloud Infrastructure Services — support@ezscale.cloud</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -36,6 +36,7 @@ Route::post('services/{service}/unsuspend', [ServiceController::class, 'unsuspen
|
|||||||
Route::post('services/{service}/terminate', [ServiceController::class, 'terminate'])->name('services.terminate');
|
Route::post('services/{service}/terminate', [ServiceController::class, 'terminate'])->name('services.terminate');
|
||||||
|
|
||||||
Route::resource('invoices', InvoiceController::class)->only(['index', 'show']);
|
Route::resource('invoices', InvoiceController::class)->only(['index', 'show']);
|
||||||
|
Route::get('invoices/{invoice}/download', [InvoiceController::class, 'download'])->name('invoices.download');
|
||||||
Route::post('invoices/{invoice}/void', [InvoiceController::class, 'void'])->name('invoices.void');
|
Route::post('invoices/{invoice}/void', [InvoiceController::class, 'void'])->name('invoices.void');
|
||||||
|
|
||||||
Route::resource('coupons', CouponController::class)->names([
|
Route::resource('coupons', CouponController::class)->names([
|
||||||
|
|||||||
99
website/tests/Feature/InvoicePdfTest.php
Normal file
99
website/tests/Feature/InvoicePdfTest.php
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Models\Invoice;
|
||||||
|
use App\Models\InvoiceItem;
|
||||||
|
use App\Models\User;
|
||||||
|
use Database\Seeders\RoleAndPermissionSeeder;
|
||||||
|
|
||||||
|
beforeEach(function (): void {
|
||||||
|
$this->seed(RoleAndPermissionSeeder::class);
|
||||||
|
$this->accountUrl = 'http://'.config('app.domains.account');
|
||||||
|
$this->adminUrl = 'http://'.config('app.domains.admin');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows customer to download their own invoice as PDF', function (): void {
|
||||||
|
$user = User::factory()->customer()->create();
|
||||||
|
$invoice = Invoice::factory()->create(['user_id' => $user->id]);
|
||||||
|
InvoiceItem::factory()->create([
|
||||||
|
'invoice_id' => $invoice->id,
|
||||||
|
'description' => 'VPS Hosting - Basic Plan',
|
||||||
|
'amount' => '29.99',
|
||||||
|
'quantity' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->actingAs($user)
|
||||||
|
->get($this->accountUrl.'/billing/invoices/'.$invoice->id.'/download');
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
$response->assertHeader('content-type', 'application/pdf');
|
||||||
|
expect($response->headers->get('content-disposition'))
|
||||||
|
->toContain("invoice-{$invoice->number}.pdf");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prevents customer from downloading another users invoice', function (): void {
|
||||||
|
$user = User::factory()->customer()->create();
|
||||||
|
$otherUser = User::factory()->customer()->create();
|
||||||
|
$invoice = Invoice::factory()->create(['user_id' => $otherUser->id]);
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->get($this->accountUrl.'/billing/invoices/'.$invoice->id.'/download')
|
||||||
|
->assertForbidden();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows admin to download any invoice as PDF', function (): void {
|
||||||
|
$admin = User::factory()->admin()->create();
|
||||||
|
$customer = User::factory()->customer()->create();
|
||||||
|
$invoice = Invoice::factory()->create(['user_id' => $customer->id]);
|
||||||
|
|
||||||
|
$response = $this->actingAs($admin)
|
||||||
|
->get($this->adminUrl.'/invoices/'.$invoice->id.'/download');
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
$response->assertHeader('content-type', 'application/pdf');
|
||||||
|
expect($response->headers->get('content-disposition'))
|
||||||
|
->toContain("invoice-{$invoice->number}.pdf");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns PDF with correct content-type header', function (): void {
|
||||||
|
$user = User::factory()->customer()->create();
|
||||||
|
$invoice = Invoice::factory()->paid()->create(['user_id' => $user->id]);
|
||||||
|
|
||||||
|
$response = $this->actingAs($user)
|
||||||
|
->get($this->accountUrl.'/billing/invoices/'.$invoice->id.'/download');
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
expect($response->headers->get('content-type'))->toBe('application/pdf');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes invoice number in the PDF filename', function (): void {
|
||||||
|
$user = User::factory()->customer()->create();
|
||||||
|
$invoice = Invoice::factory()->create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'number' => 'INV-TEST-001',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->actingAs($user)
|
||||||
|
->get($this->accountUrl.'/billing/invoices/'.$invoice->id.'/download');
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
expect($response->headers->get('content-disposition'))
|
||||||
|
->toContain('invoice-INV-TEST-001.pdf');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('requires authentication to download invoice', function (): void {
|
||||||
|
$invoice = Invoice::factory()->create();
|
||||||
|
|
||||||
|
$this->get($this->accountUrl.'/billing/invoices/'.$invoice->id.'/download')
|
||||||
|
->assertRedirect();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('denies customer access to admin invoice download route', function (): void {
|
||||||
|
$customer = User::factory()->customer()->create();
|
||||||
|
$invoice = Invoice::factory()->create(['user_id' => $customer->id]);
|
||||||
|
|
||||||
|
$this->actingAs($customer)
|
||||||
|
->get($this->adminUrl.'/invoices/'.$invoice->id.'/download')
|
||||||
|
->assertForbidden();
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user