Files
myep/tests/BusProNet/DataProcessor/BookingDataProcessorTest.php
T

1080 lines
41 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\BusProNet\DataProcessor;
use App\BusProNet\DataProcessor\BookingDataProcessor;
use App\BusProNet\DataProcessor\BookingPayloadBuilder;
use App\BusProNet\DataProcessor\ParticipantServiceProcessor;
use App\BusProNet\DataProcessor\PersonalDataSynchronizer;
use App\BusProNet\DataProcessor\ServiceMappingCollector;
use App\BusProNet\Model\Address;
use App\BusProNet\Model\BankAccount;
use App\BusProNet\Model\Booking;
use App\BusProNet\Model\Communication;
use App\BusProNet\Model\Insurance;
use App\BusProNet\Model\PersonalData;
use App\BusProNet\Model\Pickup;
use App\BusProNet\Model\Room;
use App\BusProNet\Model\Service;
use App\BusProNet\Model\Travel;
use App\Form\Model\BookingDto;
use App\Form\Model\ParticipantDto;
use App\Service\BookingPriceCalculator;
use App\Service\InsuranceManager;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
/**
* Comprehensive test suite for BookingDataProcessor.
*
* Tests all aspects of booking data processing including service mappings,
* participant data updates, and API payload generation.
*/
class BookingDataProcessorTest extends TestCase
{
private BookingDataProcessor $processor;
protected function setUp(): void
{
// Create a mock InsuranceManager
$insuranceService = $this->createMock(InsuranceManager::class);
$priceCalculatorService = $this->createMock(BookingPriceCalculator::class);
// Mock getSelectableInsurances to return empty array (not used in these tests)
$insuranceService->method('getSelectableInsurances')
->willReturn([]);
// Mock price calculator to return default prices (not used in most tests)
$priceCalculatorService->method('calculateIndividualParticipantPriceExcludingInsurance')
->willReturn(500.0);
// Create the new dependencies
$mappingCollector = new ServiceMappingCollector();
$serviceProcessor = new ParticipantServiceProcessor(new NullLogger());
$payloadBuilder = new BookingPayloadBuilder($mappingCollector);
$personalDataSynchronizer = new PersonalDataSynchronizer();
$this->processor = new BookingDataProcessor(
$insuranceService,
$priceCalculatorService,
$mappingCollector,
$serviceProcessor,
$payloadBuilder,
$personalDataSynchronizer,
);
}
public function testCreateUpdateRequestPayloadWithCompleteData(): void
{
$formData = $this->createCompleteFormData();
$result = $this->processor->createUpdateRequestPayload($formData);
$this->assertIsArray($result);
$this->assertArrayHasKey('idbuchung', $result);
$this->assertArrayHasKey('teilnehmerliste', $result);
$this->assertArrayHasKey('zusatzleistungen', $result);
$this->assertArrayHasKey('beförderungen', $result);
$this->assertArrayHasKey('ferienzielunterbringungen', $result);
$this->assertEquals(123, $result['idbuchung']);
$this->assertEquals('ACTIVE', $result['status']);
$this->assertCount(2, $result['teilnehmerliste']['teilnehmer']);
}
public function testCanceledParticipantsAreSkipped(): void
{
$formData = $this->createFormDataWithCanceledParticipant();
$result = $this->processor->createUpdateRequestPayload($formData);
$this->assertCount(2, $result['teilnehmerliste']['teilnehmer']);
$this->assertEquals(1, $result['teilnehmerliste']['teilnehmer'][0]['@id']);
$this->assertEquals(2, $result['teilnehmerliste']['teilnehmer'][1]['@id']);
}
public function testAdditionalServicesProcessing(): void
{
$formData = $this->createFormDataWithAdditionalServices();
$result = $this->processor->createUpdateRequestPayload($formData);
$this->assertNotEmpty($result['zusatzleistungen']['zusatzleistung']);
$this->assertCount(2, $result['zusatzleistungen']['zusatzleistung']);
$service = $result['zusatzleistungen']['zusatzleistung'][0];
$this->assertEquals(1, $service['@idleistung']);
$this->assertEquals(1, $service['@anzahl']);
$this->assertEquals('1', $service['@zuordnung']);
}
public function testVegServiceIsIncludedInUpdatePayload(): void
{
$formData = $this->createCompleteFormData();
$vegService = $this->createMockService(194672);
$formData->travel->additionalServices[194672] = $vegService;
$formData->participants[0]->veg = $vegService;
$result = $this->processor->createUpdateRequestPayload($formData);
$this->assertNotEmpty($result['zusatzleistungen']['zusatzleistung']);
$vegPayload = null;
foreach ($result['zusatzleistungen']['zusatzleistung'] as $service) {
if (194672 === $service['@idleistung']) {
$vegPayload = $service;
break;
}
}
$this->assertNotNull($vegPayload);
$this->assertSame(1, $vegPayload['@anzahl']);
$this->assertSame('1', $vegPayload['@zuordnung']);
}
public function testVegSelectionIsRestoredWhenHydratingBookingDto(): void
{
$booking = $this->createMockBooking();
$travel = $this->createMockTravel();
$vegService = $this->createMockService(194672);
$vegService->subType = 'VEG';
$vegService->mapping = [0];
$booking->additionalServices[194672] = $vegService;
$travel->additionalServices[194672] = $vegService;
$dto = $this->processor->createBookingDtoFromBooking($booking, $travel);
$this->assertNotNull($dto->participants[0]->veg);
$this->assertSame(194672, $dto->participants[0]->veg?->id);
}
public function testTransportationServicesProcessing(): void
{
$formData = $this->createFormDataWithTransportation();
$result = $this->processor->createUpdateRequestPayload($formData);
$this->assertNotEmpty($result['beförderungen']['beförderung']);
$this->assertCount(2, $result['beförderungen']['beförderung']);
}
public function testBusPickupLocationsProcessing(): void
{
$formData = $this->createFormDataWithBusPickup();
$result = $this->processor->createUpdateRequestPayload($formData);
$this->assertArrayHasKey('zustiege', $result);
$this->assertNotEmpty($result['zustiege']['zustieg']);
$this->assertEquals(1, $result['zustiege']['zustieg'][0]['@idzustieg']);
}
public function testNonBusTransportationSkipsPickup(): void
{
$formData = $this->createFormDataWithNonBusTransportation();
$result = $this->processor->createUpdateRequestPayload($formData);
$this->assertArrayHasKey('zustiege', $result);
$this->assertSame([], $result['zustiege']);
}
public function testUnusedServicesAreRemoved(): void
{
$formData = $this->createFormDataWithUnusedServices();
$result = $this->processor->createUpdateRequestPayload($formData);
$this->assertEmpty($result['zusatzleistungen']['zusatzleistung']);
$this->assertEmpty($result['beförderungen']['beförderung']);
}
public function testParticipantPersonalDataUpdate(): void
{
$formData = $this->createFormDataWithUpdatedPersonalData();
$this->processor->createUpdateRequestPayload($formData);
$participant = $formData->booking->participants[0];
$this->assertEquals('Updated', $participant->firstName);
$this->assertEquals('Participant', $participant->name);
$this->assertEquals('[email protected]', $participant->communication->email);
$this->assertEquals('+49123456789', $participant->communication->mobile);
}
public function testCanceledParticipantsPersonalDataIsUpdated(): void
{
$formData = $this->createFormDataWithInactiveParticipant();
$this->processor->createUpdateRequestPayload($formData);
$participant = $formData->booking->participants[0];
$this->assertEquals('Updated', $participant->firstName);
$this->assertEquals('Participant', $participant->name);
}
public function testApplicantDataSyncWithFirstParticipant(): void
{
$formData = $this->createFormDataForApplicantSync();
$this->processor->createUpdateRequestPayload($formData);
$applicant = $formData->booking->applicant;
$firstParticipant = $formData->booking->participants[0];
$this->assertEquals($firstParticipant->height, $applicant->height);
$this->assertEquals($firstParticipant->weight, $applicant->weight);
$this->assertEquals($firstParticipant->shoeSize, $applicant->shoeSize);
}
public function testBankAccountIncludedInPayload(): void
{
$formData = $this->createFormDataWithBankAccount();
$result = $this->processor->createUpdateRequestPayload($formData);
$this->assertArrayHasKey('bankverbindung', $result['zahlung']);
$this->assertEquals('Test Bank', $result['zahlung']['bankverbindung']['@kreditinstitut']);
$this->assertEquals('DE89370400440532013000', $result['zahlung']['bankverbindung']['@iban']);
}
public function testBankAccountNotIncludedWhenNull(): void
{
$formData = $this->createFormDataWithoutBankAccount();
$result = $this->processor->createUpdateRequestPayload($formData);
$this->assertArrayNotHasKey('bankverbindung', $result['zahlung']);
}
public function testAccommodationRoomsProcessing(): void
{
$formData = $this->createFormDataWithRooms();
$result = $this->processor->createUpdateRequestPayload($formData);
$this->assertNotEmpty($result['ferienzielunterbringungen']['ferienzielunterbringung']);
$room = $result['ferienzielunterbringungen']['ferienzielunterbringung'][0];
$this->assertEquals(1, $room['@idzimmer']);
$this->assertEquals('DOUBLE', $room['@kategorie']);
$this->assertEquals('01.01.2024', $room['@anreise']);
$this->assertEquals('07.01.2024', $room['@abreise']);
}
public function testCommunicationObjectCreation(): void
{
$formData = $this->createFormDataWithoutExistingCommunication();
$this->processor->createUpdateRequestPayload($formData);
$participant = $formData->booking->participants[0];
$this->assertInstanceOf(Communication::class, $participant->communication);
$this->assertEquals('[email protected]', $participant->communication->email);
}
public function testEmptyPickupsToDoesNotCreateZustiegeSection(): void
{
$formData = $this->createFormDataWithoutPickups();
$result = $this->processor->createUpdateRequestPayload($formData);
$this->assertArrayHasKey('zustiege', $result);
$this->assertSame([], $result['zustiege']);
}
private function createCompleteFormData(): BookingDto
{
$bookingDto = new BookingDto($this->createMockTravel(), 1);
$bookingDto->booking = $this->createMockBooking();
$bookingDto->participants = [
$this->createMockParticipantDto(0, 'F'),
$this->createMockParticipantDto(1, 'F'),
];
return $bookingDto;
}
private function createFormDataWithCanceledParticipant(): BookingDto
{
$bookingDto = new BookingDto($this->createMockTravel(), 1);
$bookingDto->booking = $this->createMockBooking();
$bookingDto->participants = [
$this->createMockParticipantDto(0, 'F'),
$this->createMockParticipantDto(1, 'S'), // Canceled
];
return $bookingDto;
}
private function createFormDataWithAdditionalServices(): BookingDto
{
$formData = $this->createCompleteFormData();
$participant = $formData->participants[0];
$participant->courses = [$this->createMockService(1)];
$participant->additionalServices = [$this->createMockService(2)];
return $formData;
}
private function createFormDataWithTransportation(): BookingDto
{
$formData = $this->createCompleteFormData();
$participant = $formData->participants[0];
$participant->transportationOutbound = $this->createMockService(1);
$participant->transportationInbound = $this->createMockService(2);
return $formData;
}
private function createFormDataWithBusPickup(): BookingDto
{
$formData = $this->createCompleteFormData();
$participant = $formData->participants[0];
$busService = $this->createMockService(1);
$busService->subType = 'BUS';
$participant->transportationOutbound = $busService;
$participant->pickup = $this->createMockPickup(1);
return $formData;
}
private function createFormDataWithNonBusTransportation(): BookingDto
{
$formData = $this->createCompleteFormData();
$participant = $formData->participants[0];
$trainService = $this->createMockService(1);
$trainService->subType = 'TRAIN';
$participant->transportationOutbound = $trainService;
return $formData;
}
private function createFormDataWithUnusedServices(): BookingDto
{
$formData = $this->createCompleteFormData();
// Remove all participants so services become unused
$formData->participants = [];
return $formData;
}
private function createFormDataWithUpdatedPersonalData(): BookingDto
{
$formData = $this->createCompleteFormData();
$participant = $formData->participants[0];
$participant->firstName = 'Updated';
$participant->lastName = 'Participant';
$participant->email = '[email protected]';
$participant->mobile = '+49123456789';
return $formData;
}
private function createFormDataWithInactiveParticipant(): BookingDto
{
$formData = $this->createCompleteFormData();
$participant = $formData->participants[0];
$participant->status = 'S'; // Canceled status
$participant->firstName = 'Updated';
$participant->lastName = 'Participant';
// Ensure original data remains unchanged
$formData->booking->participants[0]->firstName = 'Original';
$formData->booking->participants[0]->name = 'Name';
return $formData;
}
private function createFormDataForApplicantSync(): BookingDto
{
$formData = $this->createCompleteFormData();
$firstParticipant = $formData->booking->participants[0];
$firstParticipant->height = '180';
$firstParticipant->weight = '75';
$firstParticipant->shoeSize = '42';
return $formData;
}
private function createFormDataWithBankAccount(): BookingDto
{
$formData = $this->createCompleteFormData();
$bankAccount = new BankAccount();
$bankAccount->bankName = 'Test Bank';
$bankAccount->iban = 'DE89370400440532013000';
$bankAccount->bic = 'COBADEFFXXX';
$bankAccount->holder = 'Test Holder';
$formData->booking->bankAccount = $bankAccount;
return $formData;
}
private function createFormDataWithoutBankAccount(): BookingDto
{
$formData = $this->createCompleteFormData();
$formData->booking->bankAccount = null;
return $formData;
}
private function createFormDataWithRooms(): BookingDto
{
$formData = $this->createCompleteFormData();
$room = new Room();
$room->id = 1;
$room->category = 'DOUBLE';
$room->boardId = 1;
$room->dateFrom = new \DateTimeImmutable('2024-01-01');
$room->dateTo = new \DateTimeImmutable('2024-01-07');
$room->totalCount = 2;
$room->mapping = [0, 1];
$formData->booking->rooms = [$room];
return $formData;
}
private function createFormDataWithoutExistingCommunication(): BookingDto
{
$formData = $this->createCompleteFormData();
$participant = $formData->participants[0];
$participant->email = '[email protected]';
$participant->mobile = '+49987654321';
$formData->booking->participants[0]->communication = new Communication();
return $formData;
}
private function createFormDataWithoutPickups(): BookingDto
{
$formData = $this->createCompleteFormData();
$formData->booking->pickups = [];
return $formData;
}
private function createMockBooking(): Booking
{
$booking = new Booking();
$booking->id = 123;
$booking->status = 'ACTIVE';
$booking->agencyId = 1;
$booking->dateId = 1;
$booking->hotelId = 1;
$booking->paymentId = '1';
$booking->paymentLabel = 'Credit Card';
$booking->paymentType = 'CC';
$booking->additionalServices = [];
$booking->transportationServices = [];
$booking->pickups = [];
$booking->dropOffs = [];
$booking->participants = [
$this->createMockPersonalData('Participant0'),
$this->createMockPersonalData('Participant1'),
];
$booking->participantsStatus = ['F', 'F'];
$booking->applicant = $this->createMockPersonalData('Applicant');
$booking->bankAccount = null;
$booking->rooms = [];
return $booking;
}
private function createMockTravel(): Travel
{
$travel = new Travel();
$travel->additionalServices = [
1 => $this->createMockService(1),
2 => $this->createMockService(2),
];
$travel->transportationServices = [
1 => $this->createMockService(1),
2 => $this->createMockService(2),
];
return $travel;
}
private function createMockParticipantDto(int $index, string $status): ParticipantDto
{
$participant = new ParticipantDto();
$participant->index = $index;
$participant->status = $status;
$participant->firstName = "Participant{$index}";
$participant->lastName = 'LastName';
$participant->dateOfBirth = new \DateTimeImmutable('1990-01-01');
$participant->gender = 'M';
$participant->nationality = 'DE';
$participant->height = '175';
$participant->weight = '70';
$participant->shoeSize = '40';
$participant->email = null;
$participant->mobile = null;
$participant->courses = [];
$participant->additionalServices = [];
$participant->skiPass = null;
$participant->board = [];
$participant->rentals = [];
$participant->transportationOutbound = $this->createMockService(1);
$participant->transportationInbound = $this->createMockService(2);
$participant->pickup = null;
return $participant;
}
private function createMockPersonalData(string $firstName): PersonalData
{
$personalData = new PersonalData();
$personalData->firstName = $firstName;
$personalData->name = 'LastName';
$personalData->dateOfBirth = new \DateTimeImmutable('1990-01-01');
$personalData->gender = 'M';
$personalData->nationality = 'DE';
$personalData->height = '175';
$personalData->weight = '70';
$personalData->shoeSize = '40';
$personalData->address = new Address();
$personalData->communication = new Communication();
return $personalData;
}
private function createMockService(int $id): Service
{
$service = new Service();
$service->id = $id;
$service->price = 50.0;
$service->mapping = [];
$service->individualPrice = [];
$service->subType = 'STANDARD';
return $service;
}
private function createMockPickup(int $id): Pickup
{
$pickup = new Pickup();
$pickup->id = $id;
$pickup->mapping = [];
return $pickup;
}
public function testCanceledParticipantEmptyNamesFilledForAgencyBooking(): void
{
$formData = $this->createFormDataWithCanceledParticipantEmptyNames();
$this->processor->createUpdateRequestPayload($formData);
// Canceled participant (index 1) should have default names filled
$canceledParticipant = $formData->booking->participants[1];
$this->assertEquals('Teilnehmer:in 2', $canceledParticipant->firstName);
$this->assertEquals('Teilnehmer:in 2', $canceledParticipant->name);
// Active participant (index 0) should retain original names from form
$activeParticipant = $formData->booking->participants[0];
$this->assertEquals('Participant0', $activeParticipant->firstName);
$this->assertEquals('LastName', $activeParticipant->name);
}
public function testCanceledParticipantNamesNotFilledForNonAgencyBooking(): void
{
$formData = $this->createFormDataWithCanceledParticipantEmptyNamesNonAgency();
$this->processor->createUpdateRequestPayload($formData);
// Canceled participant should retain empty names (non-agency booking)
$canceledParticipant = $formData->booking->participants[1];
$this->assertEquals('', $canceledParticipant->firstName);
$this->assertEquals('', $canceledParticipant->name);
}
public function testConfirmedParticipantEmptyNamesFilledForAgencyBooking(): void
{
$formData = $this->createFormDataWithConfirmedParticipantEmptyNames();
$this->processor->createUpdateRequestPayload($formData);
// Confirmed participant (index 1) with empty names should have default names filled
$participant = $formData->booking->participants[1];
$this->assertEquals('Teilnehmer:in 2', $participant->firstName);
$this->assertEquals('Teilnehmer:in 2', $participant->name);
// Active participant (index 0) should retain original names from form
$activeParticipant = $formData->booking->participants[0];
$this->assertEquals('Participant0', $activeParticipant->firstName);
$this->assertEquals('LastName', $activeParticipant->name);
}
public function testConfirmedParticipantPartialEmptyNameFilledForAgencyBooking(): void
{
$formData = $this->createFormDataWithConfirmedParticipantPartialEmptyName();
$this->processor->createUpdateRequestPayload($formData);
// Participant with lastName but empty firstName should get default firstName only
$participant = $formData->booking->participants[1];
$this->assertEquals('Teilnehmer:in 2', $participant->firstName);
$this->assertEquals('Schnekenburger', $participant->name);
}
private function createFormDataWithConfirmedParticipantEmptyNames(): BookingDto
{
$bookingDto = new BookingDto($this->createMockTravel(), 1);
$bookingDto->booking = $this->createMockBookingWithEmptyNameParticipant('F', '', '');
$bookingDto->agencyCode = '0004';
$bookingDto->participants = [
$this->createMockParticipantDto(0, 'F'),
$this->createMockParticipantDtoWithEmptyNames(1, 'F'),
];
return $bookingDto;
}
private function createFormDataWithConfirmedParticipantPartialEmptyName(): BookingDto
{
$bookingDto = new BookingDto($this->createMockTravel(), 1);
$bookingDto->booking = $this->createMockBookingWithEmptyNameParticipant('F', '', 'Schnekenburger');
$bookingDto->agencyCode = '0004';
$bookingDto->participants = [
$this->createMockParticipantDto(0, 'F'),
$this->createMockParticipantDtoWithPartialName(1, 'F', '', 'Schnekenburger'),
];
return $bookingDto;
}
private function createMockBookingWithEmptyNameParticipant(
string $status,
string $firstName,
string $lastName,
): Booking {
$booking = new Booking();
$booking->id = 123;
$booking->status = 'ACTIVE';
$booking->agencyId = 999;
$booking->dateId = 1;
$booking->hotelId = 1;
$booking->paymentId = '1';
$booking->paymentLabel = 'Credit Card';
$booking->paymentType = 'CC';
$booking->additionalServices = [];
$booking->transportationServices = [];
$booking->pickups = [];
$booking->dropOffs = [];
$emptyNameParticipant = new PersonalData();
$emptyNameParticipant->firstName = $firstName;
$emptyNameParticipant->name = $lastName;
$emptyNameParticipant->dateOfBirth = new \DateTimeImmutable('1990-01-01');
$emptyNameParticipant->gender = 'M';
$emptyNameParticipant->nationality = 'DE';
$emptyNameParticipant->address = new Address();
$emptyNameParticipant->communication = new Communication();
$booking->participants = [
$this->createMockPersonalData('Participant0'),
$emptyNameParticipant,
];
$booking->participantsStatus = ['F', $status];
$booking->applicant = $this->createMockPersonalData('Applicant');
$booking->bankAccount = null;
$booking->rooms = [];
return $booking;
}
private function createMockParticipantDtoWithPartialName(
int $index,
string $status,
string $firstName,
string $lastName,
): ParticipantDto {
$participant = new ParticipantDto();
$participant->index = $index;
$participant->status = $status;
$participant->firstName = $firstName;
$participant->lastName = $lastName;
$participant->dateOfBirth = new \DateTimeImmutable('1990-01-01');
$participant->gender = 'M';
$participant->nationality = 'DE';
$participant->height = '175';
$participant->weight = '70';
$participant->shoeSize = '40';
$participant->email = null;
$participant->mobile = null;
$participant->courses = [];
$participant->additionalServices = [];
$participant->skiPass = null;
$participant->board = [];
$participant->rentals = [];
$participant->transportationOutbound = $this->createMockService(1);
$participant->transportationInbound = $this->createMockService(2);
$participant->pickup = null;
return $participant;
}
private function createFormDataWithCanceledParticipantEmptyNames(): BookingDto
{
$bookingDto = new BookingDto($this->createMockTravel(), 1);
$bookingDto->booking = $this->createMockBookingWithCanceledParticipantEmptyNames();
$bookingDto->agencyCode = '0004'; // Internal agency code to trigger isInternalAgencyBooking()
$bookingDto->participants = [
$this->createMockParticipantDto(0, 'F'),
$this->createMockParticipantDtoWithEmptyNames(1, 'S'),
];
return $bookingDto;
}
private function createFormDataWithCanceledParticipantEmptyNamesNonAgency(): BookingDto
{
$bookingDto = new BookingDto($this->createMockTravel(), 1);
$bookingDto->booking = $this->createMockBookingWithCanceledParticipantEmptyNames();
$bookingDto->agencyCode = null; // No agency - not an internal agency booking
$bookingDto->participants = [
$this->createMockParticipantDto(0, 'F'),
$this->createMockParticipantDtoWithEmptyNames(1, 'S'),
];
return $bookingDto;
}
private function createMockBookingWithCanceledParticipantEmptyNames(): Booking
{
$booking = new Booking();
$booking->id = 123;
$booking->status = 'ACTIVE';
$booking->agencyId = 999;
$booking->dateId = 1;
$booking->hotelId = 1;
$booking->paymentId = '1';
$booking->paymentLabel = 'Credit Card';
$booking->paymentType = 'CC';
$booking->additionalServices = [];
$booking->transportationServices = [];
$booking->pickups = [];
$booking->dropOffs = [];
$canceledParticipant = new PersonalData();
$canceledParticipant->firstName = '';
$canceledParticipant->name = '';
$canceledParticipant->dateOfBirth = new \DateTimeImmutable('1990-01-01');
$canceledParticipant->gender = 'M';
$canceledParticipant->nationality = 'DE';
$canceledParticipant->address = new Address();
$canceledParticipant->communication = new Communication();
$booking->participants = [
$this->createMockPersonalData('Participant0'),
$canceledParticipant,
];
$booking->participantsStatus = ['F', 'S']; // Second participant is canceled
$booking->applicant = $this->createMockPersonalData('Applicant');
$booking->bankAccount = null;
$booking->rooms = [];
return $booking;
}
private function createMockParticipantDtoWithEmptyNames(int $index, string $status): ParticipantDto
{
$participant = new ParticipantDto();
$participant->index = $index;
$participant->status = $status;
$participant->firstName = '';
$participant->lastName = '';
$participant->dateOfBirth = new \DateTimeImmutable('1990-01-01');
$participant->gender = 'M';
$participant->nationality = 'DE';
$participant->height = '175';
$participant->weight = '70';
$participant->shoeSize = '40';
$participant->email = null;
$participant->mobile = null;
$participant->courses = [];
$participant->additionalServices = [];
$participant->skiPass = null;
$participant->board = [];
$participant->rentals = [];
$participant->transportationOutbound = $this->createMockService(1);
$participant->transportationInbound = $this->createMockService(2);
$participant->pickup = null;
return $participant;
}
/**
* Regression: family insurance covers the whole family under a single policy on the
* applicant, so dependents must never keep their own insurance selection - even when
* the applicant never checked the "book for everyone" (bulkInsuranceBooking) box.
*/
public function testFamilyInsuranceClearsDependentInsuranceWithoutBulkCheckbox(): void
{
$familyInsurance = new Insurance();
$familyInsurance->id = 'family-1';
$familyInsurance->familyInsurance = true;
$applicant = $this->createMockParticipantDto(0, 'F');
$applicant->bulkInsuranceBooking = false;
$applicant->insurance = $familyInsurance;
$dependentInsurance = new Insurance();
$dependentInsurance->id = 'dependent-own-1';
$dependentInsurance->familyInsurance = false;
$dependent = $this->createMockParticipantDto(1, 'F');
$dependent->insurance = $dependentInsurance;
$bookingDto = new BookingDto($this->createMockTravel(), 1);
$bookingDto->participants = [$applicant, $dependent];
$insuranceService = $this->createMock(InsuranceManager::class);
// Both insurances must already look eligible, otherwise the submit-time
// reconciliation step (added for stale-insurance re-validation) would clear
// them before propagation even runs - this test is only about propagation.
$insuranceService->method('getSelectableInsurances')->willReturn([$familyInsurance, $dependentInsurance]);
$insuranceService->method('getEligibleInsurances')->willReturnCallback(
fn (array $insurances) => $insurances
);
$insuranceService->method('batchAssignInsuranceToParticipants')->willReturn([
0 => $familyInsurance,
1 => null,
]);
$priceCalculatorService = $this->createMock(BookingPriceCalculator::class);
$priceCalculatorService->method('calculateIndividualParticipantPriceExcludingInsurance')->willReturn(0.0);
$priceCalculatorService->method('resolveInsuranceTravelPrice')->willReturn(0.0);
$payloadBuilder = $this->createMock(BookingPayloadBuilder::class);
$payloadBuilder->method('buildCreatePayload')->willReturn([]);
$processor = new BookingDataProcessor(
$insuranceService,
$priceCalculatorService,
new ServiceMappingCollector(),
new ParticipantServiceProcessor(new NullLogger()),
$payloadBuilder,
new PersonalDataSynchronizer(),
);
$processor->createBookingRequestPayload($bookingDto, 'ANFRAGE');
$this->assertSame($familyInsurance, $bookingDto->participants[0]->insurance);
$this->assertNull($bookingDto->participants[1]->insurance, 'Dependent must lose their own insurance once the applicant selects family insurance');
}
/**
* Regression: the edit-mode guard that protects an individually-locked non-family
* insurance from being overwritten must not also block clearing a dependent's
* pre-existing insurance when the applicant switches to family insurance.
*/
public function testFamilyInsuranceClearsDependentInsuranceInEditMode(): void
{
$familyInsurance = new Insurance();
$familyInsurance->id = 'family-1';
$familyInsurance->familyInsurance = true;
$formData = $this->createCompleteFormData();
$applicant = $formData->participants[0];
$applicant->bulkInsuranceBooking = false;
$applicant->insurance = $familyInsurance;
$dependentInsurance = new Insurance();
$dependentInsurance->id = 'dependent-own-1';
$dependentInsurance->familyInsurance = false;
$dependent = $formData->participants[1];
$dependent->insurance = $dependentInsurance;
$insuranceService = $this->createMock(InsuranceManager::class);
$insuranceService->method('getSelectableInsurances')->willReturn([]);
$insuranceService->method('batchAssignInsuranceToParticipants')->willReturn([
0 => $familyInsurance,
1 => null,
]);
$priceCalculatorService = $this->createMock(BookingPriceCalculator::class);
$priceCalculatorService->method('calculateIndividualParticipantPriceExcludingInsurance')->willReturn(0.0);
$processor = new BookingDataProcessor(
$insuranceService,
$priceCalculatorService,
new ServiceMappingCollector(),
new ParticipantServiceProcessor(new NullLogger()),
new BookingPayloadBuilder(new ServiceMappingCollector()),
new PersonalDataSynchronizer(),
);
$processor->createUpdateRequestPayload($formData);
$this->assertSame($familyInsurance, $formData->participants[0]->insurance);
$this->assertNull($formData->participants[1]->insurance, 'Dependent must lose their pre-existing individual insurance in edit mode once the applicant selects family insurance');
}
/**
* Regression: an insurance selected earlier can go stale by submission time (e.g. its
* price tier no longer matches the current total, because the user added services on an
* earlier step after choosing insurance and jumped straight to submission without
* revisiting the insurance field). createBookingRequestPayload() must re-validate and
* reassign to the correct tier right before the payload is built, so a stale ID never
* reaches the BPN API.
*/
public function testCreateBookingRequestPayloadReassignsStaleInsuranceToCurrentPriceTier(): void
{
$staleInsurance = new Insurance();
$staleInsurance->id = 'tier-low';
$staleInsurance->familyInsurance = false;
$correctTierInsurance = new Insurance();
$correctTierInsurance->id = 'tier-high';
$correctTierInsurance->familyInsurance = false;
$applicant = $this->createMockParticipantDto(0, 'F');
$applicant->insurance = $staleInsurance;
$bookingDto = new BookingDto($this->createMockTravel(), 1);
$bookingDto->participants = [$applicant];
$insuranceService = $this->createMock(InsuranceManager::class);
$insuranceService->method('getSelectableInsurances')->willReturn([$staleInsurance, $correctTierInsurance]);
// Only the higher tier is eligible at the current (updated) price.
$insuranceService->method('getEligibleInsurances')->willReturn([$correctTierInsurance]);
$insuranceService->method('reassignInsuranceForPriceChange')->willReturn($correctTierInsurance);
$insuranceService->method('batchAssignInsuranceToParticipants')->willReturn([0 => $correctTierInsurance]);
$priceCalculatorService = $this->createMock(BookingPriceCalculator::class);
$priceCalculatorService->method('calculateIndividualParticipantPriceExcludingInsurance')->willReturn(0.0);
$priceCalculatorService->method('resolveInsuranceTravelPrice')->willReturn(2600.0);
$payloadBuilder = $this->createMock(BookingPayloadBuilder::class);
$payloadBuilder->method('buildCreatePayload')->willReturn([]);
$processor = new BookingDataProcessor(
$insuranceService,
$priceCalculatorService,
new ServiceMappingCollector(),
new ParticipantServiceProcessor(new NullLogger()),
$payloadBuilder,
new PersonalDataSynchronizer(),
);
$processor->createBookingRequestPayload($bookingDto, 'ANFRAGE');
$this->assertSame($correctTierInsurance, $applicant->insurance);
$this->assertCount(1, $applicant->notifications);
$this->assertSame('warning', array_values($applicant->notifications)[0]['type']);
}
/**
* Regression: when a stale insurance has no eligible replacement at all (e.g. the total
* price now falls outside every tier of that type), it must be cleared rather than
* silently left in place and submitted anyway.
*/
public function testCreateBookingRequestPayloadClearsStaleInsuranceWithoutEligibleReplacement(): void
{
$staleInsurance = new Insurance();
$staleInsurance->id = 'tier-low';
$staleInsurance->familyInsurance = false;
$applicant = $this->createMockParticipantDto(0, 'F');
$applicant->insurance = $staleInsurance;
$bookingDto = new BookingDto($this->createMockTravel(), 1);
$bookingDto->participants = [$applicant];
$insuranceService = $this->createMock(InsuranceManager::class);
$insuranceService->method('getSelectableInsurances')->willReturn([$staleInsurance]);
$insuranceService->method('getEligibleInsurances')->willReturn([]);
$insuranceService->method('reassignInsuranceForPriceChange')->willReturn(null);
$insuranceService->method('batchAssignInsuranceToParticipants')->willReturn([0 => null]);
$priceCalculatorService = $this->createMock(BookingPriceCalculator::class);
$priceCalculatorService->method('calculateIndividualParticipantPriceExcludingInsurance')->willReturn(0.0);
$priceCalculatorService->method('resolveInsuranceTravelPrice')->willReturn(9999.0);
$payloadBuilder = $this->createMock(BookingPayloadBuilder::class);
$payloadBuilder->method('buildCreatePayload')->willReturn([]);
$processor = new BookingDataProcessor(
$insuranceService,
$priceCalculatorService,
new ServiceMappingCollector(),
new ParticipantServiceProcessor(new NullLogger()),
$payloadBuilder,
new PersonalDataSynchronizer(),
);
$processor->createBookingRequestPayload($bookingDto, 'ANFRAGE');
$this->assertNull($applicant->insurance);
$this->assertCount(1, $applicant->notifications);
$this->assertSame('warning', array_values($applicant->notifications)[0]['type']);
}
/**
* Regression: an insurance that's still eligible at submission time must be left
* untouched - the reconciliation step must not fire (or notify) on the common,
* unremarkable case where nothing has gone stale.
*/
public function testCreateBookingRequestPayloadLeavesStillEligibleInsuranceUntouched(): void
{
$insurance = new Insurance();
$insurance->id = 'tier-1';
$insurance->familyInsurance = false;
$applicant = $this->createMockParticipantDto(0, 'F');
$applicant->insurance = $insurance;
$bookingDto = new BookingDto($this->createMockTravel(), 1);
$bookingDto->participants = [$applicant];
$insuranceService = $this->createMock(InsuranceManager::class);
$insuranceService->method('getSelectableInsurances')->willReturn([$insurance]);
$insuranceService->method('getEligibleInsurances')->willReturn([$insurance]);
$insuranceService->method('batchAssignInsuranceToParticipants')->willReturn([0 => $insurance]);
$priceCalculatorService = $this->createMock(BookingPriceCalculator::class);
$priceCalculatorService->method('calculateIndividualParticipantPriceExcludingInsurance')->willReturn(0.0);
$priceCalculatorService->method('resolveInsuranceTravelPrice')->willReturn(500.0);
$payloadBuilder = $this->createMock(BookingPayloadBuilder::class);
$payloadBuilder->method('buildCreatePayload')->willReturn([]);
$processor = new BookingDataProcessor(
$insuranceService,
$priceCalculatorService,
new ServiceMappingCollector(),
new ParticipantServiceProcessor(new NullLogger()),
$payloadBuilder,
new PersonalDataSynchronizer(),
);
$processor->createBookingRequestPayload($bookingDto, 'ANFRAGE');
$this->assertSame($insurance, $applicant->insurance);
$this->assertCount(0, $applicant->notifications);
}
}