booking; $travelData = $formData->travel; $this->resetServiceMappings($bookingData); foreach ($formData->participants as $participant) { $this->processParticipantServices($participant, $bookingData, $travelData); } $this->removeUnusedServices($bookingData); $this->updateParticipantPersonalData($formData->participants, $bookingData); $this->syncApplicantData($bookingData); $payload = $this->buildBasePayload($bookingData); $this->addBankAccountToPayload($payload, $bookingData); $this->buildParticipantPayload($payload, $bookingData); $this->buildServicesPayload($payload, $bookingData); $this->buildPickupPayload($payload, $bookingData); 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 object $bookingData The booking data object containing services to reset */ private function resetServiceMappings(object $bookingData): void { $servicesToReset = [ ...$bookingData->additionalServices, ...$bookingData->transportationServices, ...$bookingData->pickupsOutbound, ...$bookingData->pickupsInbound, ...$bookingData->rooms, ]; 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 object $participant The participant data from the form * @param object $bookingData The booking data object to update * @param object $travelData The travel data containing available services */ private function processParticipantServices(object $participant, object $bookingData, object $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); } /** * 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 object $participant The participant data from the form * @param object $bookingData The booking data object to update * @param object $travelData The travel data containing available services */ private function processAdditionalServices(object $participant, object $bookingData, object $travelData): void { $servicesToMap = [ ...$participant->courses, ...$participant->additionalServices, ...$participant->board, ...$participant->rentals, ]; // Add ski pass if selected (single service, not an array) if (null !== $participant->skiPass) { $servicesToMap[] = $participant->skiPass; } 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 object $participant The participant data from the form * @param object $bookingData The booking data object to update * @param object $travelData The travel data containing available services */ private function processTransportationServices(object $participant, object $bookingData, object $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 object $participant The participant data from the form * @param object $bookingData The booking data object to update */ private function processPickupLocations(object $participant, object $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 object $participant The participant data from the form * @param object $bookingData The booking data object to update */ private function processRoomAssignment(object $participant, object $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; } } } /** * 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, and pickup locations. * * @param object $bookingData The booking data object to clean up */ private function removeUnusedServices(object $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]); } } } /** * Updates participant personal data from form input. * * Only processes participants with status 'F' (active/confirmed participants). * Updates all personal data fields and communication information. * * @param array $participants The participants array from the form * @param object $bookingData The booking data object to update */ private function updateParticipantPersonalData(array $participants, object $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; 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; } } } /** * Synchronizes applicant data with first participant's physical characteristics. * * The applicant (booking holder) inherits physical data from the first participant. * * @param object $bookingData The booking data object to update */ private function syncApplicantData(object $bookingData): void { if (false !== $firstParticipant = reset($bookingData->participants)) { $bookingData->applicant->height = $firstParticipant->height; $bookingData->applicant->weight = $firstParticipant->weight; $bookingData->applicant->shoeSize = $firstParticipant->shoeSize; } } /** * Builds the base API payload structure with booking information. * * Creates the main structure that will be populated with detailed data sections. * * @param object $bookingData The booking data object * * @return array The base payload structure */ private function buildBasePayload(object $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 object $bookingData The booking data object */ private function addBankAccountToPayload(array &$payload, object $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 and personal data for each participant. * * @param array $payload The payload array to modify * @param object $bookingData The booking data object */ private function buildParticipantPayload(array &$payload, object $bookingData): void { foreach ($bookingData->participants as $index => $participant) { $payload['teilnehmerliste']['teilnehmer'][] = [ '@id' => $index + 1, 'status' => $bookingData->participantsStatus[$index], ...$participant->toPayload(), ]; } } /** * 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 object $bookingData The booking data object */ private function buildServicesPayload(array &$payload, object $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)), ]; } } /** * 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 object $bookingData The booking data object */ private function buildPickupPayload(array &$payload, object $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 BookingCreateDto $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(BookingCreateDto $bookingDto, string $bookingType): array { $firstParticipant = $bookingDto->participants[0]; $payload = [ 'buchungsart' => $bookingType, 'status' => 'F', '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 ?? '', ]; 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 ?? '', ]; 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; } } $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 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 BookingCreateDto $bookingDto The booking data for accessing room details and quantities */ private function addRoomMappingsToPayload(array &$payload, array $roomMap, BookingCreateDto $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> Map of room ID to participant IDs */ private function collectRoomMappings(BookingCreateDto $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> Map of service ID to participant IDs */ private function collectServiceMappings(BookingCreateDto $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> Map of transportation service ID to participant IDs */ private function collectTransportationMappings(BookingCreateDto $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> Map of pickup ID to participant IDs */ private function collectPickupMappings(BookingCreateDto $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. * * @return array> Map of insurance ID to participant IDs */ private function collectInsuranceMappings(BookingCreateDto $bookingDto): array { $insuranceMap = []; foreach ($bookingDto->participants as $index => $participant) { $participantId = $index + 1; if (null !== $participant->insurance) { $insuranceMap[$participant->insurance->id][] = $participantId; } } return $insuranceMap; } }