From 6b0602877104caf96271bb8e566fda034d5e4bd2 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Mon, 24 Aug 2026 16:42:38 -0300 Subject: [PATCH] fix(sales): render PDFs in client timezone --- .../Controllers/AdminApp/SaleController.php | 8 ++- .../Sale/Services/AdminAppSalePdfService.php | 23 ++++++--- .../pdf/adminapp/sale-modifications.blade.php | 6 +-- resources/views/pdf/adminapp/sales.blade.php | 4 +- .../Unit/Sale/AdminAppSalePdfServiceTest.php | 50 ++++++++++++++++++- 5 files changed, 75 insertions(+), 16 deletions(-) diff --git a/app/Domains/Sale/Controllers/AdminApp/SaleController.php b/app/Domains/Sale/Controllers/AdminApp/SaleController.php index d02db6f..fcc37ae 100644 --- a/app/Domains/Sale/Controllers/AdminApp/SaleController.php +++ b/app/Domains/Sale/Controllers/AdminApp/SaleController.php @@ -3,6 +3,8 @@ namespace App\Domains\Sale\Controllers\AdminApp; use App\Domains\Sale\Requests\AdminAppSaleIndexRequest; +use App\Domains\Sale\Requests\AdminAppSaleModificationPdfRequest; +use App\Domains\Sale\Requests\AdminAppSalePdfRequest; use App\Domains\Sale\Resources\AdminApp\SaleDetailResource; use App\Domains\Sale\Resources\AdminApp\SaleModificationResource; use App\Domains\Sale\Resources\AdminApp\SaleResource; @@ -71,23 +73,25 @@ class SaleController extends Controller ); } - public function downloadPdf(AdminAppSaleIndexRequest $request): Response + public function downloadPdf(AdminAppSalePdfRequest $request): Response { $tenant = $request->user()->tenant()->firstOrFail(); return $this->salePdfService->downloadSales( $tenant, $this->saleService->salesForExport($tenant, $request->validated()), + $request->validated('timezone'), ); } - public function downloadModificationsPdf(Request $request): Response + public function downloadModificationsPdf(AdminAppSaleModificationPdfRequest $request): Response { $tenant = $request->user()->tenant()->firstOrFail(); return $this->salePdfService->downloadModifications( $tenant, $this->saleService->modificationsForExport($tenant), + $request->validated('timezone'), ); } } diff --git a/app/Domains/Sale/Services/AdminAppSalePdfService.php b/app/Domains/Sale/Services/AdminAppSalePdfService.php index 2712fb1..cca6f04 100644 --- a/app/Domains/Sale/Services/AdminAppSalePdfService.php +++ b/app/Domains/Sale/Services/AdminAppSalePdfService.php @@ -13,12 +13,14 @@ use Illuminate\Support\Collection; class AdminAppSalePdfService { /** @param Collection $sales */ - public function downloadSales(Tenant $tenant, Collection $sales): Response + public function downloadSales(Tenant $tenant, Collection $sales, string $timeZone): Response { + $generatedAt = now(); $pdf = Pdf::loadView('pdf.adminapp.sales', [ 'tenant' => $tenant, 'sales' => $sales, - 'generatedAt' => now(), + 'generatedAt' => $generatedAt, + 'timeZone' => $timeZone, 'confirmedSalesTotal' => number_format( (float) $sales->where('status', Purchase::STATUS_PAID)->sum('total'), 2, @@ -30,23 +32,30 @@ class AdminAppSalePdfService $this->addPageNumbers($pdf); return $pdf->download( - 'ventas_'.$tenant->codigo.'_'.now()->format('Ymd_His').'.pdf' + 'ventas_'.$tenant->codigo.'_' + .$generatedAt->copy()->timezone($timeZone)->format('Ymd_His').'.pdf' ); } /** @param Collection $modifications */ - public function downloadModifications(Tenant $tenant, Collection $modifications): Response - { + public function downloadModifications( + Tenant $tenant, + Collection $modifications, + string $timeZone, + ): Response { + $generatedAt = now(); $pdf = Pdf::loadView('pdf.adminapp.sale-modifications', [ 'tenant' => $tenant, 'modifications' => $modifications, - 'generatedAt' => now(), + 'generatedAt' => $generatedAt, + 'timeZone' => $timeZone, ])->setPaper('a4', 'landscape'); $this->addPageNumbers($pdf); return $pdf->download( - 'historial_modificaciones_'.$tenant->codigo.'_'.now()->format('Ymd_His').'.pdf' + 'historial_modificaciones_'.$tenant->codigo.'_' + .$generatedAt->copy()->timezone($timeZone)->format('Ymd_His').'.pdf' ); } diff --git a/resources/views/pdf/adminapp/sale-modifications.blade.php b/resources/views/pdf/adminapp/sale-modifications.blade.php index 4cff849..b0024bc 100644 --- a/resources/views/pdf/adminapp/sale-modifications.blade.php +++ b/resources/views/pdf/adminapp/sale-modifications.blade.php @@ -25,7 +25,7 @@

Historial de modificaciones de ventas

-

{{ $tenant->nombre }} · Generado el {{ $generatedAt->format('d/m/Y H:i') }}

+

{{ $tenant->nombre }} · Generado el {{ $generatedAt->copy()->timezone($timeZone)->format('d/m/Y H:i') }}

@@ -44,8 +44,8 @@ @forelse ($modifications as $modification) @php($sale = $modification->trackable) - - + + diff --git a/resources/views/pdf/adminapp/sales.blade.php b/resources/views/pdf/adminapp/sales.blade.php index a258ba6..a413c20 100644 --- a/resources/views/pdf/adminapp/sales.blade.php +++ b/resources/views/pdf/adminapp/sales.blade.php @@ -22,7 +22,7 @@

Historial de ventas

-

{{ $tenant->nombre }} · Generado el {{ $generatedAt->format('d/m/Y H:i') }}

+

{{ $tenant->nombre }} · Generado el {{ $generatedAt->copy()->timezone($timeZone)->format('d/m/Y H:i') }}

Total de ventas confirmadas en este reporte: ${{ number_format((float) $confirmedSalesTotal, 2, ',', '.') }} @@ -44,7 +44,7 @@ @forelse ($sales as $sale)
- +
{{ $modification->changed_at->format('d/m/Y') }}{{ $modification->changed_at->format('H:i:s') }}{{ $modification->changed_at->copy()->timezone($timeZone)->format('d/m/Y') }}{{ $modification->changed_at->copy()->timezone($timeZone)->format('H:i:s') }} #{{ $modification->trackable_id }} {{ $sale?->nombre_apellido ?: 'Sin nombre' }} {{ $modification->attribute }}
#{{ $sale->id }}{{ $sale->created_at?->format('d/m/Y H:i') ?? '-' }}{{ $sale->created_at?->copy()->timezone($timeZone)->format('d/m/Y H:i') ?? '-' }} {{ $sale->nombre_apellido ?: 'Sin nombre' }} {{ (int) ($sale->quantity ?? 0) }} {{ match ($sale->status) { diff --git a/tests/Unit/Sale/AdminAppSalePdfServiceTest.php b/tests/Unit/Sale/AdminAppSalePdfServiceTest.php index 04625ce..ea80138 100644 --- a/tests/Unit/Sale/AdminAppSalePdfServiceTest.php +++ b/tests/Unit/Sale/AdminAppSalePdfServiceTest.php @@ -8,6 +8,7 @@ use App\Domains\Purchase\Models\Purchase; use App\Domains\Sale\Services\AdminAppSalePdfService; use App\Domains\Tenant\Models\Tenant; use Barryvdh\DomPDF\ServiceProvider; +use Illuminate\Support\Carbon; use Tests\TestCase; class AdminAppSalePdfServiceTest extends TestCase @@ -17,6 +18,14 @@ class AdminAppSalePdfServiceTest extends TestCase parent::setUp(); $this->app->register(ServiceProvider::class); + Carbon::setTestNow(Carbon::parse('2026-08-24 17:53:00', 'UTC')); + } + + protected function tearDown(): void + { + Carbon::setTestNow(); + + parent::tearDown(); } public function test_it_downloads_the_sales_report_as_a_pdf(): void @@ -24,11 +33,12 @@ class AdminAppSalePdfServiceTest extends TestCase $response = app(AdminAppSalePdfService::class)->downloadSales( $this->tenant(), collect([$this->sale()]), + 'America/La_Paz', ); $this->assertSame('application/pdf', $response->headers->get('content-type')); $this->assertStringContainsString( - 'attachment; filename=ventas_acme_', + 'attachment; filename=ventas_acme_20260824_135300.pdf', (string) $response->headers->get('content-disposition'), ); $this->assertStringStartsWith('%PDF', $response->getContent()); @@ -57,16 +67,52 @@ class AdminAppSalePdfServiceTest extends TestCase $response = app(AdminAppSalePdfService::class)->downloadModifications( $this->tenant(), collect([$modification]), + 'America/La_Paz', ); $this->assertSame('application/pdf', $response->headers->get('content-type')); $this->assertStringContainsString( - 'attachment; filename=historial_modificaciones_acme_', + 'attachment; filename=historial_modificaciones_acme_20260824_135300.pdf', (string) $response->headers->get('content-disposition'), ); $this->assertStringStartsWith('%PDF', $response->getContent()); } + public function test_it_renders_pdf_dates_in_the_requested_timezone(): void + { + $sale = $this->sale(); + $modification = (new ValueChange)->forceFill([ + 'id' => 1, + 'trackable_id' => $sale->id, + 'attribute' => 'status', + 'old_value' => Purchase::STATUS_PENDING_PAYMENT, + 'new_value' => Purchase::STATUS_PAID, + 'changed_at' => now(), + 'actor_type' => 'system', + ]); + $modification->setRelation('trackable', $sale); + $modification->setRelation('user', null); + + $salesHtml = view('pdf.adminapp.sales', [ + 'tenant' => $this->tenant(), + 'sales' => collect([$sale]), + 'generatedAt' => now(), + 'timeZone' => 'America/La_Paz', + 'confirmedSalesTotal' => '25000.00', + ])->render(); + $modificationsHtml = view('pdf.adminapp.sale-modifications', [ + 'tenant' => $this->tenant(), + 'modifications' => collect([$modification]), + 'generatedAt' => now(), + 'timeZone' => 'America/La_Paz', + ])->render(); + + $this->assertStringContainsString('Generado el 24/08/2026 13:53', $salesHtml); + $this->assertStringContainsString('24/08/2026 13:53', $salesHtml); + $this->assertStringContainsString('Generado el 24/08/2026 13:53', $modificationsHtml); + $this->assertStringContainsString('13:53:00', $modificationsHtml); + } + private function tenant(): Tenant { return (new Tenant)->forceFill([