fix(sales): render PDFs in client timezone
This commit is contained in:
@@ -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'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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([
|
||||||
|
|||||||
Reference in New Issue
Block a user