feat: refactoring and cleanup

This commit is contained in:
Björn Fromme
2025-12-06 15:02:13 +01:00
parent f4691ec67e
commit 696800aafd
41 changed files with 2591 additions and 2233 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,485 @@
<?php
declare(strict_types=1);
namespace App\BusProNet\DataProcessor;
use App\BusProNet\Constants;
use App\BusProNet\Model\Booking;
use App\Form\Model\BookingDto;
/**
* Builds API payload structures for BusProNet booking requests.
*
* Transforms booking data into the structured array format expected by the
* BusProNet XML API for both create and update operations.
*/
class BookingPayloadBuilder
{
public function __construct(
private readonly ServiceMappingCollector $mappingCollector,
) {
}
/**
* 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
*/
public 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
*/
public 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)
*/
public 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 promotional voucher as promotionalCode
$promotionalCode = $this->mappingCollector->getPromoVoucherCodeForParticipant($dto);
if (null !== $promotionalCode) {
$participantPayload['aktionscode'] = $promotionalCode;
}
// Add goodwill voucher as participant-level einloesecode
$goodwillCode = $this->mappingCollector->getGoodwillVoucherCodeForParticipant($dto);
if (null !== $goodwillCode) {
$participantPayload['einloesecode'] = $goodwillCode;
}
}
$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
*/
public 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
*/
public 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)),
];
}
}
}
/**
* Builds a create booking request payload.
*
* Generates the array payload structure for creating new bookings through the BusProNet API.
*
* @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 buildCreatePayload(BookingDto $bookingDto, string $bookingType): array
{
$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;
}
}
// Determine if this is an inquiry booking for voucher handling
$isInquiryBooking = 'A' === $bookingDto->bookingStatus;
// 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 promotional voucher as aktionscode (excluded from inquiry bookings)
if (false === $isInquiryBooking) {
$promotionalCode = $this->mappingCollector->getPromoVoucherCodeForParticipant($participant);
if (null !== $promotionalCode) {
$participantData['aktionscode'] = $promotionalCode;
}
}
// Add goodwill voucher as participant-level einloesecode
// Goodwill vouchers are excluded from inquiry bookings (status='A') but included in final bookings (status='F')
// They can be sent in both validation requests (buchungsart=Anfrage) and commit requests (buchungsart=Buchung)
if (false === $isInquiryBooking) {
$goodwillCode = $this->mappingCollector->getGoodwillVoucherCodeForParticipant($participant);
if (null !== $goodwillCode) {
$participantData['einloesecode'] = $goodwillCode;
}
}
$payload['teilnehmerliste']['teilnehmer'][] = $participantData;
}
// Collect and group all services by ID with participant mappings
$serviceMap = $this->mappingCollector->collectServiceMappings($bookingDto);
$transportationMap = $this->mappingCollector->collectTransportationMappings($bookingDto);
$roomMap = $this->mappingCollector->collectRoomMappings($bookingDto);
$pickupMap = $this->mappingCollector->collectPickupMappings($bookingDto);
$insuranceMap = $this->mappingCollector->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 (regular purchase vouchers, excluding goodwill vouchers)
// All vouchers (purchase, promotional, goodwill) are excluded from inquiry bookings
if (false === $isInquiryBooking) {
$purchaseVouchers = $this->mappingCollector->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
*/
public 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
*/
public 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),
];
}
}
/**
* Adds purchase vouchers to the payload.
*
* @param array $payload The payload array to modify
* @param BookingDto $bookingDto The booking data
*/
public function addPurchaseVouchersToPayload(array &$payload, BookingDto $bookingDto): void
{
$purchaseVouchers = $this->mappingCollector->collectPurchaseVouchers($bookingDto);
if (false === empty($purchaseVouchers)) {
$payload['gutscheine']['gutschein'] = [];
foreach ($purchaseVouchers as $code) {
$payload['gutscheine']['gutschein'][] = [
'@einloesecode' => $code,
];
}
}
}
}
@@ -0,0 +1,273 @@
<?php
declare(strict_types=1);
namespace App\BusProNet\DataProcessor;
use App\BusProNet\Model\Booking;
use App\BusProNet\Model\Travel;
use App\Form\Model\ParticipantDto;
/**
* Processes participant service selections for booking updates.
*
* Handles the assignment and mapping of services (additional, transportation,
* pickup, room, insurance) from participant DTOs to the booking model during
* the update request workflow.
*/
class ParticipantServiceProcessor
{
/**
* 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
*/
public 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
*/
public 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);
}
/**
* 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
*/
public 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]);
}
}
}
/**
* 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<\App\BusProNet\Model\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;
}
}
}
@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace App\BusProNet\DataProcessor;
use App\BusProNet\Model\Address;
use App\BusProNet\Model\Booking;
use App\BusProNet\Model\Communication;
use App\Form\Model\ParticipantDto;
/**
* Synchronizes personal data between form DTOs and booking models.
*
* Handles the update of participant personal data, addresses, and communication
* information from form submissions to the booking model during update operations.
*/
class PersonalDataSynchronizer
{
/**
* 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<ParticipantDto> $participants The participants array from the form
* @param Booking $bookingData The booking data object to update
*/
public 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;
}
}
}
}
@@ -0,0 +1,225 @@
<?php
declare(strict_types=1);
namespace App\BusProNet\DataProcessor;
use App\Form\Model\BookingDto;
use App\Form\Model\ParticipantDto;
/**
* Collects service-to-participant mappings for BusProNet API payloads.
*
* Extracts and groups participant service selections into structured maps
* keyed by service ID with arrays of participant IDs (1-based for API).
*/
class ServiceMappingCollector
{
/**
* Collects room mappings.
*
* Groups participants by their assigned room ID.
*
* @return array<string, array<int>> Map of room ID to participant IDs
*/
public 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
*/
public 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
*/
public 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
*/
public 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
*/
public 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)
*/
public function collectPurchaseVouchers(BookingDto $bookingDto): array
{
$vouchers = [];
foreach ($bookingDto->participants as $participant) {
if (null !== $participant->purchaseVoucherCode && '' !== trim($participant->purchaseVoucherCode)) {
// Exclude goodwill vouchers
if (false === $participant->hasGoodwillVoucher()) {
$vouchers[] = trim($participant->purchaseVoucherCode);
}
}
}
// Remove duplicates (in case multiple participants enter same code)
return array_unique($vouchers);
}
/**
* Gets the aktionscode (promotional voucher code) for a participant.
*
* Returns the promotional voucher code if present.
* Goodwill vouchers are handled separately via getGoodwillVoucherCodeForParticipant().
*
* @return string|null The aktionscode to use, or null if none
*/
public function getPromoVoucherCodeForParticipant(ParticipantDto $participant): ?string
{
// Only return promotional voucher code (goodwill vouchers are handled separately)
if (null !== $participant->promoVoucherCode && '' !== trim($participant->promoVoucherCode)) {
return trim($participant->promoVoucherCode);
}
return null;
}
/**
* Extracts goodwill voucher code for a participant.
*
* Goodwill vouchers are sent as <einloesecode> per participant (not in aggregate),
* allowing them to coexist with promotional codes.
*/
public function getGoodwillVoucherCodeForParticipant(ParticipantDto $participant): ?string
{
if ($participant->hasGoodwillVoucher() && null !== $participant->purchaseVoucherCode && '' !== trim($participant->purchaseVoucherCode)) {
return trim($participant->purchaseVoucherCode);
}
return null;
}
}