fix: do not copy applicant address into participant for agency bookings

This commit is contained in:
Björn Fromme
2026-06-24 13:13:14 +02:00
parent 08f6633507
commit a15c7578c3
3 changed files with 24 additions and 13 deletions
@@ -8,6 +8,7 @@ use App\BusProNet\Constants;
use App\BusProNet\Model\Booking;
use App\BusProNet\Model\Travel;
use App\BusProNet\Utility\DirectionMapper;
use App\BusProNet\XmlLoader\AgencyLoader;
use App\Form\Model\BankAccountDto;
use App\Form\Model\BookingDto;
use App\Form\Model\ParticipantDto;
@@ -42,11 +43,12 @@ class BookingDataProcessor
*
* 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
* - For normal bookings, participant[0]'s missing address/dimensions are filled from the
* applicant (same person in normal bookings, but BPN may omit fields in <teilnehmer>)
* - For internal agency bookings, this copy is skipped: the applicant is a customer, not
* the first participant, so copying applicant data into participant[0] would be incorrect
*/
public function createBookingDtoFromBooking(Booking $booking, Travel $travel): BookingDto
public function createBookingDtoFromBooking(Booking $booking, Travel $travel, bool $isInternalAgencyBooking = false): BookingDto
{
$dto = new BookingDto($travel, $booking->hotelId);
@@ -68,11 +70,12 @@ class BookingDataProcessor
$participantData = ParticipantDto::fromPersonalData($participant);
$participantData->index = $index;
// First participant (applicant): copy data from applicant if participant data is empty
// BPN API may return full data only in <anmelder> but minimal/empty data in <teilnehmer id="1">
if (0 === $index) {
// For normal bookings, applicant === participant[0]: copy address and body dimensions
// from applicant when BPN returns full data only in <anmelder> and minimal data in
// <teilnehmer id="1">. Skipped for internal agency bookings where the applicant is
// a customer (a different entity from the first traveler).
if (0 === $index && false === $isInternalAgencyBooking) {
// Copy address if first participant has no street (indicating empty/incomplete address)
// This allows applicant and first participant to be different people with different addresses
$isEmpty = null === $participantData->address
|| null === $participantData->address->street
|| '' === trim($participantData->address->street);
+12 -4
View File
@@ -138,13 +138,21 @@ class BookingEditDataLoader
$this->travelDataService->patchAvailabilities($travelData, $availabilities);
$this->travelDataService->patchMutability($travelData, $mutableData);
$formData = $this->bookingDataProcessor->createBookingDtoFromBooking($bookingData, $travelData);
// Set agency code for internal agency detection (used by field state conditions)
$formData->agencyCode = null !== $bookingData->agencyId
// Resolve agency code before building the DTO so the internal-agency flag can be
// passed to createBookingDtoFromBooking, preventing applicant data from being
// copied into participant[0] for agency bookings where they are different people.
$agencyCode = null !== $bookingData->agencyId
? $this->agencyLoader->loadById($bookingData->agencyId)?->code
: null;
$formData = $this->bookingDataProcessor->createBookingDtoFromBooking(
$bookingData,
$travelData,
AgencyLoader::INTERNAL_AGENCY_CODE === $agencyCode,
);
$formData->agencyCode = $agencyCode;
// Set original fingerprint BEFORE applying draft, so dirty detection
// compares against the original API data (not the draft-modified data)
$formData->originalFingerprint = $this->fingerprintService->generateFingerprint($formData, true);
+1 -1
View File
@@ -34,7 +34,7 @@ class BookingEditSubmitGuard
*/
public function reconcileImmutableCategories(BookingDto $workingDto, Booking $freshBooking): bool
{
$baselineDto = $this->bookingDataProcessor->createBookingDtoFromBooking($freshBooking, $workingDto->travel);
$baselineDto = $this->bookingDataProcessor->createBookingDtoFromBooking($freshBooking, $workingDto->travel, $workingDto->isInternalAgencyBooking());
// Booking-level D-4 cutoff used as submit-time safety net for rentals.
$serviceCutoffReachedCondition = new TravelStartCutoffReachedCondition(
TravelStartCutoffReachedCondition::DEFAULT_DAYS_BEFORE_START