fix(sales): render PDFs in client timezone

This commit is contained in:
2026-08-24 16:42:38 -03:00
parent 0aa423e90e
commit 6b06028771
5 changed files with 75 additions and 16 deletions

View File

@@ -3,6 +3,8 @@
namespace App\Domains\Sale\Controllers\AdminApp; namespace App\Domains\Sale\Controllers\AdminApp;
use App\Domains\Sale\Requests\AdminAppSaleIndexRequest; 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\SaleDetailResource;
use App\Domains\Sale\Resources\AdminApp\SaleModificationResource; use App\Domains\Sale\Resources\AdminApp\SaleModificationResource;
use App\Domains\Sale\Resources\AdminApp\SaleResource; 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(); $tenant = $request->user()->tenant()->firstOrFail();
return $this->salePdfService->downloadSales( return $this->salePdfService->downloadSales(
$tenant, $tenant,
$this->saleService->salesForExport($tenant, $request->validated()), $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(); $tenant = $request->user()->tenant()->firstOrFail();
return $this->salePdfService->downloadModifications( return $this->salePdfService->downloadModifications(
$tenant, $tenant,
$this->saleService->modificationsForExport($tenant), $this->saleService->modificationsForExport($tenant),
$request->validated('timezone'),
); );
} }
} }

View File

