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\Booking;
use App\BusProNet\Model\Travel; use App\BusProNet\Model\Travel;
use App\BusProNet\Utility\DirectionMapper; use App\BusProNet\Utility\DirectionMapper;
use App\BusProNet\XmlLoader\AgencyLoader;
use App\Form\Model\BankAccountDto; use App\Form\Model\BankAccountDto;
use App\Form\Model\BookingDto; use App\Form\Model\BookingDto;
use App\Form\Model\ParticipantDto; use App\Form\Model\ParticipantDto;
@@ -42,11 +43,12 @@ class BookingDataProcessor
* *
* IMPORTANT: Address handling for edit mode * IMPORTANT: Address handling for edit mode
* - ParticipantDto::fromPersonalData() clones address objects to prevent shared references * - ParticipantDto::fromPersonalData() clones address objects to prevent shared references
* - First participant's address is NOT auto-populated from applicant * - For normal bookings, participant[0]'s missing address/dimensions are filled from the
* - Template uses placeholders to show applicant's address as hints * applicant (same person in normal bookings, but BPN may omit fields in <teilnehmer>)
* - This prevents address data from being inadvertently modified during form binding * - 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); $dto = new BookingDto($travel, $booking->hotelId);
@@ -68,11 +70,12 @@ class BookingDataProcessor
$participantData = ParticipantDto::fromPersonalData($participant); $participantData = ParticipantDto::fromPersonalData($participant);
$participantData->index = $index; $participantData->index = $index;
// First participant (applicant): copy data from applicant if participant data is empty // For normal bookings, applicant === participant[0]: copy address and body dimensions
// BPN API may return full data only in <anmelder> but minimal/empty data in <teilnehmer id="1"> // from applicant when BPN returns full data only in <anmelder> and minimal data in
if (0 === $index) { // <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) // 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 $isEmpty = null === $participantData->address
|| null === $participantData->address->street || null === $participantData->address->street
|| '' === trim($participantData->address->street); || '' === trim($participantData->address->street);
+12 -4
View File
@@ -138,13 +138,21 @@ class BookingEditDataLoader
$this->travelDataService->patchAvailabilities($travelData, $availabilities); $this->travelDataService->patchAvailabilities($travelData, $availabilities);
$this->travelDataService->patchMutability($travelData, $mutableData); $this->travelDataService->patchMutability($travelData, $mutableData);
$formData = $this->bookingDataProcessor->createBookingDtoFromBooking($bookingData, $travelData); // Resolve agency code before building the DTO so the internal-agency flag can be
// passed to createBookingDtoFromBooking, preventing applicant data from being
// Set agency code for internal agency detection (used by field state conditions) // copied into participant[0] for agency bookings where they are different people.
$formData->agencyCode = null !== $bookingData->agencyId $agencyCode = null !== $bookingData->agencyId
? $this->agencyLoader->loadById($bookingData->agencyId)?->code ? $this->agencyLoader->loadById($bookingData->agencyId)?->code
: null; : null;
$formData = $this->bookingDataProcessor->createBookingDtoFromBooking(
$bookingData,
$travelData,
AgencyLoader::INTERNAL_AGENCY_CODE === $agencyCode,
);
$formData->agencyCode = $agencyCode;
// Set original fingerprint BEFORE applying draft, so dirty detection // Set original fingerprint BEFORE applying draft, so dirty detection
// compares against the original API data (not the draft-modified data) // compares against the original API data (not the draft-modified data)
$formData->originalFingerprint = $this->fingerprintService->generateFingerprint($formData, true); $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 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. // Booking-level D-4 cutoff used as submit-time safety net for rentals.
$serviceCutoffReachedCondition = new TravelStartCutoffReachedCondition( $serviceCutoffReachedCondition = new TravelStartCutoffReachedCondition(
TravelStartCutoffReachedCondition::DEFAULT_DAYS_BEFORE_START TravelStartCutoffReachedCondition::DEFAULT_DAYS_BEFORE_START