feat: booking details pdf
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Service;
|
||||
|
||||
use App\Entity\Groups\Accommodation;
|
||||
use App\Entity\Groups\AccommodationBooking;
|
||||
use App\Service\AccommodationBookingBreakdownCalculator;
|
||||
use App\Service\AccommodationBookingPdfGenerator;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Twig\Environment;
|
||||
use Twig\Extra\Intl\IntlExtension;
|
||||
use Twig\Loader\FilesystemLoader;
|
||||
|
||||
/**
|
||||
* Renders through the real templates and dompdf — the point of this service is that the
|
||||
* document survives the trip through dompdf, which a mocked Twig would not prove.
|
||||
*/
|
||||
class AccommodationBookingPdfGeneratorTest extends TestCase
|
||||
{
|
||||
public function testTheDocumentFitsOnASingleA4Page(): void
|
||||
{
|
||||
$pdf = $this->createGenerator($this->breakdown())->render($this->booking());
|
||||
|
||||
self::assertStringStartsWith('%PDF-', $pdf);
|
||||
self::assertSame(1, $this->pageCount($pdf));
|
||||
}
|
||||
|
||||
public function testTheDownloadIsAnAttachmentNamedAfterTheGroup(): void
|
||||
{
|
||||
$response = $this->createGenerator($this->breakdown())->createDownloadResponse($this->booking());
|
||||
|
||||
self::assertSame('application/pdf', $response->headers->get('Content-Type'));
|
||||
self::assertStringContainsString('attachment;', (string) $response->headers->get('Content-Disposition'));
|
||||
self::assertStringContainsString('buchung-2026-07-06-schulklasse-7b.pdf', (string) $response->headers->get('Content-Disposition'));
|
||||
}
|
||||
|
||||
public function testABookingWithoutPricesCannotBeRendered(): void
|
||||
{
|
||||
$this->expectException(\RuntimeException::class);
|
||||
|
||||
$this->createGenerator(null)->render($this->booking());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|null $breakdown
|
||||
*/
|
||||
private function createGenerator(?array $breakdown): AccommodationBookingPdfGenerator
|
||||
{
|
||||
$twig = new Environment(new FilesystemLoader(__DIR__.'/../../templates'));
|
||||
$twig->addExtension(new IntlExtension());
|
||||
|
||||
$calculator = $this->createMock(AccommodationBookingBreakdownCalculator::class);
|
||||
$calculator->method('compute')->willReturn($breakdown);
|
||||
|
||||
return new AccommodationBookingPdfGenerator($twig, $calculator, \dirname(__DIR__, 2));
|
||||
}
|
||||
|
||||
private function booking(): AccommodationBooking
|
||||
{
|
||||
$accommodation = new Accommodation();
|
||||
$accommodation->setName('Haus Bergblick');
|
||||
$accommodation->setMaxAdolescentAge(11);
|
||||
$accommodation->setCurrency('EUR');
|
||||
|
||||
$booking = new AccommodationBooking();
|
||||
$booking->setAccommodation($accommodation);
|
||||
$booking->setGroupName('Schulklasse 7b');
|
||||
$booking->setSalutation('Frau');
|
||||
$booking->setFirstName('Änne');
|
||||
$booking->setLastName('Müller');
|
||||
$booking->setEmail('[email protected]');
|
||||
$booking->setStreet('Hauptstraße 1');
|
||||
$booking->setZip('50667');
|
||||
$booking->setCity('Köln');
|
||||
$booking->setDateFrom(new \DateTimeImmutable('2026-07-06'));
|
||||
$booking->setDateTo(new \DateTimeImmutable('2026-07-11'));
|
||||
$booking->setPaxCount(32);
|
||||
$booking->setChildrenCount(4);
|
||||
$booking->setBoardServiceLabel('Vollpension');
|
||||
$booking->addAdditionalServiceSnapshot('Bettwäsche', 900, 'per_person', 5);
|
||||
$booking->setRemarks('Zwei Vegetarier.');
|
||||
$booking->setConfirmedAt(new \DateTimeImmutable('2026-08-18'));
|
||||
|
||||
return $booking;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function breakdown(): array
|
||||
{
|
||||
return [
|
||||
'effectivePax' => 32,
|
||||
'totalPax' => 32,
|
||||
'includedPax' => 25,
|
||||
'nights' => 5,
|
||||
'basePrice' => 250000,
|
||||
'additionalPersonsPrice' => 70000,
|
||||
'shortTermSurcharge' => 0,
|
||||
'undersubscriptionSurcharge' => 0,
|
||||
'undersubscriptionThreshold' => 40,
|
||||
'boardPrice' => 480000,
|
||||
'serviceDetails' => [['label' => 'Bettwäsche', 'price' => 28800]],
|
||||
'servicesPrice' => 28800,
|
||||
'runningCosts' => 16000,
|
||||
'total' => 844800,
|
||||
'currency' => 'EUR',
|
||||
'discounts' => [['label' => 'Unterkunft', 'percent' => 5, 'amount' => 16000]],
|
||||
'discountedTotal' => 828800,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* dompdf's CPDF backend writes one /MediaBox for the page tree node and one per page,
|
||||
* so the page count is the number of occurrences minus one. Reading it back this way
|
||||
* avoids re-implementing the render pipeline just to reach the canvas.
|
||||
*/
|
||||
private function pageCount(string $pdf): int
|
||||
{
|
||||
return substr_count($pdf, '/MediaBox') - 1;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user