@@ -13,12 +13,14 @@ use Illuminate\Support\Collection;
class AdminAppSalePdfService class AdminAppSalePdfService
{ {
/** @param Collection<int, Purchase> $sales */ /** @param Collection<int, Purchase> $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', [ $pdf = Pdf::loadView('pdf.adminapp.sales', [
'tenant' => $tenant, 'tenant' => $tenant,
'sales' => $sales, 'sales' => $sales,
'generatedAt' => now(), 'generatedAt' => $generatedAt,
'timeZone' => $timeZone,
'confirmedSalesTotal' => number_format( 'confirmedSalesTotal' => number_format(
(float) $sales->where('status', Purchase::STATUS_PAID)->sum('total'), (float) $sales->where('status', Purchase::STATUS_PAID)->sum('total'),
2, 2,
@@ -30,23 +32,30 @@ class AdminAppSalePdfService
$this->addPageNumbers($pdf); $this->addPageNumbers($pdf);
return $pdf->download( return $pdf->download(
'ventas_'.$tenant->codigo.'_'.now()->format('Ymd_His').'.pdf' 'ventas_'.$tenant->codigo.'_'
.$generatedAt->copy()->timezone($timeZone)->format('Ymd_His').'.pdf'
); );
} }
/** @param Collection<int, ValueChange> $modifications */ /** @param Collection<int, ValueChange> $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', [ $pdf = Pdf::loadView('pdf.adminapp.sale-modifications', [
'tenant' => $tenant, 'tenant' => $tenant,
'modifications' => $modifications, 'modifications' => $modifications,
'generatedAt' => now(), 'generatedAt' => $generatedAt,
'timeZone' => $timeZone,
])->setPaper('a4', 'landscape'); ])->setPaper('a4', 'landscape');
$this->addPageNumbers($pdf); $this->addPageNumbers($pdf);
return $pdf->download( return $pdf->download(
'historial_modificaciones_'.$tenant->codigo.'_'.now()->format('Ymd_His').'.pdf' 'historial_modificaciones_'.$tenant->codigo.'_'
.$generatedAt->copy()->timezone($timeZone)->format('Ymd_His').'.pdf'
); );
} }

View File

@@ -25,7 +25,7 @@
</head> </head>
<body> <body>
<h1>Historial de modificaciones de ventas</h1> <h1>Historial de modificaciones de ventas</h1>
<p class="subtitle">{{ $tenant->nombre }} · Generado el {{ $generatedAt->format('d/m/Y H:i') }}</p> <p class="subtitle">{{ $tenant->nombre }} · Generado el {{ $generatedAt->copy()->timezone($timeZone)->format('d/m/Y H:i') }}</p>
<table> <table>
<thead> <thead>
@@ -44,8 +44,8 @@
@forelse ($modifications as $modification) @forelse ($modifications as $modification)
@php($sale = $modification->trackable) @php($sale = $modification->trackable)
<tr> <tr>
<td>{{ $modification->changed_at->format('d/m/Y') }}</td> <td>{{ $modification->changed_at->copy()->timezone($timeZone)->format('d/m/Y') }}</td>
<td>{{ $modification->changed_at->format('H:i:s') }}</td> <td>{{ $modification->changed_at->copy()->timezone($timeZone)->format('H:i:s') }}</td>
<td>#{{ $modification->trackable_id }}</td> <td>#{{ $modification->trackable_id }}</td>
<td>{{ $sale?->nombre_apellido ?: 'Sin nombre' }}</td> <td>{{ $sale?->nombre_apellido ?: 'Sin nombre' }}</td>
<td>{{ $modification->attribute }}</td> <td>{{ $modification->attribute }}</td>

View File

@@ -22,7 +22,7 @@
</head> </head>
<body> <body>
<h1>Historial de ventas</h1> <h1>Historial de ventas</h1>
<p class="subtitle">{{ $tenant->nombre }} · Generado el {{ $generatedAt->format('d/m/Y H:i') }}</p> <p class="subtitle">{{ $tenant->nombre }} · Generado el {{ $generatedAt->copy()->timezone($timeZone)->format('d/m/Y H:i') }}</p>
<div class="summary"> <div class="summary">
Total de ventas confirmadas en este reporte: <strong>${{ number_format((float) $confirmedSalesTotal, 2, ',', '.') }}</strong> Total de ventas confirmadas en este reporte: <strong>${{ number_format((float) $confirmedSalesTotal, 2, ',', '.') }}</strong>
@@ -44,7 +44,7 @@
@forelse ($sales as $sale) @forelse ($sales as $sale)
<tr> <tr>
<td>#{{ $sale->id }}</td> <td>#{{ $sale->id }}</td>
<td>{{ $sale->created_at?->format('d/m/Y H:i') ?? '-' }}</td> <td>{{ $sale->created_at?->copy()->timezone($timeZone)->format('d/m/Y H:i') ?? '-' }}</td>
<td>{{ $sale->nombre_apellido ?: 'Sin nombre' }}</td> <td>{{ $sale->nombre_apellido ?: 'Sin nombre' }}</td>
<td class="center">{{ (int) ($sale->quantity ?? 0) }}</td> <td class="center">{{ (int) ($sale->quantity ?? 0) }}</td>
<td>{{ match ($sale->status) { <td>{{ match ($sale->status) {

View File

@@ -8,6 +8,7 @@ use App\Domains\Purchase\Models\Purchase;
use App\Domains\Sale\Services\AdminAppSalePdfService; use App\Domains\Sale\Services\AdminAppSalePdfService;
use App\Domains\Tenant\Models\Tenant; use App\Domains\Tenant\Models\Tenant;
use Barryvdh\DomPDF\ServiceProvider; use Barryvdh\DomPDF\ServiceProvider;
use Illuminate\Support\Carbon;
use Tests\TestCase; use Tests\TestCase;
class AdminAppSalePdfServiceTest extends TestCase class AdminAppSalePdfServiceTest extends TestCase
@@ -17,6 +18,14 @@ class AdminAppSalePdfServiceTest extends TestCase
parent::setUp(); parent::setUp();
$this->app->register(ServiceProvider::class); $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 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( $response = app(AdminAppSalePdfService::class)->downloadSales(
$this->tenant(), $this->tenant(),
collect([$this->sale()]), collect([$this->sale()]),
'America/La_Paz',
); );
$this->assertSame('application/pdf', $response->headers->get('content-type')); $this->assertSame('application/pdf', $response->headers->get('content-type'));
$this->assertStringContainsString( $this->assertStringContainsString(
'attachment; filename=ventas_acme_', 'attachment; filename=ventas_acme_20260824_135300.pdf',
(string) $response->headers->get('content-disposition'), (string) $response->headers->get('content-disposition'),
); );
$this->assertStringStartsWith('%PDF', $response->getContent()); $this->assertStringStartsWith('%PDF', $response->getContent());
@@ -57,16 +67,52 @@ class AdminAppSalePdfServiceTest extends TestCase
$response = app(AdminAppSalePdfService::class)->downloadModifications( $response = app(AdminAppSalePdfService::class)->downloadModifications(
$this->tenant(), $this->tenant(),
collect([$modification]), collect([$modification]),
'America/La_Paz',
); );
$this->assertSame('application/pdf', $response->headers->get('content-type')); $this->assertSame('application/pdf', $response->headers->get('content-type'));
$this->assertStringContainsString( $this->assertStringContainsString(
'attachment; filename=historial_modificaciones_acme_', 'attachment; filename=historial_modificaciones_acme_20260824_135300.pdf',
(string) $response->headers->get('content-disposition'), (string) $response->headers->get('content-disposition'),
); );
$this->assertStringStartsWith('%PDF', $response->getContent()); $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 private function tenant(): Tenant
{ {
return (new Tenant)->forceFill([ return (new Tenant)->forceFill([