448 lines
22 KiB
PHP
448 lines
22 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\BusProNet\DataProcessor;
|
|
|
|
use App\BusProNet\Constants;
|
|
use App\BusProNet\Model\Booking;
|
|
use App\BusProNet\Model\Travel;
|
|
use App\BusProNet\Utility\DirectionMapper;
|
|
use App\BusProNet\XmlLoader\AgencyLoader;
|
|
use App\Form\Model\BankAccountDto;
|
|
use App\Form\Model\BookingDto;
|
|
use App\Form\Model\ParticipantDto;
|
|
use App\Form\Model\RoomSelectionDto;
|
|
use App\Service\BookingPriceCalculator;
|
|
use App\Service\InsuranceManager;
|
|
use function Symfony\Component\String\u;
|
|
|
|
/**
|
|
* Processes booking form data and converts it into BusProNet API payload format.
|
|
*
|
|
* This processor orchestrates the transformation of booking data between DTO and API formats.
|
|
* It delegates to specialized classes for service mapping, participant processing,
|
|
* payload building, and personal data synchronization.
|
|
*/
|
|
class BookingDataProcessor
|
|
{
|
|
public function __construct(
|
|
private readonly InsuranceManager $insuranceService,
|
|
private readonly BookingPriceCalculator $priceCalculatorService,
|
|
private readonly ServiceMappingCollector $mappingCollector,
|
|
private readonly ParticipantServiceProcessor $serviceProcessor,
|
|
private readonly BookingPayloadBuilder $payloadBuilder,
|
|
private readonly PersonalDataSynchronizer $personalDataSynchronizer,
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* Creates a BookingDto from an existing Booking entity (for edit mode).
|
|
*
|
|
* Extracts all service selections from the booking entity and assigns them
|
|
* to participant DTOs, creating a unified data structure identical to create mode.
|
|
*
|
|
* IMPORTANT: Address handling for edit mode
|
|
* - ParticipantDto::fromPersonalData() clones address objects to prevent shared references
|
|
* - For normal bookings, participant[0]'s missing address/dimensions are filled from the
|
|
* applicant (same person in normal bookings, but BPN may omit fields in <teilnehmer>)
|
|
* - For internal agency bookings, this copy is skipped: the applicant is a customer, not
|
|
* the first participant, so copying applicant data into participant[0] would be incorrect
|
|
*/
|
|
public function createBookingDtoFromBooking(Booking $booking, Travel $travel, bool $isInternalAgencyBooking = false): BookingDto
|
|
{
|
|
$dto = new BookingDto($travel, $booking->hotelId);
|
|
|
|
$dto->booking = $booking;
|
|
$dto->agencyId = $booking->agencyId;
|
|
|
|
// Map payment ID from API to form payment method
|
|
$dto->paymentMethod = match ($booking->paymentId) {
|
|
(string) Constants::PAYMENT_TYPE_ID_DEBIT => Constants::PAYMENT_METHOD_DEBIT,
|
|
(string) Constants::PAYMENT_TYPE_ID_TRANSFER => Constants::PAYMENT_METHOD_TRANSFER,
|
|
default => Constants::PAYMENT_METHOD_TRANSFER,
|
|
};
|
|
|
|
if (null !== $booking->bankAccount) {
|
|
$dto->bankAccount = BankAccountDto::fromBankAccount($booking->bankAccount);
|
|
}
|
|
|
|
foreach ($booking->participants as $index => $participant) {
|
|
$participantData = ParticipantDto::fromPersonalData($participant);
|
|
$participantData->index = $index;
|
|
|
|
// For normal bookings, applicant === participant[0]: copy address and body dimensions
|
|
// from applicant when BPN returns full data only in <anmelder> and minimal data in
|
|
// <teilnehmer id="1">. Skipped for internal agency bookings where the applicant is
|
|
// a customer (a different entity from the first traveler).
|
|
if (0 === $index && false === $isInternalAgencyBooking) {
|
|
// Copy address if first participant has no street (indicating empty/incomplete address)
|
|
$isEmpty = null === $participantData->address
|
|
|| null === $participantData->address->street
|
|
|| u($participantData->address->street)->trim()->isEmpty();
|
|
|
|
if ($isEmpty) {
|
|
$participantData->address = clone $booking->applicant->address;
|
|
}
|
|
|
|
// Copy body dimensions from applicant if not present in participant
|
|
if (null === $participantData->height && null !== $booking->applicant->height) {
|
|
$participantData->height = $booking->applicant->height;
|
|
}
|
|
if (null === $participantData->weight && null !== $booking->applicant->weight) {
|
|
$participantData->weight = $booking->applicant->weight;
|
|
}
|
|
if (null === $participantData->shoeSize && null !== $booking->applicant->shoeSize) {
|
|
$participantData->shoeSize = $booking->applicant->shoeSize;
|
|
}
|
|
}
|
|
|
|
// Extract service selections from booking and assign to participant DTO
|
|
$participantData->courses = $booking
|
|
->getAdditionalServicesForParticipantByGroup($index, Constants::TOKEN_COURSES);
|
|
|
|
$participantData->skiPass = $booking->getSkiPassForParticipant($index);
|
|
|
|
$participantData->additionalServices = $booking
|
|
->getAdditionalServicesForParticipantByGroup($index, Constants::TOKEN_ADDITIONAL);
|
|
|
|
$participantData->board = $booking
|
|
->getAdditionalServicesForParticipantByGroup($index, Constants::TOKEN_BOARD);
|
|
|
|
$vegServices = $booking->getAdditionalServicesForParticipantByGroup($index, Constants::TOKEN_VEG);
|
|
$participantData->veg = false === empty($vegServices) ? reset($vegServices) : null;
|
|
|
|
$participantData->rentals = $booking
|
|
->getAdditionalServicesForParticipantByGroup($index, Constants::TOKEN_RENTALS);
|
|
|
|
// Rental insurance (single service, not array)
|
|
$participantData->rentalInsurance = $booking->getRentalInsuranceForParticipant($index);
|
|
$participantData->rentalInsuranceSelected = null !== $participantData->rentalInsurance;
|
|
|
|
// Transportation services
|
|
$participantData->transportationOutbound = $booking
|
|
->getTransportationServiceForParticipantAndDirection($index, DirectionMapper::OUTBOUND_BOOKING);
|
|
|
|
$participantData->transportationInbound = $booking
|
|
->getTransportationServiceForParticipantAndDirection($index, DirectionMapper::INBOUND_BOOKING);
|
|
|
|
// Pickup location
|
|
$participantData->pickup = $booking->getPickupForParticipant($index);
|
|
|
|
// Drop-off location
|
|
$participantData->dropOff = $booking->getDropOffForParticipant($index);
|
|
$participantData->differentDropOff = null !== $participantData->dropOff;
|
|
|
|
// Parking service (for self-organized PKW transportation)
|
|
$parkingServices = $booking->getAdditionalServicesForParticipantByGroup($index, Constants::TOKEN_PARKING);
|
|
if (false === empty($parkingServices)) {
|
|
$participantData->parking = true;
|
|
$participantData->parkingService = reset($parkingServices);
|
|
} else {
|
|
$participantData->parking = false;
|
|
$participantData->parkingService = null;
|
|
}
|
|
|
|
// License plate (not stored in booking entity)
|
|
$participantData->licensePlate = null;
|
|
|
|
// Insurance
|
|
$participantData->insurance = $booking->getInsuranceForParticipant($index);
|
|
|
|
// Room assignment
|
|
$room = $booking->getRoomForParticipant($index);
|
|
$participantData->assignedRoomId = $room?->id;
|
|
|
|
// Enrich services with data from travel (especially prices)
|
|
$this->enrichParticipantServicesFromTravel($participantData, $travel);
|
|
|
|
// Normalize loaded participant data so the edit flow sees canonical values.
|
|
$participantData->normalizeLoadedData();
|
|
|
|
$dto->participants[$index] = $participantData;
|
|
}
|
|
|
|
// Rebuild roomSelections array from booking room data
|
|
// This is needed for conditions like SingleRoomTypeCondition that rely on roomSelections
|
|
// The booking already contains rooms with actual quantities (anzahl from XML)
|
|
$availableRooms = $travel->getAvailableRooms();
|
|
foreach ($booking->rooms as $bookingRoom) {
|
|
$room = $availableRooms[$bookingRoom->id] ?? null;
|
|
if (null !== $room && null !== $bookingRoom->totalCount && $bookingRoom->totalCount > 0) {
|
|
$roomSelection = new RoomSelectionDto();
|
|
$roomSelection->id = $bookingRoom->id;
|
|
$roomSelection->label = $room->label;
|
|
$roomSelection->price = $room->price;
|
|
$roomSelection->quantity = $bookingRoom->totalCount; // Actual quantity from booking XML (anzahl)
|
|
$roomSelection->capacity = $room->minPax ?? 1;
|
|
$roomSelection->maxQuantity = $bookingRoom->totalCount + ($room->available ?? 0);
|
|
$dto->roomSelections[] = $roomSelection;
|
|
}
|
|
}
|
|
|
|
return $dto;
|
|
}
|
|
|
|
/**
|
|
* Enriches participant service selections with data from travel model.
|
|
*
|
|
* Services extracted from booking API responses might not include all necessary data
|
|
* (especially prices). This method looks up each service in the travel data and copies
|
|
* over missing properties to ensure proper pricing calculations.
|
|
*
|
|
* @param ParticipantDto $participant The participant with service selections
|
|
* @param Travel $travel The travel data containing full service information
|
|
*/
|
|
private function enrichParticipantServicesFromTravel(ParticipantDto $participant, Travel $travel): void
|
|
{
|
|
// Enrich courses
|
|
foreach ($participant->courses as $key => $course) {
|
|
if (isset($travel->additionalServices[$course->id])) {
|
|
$participant->courses[$key] = $travel->additionalServices[$course->id];
|
|
}
|
|
}
|
|
|
|
// Enrich ski pass
|
|
if (null !== $participant->skiPass && isset($travel->additionalServices[$participant->skiPass->id])) {
|
|
$participant->skiPass = $travel->additionalServices[$participant->skiPass->id];
|
|
}
|
|
|
|
// Enrich additional services
|
|
foreach ($participant->additionalServices as $key => $service) {
|
|
if (isset($travel->additionalServices[$service->id])) {
|
|
$participant->additionalServices[$key] = $travel->additionalServices[$service->id];
|
|
}
|
|
}
|
|
|
|
// Enrich board
|
|
foreach ($participant->board as $key => $board) {
|
|
if (isset($travel->additionalServices[$board->id])) {
|
|
$participant->board[$key] = $travel->additionalServices[$board->id];
|
|
}
|
|
}
|
|
|
|
// Enrich veg
|
|
if (null !== $participant->veg && isset($travel->additionalServices[$participant->veg->id])) {
|
|
$participant->veg = $travel->additionalServices[$participant->veg->id];
|
|
}
|
|
|
|
// Enrich rentals
|
|
foreach ($participant->rentals as $key => $rental) {
|
|
if (isset($travel->additionalServices[$rental->id])) {
|
|
$participant->rentals[$key] = $travel->additionalServices[$rental->id];
|
|
}
|
|
}
|
|
|
|
// Enrich rental insurance
|
|
if (null !== $participant->rentalInsurance && isset($travel->additionalServices[$participant->rentalInsurance->id])) {
|
|
$participant->rentalInsurance = $travel->additionalServices[$participant->rentalInsurance->id];
|
|
}
|
|
|
|
// Enrich parking service
|
|
if (null !== $participant->parkingService && isset($travel->additionalServices[$participant->parkingService->id])) {
|
|
$participant->parkingService = $travel->additionalServices[$participant->parkingService->id];
|
|
}
|
|
|
|
// Enrich transportation services
|
|
if (null !== $participant->transportationOutbound && isset($travel->transportationServices[$participant->transportationOutbound->id])) {
|
|
$participant->transportationOutbound = $travel->transportationServices[$participant->transportationOutbound->id];
|
|
}
|
|
|
|
if (null !== $participant->transportationInbound && isset($travel->transportationServices[$participant->transportationInbound->id])) {
|
|
$participant->transportationInbound = $travel->transportationServices[$participant->transportationInbound->id];
|
|
}
|
|
|
|
// Enrich pickup with data from travel (includes both outbound and inbound prices)
|
|
// The pickups already have inbound prices merged in from TravelParser
|
|
if (null !== $participant->pickup && isset($travel->pickups[$participant->pickup->id])) {
|
|
$enrichedPickup = $travel->pickups[$participant->pickup->id];
|
|
|
|
// Copy all properties from enriched pickup to participant's pickup
|
|
$participant->pickup->price = $enrichedPickup->price;
|
|
$participant->pickup->priceOutbound = $enrichedPickup->priceOutbound;
|
|
$participant->pickup->priceInbound = $enrichedPickup->priceInbound;
|
|
$participant->pickup->time = $enrichedPickup->time;
|
|
$participant->pickup->city = $enrichedPickup->city;
|
|
$participant->pickup->street = $enrichedPickup->street;
|
|
$participant->pickup->postalCode = $enrichedPickup->postalCode;
|
|
$participant->pickup->code = $enrichedPickup->code;
|
|
}
|
|
|
|
// Enrich drop-off with data from travel
|
|
if (null !== $participant->dropOff && isset($travel->dropOffs[$participant->dropOff->id])) {
|
|
$enrichedDropOff = $travel->dropOffs[$participant->dropOff->id];
|
|
$participant->dropOff->price = $enrichedDropOff->price;
|
|
$participant->dropOff->priceOutbound = $enrichedDropOff->priceOutbound;
|
|
$participant->dropOff->priceInbound = $enrichedDropOff->priceInbound;
|
|
$participant->dropOff->time = $enrichedDropOff->time;
|
|
$participant->dropOff->city = $enrichedDropOff->city;
|
|
$participant->dropOff->street = $enrichedDropOff->street;
|
|
$participant->dropOff->postalCode = $enrichedDropOff->postalCode;
|
|
$participant->dropOff->code = $enrichedDropOff->code;
|
|
}
|
|
|
|
// Enrich insurance
|
|
if (null !== $participant->insurance && isset($travel->insurances[$participant->insurance->id])) {
|
|
$participant->insurance = $travel->insurances[$participant->insurance->id];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates an update request payload for the BusProNet API from booking form data.
|
|
*
|
|
* This method processes booking edit form data and transforms it into the structured array
|
|
* format expected by the BusProNet XML API. It handles service mappings, participant data
|
|
* updates, and generates the complete payload structure including booking metadata,
|
|
* participant information, services, transportation, and accommodation details.
|
|
*
|
|
* @param BookingDto|null $formData The booking edit form data containing updated participant and service selections
|
|
*
|
|
* @return array<string, mixed> The structured payload array ready for BusProNet API submission
|
|
*/
|
|
public function createUpdateRequestPayload(?BookingDto $formData): array
|
|
{
|
|
// Apply bulk or family insurance if applicable (modifies DTO in place)
|
|
$this->applyApplicantInsuranceToParticipants($formData);
|
|
|
|
$bookingData = $formData->booking;
|
|
$travelData = $formData->travel;
|
|
|
|
// CRITICAL: The applicant address may have been lost during form binding because ParticipantDto::fromPersonalData()
|
|
// stores a reference to the address object, and Symfony's form binding can modify it in place.
|
|
// Since we can't easily restore it here without the original booking, we rely on the EditController
|
|
// to preserve the original booking's applicant data by fetching fresh data before calling updateBooking().
|
|
// The applicant data should NEVER be modified in edit mode.
|
|
|
|
$this->serviceProcessor->resetServiceMappings($bookingData);
|
|
|
|
foreach ($formData->participants as $participant) {
|
|
$this->serviceProcessor->processParticipantServices($participant, $bookingData, $travelData);
|
|
}
|
|
|
|
$this->serviceProcessor->removeUnusedServices($bookingData);
|
|
$this->personalDataSynchronizer->updateParticipantPersonalData(
|
|
$formData->participants,
|
|
$bookingData,
|
|
$formData->isInternalAgencyBooking()
|
|
);
|
|
|
|
$payload = $this->payloadBuilder->buildBasePayload($bookingData);
|
|
$this->payloadBuilder->addBankAccountToPayload($payload, $bookingData);
|
|
$this->payloadBuilder->buildParticipantPayload($payload, $bookingData, $formData->participants);
|
|
$this->payloadBuilder->buildServicesPayload($payload, $bookingData);
|
|
$this->payloadBuilder->buildPickupPayload($payload, $bookingData);
|
|
$this->payloadBuilder->buildDropOffPayload($payload, $bookingData);
|
|
|
|
// Add purchase vouchers if any exist
|
|
$purchaseVouchers = $this->mappingCollector->collectPurchaseVouchers($formData);
|
|
if (false === empty($purchaseVouchers)) {
|
|
$payload['gutscheine']['gutschein'] = [];
|
|
foreach ($purchaseVouchers as $code) {
|
|
$payload['gutscheine']['gutschein'][] = [
|
|
'@einloesecode' => $code,
|
|
];
|
|
}
|
|
}
|
|
|
|
return $payload;
|
|
}
|
|
|
|
/**
|
|
* Creates a booking request payload for new bookings (inquiry or final booking).
|
|
*
|
|
* Generates the array payload structure for creating new bookings through the BusProNet API.
|
|
* This includes all participant data, room selections, services (including insurance), and
|
|
* payment information. The booking type determines whether this is an inquiry validation
|
|
* ('Anfrage') or a final booking commit ('Buchung').
|
|
*
|
|
* @param BookingDto $bookingDto The booking creation form data
|
|
* @param string $bookingType Either 'Anfrage' (inquiry) or 'Buchung' (final booking)
|
|
*
|
|
* @return array<string, mixed> The structured payload array for BusProNet API submission
|
|
*/
|
|
public function createBookingRequestPayload(BookingDto $bookingDto, string $bookingType): array
|
|
{
|
|
// Apply bulk or family insurance if applicable (modifies DTO in place)
|
|
$this->applyApplicantInsuranceToParticipants($bookingDto);
|
|
|
|
return $this->payloadBuilder->buildCreatePayload($bookingDto, $bookingType);
|
|
}
|
|
|
|
/**
|
|
* Applies the applicant's insurance to other participants if their selection requires it.
|
|
*
|
|
* Runs when either:
|
|
* - Bulk insurance is active (applicant's bulkInsuranceBooking = true): assigns the
|
|
* applicant's insurance TYPE to all dependent participants with automatic price tier
|
|
* adjustment based on each participant's individual cost.
|
|
* - The applicant selected a family insurance: family insurance covers the whole family
|
|
* under a single policy, so dependents must never hold their own insurance, regardless
|
|
* of whether the bulk checkbox was checked.
|
|
*
|
|
* IMPORTANT: In edit mode, non-family bulk insurance only applies to participants who:
|
|
* 1. Currently have NO insurance assigned (insurance === null)
|
|
* 2. OR whose insurance was already assigned via previous bulk operation
|
|
*
|
|
* This prevents overriding individually selected insurances that are locked. Family
|
|
* insurance is exempt from this protection: dependents can never legitimately hold
|
|
* their own policy alongside it, so their insurance is always cleared, in edit mode too.
|
|
*
|
|
* @param BookingDto $bookingDto The booking DTO (create or edit flow)
|
|
*/
|
|
private function applyApplicantInsuranceToParticipants(BookingDto $bookingDto): void
|
|
{
|
|
$applicant = $bookingDto->getParticipant(0);
|
|
|
|
if (null === $applicant || null === $applicant->insurance) {
|
|
return;
|
|
}
|
|
|
|
$isFamilyInsurance = true === $applicant->insurance->familyInsurance;
|
|
|
|
// Check if bulk insurance is enabled OR the applicant's selection is a family
|
|
// insurance (which always applies to dependents, independent of the bulk checkbox)
|
|
if (false === $applicant->bulkInsuranceBooking && false === $isFamilyInsurance) {
|
|
return;
|
|
}
|
|
|
|
// Get selectable (non-complementary) insurances with caching
|
|
$selectableInsurances = $this->insuranceService->getSelectableInsurances($bookingDto->travel);
|
|
|
|
// Calculate travel prices for all participants (excluding insurance)
|
|
$participantPrices = [];
|
|
foreach ($bookingDto->getParticipants() as $index => $participant) {
|
|
$participantPrices[$index] = $this->priceCalculatorService->calculateIndividualParticipantPriceExcludingInsurance($bookingDto, $index);
|
|
}
|
|
|
|
// Use InsuranceManager for proper type-based assignment with price tier matching
|
|
$assignments = $this->insuranceService->batchAssignInsuranceToParticipants(
|
|
$selectableInsurances,
|
|
$applicant->insurance,
|
|
$bookingDto,
|
|
$participantPrices
|
|
);
|
|
|
|
// Apply assignments to all participants; for non-family insurance skip the applicant
|
|
// (they already hold the insurance they selected themselves)
|
|
foreach ($assignments as $index => $insurance) {
|
|
if (0 === $index && false === $isFamilyInsurance) {
|
|
continue;
|
|
}
|
|
|
|
$participant = $bookingDto->getParticipant($index);
|
|
if (null === $participant) {
|
|
continue;
|
|
}
|
|
|
|
// In edit mode: non-family bulk assignment must not overwrite an individually
|
|
// locked insurance. Family insurance always overrides, since dependents can
|
|
// never legitimately keep their own policy alongside it.
|
|
if (BookingDto::MODE_EDIT === $bookingDto->getMode() && false === $isFamilyInsurance && null !== $participant->insurance) {
|
|
continue; // Skip participants with existing insurance assignment
|
|
}
|
|
|
|
$participant->insurance = $insurance;
|
|
}
|
|
}
|
|
}
|