feat: split booking between participants and applicant

addresses #869f2q1vu
This commit is contained in:
fromme committed 2026-10-19 09:18:22 +02:00
1 parent 2d49212b08
commit 1ff293f41f
44 files changed
+2024 -316

No files matched your search

+1 -1
View File
@@ -83,7 +83,7 @@ class BookingEditPreFlightChecker
}
}
if (false === $bookingDto->isInternalAgencyBooking() && true === $participant->isApplicant()) {
if (true === $bookingDto->isFirstParticipantApplicant() && true === $participant->isApplicant()) {
foreach ($this->getMissingValueAddressLabels($participant) as $valueLabel) {
$valueLabels[] = $valueLabel;
}
+18 -5
View File
@@ -33,8 +33,8 @@ class BookingNewsletterOptIn
$email = $currentUser->getEmail();
}
if ((null === $email || '' === trim((string) $email)) && isset($bookingDto->participants[0])) {
$email = $bookingDto->participants[0]->email;
if (null === $email || '' === trim((string) $email)) {
$email = $this->resolveApplicantParticipant($bookingDto)?->email;
}
if (null === $email) {
@@ -46,9 +46,16 @@ class BookingNewsletterOptIn
return false !== filter_var($normalizedEmail, FILTER_VALIDATE_EMAIL) ? $normalizedEmail : null;
}
/**
* The person who is the booking's contact: participant[0] on an ordinary booking, and the
* separate applicant record when they do not travel along.
*/
public function resolveApplicantParticipant(BookingDto $bookingDto): ?ParticipantDto
{
$participant = $bookingDto->participants[0] ?? null;
$participant = $bookingDto->applicantTravels
? ($bookingDto->participants[0] ?? null)
: $bookingDto->applicant;
if (null === $participant) {
return null;
}
@@ -63,7 +70,7 @@ class BookingNewsletterOptIn
/**
* Companions eligible for a third-party opt-in offer.
*
* Children are excluded because they are allowed to share an adult's address, so subscribing
* Children are excluded because they carry the applicant's address by default, so subscribing
* them would mail somebody else and would rest on a minor's consent. Deliberately no consent
* lookups here: whether the checkbox appears must not disclose who is already subscribed.
*
@@ -75,7 +82,13 @@ class BookingNewsletterOptIn
$seenEmails = [];
foreach ($bookingDto->participants as $participant) {
if (true === $participant->isApplicant() || true === $participant->isChild()) {
// participant[0] is only the booking contact when the applicant travels along; on a
// split booking they are an ordinary companion like everyone else.
if (true === $participant->isApplicant() && true === $bookingDto->applicantTravels) {
continue;
}
if (true === $participant->isChild()) {
continue;
}
+1 -1
View File
@@ -99,7 +99,7 @@ class ParticipantCardAssembler
if ('' === $name) {
// For internal agency bookings, first participant is not the applicant
if (0 === $index && false === $bookingDto->isInternalAgencyBooking()) {
if (0 === $index && $bookingDto->isFirstParticipantApplicant()) {
return 'Anmelder:in';
}
+28 -1
View File
@@ -135,6 +135,33 @@ class ParticipantDataPrefiller
/**
* Fills the participant DTO with generated dummy personal data.
*/
/**
* Fills the applicant DTO with generated dummy contact data.
*
* Same `#KUN#` shortcut as fillDummyParticipant(), but applicant-shaped: the applicant has to
* be a complete adult, so gender and nationality are filled too and the date of birth is well
* clear of 18. Used for manual testing against the BusPro test system.
*/
public function fillDummyApplicant(ParticipantDto $applicant): void
{
$now = CarbonImmutable::now();
$applicant->firstName = 'Probeelternteil';
$applicant->lastName = sprintf('Elternteil %s', $now->format('H:i'));
$applicant->gender = 'W';
$applicant->nationality = 'D';
$applicant->mobile = '0171/222222';
$applicant->email = '[email protected]';
$applicant->dateOfBirth = $now->subYears(45)->toImmutable();
$address = new Address();
$address->street = 'Probeweg 7';
$address->postCode = '99999';
$address->city = 'MusterOrt';
$address->country = 'D';
$applicant->address = $address;
}
public function fillDummyParticipant(ParticipantDto $participant, int $participantIndex): void
{
$participantNumber = $participantIndex + 1;
@@ -150,7 +177,7 @@ class ParticipantDataPrefiller
$address->street = 'Musterstr. 123';
$address->postCode = '99999';
$address->city = 'MusterOrt';
$address->country = 'DE';
$address->country = 'D';
$participant->address = $address;
}
}
+31
View File
@@ -40,6 +40,37 @@ class ParticipantFormSupport
return $participant;
}
/**
* Builds the traveller that stays behind at index 0 when the applicant stops travelling.
*
* Only the booking-structural fields carry over. Everything service-shaped was derived from
* the applicant's age and no longer applies to whoever actually travels in that slot.
*/
public function createBlankTravellerFrom(ParticipantDto $previous): ParticipantDto
{
$traveller = new ParticipantDto();
$traveller->index = 0;
$traveller->assignedRoomId = $previous->assignedRoomId;
return $traveller;
}
/**
* Drops a family insurance selection from every participant.
*
* Used when a booking turns into a split booking, where family insurance is not offered.
*/
public function clearFamilyInsurance(BookingDto $bookingDto): void
{
foreach ($bookingDto->participants as $participant) {
if (true === $participant->insurance?->familyInsurance) {
$participant->insurance = null;
}
$participant->bulkInsuranceBooking = false;
}
}
public function createParticipantEditDto(BookingDto $bookingDto, int $index): ParticipantEditDto
{
return new ParticipantEditDto(