125 lines
4.5 KiB
PHP
125 lines
4.5 KiB
PHP
<?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;
|
|
}
|
|
}
|