From 754aeebe3af547d1b49b3b7fc80064b266d62f38 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Tue, 28 Jul 2026 09:54:49 -0300 Subject: [PATCH] Squashed commit of the following: commit c993da3397b65488d919d1b929cbd4638754601b Author: ncoronel Date: Tue Jul 28 09:54:10 2026 -0300 feat(tickets): update ticket metadata layout for improved readability and clarity commit 140fa4676d84bed788894e1d7994b26dccac7a34 Author: ncoronel Date: Tue Jul 28 09:43:21 2026 -0300 feat(tickets): enhance PDF ticket download with dynamic filename and improved styling commit 8875c047b198776e61e8353c99cf4c7b00bc8fc9 Author: ncoronel Date: Tue Jul 28 09:03:17 2026 -0300 feat(tickets): add PDF download functionality for tickets - Implemented a new endpoint to download tickets as a PDF. - Created DownloadTicketsPdfRequest for validating ticket IDs. - Added TicketPdfService to handle PDF generation and QR code embedding. - Developed a Blade view for rendering ticket details in PDF format. - Updated API routes to include the new PDF download route. - Added tests to ensure authenticated users can download their tickets and that users cannot download tickets belonging to others. - Updated composer.json and composer.lock to include necessary dependencies for PDF generation and QR code creation. --- .../Ticket/Controllers/TicketController.php | 24 + .../Requests/DownloadTicketsPdfRequest.php | 22 + .../Ticket/Services/TicketPdfService.php | 88 +++ app/Domains/Ticket/routes/api.php | 1 + composer.json | 2 + composer.lock | 701 +++++++++++++++++- resources/views/pdf/tickets.blade.php | 213 ++++++ tests/Feature/Ticket/TicketControllerTest.php | 37 + 8 files changed, 1087 insertions(+), 1 deletion(-) create mode 100644 app/Domains/Ticket/Requests/DownloadTicketsPdfRequest.php create mode 100644 app/Domains/Ticket/Services/TicketPdfService.php create mode 100644 resources/views/pdf/tickets.blade.php diff --git a/app/Domains/Ticket/Controllers/TicketController.php b/app/Domains/Ticket/Controllers/TicketController.php index a7f9cfe..9c118a8 100644 --- a/app/Domains/Ticket/Controllers/TicketController.php +++ b/app/Domains/Ticket/Controllers/TicketController.php @@ -4,13 +4,18 @@ namespace App\Domains\Ticket\Controllers; use App\Domains\Tenant\Models\Tenant; use App\Domains\Ticket\Models\Ticket; +use App\Domains\Ticket\Requests\DownloadTicketsPdfRequest; use App\Domains\Ticket\Resources\TicketResource; +use App\Domains\Ticket\Services\TicketPdfService; use App\Http\Controllers\Controller; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Http\Response; class TicketController extends Controller { + public function __construct(private readonly TicketPdfService $ticketPdfService) {} + public function index(Request $request, Tenant $tenant): JsonResponse { $tickets = Ticket::query() @@ -21,4 +26,23 @@ class TicketController extends Controller return TicketResource::collection($tickets)->response(); } + + public function downloadPdf(DownloadTicketsPdfRequest $request, Tenant $tenant): Response + { + $ticketIds = $request->validated('ticket_ids'); + $tickets = Ticket::query() + ->where('tenant_code', $tenant->codigo) + ->where('user_id', $request->user()->getKey()) + ->whereIn('id', $ticketIds) + ->orderByDesc('id') + ->get(); + + abort_if( + $tickets->count() !== count($ticketIds), + 404, + 'Uno o más tickets no están disponibles.' + ); + + return $this->ticketPdfService->download($tenant, $tickets); + } } diff --git a/app/Domains/Ticket/Requests/DownloadTicketsPdfRequest.php b/app/Domains/Ticket/Requests/DownloadTicketsPdfRequest.php new file mode 100644 index 0000000..ae540e0 --- /dev/null +++ b/app/Domains/Ticket/Requests/DownloadTicketsPdfRequest.php @@ -0,0 +1,22 @@ +> */ + public function rules(): array + { + return [ + 'ticket_ids' => ['required', 'array', 'min:1', 'max:25'], + 'ticket_ids.*' => ['required', 'integer', 'distinct'], + ]; + } +} diff --git a/app/Domains/Ticket/Services/TicketPdfService.php b/app/Domains/Ticket/Services/TicketPdfService.php new file mode 100644 index 0000000..34bc681 --- /dev/null +++ b/app/Domains/Ticket/Services/TicketPdfService.php @@ -0,0 +1,88 @@ + $tickets + */ + public function download(Tenant $tenant, Collection $tickets): Response + { + $tenant->loadMissing('headerLogo'); + $primaryColor = $this->color($tenant->primary_color, '#009933'); + $headerBackgroundColor = $this->color($tenant->header_bg_color, $primaryColor); + + $pdf = Pdf::loadView('pdf.tickets', [ + 'tenant' => $tenant, + 'tickets' => $tickets, + 'logoDataUri' => $this->logoDataUri($tenant), + 'primaryColor' => $primaryColor, + 'headerBackgroundColor' => $headerBackgroundColor, + 'headerTextColor' => $this->contrastingTextColor($headerBackgroundColor), + 'qrCodes' => $tickets->mapWithKeys( + fn (Ticket $ticket): array => [$ticket->id => $this->qrCodeDataUri($ticket->ticket)] + ), + ])->setPaper('a4'); + + $ticketIds = $tickets->pluck('id')->implode('_'); + + return $pdf->download("tickets_{$ticketIds}.pdf"); + } + + private function qrCodeDataUri(string $value): string + { + $qrCode = new QrCode( + data: $value, + errorCorrectionLevel: ErrorCorrectionLevel::Medium, + size: 700, + margin: 10, + ); + + return (new PngWriter)->write($qrCode)->getDataUri(); + } + + private function logoDataUri(Tenant $tenant): ?string + { + $logo = $tenant->headerLogo; + if ($logo === null) { + return null; + } + + try { + $contents = Storage::disk('s3')->get($logo->path); + } catch (Throwable) { + return null; + } + + return 'data:'.($logo->mime_type ?: 'image/png').';base64,'.base64_encode($contents); + } + + private function color(?string $color, string $fallback): string + { + return is_string($color) && preg_match('/^#[0-9A-Fa-f]{6}$/', $color) + ? $color + : $fallback; + } + + private function contrastingTextColor(string $backgroundColor): string + { + $red = hexdec(substr($backgroundColor, 1, 2)); + $green = hexdec(substr($backgroundColor, 3, 2)); + $blue = hexdec(substr($backgroundColor, 5, 2)); + $luminance = ($red * 299 + $green * 587 + $blue * 114) / 1000; + + return $luminance > 160 ? '#17211b' : '#ffffff'; + } +} diff --git a/app/Domains/Ticket/routes/api.php b/app/Domains/Ticket/routes/api.php index f1de10a..9f9f343 100644 --- a/app/Domains/Ticket/routes/api.php +++ b/app/Domains/Ticket/routes/api.php @@ -7,4 +7,5 @@ Route::prefix('tenants/{tenant:codigo}') ->middleware('auth:sanctum') ->group(function (): void { Route::get('tickets', [TicketController::class, 'index']); + Route::post('tickets/pdf', [TicketController::class, 'downloadPdf']); }); diff --git a/composer.json b/composer.json index efc4b13..b85e86f 100644 --- a/composer.json +++ b/composer.json @@ -7,6 +7,8 @@ "license": "MIT", "require": { "php": "^8.3", + "barryvdh/laravel-dompdf": "^3.1", + "endroid/qr-code": "^6.1", "laravel/framework": "^13.8", "laravel/sanctum": "^4.3", "laravel/socialite": "^5.29", diff --git a/composer.lock b/composer.lock index 001e9af..49bee22 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "930f49a939d8b56a337d2fca9c48ad86", + "content-hash": "aa7e98d017dd610946c62579b20c501f", "packages": [ { "name": "aws/aws-crt-php", @@ -157,6 +157,138 @@ }, "time": "2026-07-23T18:06:54+00:00" }, + { + "name": "bacon/bacon-qr-code", + "version": "v3.1.1", + "source": { + "type": "git", + "url": "https://github.com/Bacon/BaconQrCode.git", + "reference": "4da2233e72eeecd9be3b62e0dc2cc9ed8e2e31c2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/4da2233e72eeecd9be3b62e0dc2cc9ed8e2e31c2", + "reference": "4da2233e72eeecd9be3b62e0dc2cc9ed8e2e31c2", + "shasum": "" + }, + "require": { + "dasprid/enum": "^1.0.3", + "ext-iconv": "*", + "php": "^8.1" + }, + "require-dev": { + "phly/keep-a-changelog": "^2.12", + "phpunit/phpunit": "^10.5.11 || ^11.0.4", + "spatie/phpunit-snapshot-assertions": "^5.1.5", + "spatie/pixelmatch-php": "^1.2.0", + "squizlabs/php_codesniffer": "^3.9" + }, + "suggest": { + "ext-imagick": "to generate QR code images" + }, + "type": "library", + "autoload": { + "psr-4": { + "BaconQrCode\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "https://dasprids.de/", + "role": "Developer" + } + ], + "description": "BaconQrCode is a QR code generator for PHP.", + "homepage": "https://github.com/Bacon/BaconQrCode", + "support": { + "issues": "https://github.com/Bacon/BaconQrCode/issues", + "source": "https://github.com/Bacon/BaconQrCode/tree/v3.1.1" + }, + "time": "2026-04-05T21:06:35+00:00" + }, + { + "name": "barryvdh/laravel-dompdf", + "version": "v3.1.2", + "source": { + "type": "git", + "url": "https://github.com/barryvdh/laravel-dompdf.git", + "reference": "ee3b72b19ccdf57d0243116ecb2b90261344dedc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/barryvdh/laravel-dompdf/zipball/ee3b72b19ccdf57d0243116ecb2b90261344dedc", + "reference": "ee3b72b19ccdf57d0243116ecb2b90261344dedc", + "shasum": "" + }, + "require": { + "dompdf/dompdf": "^3.0", + "illuminate/support": "^9|^10|^11|^12|^13.0", + "php": "^8.1" + }, + "require-dev": { + "larastan/larastan": "^2.7|^3.0", + "orchestra/testbench": "^7|^8|^9.16|^10|^11.0", + "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.2" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2026-02-21T08:51:10+00:00" + }, { "name": "brick/math", "version": "0.18.0", @@ -285,6 +417,56 @@ ], "time": "2024-02-09T16:56:22+00:00" }, + { + "name": "dasprid/enum", + "version": "1.0.7", + "source": { + "type": "git", + "url": "https://github.com/DASPRiD/Enum.git", + "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/b5874fa9ed0043116c72162ec7f4fb50e02e7cce", + "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce", + "shasum": "" + }, + "require": { + "php": ">=7.1 <9.0" + }, + "require-dev": { + "phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11", + "squizlabs/php_codesniffer": "*" + }, + "type": "library", + "autoload": { + "psr-4": { + "DASPRiD\\Enum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "https://dasprids.de/", + "role": "Developer" + } + ], + "description": "PHP 7.1 enum implementation", + "keywords": [ + "enum", + "map" + ], + "support": { + "issues": "https://github.com/DASPRiD/Enum/issues", + "source": "https://github.com/DASPRiD/Enum/tree/1.0.7" + }, + "time": "2025-09-16T12:23:56+00:00" + }, { "name": "dflydev/dot-access-data", "version": "v3.0.3", @@ -527,6 +709,161 @@ ], "time": "2024-02-05T11:56:58+00:00" }, + { + "name": "dompdf/dompdf", + "version": "v3.1.6", + "source": { + "type": "git", + "url": "https://github.com/dompdf/dompdf.git", + "reference": "6d4b4eb8500f7a786da8868ba463a71b725a4005" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dompdf/dompdf/zipball/6d4b4eb8500f7a786da8868ba463a71b725a4005", + "reference": "6d4b4eb8500f7a786da8868ba463a71b725a4005", + "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.6" + }, + "time": "2026-07-20T12:29:38+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", "version": "v3.6.0", @@ -658,6 +995,78 @@ ], "time": "2025-03-06T22:45:56+00:00" }, + { + "name": "endroid/qr-code", + "version": "6.1.3", + "source": { + "type": "git", + "url": "https://github.com/endroid/qr-code.git", + "reference": "5fa534856ed95649d67c0eab0cabc03ab1d8e0e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/endroid/qr-code/zipball/5fa534856ed95649d67c0eab0cabc03ab1d8e0e2", + "reference": "5fa534856ed95649d67c0eab0cabc03ab1d8e0e2", + "shasum": "" + }, + "require": { + "bacon/bacon-qr-code": "^3.0", + "php": "^8.4" + }, + "require-dev": { + "endroid/quality": "dev-main", + "ext-gd": "*", + "khanamiryan/qrcode-detector-decoder": "^2.0.3", + "setasign/fpdf": "^1.8.2" + }, + "suggest": { + "ext-gd": "Enables you to write PNG images", + "khanamiryan/qrcode-detector-decoder": "Enables you to use the image validator", + "roave/security-advisories": "Makes sure package versions with known security issues are not installed", + "setasign/fpdf": "Enables you to use the PDF writer" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.x-dev" + } + }, + "autoload": { + "psr-4": { + "Endroid\\QrCode\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jeroen van den Enden", + "email": "info@endroid.nl" + } + ], + "description": "Endroid QR Code", + "homepage": "https://github.com/endroid/qr-code", + "keywords": [ + "code", + "endroid", + "php", + "qr", + "qrcode" + ], + "support": { + "issues": "https://github.com/endroid/qr-code/issues", + "source": "https://github.com/endroid/qr-code/tree/6.1.3" + }, + "funding": [ + { + "url": "https://github.com/endroid", + "type": "github" + } + ], + "time": "2026-02-05T07:01:58+00:00" + }, { "name": "firebase/php-jwt", "version": "v7.1.0", @@ -2515,6 +2924,73 @@ ], "time": "2026-03-08T20:05:35+00:00" }, + { + "name": "masterminds/html5", + "version": "2.10.1", + "source": { + "type": "git", + "url": "https://github.com/Masterminds/html5-php.git", + "reference": "fd5018f6815fff903946d0564977b44ce8010e29" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fd5018f6815fff903946d0564977b44ce8010e29", + "reference": "fd5018f6815fff903946d0564977b44ce8010e29", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9 || ^10" + }, + "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.1" + }, + "time": "2026-06-23T18:43:15+00:00" + }, { "name": "monolog/monolog", "version": "3.10.0", @@ -4084,6 +4560,86 @@ }, "time": "2026-06-18T03:57:49+00:00" }, + { + "name": "sabberworm/php-css-parser", + "version": "v9.4.0", + "source": { + "type": "git", + "url": "https://github.com/MyIntervals/PHP-CSS-Parser.git", + "reference": "fd3bf9fb173e0df649bc4e3e0d088a1b2417c08f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/fd3bf9fb173e0df649bc4e3e0d088a1b2417c08f", + "reference": "fd3bf9fb173e0df649bc4e3e0d088a1b2417c08f", + "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.4" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "1.4.0", + "phpstan/extension-installer": "1.4.3", + "phpstan/phpstan": "1.12.33 || 2.2.2", + "phpstan/phpstan-phpunit": "1.4.2 || 2.0.16", + "phpstan/phpstan-strict-rules": "1.6.2 || 2.0.11", + "phpunit/phpunit": "8.5.52", + "rawr/phpunit-data-provider": "3.3.1", + "rector/rector": "1.2.10 || 2.4.6", + "rector/type-perfect": "1.0.0 || 2.1.3", + "squizlabs/php_codesniffer": "4.0.1", + "thecodingmachine/phpstan-safe-rule": "1.2.0 || 1.4.3" + }, + "suggest": { + "ext-mbstring": "for parsing UTF-8 CSS" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.5.x-dev" + } + }, + "autoload": { + "files": [ + "src/Rule/Rule.php", + "src/RuleSet/RuleContainer.php" + ], + "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.4.0" + }, + "time": "2026-06-18T15:10:53+00:00" + }, { "name": "symfony/clock", "version": "v8.1.0", @@ -6634,6 +7190,149 @@ ], "time": "2026-06-09T10:54:51+00:00" }, + { + "name": "thecodingmachine/safe", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/thecodingmachine/safe.git", + "reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/705683a25bacf0d4860c7dea4d7947bfd09eea19", + "reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19", + "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.4.0" + }, + "funding": [ + { + "url": "https://github.com/OskarStark", + "type": "github" + }, + { + "url": "https://github.com/shish", + "type": "github" + }, + { + "url": "https://github.com/silasjoisten", + "type": "github" + }, + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2026-02-04T18:08:13+00:00" + }, { "name": "tijsverkoyen/css-to-inline-styles", "version": "v2.4.0", diff --git a/resources/views/pdf/tickets.blade.php b/resources/views/pdf/tickets.blade.php new file mode 100644 index 0000000..d1ec952 --- /dev/null +++ b/resources/views/pdf/tickets.blade.php @@ -0,0 +1,213 @@ + + + + + + + +@foreach ($tickets as $ticket) +
+
+ +
+ + + + + + +
+ +
+

Tu acceso

+

{{ $ticket->name }}

+ +
+ + + + + + + +
+ +
+
+ Código QR del ticket {{ $ticket->id }} +
+

Escaneá este código al ingresar

+

Aumentá el brillo de la pantalla para una lectura más rápida.

+
+ +
+ Importante: este ticket es personal. No compartas el código QR con otras personas. +
+
+ + +
+@endforeach + + diff --git a/tests/Feature/Ticket/TicketControllerTest.php b/tests/Feature/Ticket/TicketControllerTest.php index 23ef1cb..a04a1dd 100644 --- a/tests/Feature/Ticket/TicketControllerTest.php +++ b/tests/Feature/Ticket/TicketControllerTest.php @@ -61,6 +61,43 @@ class TicketControllerTest extends TestCase ->assertUnauthorized(); } + public function test_an_authenticated_user_can_download_a_pdf_for_their_tickets(): void + { + $tenant = $this->createTenant('current'); + $user = User::factory()->create(); + $firstTicket = $this->createTicket($tenant, $user, 'First ticket'); + $secondTicket = $this->createTicket($tenant, $user, 'Second ticket'); + + $response = $this->actingAs($user, 'sanctum') + ->post("/api/tenants/{$tenant->codigo}/tickets/pdf", [ + 'ticket_ids' => [$firstTicket->id, $secondTicket->id], + ]); + + $response + ->assertOk() + ->assertHeader('content-type', 'application/pdf') + ->assertHeader( + 'content-disposition', + "attachment; filename=tickets_{$secondTicket->id}_{$firstTicket->id}.pdf" + ); + $this->assertStringStartsWith('%PDF', $response->getContent()); + } + + public function test_a_user_cannot_download_someone_elses_ticket(): void + { + $tenant = $this->createTenant('current'); + $user = User::factory()->create(); + $otherUser = User::factory()->create(); + $ownTicket = $this->createTicket($tenant, $user, 'Own ticket'); + $otherTicket = $this->createTicket($tenant, $otherUser, 'Other ticket'); + + $this->actingAs($user, 'sanctum') + ->postJson("/api/tenants/{$tenant->codigo}/tickets/pdf", [ + 'ticket_ids' => [$ownTicket->id, $otherTicket->id], + ]) + ->assertNotFound(); + } + private function createTicket(Tenant $tenant, User $user, string $name): Ticket { return Ticket::query()->create([