feat: selectable drop-offs for inbound bus travel

This commit is contained in:
Björn Fromme
2026-02-17 11:53:39 +01:00
parent 41b0f56b5b
commit 0884a39f3d
31 changed files with 701 additions and 200 deletions
@@ -124,6 +124,10 @@ class BookingDataProcessor
// Pickup location
$participantData->pickup = $booking->getPickupForParticipant($index);
// Drop-off location
$participantData->dropOff = $booking->getDropOffForParticipant($index);
$participantData->differentDropOff = null !== $participantData->dropOff;
// Parking service (for self-organized PKW transportation)
$parkingServices = $booking->getAdditionalServicesForParticipantByGroup($index, Constants::TOKEN_PARKING);
if (false === empty($parkingServices)) {
@@ -236,9 +240,9 @@ class BookingDataProcessor
}
// Enrich pickup with data from travel (includes both outbound and inbound prices)
// The outbound pickups already have inbound prices merged in from TravelParser
if (null !== $participant->pickup && isset($travel->pickupsOutbound[$participant->pickup->id])) {
$enrichedPickup = $travel->pickupsOutbound[$participant->pickup->id];
// The pickups already have inbound prices merged in from TravelParser
if (null !== $participant->pickup && isset($travel->pickups[$participant->pickup->id])) {
$enrichedPickup = $travel->pickups[$participant->pickup->id];
// Copy all properties from enriched pickup to participant's pickup
$participant->pickup->price = $enrichedPickup->price;
@@ -251,6 +255,19 @@ class BookingDataProcessor
$participant->pickup->code = $enrichedPickup->code;
}
// Enrich drop-off with data from travel
if (null !== $participant->dropOff && isset($travel->dropOffs[$participant->dropOff->id])) {
$enrichedDropOff = $travel->dropOffs[$participant->dropOff->id];
$participant->dropOff->price = $enrichedDropOff->price;
$participant->dropOff->priceOutbound = $enrichedDropOff->priceOutbound;
$participant->dropOff->priceInbound = $enrichedDropOff->priceInbound;
$participant->dropOff->time = $enrichedDropOff->time;
$participant->dropOff->city = $enrichedDropOff->city;
$participant->dropOff->street = $enrichedDropOff->street;
$participant->dropOff->postalCode = $enrichedDropOff->postalCode;
$participant->dropOff->code = $enrichedDropOff->code;
}
// Enrich insurance
if (null !== $participant->insurance && isset($travel->insurances[$participant->insurance->id])) {
$participant->insurance = $travel->insurances[$participant->insurance->id];
@@ -301,6 +318,7 @@ class BookingDataProcessor
$this->payloadBuilder->buildParticipantPayload($payload, $bookingData, $formData->participants);
$this->payloadBuilder->buildServicesPayload($payload, $bookingData);
$this->payloadBuilder->buildPickupPayload($payload, $bookingData);
$this->payloadBuilder->buildDropOffPayload($payload, $bookingData);
// Add purchase vouchers if any exist
$purchaseVouchers = $this->mappingCollector->collectPurchaseVouchers($formData);
@@ -203,9 +203,9 @@ class BookingPayloadBuilder
*/
public function buildPickupPayload(array &$payload, Booking $bookingData): void
{
if (0 < count($bookingData->pickupsOutbound)) {
if (0 < count($bookingData->pickups)) {
$payload['zustiege']['zustieg'] = [];
foreach ($bookingData->pickupsOutbound as $pickup) {
foreach ($bookingData->pickups as $pickup) {
$uniqueMapping = array_unique($pickup->mapping);
$payload['zustiege']['zustieg'][] = [
'@idzustieg' => $pickup->id,
@@ -216,6 +216,29 @@ class BookingPayloadBuilder
}
}
/**
* Builds the drop-off locations section of the payload.
*
* Only included in payload if there are actual drop-off assignments for bus transportation.
*
* @param array $payload The payload array to modify
* @param Booking $bookingData The booking data object
*/
public function buildDropOffPayload(array &$payload, Booking $bookingData): void
{
if (0 < count($bookingData->dropOffs)) {
$payload['ausstiege']['ausstieg'] = [];
foreach ($bookingData->dropOffs as $dropOff) {
$uniqueMapping = array_unique($dropOff->mapping);
$payload['ausstiege']['ausstieg'][] = [
'@idzustieg' => $dropOff->id,
'@anzahl' => count($uniqueMapping),
'@zuordnung' => implode(',', array_map(fn ($index) => $index + 1, $uniqueMapping)),
];
}
}
}
/**
* Builds a create booking request payload.
*
@@ -367,6 +390,10 @@ class BookingPayloadBuilder
$this->addRoomMappingsToPayload($payload, $roomMap, $bookingDto);
$this->addServicesFromMap($payload, 'zusatzleistungen', 'zusatzleistung', '@idleistung', $serviceMap);
$this->addServicesFromMap($payload, 'zustiege', 'zustieg', '@idzustieg', $pickupMap);
$dropOffMap = $this->mappingCollector->collectDropOffMappings($bookingDto);
$this->addServicesFromMap($payload, 'ausstiege', 'ausstieg', '@idzustieg', $dropOffMap);
$this->addServicesFromMap($payload, 'versicherungen', 'versicherung', '@idversicherung', $insuranceMap);
// Add purchase vouchers (regular purchase vouchers, excluding goodwill vouchers)
@@ -36,8 +36,8 @@ class ParticipantServiceProcessor
$servicesToReset = [
...$bookingData->additionalServices,
...$bookingData->transportationServices,
...$bookingData->pickupsOutbound,
...$bookingData->pickupsInbound,
...$bookingData->pickups,
...$bookingData->dropOffs,
...$bookingData->rooms,
...$bookingData->insurances,
];
@@ -66,6 +66,7 @@ class ParticipantServiceProcessor
$this->processAdditionalServices($participant, $bookingData, $travelData);
$this->processTransportationServices($participant, $bookingData, $travelData);
$this->processPickupLocations($participant, $bookingData);
$this->processDropOffLocations($participant, $bookingData);
$this->processRoomAssignment($participant, $bookingData);
$this->processInsurance($participant, $bookingData, $travelData);
}
@@ -92,15 +93,15 @@ class ParticipantServiceProcessor
}
}
foreach ($bookingData->pickupsOutbound as $pickup) {
foreach ($bookingData->pickups as $pickup) {
if (0 === count($pickup->mapping)) {
unset($bookingData->pickupsOutbound[$pickup->id]);
unset($bookingData->pickups[$pickup->id]);
}
}
foreach ($bookingData->pickupsInbound as $pickup) {
if (0 === count($pickup->mapping)) {
unset($bookingData->pickupsInbound[$pickup->id]);
foreach ($bookingData->dropOffs as $dropOff) {
if (0 === count($dropOff->mapping)) {
unset($bookingData->dropOffs[$dropOff->id]);
}
}
@@ -232,10 +233,34 @@ class ParticipantServiceProcessor
$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;
if (false === isset($bookingData->pickups[$selectedPickup->id])) {
$bookingData->pickups[$selectedPickup->id] = $selectedPickup;
}
$bookingData->pickupsOutbound[$selectedPickup->id]->mapping[] = $participant->index;
$bookingData->pickups[$selectedPickup->id]->mapping[] = $participant->index;
}
}
/**
* Processes drop-off locations for participants using bus transportation.
*
* Only processes drop-off locations for bus transportation services and maps the participant
* to their selected drop-off location.
*
* @param ParticipantDto $participant The participant data from the form
* @param Booking $bookingData The booking data object to update
*/
private function processDropOffLocations(ParticipantDto $participant, Booking $bookingData): void
{
$hasOutboundBus = null !== $participant->transportationOutbound
&& 'BUS' === $participant->transportationOutbound->subType;
$hasInboundBus = null !== $participant->transportationInbound
&& 'BUS' === $participant->transportationInbound->subType;
if (($hasOutboundBus || $hasInboundBus) && null !== $selectedDropOff = $participant->dropOff) {
if (false === isset($bookingData->dropOffs[$selectedDropOff->id])) {
$bookingData->dropOffs[$selectedDropOff->id] = $selectedDropOff;
}
$bookingData->dropOffs[$selectedDropOff->id]->mapping[] = $participant->index;
}
}
@@ -31,14 +31,14 @@ class PersonalDataSynchronizer
* 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
* @param bool $isInternalAgencyBooking Whether to fill default names for canceled participants
* @param array<ParticipantDto> $participants The participants array from the form
* @param Booking $bookingData The booking data object to update
* @param bool $isInternalAgencyBooking Whether to fill default names for canceled participants
*/
public function updateParticipantPersonalData(
array $participants,
Booking $bookingData,
bool $isInternalAgencyBooking = false
bool $isInternalAgencyBooking = false,
): void {
// Fill missing mandatory fields on applicant (for company bookings by travel agencies)
// This is separate from participants - applicant may be a company while participants are real people
@@ -143,6 +143,28 @@ class ServiceMappingCollector
return $pickupMap;
}
/**
* Collects drop-off location mappings.
*
* Collects the drop-off selection for the inbound/return direction.
*
* @return array<string, array<int>> Map of drop-off ID to participant IDs
*/
public function collectDropOffMappings(BookingDto $bookingDto): array
{
$dropOffMap = [];
foreach ($bookingDto->participants as $index => $participant) {
$participantId = $index + 1;
if (null !== $participant->dropOff) {
$dropOffMap[$participant->dropOff->id][] = $participantId;
}
}
return $dropOffMap;
}
/**
* Collects insurance mappings.
*
+25 -5
View File
@@ -41,8 +41,8 @@ class Booking
public array $transportationServices = [];
public array $additionalServices = [];
public array $rooms = [];
public array $pickupsOutbound = [];
public array $pickupsInbound = [];
public array $pickups = [];
public array $dropOffs = [];
public array $surcharges = [];
public array $insurances = [];
public ?int $invoiceNumber = null;
@@ -136,8 +136,7 @@ class Booking
* Retrieves pickup service for a specific participant.
*
* Finds the pickup service assigned to the specified participant
* from the outbound pickup services. The API only supports pickups
* when outbound transportation is bus.
* from the pickup services.
*
* @param int $participantIndex The participant index to search for
*
@@ -145,7 +144,7 @@ class Booking
*/
public function getPickupForParticipant(int $participantIndex): ?Pickup
{
foreach ($this->pickupsOutbound as $pickup) {
foreach ($this->pickups as $pickup) {
if (in_array($participantIndex, $pickup->mapping)) {
return $pickup;
}
@@ -154,6 +153,27 @@ class Booking
return null;
}
/**
* Retrieves drop-off service for a specific participant.
*
* Finds the drop-off service assigned to the specified participant
* from the drop-off services (inbound/return direction).
*
* @param int $participantIndex The participant index to search for
*
* @return Pickup|null The matching drop-off service or null if not found
*/
public function getDropOffForParticipant(int $participantIndex): ?Pickup
{
foreach ($this->dropOffs as $dropOff) {
if (in_array($participantIndex, $dropOff->mapping)) {
return $dropOff;
}
}
return null;
}
/**
* Gets the insurance assigned to a specific participant.
*
+2 -2
View File
@@ -84,10 +84,10 @@ class Travel
public bool $transportationServicesMutable = true;
#[Groups(['api:single'])]
public array $pickupsOutbound = [];
public array $pickups = [];
#[Groups(['api:single'])]
public array $pickupsInbound = [];
public array $dropOffs = [];
#[Groups(['api:single'])]
public bool $pickupsMutable = true;
+2 -2
View File
@@ -61,8 +61,8 @@ class PickupLoader extends AbstractLoader
public function patchPickupsDetails(Travel $travel): void
{
$travel->pickupsOutbound = $this->patchAndSortPickups($travel->pickupsOutbound);
$travel->pickupsInbound = $this->patchAndSortPickups($travel->pickupsInbound);
$travel->pickups = $this->patchAndSortPickups($travel->pickups);
$travel->dropOffs = $this->patchAndSortPickups($travel->dropOffs);
}
private function patchAndSortPickups(array $pickups): array
+5 -4
View File
@@ -91,11 +91,12 @@ class BookingParser extends AbstractParser
$pickupsData = $node->filterXPath('//zustiege/zustieg');
if (0 < $pickupsData->count()) {
$booking->pickupsOutbound = $this->pickupsParser->parse($pickupsData);
$booking->pickups = $this->pickupsParser->parse($pickupsData);
}
$pickupsData = $node->filterXPath('//zustiege_rueck/zustieg_rueck');
if (0 < $pickupsData->count()) {
$booking->pickupsInbound = $this->pickupsParser->parse($pickupsData);
$dropOffsData = $node->filterXPath('//ausstiege/ausstieg');
if (0 < $dropOffsData->count()) {
$booking->dropOffs = $this->pickupsParser->parse($dropOffsData);
}
$surchargesData = $node->filterXPath('//zuschlaege/zuschlag');
+14 -14
View File
@@ -76,11 +76,11 @@ class TravelParser extends AbstractParser
$travel->transportationServices = $this
->getTransportationServices($node->filterXPath('//lei_befoerderung/leistung'));
$travel->rooms = $this->getRooms($hotelNode);
$travel->pickupsOutbound = $this->getPickups($node->filterXPath('//zustiege/zustieg'), $dateFrom, true);
$travel->pickupsInbound = $this->getPickups($node->filterXPath('//zustiege_rueck/zustieg_rueck'), $dateTo, false);
$travel->pickups = $this->getPickups($node->filterXPath('//zustiege/zustieg'), $dateFrom, true);
$travel->dropOffs = $this->getPickups($node->filterXPath('//zustiege_rueck/zustieg_rueck'), $dateTo, false);
// Merge inbound prices into outbound pickups for split pricing support
$this->mergeInboundPricesIntoOutboundPickups($travel->pickupsOutbound, $travel->pickupsInbound);
// Merge drop-off prices into pickups for split pricing support
$this->mergeDropOffPricesIntoPickups($travel->pickups, $travel->dropOffs);
$travel->guide = $this->getGuide($node);
@@ -422,21 +422,21 @@ class TravelParser extends AbstractParser
}
/**
* Merges inbound pickup prices into outbound pickups for split pricing support.
* Merges drop-off prices into pickups for split pricing support.
*
* For each outbound pickup, finds the matching inbound pickup by ID and copies
* the inbound price to the outbound pickup's priceInbound property. This enables
* split pricing calculations where pickup costs are distributed between outbound
* For each pickup, finds the matching drop-off by ID and copies
* the inbound price to the pickup's priceInbound property. This enables
* split pricing calculations where costs are distributed between outbound
* and inbound transportation.
*
* @param array<int, Pickup> $pickupsOutbound Outbound pickups indexed by ID (modified in place)
* @param array<int, Pickup> $pickupsInbound Inbound pickups indexed by ID
* @param array<int, Pickup> $pickups Pickups indexed by ID (modified in place)
* @param array<int, Pickup> $dropOffs Drop-offs indexed by ID
*/
private function mergeInboundPricesIntoOutboundPickups(array &$pickupsOutbound, array $pickupsInbound): void
private function mergeDropOffPricesIntoPickups(array &$pickups, array $dropOffs): void
{
foreach ($pickupsOutbound as $pickupId => $outboundPickup) {
if (isset($pickupsInbound[$pickupId])) {
$outboundPickup->priceInbound = $pickupsInbound[$pickupId]->priceInbound;
foreach ($pickups as $pickupId => $pickup) {
if (isset($dropOffs[$pickupId])) {
$pickup->priceInbound = $dropOffs[$pickupId]->priceInbound;
}
}
}