1284 lines
55 KiB
PHP
1284 lines
55 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\BusProNet\DataProcessor;
|
|
|
|
use App\BusProNet\Constants;
|
|
use App\BusProNet\Model\Address;
|
|
use App\BusProNet\Model\Booking;
|
|
use App\BusProNet\Model\Communication;
|
|
use App\BusProNet\Model\PersonalData;
|
|
use App\BusProNet\Model\Travel;
|
|
use App\BusProNet\Utility\DirectionMapper;
|
|
use App\Form\Model\BankAccountDto;
|
|
use App\Form\Model\BookingDto;
|
|
use App\Form\Model\ParticipantDto;
|
|
use App\Form\Model\RoomSelectionDto;
|
|
use App\Service\BookingPriceCalculatorService;
|
|
use App\Service\InsuranceService;
|
|
|
|
/**
|
|
* Processes booking form data and converts it into BusProNet API payload format.
|
|
*
|
|
* This processor handles the complex transformation of booking edit form data into the structured
|
|
* array format required by the BusProNet API. It manages service mappings between participants
|
|
* and various booking components like additional services, transportation, and accommodations.
|
|
* The processor ensures data consistency by resetting and rebuilding participant-to-service
|
|
* mappings based on form selections, while maintaining proper API payload structure.
|
|
*/
|
|
class BookingDataProcessor
|
|
{
|
|
public function __construct(
|
|
private readonly InsuranceService $insuranceService,
|
|
private readonly BookingPriceCalculatorService $priceCalculatorService,
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
* - First participant's address is NOT auto-populated from applicant
|
|
* - Template uses placeholders to show applicant's address as hints
|
|
* - This prevents address data from being inadvertently modified during form binding
|
|
*/
|
|
public function createBookingDtoFromBooking(Booking $booking, Travel $travel): 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) {
|
|
/** @var PersonalData $participant */
|
|
$participantData = ParticipantDto::fromPersonalData($participant);
|
|
$participantData->index = $index;
|
|
|
|
// First participant (applicant): copy address from applicant if participant address is empty
|
|
// BPN API may return full address only in <anmelder> but minimal/empty address in <teilnehmer id="1">
|
|
// Only copy if first participant has no street (indicating empty/incomplete address)
|
|
// This allows applicant and first participant to be different people with different addresses
|
|
if (0 === $index && null !== $booking->applicant->address) {
|
|
$isEmpty = null === $participantData->address
|
|
|| null === $participantData->address->street
|
|
|| '' === trim($participantData->address->street);
|
|
|
|
if ($isEmpty) {
|
|
$participantData->address = clone $booking->applicant->address;
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
|
|
$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);
|
|
|
|
// Parking (stored in form data, not in booking entity - needs special handling)
|
|
// For now, leave as false - may need to extract from transportation services
|
|
$participantData->parking = false;
|
|
|
|
// 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);
|
|
|
|
$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->roomId = $bookingRoom->id;
|
|
$roomSelection->roomLabel = $room->label;
|
|
$roomSelection->roomPrice = $room->price;
|
|
$roomSelection->quantity = $bookingRoom->totalCount; // Actual quantity from booking XML (anzahl)
|
|
$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 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 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
|
|
if (null !== $participant->pickup && isset($travel->pickupsOutbound[$participant->pickup->id])) {
|
|
$participant->pickup = $travel->pickupsOutbound[$participant->pickup->id];
|
|
}
|
|
|
|
// 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.
|
|
*
|
|
* The process involves:
|
|
* - Resetting existing service-to-participant mappings
|
|
* - Rebuilding mappings based on current form selections
|
|
* - Adding new services from travel data when participants select them
|
|
* - Removing services with no participant mappings
|
|
* - Updating participant personal data from form input
|
|
* - Building the final API payload structure
|
|
*
|
|
* IMPORTANT: The applicant's address must be preserved from the original booking data stored in the DTO,
|
|
* as it can be lost during form binding when ParticipantDto stores address references.
|
|
*
|
|
* @param BookingDto|null $formData The booking edit form data containing updated participant and service selections
|
|
*
|
|
* @return array The structured payload array ready for BusProNet API submission
|
|
*/
|
|
public function createUpdateRequestPayload(?BookingDto $formData): array
|
|
{
|
|
// Apply bulk insurance if enabled (modifies DTO in place)
|
|
$this->applyBulkInsuranceIfActive($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->resetServiceMappings($bookingData);
|
|
|
|
foreach ($formData->participants as $participant) {
|
|
$this->processParticipantServices($participant, $bookingData, $travelData);
|
|
}
|
|
|
|
$this->removeUnusedServices($bookingData);
|
|
$this->updateParticipantPersonalData($formData->participants, $bookingData);
|
|
|
|
$payload = $this->buildBasePayload($bookingData);
|
|
$this->addBankAccountToPayload($payload, $bookingData);
|
|
$this->buildParticipantPayload($payload, $bookingData, $formData->participants);
|
|
$this->buildServicesPayload($payload, $bookingData);
|
|
$this->buildPickupPayload($payload, $bookingData);
|
|
|
|
// Add purchase vouchers if any exist
|
|
$purchaseVouchers = $this->collectPurchaseVouchers($formData);
|
|
if (false === empty($purchaseVouchers)) {
|
|
$payload['gutscheine']['gutschein'] = [];
|
|
foreach ($purchaseVouchers as $code) {
|
|
$payload['gutscheine']['gutschein'][] = [
|
|
'@einloesecode' => $code,
|
|
];
|
|
}
|
|
}
|
|
|
|
return $payload;
|
|
}
|
|
|
|
/**
|
|
* Resets all existing participant-to-service mappings to start with a clean slate.
|
|
*
|
|
* This ensures that service assignments are rebuilt from scratch based on current form selections.
|
|
* Includes resetting room mappings to allow room reassignments during edit.
|
|
*
|
|
* @param Booking $bookingData The booking data object containing services to reset
|
|
*/
|
|
private function resetServiceMappings(Booking $bookingData): void
|
|
{
|
|
$servicesToReset = [
|
|
...$bookingData->additionalServices,
|
|
...$bookingData->transportationServices,
|
|
...$bookingData->pickupsOutbound,
|
|
...$bookingData->pickupsInbound,
|
|
...$bookingData->rooms,
|
|
...$bookingData->insurances,
|
|
];
|
|
|
|
foreach ($servicesToReset as $service) {
|
|
$service->mapping = [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Processes all services for a single participant.
|
|
*
|
|
* This orchestrator method handles the complete service assignment workflow for one participant,
|
|
* including additional services, transportation services, pickup locations, and room assignments.
|
|
*
|
|
* @param ParticipantDto $participant The participant data from the form
|
|
* @param Booking $bookingData The booking data object to update
|
|
* @param Travel $travelData The travel data containing available services
|
|
*/
|
|
private function processParticipantServices(ParticipantDto $participant, Booking $bookingData, Travel $travelData): void
|
|
{
|
|
if (true === $participant->isCanceled()) {
|
|
return;
|
|
}
|
|
|
|
$this->processAdditionalServices($participant, $bookingData, $travelData);
|
|
$this->processTransportationServices($participant, $bookingData, $travelData);
|
|
$this->processPickupLocations($participant, $bookingData);
|
|
$this->processRoomAssignment($participant, $bookingData);
|
|
$this->processInsurance($participant, $bookingData, $travelData);
|
|
}
|
|
|
|
/**
|
|
* Collects additional services from a participant for mapping.
|
|
*
|
|
* Extracts all additional services (courses, board, rentals, ski pass, rental insurance,
|
|
* additional services) from a participant into a flat array of Service objects.
|
|
*
|
|
* @param ParticipantDto $participant The participant data
|
|
*
|
|
* @return array<Service> Array of services selected by this participant
|
|
*/
|
|
private function collectParticipantAdditionalServices(ParticipantDto $participant): array
|
|
{
|
|
$services = [
|
|
...$participant->courses,
|
|
...$participant->additionalServices,
|
|
...$participant->board,
|
|
...$participant->rentals,
|
|
];
|
|
|
|
// Add ski pass if selected (single service, not an array)
|
|
if (null !== $participant->skiPass) {
|
|
$services[] = $participant->skiPass;
|
|
}
|
|
|
|
// Add rental insurance if selected (single service, not an array)
|
|
if (null !== $participant->rentalInsurance) {
|
|
$services[] = $participant->rentalInsurance;
|
|
}
|
|
|
|
return $services;
|
|
}
|
|
|
|
/**
|
|
* Processes additional services for a participant.
|
|
*
|
|
* Maps additional services (courses, ski passes, board options, rentals) to the participant.
|
|
* Adds new services to the booking if they don't already exist and sets individual pricing.
|
|
*
|
|
* @param ParticipantDto $participant The participant data from the form
|
|
* @param Booking $bookingData The booking data object to update
|
|
* @param Travel $travelData The travel data containing available services
|
|
*/
|
|
private function processAdditionalServices(ParticipantDto $participant, Booking $bookingData, Travel $travelData): void
|
|
{
|
|
$servicesToMap = $this->collectParticipantAdditionalServices($participant);
|
|
|
|
foreach ($servicesToMap as $service) {
|
|
if (false === isset($bookingData->additionalServices[$service->id])) {
|
|
$serviceToAdd = $travelData->additionalServices[$service->id] ?? null;
|
|
if (null !== $serviceToAdd) {
|
|
$bookingData->additionalServices[$service->id] = $serviceToAdd;
|
|
$bookingData->additionalServices[$service->id]->individualPrice[$participant->index] = $serviceToAdd->price;
|
|
}
|
|
}
|
|
$bookingData->additionalServices[$service->id]->mapping[] = $participant->index;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Processes transportation services for a participant.
|
|
*
|
|
* Maps transportation services (both directions: to and from destination) to the participant.
|
|
* Adds new transportation services to the booking if they don't already exist.
|
|
*
|
|
* @param ParticipantDto $participant The participant data from the form
|
|
* @param Booking $bookingData The booking data object to update
|
|
* @param Travel $travelData The travel data containing available services
|
|
*/
|
|
private function processTransportationServices(ParticipantDto $participant, Booking $bookingData, Travel $travelData): void
|
|
{
|
|
foreach ([$participant->transportationOutbound, $participant->transportationInbound] as $service) {
|
|
if (false === isset($bookingData->transportationServices[$service->id])) {
|
|
$serviceToAdd = $travelData->transportationServices[$service->id] ?? null;
|
|
if (null !== $serviceToAdd) {
|
|
$bookingData->transportationServices[$service->id] = $serviceToAdd;
|
|
$bookingData->transportationServices[$service->id]->individualPrice[$participant->index] = $serviceToAdd->price;
|
|
}
|
|
}
|
|
$bookingData->transportationServices[$service->id]->mapping[] = $participant->index;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Processes pickup locations for participants using bus transportation.
|
|
*
|
|
* Only processes pickup locations for bus transportation services and maps the participant
|
|
* to their selected pickup location.
|
|
*
|
|
* @param ParticipantDto $participant The participant data from the form
|
|
* @param Booking $bookingData The booking data object to update
|
|
*/
|
|
private function processPickupLocations(ParticipantDto $participant, Booking $bookingData): void
|
|
{
|
|
// Check if either transportation direction is BUS and pickup is selected
|
|
$hasOutboundBus = null !== $participant->transportationOutbound && 'BUS' === $participant->transportationOutbound->subType;
|
|
$hasInboundBus = null !== $participant->transportationInbound && 'BUS' === $participant->transportationInbound->subType;
|
|
|
|
if (($hasOutboundBus || $hasInboundBus) && null !== $selectedPickup = $participant->pickup) {
|
|
if (false === isset($bookingData->pickupsOutbound[$selectedPickup->id])) {
|
|
$bookingData->pickupsOutbound[$selectedPickup->id] = $selectedPickup;
|
|
}
|
|
$bookingData->pickupsOutbound[$selectedPickup->id]->mapping[] = $participant->index;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Processes room assignment for a participant.
|
|
*
|
|
* Maps the participant to their assigned room. Allows room reassignment during
|
|
* booking edits while maintaining the constraint that participants can only be
|
|
* assigned to room types that have already been booked.
|
|
*
|
|
* @param ParticipantDto $participant The participant data from the form
|
|
* @param Booking $bookingData The booking data object to update
|
|
*/
|
|
private function processRoomAssignment(ParticipantDto $participant, Booking $bookingData): void
|
|
{
|
|
if (null === $participant->assignedRoomId) {
|
|
return;
|
|
}
|
|
|
|
// Find the room in the existing booking by ID
|
|
foreach ($bookingData->rooms as $room) {
|
|
if ($room->id === $participant->assignedRoomId) {
|
|
$room->mapping[] = $participant->index;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Processes travel insurance for a participant.
|
|
*
|
|
* Maps insurance to the participant. Adds new insurances to the booking if they don't exist.
|
|
* Insurance can be either individual or package-based, with automatic price-tier adjustment.
|
|
*
|
|
* IMPORTANT: Excludes synthetic "no insurance" option from BPN transmission.
|
|
*
|
|
* @param ParticipantDto $participant The participant data from the form
|
|
* @param Booking $bookingData The booking data object to update
|
|
* @param Travel $travelData The travel data containing available insurances
|
|
*/
|
|
private function processInsurance(ParticipantDto $participant, Booking $bookingData, Travel $travelData): void
|
|
{
|
|
// Skip if no insurance or if participant selected "keine Versicherung gewünscht"
|
|
if (null === $participant->insurance || $participant->insurance->isNoInsurance()) {
|
|
return;
|
|
}
|
|
|
|
$insurance = $participant->insurance;
|
|
|
|
if (false === isset($bookingData->insurances[$insurance->id])) {
|
|
$insuranceToAdd = $travelData->insurances[$insurance->id] ?? null;
|
|
if (null !== $insuranceToAdd) {
|
|
$bookingData->insurances[$insurance->id] = $insuranceToAdd;
|
|
$bookingData->insurances[$insurance->id]->individualPrice[$participant->index] = $insuranceToAdd->price;
|
|
}
|
|
}
|
|
|
|
// Only add mapping if insurance exists in booking data
|
|
// This can legitimately be false if the insurance is no longer available in current travel data
|
|
if (isset($bookingData->insurances[$insurance->id])) {
|
|
$bookingData->insurances[$insurance->id]->mapping[] = $participant->index;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes services and pickups with no participant mappings.
|
|
*
|
|
* Cleans up unused services to prevent empty services from being sent to the API.
|
|
* This includes additional services, transportation services, pickup locations, and insurances.
|
|
*
|
|
* @param Booking $bookingData The booking data object to clean up
|
|
*/
|
|
private function removeUnusedServices(Booking $bookingData): void
|
|
{
|
|
foreach ($bookingData->additionalServices as $service) {
|
|
if (0 === count($service->mapping)) {
|
|
unset($bookingData->additionalServices[$service->id]);
|
|
}
|
|
}
|
|
|
|
foreach ($bookingData->transportationServices as $service) {
|
|
if (0 === count($service->mapping)) {
|
|
unset($bookingData->transportationServices[$service->id]);
|
|
}
|
|
}
|
|
|
|
foreach ($bookingData->pickupsOutbound as $pickup) {
|
|
if (0 === count($pickup->mapping)) {
|
|
unset($bookingData->pickupsOutbound[$pickup->id]);
|
|
}
|
|
}
|
|
|
|
foreach ($bookingData->pickupsInbound as $pickup) {
|
|
if (0 === count($pickup->mapping)) {
|
|
unset($bookingData->pickupsInbound[$pickup->id]);
|
|
}
|
|
}
|
|
|
|
foreach ($bookingData->insurances as $insurance) {
|
|
if (0 === count($insurance->mapping)) {
|
|
unset($bookingData->insurances[$insurance->id]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates participant personal data from form input.
|
|
*
|
|
* Only processes participants with status 'F' (active/confirmed participants).
|
|
* Updates all personal data fields and communication information.
|
|
*
|
|
* IMPORTANT: The applicant's address must never be modified. This method updates
|
|
* participant addresses independently to ensure applicant data remains intact.
|
|
*
|
|
* @param array $participants The participants array from the form
|
|
* @param Booking $bookingData The booking data object to update
|
|
*/
|
|
private function updateParticipantPersonalData(array $participants, Booking $bookingData): void
|
|
{
|
|
foreach ($participants as $participant) {
|
|
if ('F' !== $participant->status) {
|
|
continue;
|
|
}
|
|
|
|
$bookingData->participants[$participant->index]->firstName = $participant->firstName;
|
|
$bookingData->participants[$participant->index]->name = $participant->lastName;
|
|
$bookingData->participants[$participant->index]->dateOfBirth = $participant->dateOfBirth;
|
|
$bookingData->participants[$participant->index]->gender = $participant->gender;
|
|
$bookingData->participants[$participant->index]->nationality = $participant->nationality;
|
|
$bookingData->participants[$participant->index]->height = $participant->height;
|
|
$bookingData->participants[$participant->index]->weight = $participant->weight;
|
|
$bookingData->participants[$participant->index]->shoeSize = $participant->shoeSize;
|
|
|
|
// Update address if provided
|
|
// Create new Address instance to avoid modifying any shared object references
|
|
if (null !== $participant->address) {
|
|
$newAddress = new Address();
|
|
$newAddress->street = $participant->address->street;
|
|
$newAddress->postCode = $participant->address->postCode;
|
|
$newAddress->city = $participant->address->city;
|
|
$newAddress->district = $participant->address->district;
|
|
$newAddress->country = $participant->address->country;
|
|
|
|
$bookingData->participants[$participant->index]->address = $newAddress;
|
|
}
|
|
|
|
if ($participant->email || $participant->mobile) {
|
|
if (null === $bookingData->participants[$participant->index]->communication) {
|
|
$bookingData->participants[$participant->index]->communication = new Communication();
|
|
}
|
|
$bookingData->participants[$participant->index]->communication->email = $participant->email;
|
|
$bookingData->participants[$participant->index]->communication->mobile = $participant->mobile;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Builds the base API payload structure with booking information.
|
|
*
|
|
* Creates the main structure that will be populated with detailed data sections.
|
|
*
|
|
* @param Booking $bookingData The booking data object
|
|
*
|
|
* @return array The base payload structure
|
|
*/
|
|
private function buildBasePayload(Booking $bookingData): array
|
|
{
|
|
return [
|
|
'idbuchung' => $bookingData->id,
|
|
'status' => $bookingData->status,
|
|
'idagentur' => $bookingData->agencyId,
|
|
'idreise' => $bookingData->dateId,
|
|
'idpartner' => $bookingData->hotelId,
|
|
'anmelder' => $bookingData->applicant->toPayload(),
|
|
'zahlung' => [
|
|
'@idzahlungsart' => $bookingData->paymentId,
|
|
'@bezeichnung' => $bookingData->paymentLabel,
|
|
'@art' => $bookingData->paymentType,
|
|
],
|
|
'teilnehmerliste' => [
|
|
'teilnehmer' => [],
|
|
],
|
|
'zusatzleistungen' => [
|
|
'zusatzleistung' => [],
|
|
],
|
|
'beförderungen' => [
|
|
'beförderung' => [],
|
|
],
|
|
'ferienzielunterbringungen' => [
|
|
'ferienzielunterbringung' => [],
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Adds bank account information to the payload if present.
|
|
*
|
|
* Bank account information is required for direct debit payments.
|
|
*
|
|
* @param array $payload The payload array to modify
|
|
* @param Booking $bookingData The booking data object
|
|
*/
|
|
private function addBankAccountToPayload(array &$payload, Booking $bookingData): void
|
|
{
|
|
if (null !== $bookingData->bankAccount) {
|
|
$payload['zahlung']['bankverbindung'] = [
|
|
'@kreditinstitut' => $bookingData->bankAccount->bankName,
|
|
'@iban' => $bookingData->bankAccount->iban,
|
|
'@bic' => $bookingData->bankAccount->bic,
|
|
'@kontoinhaber' => $bookingData->bankAccount->holder,
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Builds the participant list section of the payload.
|
|
*
|
|
* Includes status, personal data, and wishes (room remarks, license plate) for each participant.
|
|
*
|
|
* @param array $payload The payload array to modify
|
|
* @param Booking $bookingData The booking data object
|
|
* @param array $participantDtos The participant DTOs from the form (for wishes data)
|
|
*/
|
|
private function buildParticipantPayload(array &$payload, Booking $bookingData, array $participantDtos): void
|
|
{
|
|
foreach ($bookingData->participants as $index => $participant) {
|
|
$participantPayload = [
|
|
'@id' => $index + 1,
|
|
'status' => $bookingData->participantsStatus[$index],
|
|
...$participant->toPayload(),
|
|
];
|
|
|
|
// Add wishes (room remarks, license plate) from form DTO
|
|
if (isset($participantDtos[$index])) {
|
|
$dto = $participantDtos[$index];
|
|
if (null !== $dto->remarksRoom || null !== $dto->licensePlate) {
|
|
$wishes = [];
|
|
|
|
if (null !== $dto->remarksRoom && '' !== trim($dto->remarksRoom)) {
|
|
$wishes['unterbringungswunsch'] = $dto->remarksRoom;
|
|
}
|
|
|
|
if (null !== $dto->licensePlate && '' !== trim($dto->licensePlate)) {
|
|
$wishes['beförderungswunsch'] = $dto->licensePlate;
|
|
}
|
|
|
|
if (false === empty($wishes)) {
|
|
$participantPayload['wünsche'] = $wishes;
|
|
}
|
|
}
|
|
|
|
// Add promo voucher or goodwill voucher as aktionscode
|
|
// Goodwill vouchers (Kulanz) take precedence over promo vouchers
|
|
$aktionscode = $this->getPromoVoucherCodeForParticipant($dto);
|
|
if (null !== $aktionscode) {
|
|
$participantPayload['aktionscode'] = $aktionscode;
|
|
}
|
|
}
|
|
|
|
$payload['teilnehmerliste']['teilnehmer'][] = $participantPayload;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Builds the services sections of the payload.
|
|
*
|
|
* Includes additional services, transportation services, and accommodation details
|
|
* with participant mappings and quantities.
|
|
*
|
|
* @param array $payload The payload array to modify
|
|
* @param Booking $bookingData The booking data object
|
|
*/
|
|
private function buildServicesPayload(array &$payload, Booking $bookingData): void
|
|
{
|
|
foreach ($bookingData->additionalServices as $service) {
|
|
$payload['zusatzleistungen']['zusatzleistung'][] = [
|
|
'@idleistung' => $service->id,
|
|
'@anzahl' => count($service->mapping),
|
|
'@zuordnung' => implode(',', array_map(fn ($index) => $index + 1, $service->mapping)),
|
|
];
|
|
}
|
|
|
|
foreach ($bookingData->transportationServices as $service) {
|
|
$payload['beförderungen']['beförderung'][] = [
|
|
'@idleistung' => $service->id,
|
|
'@anzahl' => count($service->mapping),
|
|
'@zuordnung' => implode(',', array_map(fn ($index) => $index + 1, $service->mapping)),
|
|
];
|
|
}
|
|
|
|
foreach ($bookingData->rooms as $room) {
|
|
$payload['ferienzielunterbringungen']['ferienzielunterbringung'][] = [
|
|
'@idzimmer' => $room->id,
|
|
'@kategorie' => $room->category,
|
|
'@idverpflegung' => $room->boardId,
|
|
'@anreise' => $room->dateFrom ? $room->dateFrom->format('d.m.Y') : null,
|
|
'@abreise' => $room->dateTo ? $room->dateTo->format('d.m.Y') : null,
|
|
'@anzahl' => $room->totalCount,
|
|
'@zuordnung' => implode(',', array_map(fn ($index) => $index + 1, $room->mapping)),
|
|
];
|
|
}
|
|
|
|
// Add insurances with participant mappings
|
|
if (false === empty($bookingData->insurances)) {
|
|
$payload['versicherungen']['versicherung'] = [];
|
|
foreach ($bookingData->insurances as $insurance) {
|
|
if (count($insurance->mapping) > 0) {
|
|
$payload['versicherungen']['versicherung'][] = [
|
|
'@idversicherung' => $insurance->id,
|
|
'@anzahl' => count($insurance->mapping),
|
|
'@zuordnung' => implode(',', array_map(fn ($index) => $index + 1, $insurance->mapping)),
|
|
];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Builds the pickup locations section of the payload.
|
|
*
|
|
* Only included in payload if there are actual pickup assignments for bus transportation.
|
|
*
|
|
* @param array $payload The payload array to modify
|
|
* @param Booking $bookingData The booking data object
|
|
*/
|
|
private function buildPickupPayload(array &$payload, Booking $bookingData): void
|
|
{
|
|
if (0 < count($bookingData->pickupsOutbound)) {
|
|
$payload['zustiege']['zustieg'] = [];
|
|
foreach ($bookingData->pickupsOutbound as $pickup) {
|
|
$payload['zustiege']['zustieg'][] = [
|
|
'@idzustieg' => $pickup->id,
|
|
'@anzahl' => count($pickup->mapping),
|
|
'@zuordnung' => implode(',', array_map(fn ($index) => $index + 1, $pickup->mapping)),
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 The structured payload array for BusProNet API submission
|
|
*/
|
|
public function createBookingRequestPayload(BookingDto $bookingDto, string $bookingType): array
|
|
{
|
|
// Apply bulk insurance if enabled (modifies DTO in place)
|
|
$this->applyBulkInsuranceIfActive($bookingDto);
|
|
|
|
$firstParticipant = $bookingDto->participants[0];
|
|
|
|
$payload = [
|
|
'buchungsart' => $bookingType,
|
|
'status' => $bookingDto->bookingStatus,
|
|
'idreise' => $bookingDto->travel->id,
|
|
'idpartner' => $bookingDto->travel->hotelId,
|
|
'idagentur' => $bookingDto->agencyId,
|
|
];
|
|
|
|
// Add applicant (first participant data)
|
|
$payload['anmelder'] = [
|
|
'name' => $firstParticipant->lastName,
|
|
'vorname' => $firstParticipant->firstName,
|
|
'geschlecht' => $firstParticipant->gender ?? '',
|
|
'nationalitaet' => $firstParticipant->nationality ?? '',
|
|
];
|
|
|
|
// DO NOT include personId or addressId in create mode
|
|
// BPN will automatically match existing customers by exact personal data (name, DOB, address)
|
|
// Including IDs would prevent automatic matching and could cause data inconsistencies
|
|
|
|
if (null !== $firstParticipant->dateOfBirth) {
|
|
$payload['anmelder']['geburtsdatum'] = $firstParticipant->dateOfBirth->format('d.m.Y');
|
|
}
|
|
|
|
// Add address for applicant
|
|
if (null !== $firstParticipant->address) {
|
|
$payload['anmelder']['anschrift'] = $firstParticipant->address->toPayload();
|
|
}
|
|
|
|
if (null !== $firstParticipant->email || null !== $firstParticipant->mobile) {
|
|
$payload['anmelder']['kommunikation'] = [];
|
|
if (null !== $firstParticipant->email) {
|
|
$payload['anmelder']['kommunikation']['email'] = $firstParticipant->email;
|
|
}
|
|
if (null !== $firstParticipant->mobile) {
|
|
$payload['anmelder']['kommunikation']['telefonmobil'] = $firstParticipant->mobile;
|
|
}
|
|
}
|
|
|
|
// Add participants
|
|
$payload['teilnehmerliste']['teilnehmer'] = [];
|
|
foreach ($bookingDto->participants as $index => $participant) {
|
|
$participantData = [
|
|
'@id' => $index + 1,
|
|
'name' => $participant->lastName,
|
|
'vorname' => $participant->firstName,
|
|
'geschlecht' => $participant->gender ?? '',
|
|
'nationalitaet' => $participant->nationality ?? '',
|
|
];
|
|
|
|
// DO NOT include personId or addressId in create mode
|
|
// BPN will automatically match existing customers by exact personal data (name, DOB, address)
|
|
// Including IDs would prevent automatic matching and could cause data inconsistencies
|
|
|
|
if (null !== $participant->dateOfBirth) {
|
|
$participantData['geburtsdatum'] = $participant->dateOfBirth->format('d.m.Y');
|
|
}
|
|
|
|
// Add address (always include structure, even if empty)
|
|
$participantData['anschrift'] = $participant->address?->toPayload() ?? [
|
|
'strasse' => null,
|
|
'plz' => null,
|
|
'ort' => null,
|
|
'ortsteil' => null,
|
|
'land' => null,
|
|
];
|
|
|
|
// Add contact info for all participants
|
|
if (null !== $participant->email || null !== $participant->mobile) {
|
|
$participantData['kommunikation'] = [];
|
|
if (null !== $participant->email) {
|
|
$participantData['kommunikation']['email'] = $participant->email;
|
|
}
|
|
if (null !== $participant->mobile) {
|
|
$participantData['kommunikation']['telefonmobil'] = $participant->mobile;
|
|
}
|
|
}
|
|
|
|
// Add wishes (room remarks and license plate)
|
|
if (null !== $participant->remarksRoom || null !== $participant->licensePlate) {
|
|
$participantData['wünsche'] = [];
|
|
if (null !== $participant->remarksRoom && '' !== trim($participant->remarksRoom)) {
|
|
$participantData['wünsche']['unterbringungswunsch'] = $participant->remarksRoom;
|
|
}
|
|
if (null !== $participant->licensePlate && '' !== trim($participant->licensePlate)) {
|
|
$participantData['wünsche']['beförderungswunsch'] = $participant->licensePlate;
|
|
}
|
|
}
|
|
|
|
// Add promo voucher or goodwill voucher as aktionscode
|
|
// Goodwill vouchers (Kulanz) take precedence over promo vouchers
|
|
$aktionscode = $this->getPromoVoucherCodeForParticipant($participant);
|
|
if (null !== $aktionscode) {
|
|
$participantData['aktionscode'] = $aktionscode;
|
|
}
|
|
|
|
$payload['teilnehmerliste']['teilnehmer'][] = $participantData;
|
|
}
|
|
|
|
// Collect and group all services by ID with participant mappings
|
|
$serviceMap = $this->collectServiceMappings($bookingDto);
|
|
$transportationMap = $this->collectTransportationMappings($bookingDto);
|
|
$roomMap = $this->collectRoomMappings($bookingDto);
|
|
$pickupMap = $this->collectPickupMappings($bookingDto);
|
|
$insuranceMap = $this->collectInsuranceMappings($bookingDto);
|
|
|
|
// Add services using reusable helper methods
|
|
$this->addServicesFromMap($payload, 'beförderungen', 'beförderung', '@idleistung', $transportationMap);
|
|
$this->addRoomMappingsToPayload($payload, $roomMap, $bookingDto);
|
|
$this->addServicesFromMap($payload, 'zusatzleistungen', 'zusatzleistung', '@idleistung', $serviceMap);
|
|
$this->addServicesFromMap($payload, 'zustiege', 'zustieg', '@idzustieg', $pickupMap);
|
|
$this->addServicesFromMap($payload, 'versicherungen', 'versicherung', '@idversicherung', $insuranceMap);
|
|
|
|
// Add purchase vouchers if any exist
|
|
$purchaseVouchers = $this->collectPurchaseVouchers($bookingDto);
|
|
if (false === empty($purchaseVouchers)) {
|
|
$payload['gutscheine']['gutschein'] = [];
|
|
foreach ($purchaseVouchers as $code) {
|
|
$payload['gutscheine']['gutschein'][] = [
|
|
'@einloesecode' => $code,
|
|
];
|
|
}
|
|
}
|
|
|
|
// Add payment information
|
|
$payload['zahlung'] = [
|
|
'@idzahlungsart' => Constants::PAYMENT_METHOD_DEBIT === $bookingDto->paymentMethod
|
|
? Constants::PAYMENT_TYPE_ID_DEBIT
|
|
: Constants::PAYMENT_TYPE_ID_TRANSFER,
|
|
'@art' => Constants::PAYMENT_METHOD_DEBIT === $bookingDto->paymentMethod ? 'EINZUG' : 'UEBERWEISUNG',
|
|
];
|
|
|
|
if (Constants::PAYMENT_METHOD_DEBIT === $bookingDto->paymentMethod && null !== $bookingDto->bankAccount) {
|
|
$payload['zahlung']['bankverbindung'] = [
|
|
'@iban' => $bookingDto->bankAccount->iban,
|
|
'@kontoinhaber' => $bookingDto->bankAccount->accountHolder,
|
|
];
|
|
}
|
|
|
|
return $payload;
|
|
}
|
|
|
|
/**
|
|
* Adds services from a mapping to the payload.
|
|
*
|
|
* Generic helper method that converts service ID => participant IDs mappings
|
|
* into XML payload structure.
|
|
*
|
|
* @param array $payload The payload array to modify
|
|
* @param string $sectionKey The section key (e.g., 'beförderungen', 'versicherungen')
|
|
* @param string $itemKey The item key (e.g., 'beförderung', 'versicherung')
|
|
* @param string $idAttributeName The ID attribute name (e.g., '@idleistung', '@idversicherung')
|
|
* @param array $serviceMap Map of service ID to participant IDs
|
|
*/
|
|
private function addServicesFromMap(
|
|
array &$payload,
|
|
string $sectionKey,
|
|
string $itemKey,
|
|
string $idAttributeName,
|
|
array $serviceMap,
|
|
): void {
|
|
if (false === empty($serviceMap)) {
|
|
$payload[$sectionKey][$itemKey] = [];
|
|
foreach ($serviceMap as $serviceId => $participantIds) {
|
|
$payload[$sectionKey][$itemKey][] = [
|
|
$idAttributeName => $serviceId,
|
|
'@anzahl' => count($participantIds),
|
|
'@zuordnung' => implode(',', $participantIds),
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds room mappings with detailed attributes to the payload.
|
|
*
|
|
* Rooms require special attributes beyond simple service mapping:
|
|
* - kategorie (room category code)
|
|
* - idverpflegung (board type ID)
|
|
* - anreise (arrival date)
|
|
* - abreise (departure date)
|
|
* - anzahl (number of rooms of this type booked)
|
|
*
|
|
* @param array $payload The payload array to modify
|
|
* @param array $roomMap Map of room ID to participant IDs
|
|
* @param BookingDto $bookingDto The booking data for accessing room details and quantities
|
|
*/
|
|
private function addRoomMappingsToPayload(array &$payload, array $roomMap, BookingDto $bookingDto): void
|
|
{
|
|
if (empty($roomMap)) {
|
|
return;
|
|
}
|
|
|
|
$availableRooms = $bookingDto->travel->getAvailableRooms();
|
|
$payload['ferienzielunterbringungen']['ferienzielunterbringung'] = [];
|
|
|
|
// Build room selection quantity lookup
|
|
$roomQuantities = [];
|
|
foreach ($bookingDto->roomSelections as $selection) {
|
|
if ($selection->quantity > 0) {
|
|
$roomQuantities[$selection->roomId] = $selection->quantity;
|
|
}
|
|
}
|
|
|
|
foreach ($roomMap as $roomId => $participantIds) {
|
|
$room = $availableRooms[$roomId] ?? null;
|
|
if (null === $room) {
|
|
continue;
|
|
}
|
|
|
|
$quantity = $roomQuantities[$roomId] ?? 1;
|
|
|
|
$payload['ferienzielunterbringungen']['ferienzielunterbringung'][] = [
|
|
'@idzimmer' => $room->id,
|
|
'@kategorie' => $room->category,
|
|
'@idverpflegung' => $room->boardId,
|
|
'@anreise' => $bookingDto->travel->dateFrom->format('d.m.Y'),
|
|
'@abreise' => $bookingDto->travel->dateTo->format('d.m.Y'),
|
|
'@anzahl' => $quantity,
|
|
'@zuordnung' => implode(',', $participantIds),
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Collects room mappings.
|
|
*
|
|
* Groups participants by their assigned room ID.
|
|
*
|
|
* @return array<string, array<int>> Map of room ID to participant IDs
|
|
*/
|
|
private function collectRoomMappings(BookingDto $bookingDto): array
|
|
{
|
|
$roomMap = [];
|
|
|
|
foreach ($bookingDto->participants as $index => $participant) {
|
|
$participantId = $index + 1;
|
|
|
|
if (null !== $participant->assignedRoomId) {
|
|
$roomMap[$participant->assignedRoomId][] = $participantId;
|
|
}
|
|
}
|
|
|
|
return $roomMap;
|
|
}
|
|
|
|
/**
|
|
* Collects service mappings for the booking request.
|
|
*
|
|
* Groups board, ski passes, rentals, rental insurance, courses, parking, and additional services
|
|
* by service ID with their participant assignments (1-based).
|
|
*
|
|
* @return array<string, array<int>> Map of service ID to participant IDs
|
|
*/
|
|
private function collectServiceMappings(BookingDto $bookingDto): array
|
|
{
|
|
$serviceMap = [];
|
|
|
|
foreach ($bookingDto->participants as $index => $participant) {
|
|
$participantId = $index + 1;
|
|
|
|
// Board services
|
|
foreach ($participant->board as $board) {
|
|
$serviceMap[$board->id][] = $participantId;
|
|
}
|
|
|
|
// Ski pass
|
|
if (null !== $participant->skiPass) {
|
|
$serviceMap[$participant->skiPass->id][] = $participantId;
|
|
}
|
|
|
|
// Rentals
|
|
foreach ($participant->rentals as $rental) {
|
|
$serviceMap[$rental->id][] = $participantId;
|
|
}
|
|
|
|
// Rental insurance
|
|
if (null !== $participant->rentalInsurance) {
|
|
$serviceMap[$participant->rentalInsurance->id][] = $participantId;
|
|
}
|
|
|
|
// Courses
|
|
foreach ($participant->courses as $course) {
|
|
$serviceMap[$course->id][] = $participantId;
|
|
}
|
|
|
|
// Parking service (for self-organized transportation)
|
|
if (true === $participant->parking && null !== $participant->parkingService) {
|
|
$serviceMap[$participant->parkingService->id][] = $participantId;
|
|
}
|
|
|
|
// Additional services
|
|
foreach ($participant->additionalServices as $service) {
|
|
$serviceMap[$service->id][] = $participantId;
|
|
}
|
|
}
|
|
|
|
return $serviceMap;
|
|
}
|
|
|
|
/**
|
|
* Collects transportation service mappings.
|
|
*
|
|
* @return array<string, array<int>> Map of transportation service ID to participant IDs
|
|
*/
|
|
private function collectTransportationMappings(BookingDto $bookingDto): array
|
|
{
|
|
$transportationMap = [];
|
|
|
|
foreach ($bookingDto->participants as $index => $participant) {
|
|
$participantId = $index + 1;
|
|
|
|
if (null !== $participant->transportationOutbound) {
|
|
$transportationMap[$participant->transportationOutbound->id][] = $participantId;
|
|
}
|
|
|
|
if (null !== $participant->transportationInbound) {
|
|
$transportationMap[$participant->transportationInbound->id][] = $participantId;
|
|
}
|
|
}
|
|
|
|
return $transportationMap;
|
|
}
|
|
|
|
/**
|
|
* Collects pickup location mappings.
|
|
*
|
|
* Collects the unified pickup selection which applies to both directions.
|
|
* The API receives this as outbound pickup data.
|
|
*
|
|
* @return array<string, array<int>> Map of pickup ID to participant IDs
|
|
*/
|
|
private function collectPickupMappings(BookingDto $bookingDto): array
|
|
{
|
|
$pickupMap = [];
|
|
|
|
foreach ($bookingDto->participants as $index => $participant) {
|
|
$participantId = $index + 1;
|
|
|
|
if (null !== $participant->pickup) {
|
|
$pickupMap[$participant->pickup->id][] = $participantId;
|
|
}
|
|
}
|
|
|
|
return $pickupMap;
|
|
}
|
|
|
|
/**
|
|
* Collects insurance mappings.
|
|
*
|
|
* CRITICAL: Insurance data is only included in CREATE flow, not in UPDATE flow.
|
|
* IMPORTANT: Excludes synthetic "no insurance" option from BPN transmission.
|
|
*
|
|
* @return array<string, array<int>> Map of insurance ID to participant IDs
|
|
*/
|
|
private function collectInsuranceMappings(BookingDto $bookingDto): array
|
|
{
|
|
$insuranceMap = [];
|
|
|
|
foreach ($bookingDto->participants as $index => $participant) {
|
|
$participantId = $index + 1;
|
|
|
|
// Exclude synthetic "keine Versicherung gewünscht" option from BPN XML
|
|
if (null !== $participant->insurance && false === $participant->insurance->isNoInsurance()) {
|
|
$insuranceMap[$participant->insurance->id][] = $participantId;
|
|
}
|
|
}
|
|
|
|
return $insuranceMap;
|
|
}
|
|
|
|
/**
|
|
* Collects all purchase vouchers from participants.
|
|
*
|
|
* Removes duplicates and empty values, returning unique redemption codes
|
|
* for aggregation into the <gutscheine> collection in the booking payload.
|
|
*
|
|
* IMPORTANT: Goodwill vouchers (Kulanz) are excluded from this collection.
|
|
* They are added per participant as <aktionscode> instead.
|
|
*
|
|
* @return string[] Array of unique redemption codes (excluding Kulanz)
|
|
*/
|
|
private function collectPurchaseVouchers(BookingDto $bookingDto): array
|
|
{
|
|
$vouchers = [];
|
|
|
|
foreach ($bookingDto->participants as $participant) {
|
|
if (null !== $participant->purchaseVoucherCode && '' !== trim($participant->purchaseVoucherCode)) {
|
|
// Exclude goodwill vouchers (flagged by field handler)
|
|
if (false === $participant->hasGoodwillVoucher) {
|
|
$vouchers[] = trim($participant->purchaseVoucherCode);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove duplicates (in case multiple participants enter same code)
|
|
return array_unique($vouchers);
|
|
}
|
|
|
|
/**
|
|
* Gets the aktionscode for a participant.
|
|
*
|
|
* Checks if the participant has a goodwill (Kulanz) purchase voucher first.
|
|
* If yes, returns that code (goodwill vouchers override promo vouchers).
|
|
* Otherwise, returns the promo voucher code if present.
|
|
*
|
|
* @return string|null The aktionscode to use, or null if none
|
|
*/
|
|
private function getPromoVoucherCodeForParticipant(ParticipantDto $participant): ?string
|
|
{
|
|
// Check for goodwill voucher first (takes precedence)
|
|
if (true === $participant->hasGoodwillVoucher && null !== $participant->purchaseVoucherCode && '' !== trim($participant->purchaseVoucherCode)) {
|
|
return trim($participant->purchaseVoucherCode);
|
|
}
|
|
|
|
// Fall back to promo voucher if present
|
|
if (null !== $participant->promoVoucherCode && '' !== trim($participant->promoVoucherCode)) {
|
|
return trim($participant->promoVoucherCode);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Applies bulk insurance assignment if the applicant has enabled it.
|
|
*
|
|
* When bulk insurance is active (applicant's bulkInsuranceBooking = true), this method
|
|
* assigns the applicant's insurance TYPE to all dependent participants with automatic
|
|
* price tier adjustment based on each participant's total cost.
|
|
*
|
|
* IMPORTANT: In edit mode, 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.
|
|
*
|
|
* @param BookingDto $bookingDto The booking DTO (create or edit flow)
|
|
*/
|
|
private function applyBulkInsuranceIfActive(BookingDto $bookingDto): void
|
|
{
|
|
$applicant = $bookingDto->getParticipant(0);
|
|
|
|
// Check if bulk insurance is enabled and applicant has selected an insurance
|
|
if (null === $applicant || false === $applicant->bulkInsuranceBooking || null === $applicant->insurance) {
|
|
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 InsuranceService for proper type-based assignment with price tier matching
|
|
$assignments = $this->insuranceService->batchAssignInsuranceToParticipants(
|
|
$selectableInsurances,
|
|
$applicant->insurance,
|
|
$bookingDto,
|
|
$participantPrices
|
|
);
|
|
|
|
// Apply assignments to dependent participants (skip applicant at index 0)
|
|
foreach ($assignments as $index => $insurance) {
|
|
if (0 === $index) {
|
|
continue; // Skip applicant
|
|
}
|
|
|
|
$participant = $bookingDto->getParticipant($index);
|
|
if (null === $participant) {
|
|
continue;
|
|
}
|
|
|
|
// In edit mode: only apply bulk insurance if participant has no insurance
|
|
// This respects the rule that once assigned, insurance cannot be changed
|
|
if (BookingDto::MODE_EDIT === $bookingDto->getMode() && null !== $participant->insurance) {
|
|
continue; // Skip participants with existing insurance assignment
|
|
}
|
|
|
|
$participant->insurance = $insurance;
|
|
}
|
|
}
|
|
}
|