From 776b9d0912ada4d75665d96f5c9fa3b36f2fbff0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Thu, 8 Jan 2026 12:54:59 +0100 Subject: [PATCH] feat: proper determination of agency initiated bookings For booking that are agency initiated (groups) different rules apply concerning mutability and requirement of personal data and whether the first participant is the applicant or not. This commit adds logic to evaluate the agency id assigned to the booking data to decide. --- src/BusProNet/XmlLoader/AgencyLoader.php | 1 + src/Form/BookingEditType.php | 4 +- src/Form/BookingParticipantType.php | 16 +++- src/Form/Model/BookingDto.php | 15 +++ ...AuthenticatedUserPersonalDataCondition.php | 87 ----------------- .../FirstParticipantReadOnlyCondition.php | 95 +++++++++++++++++++ .../PersonalDataMutabilityCondition.php | 11 ++- src/Form/Service/CreateFieldStateProvider.php | 39 ++++---- src/Form/Service/EditFieldStateProvider.php | 14 +-- .../ParticipantAssignedRoomFieldHandler.php | 4 +- .../ParticipantFieldOptionsProvider.php | 5 +- src/Service/BookingEditDataLoaderService.php | 7 ++ src/Service/BookingService.php | 5 + src/Service/ParticipantCardDataService.php | 11 ++- templates/booking/_participant_form.html.twig | 6 +- templates/booking/create/step_2.html.twig | 2 +- templates/booking/create/step_4.html.twig | 2 +- templates/booking/edit/index.html.twig | 2 +- tests/Service/BookingServiceBabyTest.php | 6 +- tests/Service/BookingServiceStatusTest.php | 6 +- 20 files changed, 201 insertions(+), 137 deletions(-) delete mode 100644 src/Form/Service/Condition/AuthenticatedUserPersonalDataCondition.php create mode 100644 src/Form/Service/Condition/FirstParticipantReadOnlyCondition.php diff --git a/src/BusProNet/XmlLoader/AgencyLoader.php b/src/BusProNet/XmlLoader/AgencyLoader.php index 4bd9e34..dd2cc5f 100644 --- a/src/BusProNet/XmlLoader/AgencyLoader.php +++ b/src/BusProNet/XmlLoader/AgencyLoader.php @@ -15,6 +15,7 @@ use Symfony\Contracts\Cache\ItemInterface; class AgencyLoader { public const DEFAULT_AGENCY_CODE = 'INTERN'; + public const INTERNAL_AGENCY_CODE = '0004'; public function __construct( private readonly CacheInterface $cache, diff --git a/src/Form/BookingEditType.php b/src/Form/BookingEditType.php index 33de2c0..e465572 100644 --- a/src/Form/BookingEditType.php +++ b/src/Form/BookingEditType.php @@ -35,8 +35,8 @@ class BookingEditType extends AbstractType $groups = ['booking_edit']; - // Edit mode: strict only if applicant is immutable - if (false === ($data->participants[0]?->mutable ?? true)) { + // Strict validation in edit mode except for internal agency bookings + if (false === $data->isInternalAgencyBooking()) { $groups[] = 'strict_required'; } diff --git a/src/Form/BookingParticipantType.php b/src/Form/BookingParticipantType.php index f698131..395f822 100644 --- a/src/Form/BookingParticipantType.php +++ b/src/Form/BookingParticipantType.php @@ -162,10 +162,15 @@ class BookingParticipantType extends AbstractType // Helper to get field state or empty array $getFieldState = fn (string $fieldName) => $allFieldStates[$fieldName] ?? []; + // Personal data fields are optional only for internal agency bookings in edit mode + $personalDataOptional = BookingDto::MODE_EDIT === $bookingDto->getMode() + && $bookingDto->isInternalAgencyBooking(); + // Add personal data fields with conditional inclusion for authenticated users if ($this->fieldStateProvider->shouldIncludeField('firstName', $bookingDto, $participantIndex)) { $form->add('firstName', TextType::class, $this->mergeFieldState([ 'label' => 'Vorname', + 'required' => !$personalDataOptional, 'sanitize_html' => true, 'property_path' => 'participant.firstName', 'attr' => [ @@ -177,6 +182,7 @@ class BookingParticipantType extends AbstractType if ($this->fieldStateProvider->shouldIncludeField('lastName', $bookingDto, $participantIndex)) { $form->add('lastName', TextType::class, $this->mergeFieldState([ 'label' => 'Nachname', + 'required' => !$personalDataOptional, 'sanitize_html' => true, 'property_path' => 'participant.lastName', 'attr' => [ @@ -188,6 +194,7 @@ class BookingParticipantType extends AbstractType if ($this->fieldStateProvider->shouldIncludeField('dateOfBirth', $bookingDto, $participantIndex)) { $form->add('dateOfBirth', BirthdayType::class, $this->mergeFieldState([ 'label' => 'Geburtsdatum', + 'required' => !$personalDataOptional, 'widget' => 'text', 'input' => 'datetime_immutable', 'html5' => false, @@ -197,7 +204,7 @@ class BookingParticipantType extends AbstractType if ($this->fieldStateProvider->shouldIncludeField('gender', $bookingDto, $participantIndex)) { $form->add('gender', ChoiceType::class, $this->mergeFieldState([ - 'label' => 'Geschlecht', + 'label' => 'Gender', 'required' => false, 'placeholder' => 'keine Angabe', 'choices' => [ @@ -212,6 +219,7 @@ class BookingParticipantType extends AbstractType if ($this->fieldStateProvider->shouldIncludeField('nationality', $bookingDto, $participantIndex)) { $form->add('nationality', CountryType::class, $this->mergeFieldState([ 'label' => 'Nationalität', + 'required' => !$personalDataOptional, 'property' => 'nationality', 'preferred_choices' => ['D', 'A', 'CH'], 'property_path' => 'participant.nationality', @@ -221,6 +229,7 @@ class BookingParticipantType extends AbstractType if ($this->fieldStateProvider->shouldIncludeField('email', $bookingDto, $participantIndex)) { $form->add('email', EmailType::class, $this->mergeFieldState([ 'label' => 'E-Mail', + 'required' => !$personalDataOptional, 'property_path' => 'participant.email', 'attr' => [ 'autocomplete' => 'leave-me-alone-chrome', @@ -457,9 +466,8 @@ class BookingParticipantType extends AbstractType } else { $groups[] = 'booking_edit'; - // Determine if strict validation applies in edit mode - // Edit mode with immutable applicant requires strict validation - if (false === ($bookingContext->participants[0]?->mutable ?? true)) { + // Strict validation in edit mode except for internal agency bookings + if (false === $bookingContext->isInternalAgencyBooking()) { $groups[] = 'strict_required'; } } diff --git a/src/Form/Model/BookingDto.php b/src/Form/Model/BookingDto.php index 451bdc1..5730cc2 100644 --- a/src/Form/Model/BookingDto.php +++ b/src/Form/Model/BookingDto.php @@ -7,6 +7,7 @@ namespace App\Form\Model; use App\BusProNet\Constants; use App\BusProNet\Model\Booking; use App\BusProNet\Model\Travel; +use App\BusProNet\XmlLoader\AgencyLoader; use App\Validator\Constraints as AppAssert; use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Context\ExecutionContextInterface; @@ -49,6 +50,8 @@ class BookingDto public ?int $agencyId = null; + public ?string $agencyCode = null; + /** * Booking status code for API submission. * Values: 'F' (Final/Frei), 'A' (Anfrage/Inquiry). @@ -310,4 +313,16 @@ class BookingDto return $oldSnapshot !== $newSnapshot; } + + /** + * Determines if this booking is initiated by the internal agency. + * + * Internal agency bookings have a different relationship between applicant + * and first participant: the applicant is a staff member, not the first + * participant. This affects personal data editability rules. + */ + public function isInternalAgencyBooking(): bool + { + return AgencyLoader::INTERNAL_AGENCY_CODE === $this->agencyCode; + } } diff --git a/src/Form/Service/Condition/AuthenticatedUserPersonalDataCondition.php b/src/Form/Service/Condition/AuthenticatedUserPersonalDataCondition.php deleted file mode 100644 index 4617d07..0000000 --- a/src/Form/Service/Condition/AuthenticatedUserPersonalDataCondition.php +++ /dev/null @@ -1,87 +0,0 @@ - $formData Current form data (unused) - * - * @return bool True if fields should be shown as static text - */ - public function evaluate(BookingDto $bookingDto, int $participantIndex, array $formData): bool - { - $participant = $bookingDto->getParticipant($participantIndex); - if (null === $participant) { - return false; - } - - // In edit mode, compare participant's personId with applicant's personId - // This distinguishes customer-initiated (match) from agency-initiated (no match) bookings - if (BookingDto::MODE_EDIT === $bookingDto->getMode()) { - $applicantPersonId = $bookingDto->booking?->applicant?->personId; - if (null === $applicantPersonId) { - return false; - } - - return $participant->personId === $applicantPersonId; - } - - // In create mode, check if participant has personId (prepopulated from BPN account) - return null !== $participant->personId; - } - - /** - * Returns field names that this condition depends on. - * - * This condition is based on personId which is set during prepopulation - * and doesn't change during form interaction, so no field dependencies. - * - * @return string[] Empty array - no field dependencies - */ - public function getDependentFields(): array - { - return []; - } - - /** - * Returns a human-readable description of this condition. - * - * @return string Description of the authenticated user personal data protection logic - */ - public function getDescription(): string - { - return 'Personal data is shown as static text for the logged-in user (edit via personal data form)'; - } -} diff --git a/src/Form/Service/Condition/FirstParticipantReadOnlyCondition.php b/src/Form/Service/Condition/FirstParticipantReadOnlyCondition.php new file mode 100644 index 0000000..d027379 --- /dev/null +++ b/src/Form/Service/Condition/FirstParticipantReadOnlyCondition.php @@ -0,0 +1,95 @@ + $formData Current form data (unused) + * + * @return bool True if fields should be shown as static text (read-only) + */ + public function evaluate(BookingDto $bookingDto, int $participantIndex, array $formData): bool + { + // This condition only applies to first participant (agency-based read-only) + // Non-first participants: return false here, but may still be read-only via PersonalDataMutabilityCondition + if (0 !== $participantIndex) { + return false; + } + + $participant = $bookingDto->getParticipant($participantIndex); + + // CREATE mode logic + if (BookingDto::MODE_CREATE === $bookingDto->getMode()) { + // Anonymous user (no personId) = must fill in data = editable + if (null === $participant?->personId) { + return false; + } + + // Internal agency = first participant ≠ applicant = editable + if ($bookingDto->isInternalAgencyBooking()) { + return false; + } + + // Logged-in user with non-internal agency = data prepopulated = read-only + return true; + } + + // EDIT mode logic + // Internal agency = first participant ≠ applicant = editable (mutability check applies separately) + if ($bookingDto->isInternalAgencyBooking()) { + return false; + } + + // Non-internal agency = first participant = applicant = always read-only + return true; + } + + /** + * Returns field names that this condition depends on. + * + * This condition is based on agency code and personId which are set during initialization + * and don't change during form interaction, so no field dependencies. + * + * @return string[] Empty array - no field dependencies + */ + public function getDependentFields(): array + { + return []; + } + + /** + * Returns a human-readable description of this condition. + * + * @return string Description of the first participant read-only logic + */ + public function getDescription(): string + { + return 'First participant personal data is read-only for non-internal agency bookings with logged-in users'; + } +} diff --git a/src/Form/Service/Condition/PersonalDataMutabilityCondition.php b/src/Form/Service/Condition/PersonalDataMutabilityCondition.php index 19b96eb..8bd36e9 100644 --- a/src/Form/Service/Condition/PersonalDataMutabilityCondition.php +++ b/src/Form/Service/Condition/PersonalDataMutabilityCondition.php @@ -10,12 +10,13 @@ use App\Form\Service\Contract\FieldConditionInterface; /** * Condition that determines if personal data fields should be hidden based on BPN mutability flag. * - * This condition is used in EDIT MODE ONLY to respect the BPN API's per-participant - * mutability flag (`aenderungmoeglich`). When a participant's data is not mutable - * (mutable=false), personal data fields should be hidden and displayed as static text. + * This condition respects the BPN API's per-participant mutability flag (`aenderungmoeglich`). + * When a participant's data is not mutable (mutable=false), personal data fields should be + * hidden and displayed as static text. * - * This differs from AuthenticatedUserPersonalDataCondition which is used in CREATE MODE - * to protect authenticated applicant data. + * This condition is combined with FirstParticipantReadOnlyCondition via OR in edit mode. + * FirstParticipantReadOnlyCondition handles agency-based read-only logic for first participant, + * while this condition applies BPN mutability rules to all participants. * * In edit mode, BPN determines mutability based on business rules (e.g., payment status, * booking state, etc.). We must respect this flag to prevent users from attempting to diff --git a/src/Form/Service/CreateFieldStateProvider.php b/src/Form/Service/CreateFieldStateProvider.php index 54d4e83..7d80f27 100644 --- a/src/Form/Service/CreateFieldStateProvider.php +++ b/src/Form/Service/CreateFieldStateProvider.php @@ -7,7 +7,6 @@ namespace App\Form\Service; use App\BusProNet\Utility\DirectionMapper; use App\Form\Service\Abstract\AbstractFieldStateProvider; use App\Form\Service\Condition\AgeRangeCondition; -use App\Form\Service\Condition\AuthenticatedUserPersonalDataCondition; use App\Form\Service\Condition\BabyAgeCondition; use App\Form\Service\Condition\BookingEligibilityCondition; use App\Form\Service\Condition\BulkInsuranceBookingCondition; @@ -16,6 +15,7 @@ use App\Form\Service\Condition\DateOfBirthProvidedCondition; use App\Form\Service\Condition\FieldValueCondition; use App\Form\Service\Condition\FinalBookingOnlyCondition; use App\Form\Service\Condition\FirstParticipantCondition; +use App\Form\Service\Condition\FirstParticipantReadOnlyCondition; use App\Form\Service\Condition\MultipleParticipantsCondition; use App\Form\Service\Condition\RentalInsuranceAvailableCondition; use App\Form\Service\Condition\RentalSelectionCondition; @@ -80,59 +80,60 @@ class CreateFieldStateProvider extends AbstractFieldStateProvider $rentalCondition = new RentalSelectionCondition(); $skiPassCondition = new SkiPassSelectionCondition(); - // Authenticated user personal data protection - // Render personal data fields as static text for participants linked to BPN accounts (prevents duplicate records) - $authenticatedUserCondition = new AuthenticatedUserPersonalDataCondition(); + // First participant read-only condition + // Render personal data fields as static text for first participant in non-internal agency bookings + // when user is logged in (data is prepopulated from BPN account) + $firstParticipantReadOnlyCondition = new FirstParticipantReadOnlyCondition(); $this->fieldStateConditions['firstName'] = [ - 'static_text' => $authenticatedUserCondition, + 'static_text' => $firstParticipantReadOnlyCondition, ]; $this->fieldStateConditions['lastName'] = [ - 'static_text' => $authenticatedUserCondition, + 'static_text' => $firstParticipantReadOnlyCondition, ]; $this->fieldStateConditions['gender'] = [ - 'static_text' => $authenticatedUserCondition, + 'static_text' => $firstParticipantReadOnlyCondition, ]; $this->fieldStateConditions['nationality'] = [ - 'static_text' => $authenticatedUserCondition, + 'static_text' => $firstParticipantReadOnlyCondition, ]; $this->fieldStateConditions['dateOfBirth'] = [ - 'static_text' => $authenticatedUserCondition, + 'static_text' => $firstParticipantReadOnlyCondition, ]; $this->fieldStateConditions['email'] = [ - 'static_text' => $authenticatedUserCondition, + 'static_text' => $firstParticipantReadOnlyCondition, ]; - // Mobile field: static text for authenticated users, required for first participant (guest bookings) + // Mobile field: static text for first participant (when read-only), required for first participant (guest bookings) $this->fieldStateConditions['mobile'] = [ - 'static_text' => $authenticatedUserCondition, + 'static_text' => $firstParticipantReadOnlyCondition, 'required' => new FirstParticipantCondition(), ]; // Address subfields - must render as static text to prevent creating duplicate BPN records $this->fieldStateConditions['address.street'] = [ - 'static_text' => $authenticatedUserCondition, + 'static_text' => $firstParticipantReadOnlyCondition, ]; $this->fieldStateConditions['address.postCode'] = [ - 'static_text' => $authenticatedUserCondition, + 'static_text' => $firstParticipantReadOnlyCondition, ]; $this->fieldStateConditions['address.city'] = [ - 'static_text' => $authenticatedUserCondition, + 'static_text' => $firstParticipantReadOnlyCondition, ]; $this->fieldStateConditions['address.country'] = [ - 'static_text' => $authenticatedUserCondition, + 'static_text' => $firstParticipantReadOnlyCondition, ]; $this->fieldStateConditions['address.district'] = [ - 'static_text' => $authenticatedUserCondition, + 'static_text' => $firstParticipantReadOnlyCondition, ]; // Note: Body dimensions (height, weight, shoeSize) are NOT hidden for authenticated users @@ -280,9 +281,9 @@ class CreateFieldStateProvider extends AbstractFieldStateProvider ]; // Make address field required for first participant (index 0) - // Also render as static text for authenticated users to prevent duplicate BPN records + // Also render as static text for first participant when read-only $this->fieldStateConditions['address'] = [ - 'static_text' => $authenticatedUserCondition, + 'static_text' => $firstParticipantReadOnlyCondition, 'required' => new FirstParticipantCondition(), ]; diff --git a/src/Form/Service/EditFieldStateProvider.php b/src/Form/Service/EditFieldStateProvider.php index 2d6b536..67f1641 100644 --- a/src/Form/Service/EditFieldStateProvider.php +++ b/src/Form/Service/EditFieldStateProvider.php @@ -9,12 +9,12 @@ use App\Form\Model\BookingDto; use App\Form\Service\Abstract\AbstractFieldStateProvider; use App\Form\Service\Condition\AdditionalServicesMutabilityCondition; use App\Form\Service\Condition\AgeRangeCondition; -use App\Form\Service\Condition\AuthenticatedUserPersonalDataCondition; use App\Form\Service\Condition\BookingModeCondition; use App\Form\Service\Condition\CompositeCondition; use App\Form\Service\Condition\DateOfBirthProvidedCondition; use App\Form\Service\Condition\FieldValueCondition; use App\Form\Service\Condition\FirstParticipantCondition; +use App\Form\Service\Condition\FirstParticipantReadOnlyCondition; use App\Form\Service\Condition\PersonalDataMutabilityCondition; use App\Form\Service\Condition\PickupsMutabilityCondition; use App\Form\Service\Condition\RentalInsuranceAvailableCondition; @@ -50,17 +50,17 @@ class EditFieldStateProvider extends AbstractFieldStateProvider $pickupsMutabilityCondition = new PickupsMutabilityCondition(); // Personal data protection in edit mode: - // 1. Participant with personId (linked to BPN account) - edit via personal data form instead + // 1. First participant in non-internal agency booking - always read-only (first participant = applicant) // 2. BPN mutability flag (mutable=false) - respects BPN business rules // Show fields as static text if EITHER condition is true - $authenticatedUserCondition = new AuthenticatedUserPersonalDataCondition(); + $firstParticipantReadOnlyCondition = new FirstParticipantReadOnlyCondition(); $personalDataMutabilityCondition = new PersonalDataMutabilityCondition(); $personalDataHiddenCondition = CompositeCondition::or( - $authenticatedUserCondition, + $firstParticipantReadOnlyCondition, $personalDataMutabilityCondition ); - // Render all personal data fields as static text if authenticated user OR BPN indicates not mutable + // Render all personal data fields as static text if first participant in non-internal agency OR BPN indicates not mutable // Using 'static_text' state (not 'hidden') so template renders values as static text $personalDataFields = [ 'firstName', @@ -77,12 +77,12 @@ class EditFieldStateProvider extends AbstractFieldStateProvider ]; } - // Address fields - render as static text if authenticated user OR participant not mutable + // Address fields - render as static text if first participant in non-internal agency OR participant not mutable $this->fieldStateConditions['address'] = [ 'static_text' => $personalDataHiddenCondition, ]; - // Address subfields - render as static text if authenticated user OR participant not mutable + // Address subfields - render as static text if first participant in non-internal agency OR participant not mutable $this->fieldStateConditions['address.street'] = [ 'static_text' => $personalDataHiddenCondition, ]; diff --git a/src/Form/Service/ParticipantAssignedRoomFieldHandler.php b/src/Form/Service/ParticipantAssignedRoomFieldHandler.php index 00e45f3..262b6be 100644 --- a/src/Form/Service/ParticipantAssignedRoomFieldHandler.php +++ b/src/Form/Service/ParticipantAssignedRoomFieldHandler.php @@ -189,7 +189,9 @@ class ParticipantAssignedRoomFieldHandler extends AbstractParticipantFieldHandle $participant->assignedRoomId = null; // Add notification for unassigned participant - $participantLabel = $participant->isApplicant() ? 'Anmelder:in' : 'Teilnehmer:in '.($participant->index + 1); + // For internal agency bookings, first participant is not the applicant + $isApplicant = $participant->isApplicant() && false === $bookingDto->isInternalAgencyBooking(); + $participantLabel = $isApplicant ? 'Anmelder:in' : 'Teilnehmer:in '.($participant->index + 1); $participant->addNotification('warning', sprintf('%s wurde von %s entfernt', $roomLabel, $participantLabel)); ++$unassignedCount; diff --git a/src/Form/Service/ParticipantFieldOptionsProvider.php b/src/Form/Service/ParticipantFieldOptionsProvider.php index 130bc88..1303219 100644 --- a/src/Form/Service/ParticipantFieldOptionsProvider.php +++ b/src/Form/Service/ParticipantFieldOptionsProvider.php @@ -421,7 +421,10 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider 'label' => 'Für alle Teilnehmer buchen', 'required' => false, 'attr' => [ - 'data-description' => 'Der hier angezeigte Preis gilt nur für den/die Anmelder:in. Die Preise für die anderen Teilnehmer:innen werden automatisch aktualisiert.', + 'data-description' => sprintf( + 'Der hier angezeigte Preis gilt nur für %s. Die Preise für die anderen Teilnehmer:innen werden automatisch aktualisiert.', + $bookingDto->isInternalAgencyBooking() ? 'Teilnehmer:in 1' : 'den/die Anmelder:in' + ), ], ]; diff --git a/src/Service/BookingEditDataLoaderService.php b/src/Service/BookingEditDataLoaderService.php index acd8505..9ceb557 100644 --- a/src/Service/BookingEditDataLoaderService.php +++ b/src/Service/BookingEditDataLoaderService.php @@ -8,6 +8,7 @@ use App\BusProNet\ApiClient; use App\BusProNet\DataProcessor\BookingDataProcessor; use App\BusProNet\Model\Booking; use App\BusProNet\Model\Notification; +use App\BusProNet\XmlLoader\AgencyLoader; use App\Entity\User; use App\Form\Model\BookingDto; use App\Security\Crypt; @@ -35,6 +36,7 @@ class BookingEditDataLoaderService private readonly BookingFingerprintService $fingerprintService, private readonly TravelDataService $travelDataService, private readonly BookingEditDraftService $draftService, + private readonly AgencyLoader $agencyLoader, private readonly Crypt $crypt, private readonly TagAwareCacheInterface $bpnCache, ) { @@ -125,6 +127,11 @@ class BookingEditDataLoaderService $formData = $this->bookingDataProcessor->createBookingDtoFromBooking($bookingData, $travelData); + // Set agency code for internal agency detection (used by field state conditions) + $formData->agencyCode = null !== $bookingData->agencyId + ? $this->agencyLoader->loadById($bookingData->agencyId)?->code + : null; + // 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); diff --git a/src/Service/BookingService.php b/src/Service/BookingService.php index fc7757e..c9933e4 100644 --- a/src/Service/BookingService.php +++ b/src/Service/BookingService.php @@ -5,6 +5,7 @@ namespace App\Service; use App\BusProNet\Constants; use App\BusProNet\Model\Room; use App\BusProNet\Model\Travel; +use App\BusProNet\XmlLoader\AgencyLoader; use App\Exception\BookingSessionNotFoundException; use App\Exception\NoRoomsAvailableException; use App\Form\Model\BookingDto; @@ -28,6 +29,7 @@ class BookingService private readonly TravelDataService $travelDataService, private readonly BookingPriceCalculatorService $priceCalculator, private readonly ParticipantEligibilityService $participantEligibilityService, + private readonly AgencyLoader $agencyLoader, #[Autowire('%default_booking_status%')] private readonly string $defaultBookingStatus, ) { @@ -267,6 +269,9 @@ class BookingService $bookingCreateDto->roomSelections = $roomSelections; $bookingCreateDto->currentStep = 1; $bookingCreateDto->agencyId = $agencyId; + $bookingCreateDto->agencyCode = null !== $agencyId + ? $this->agencyLoader->loadById($agencyId)?->code + : null; $bookingCreateDto->bookingStatus = $bookingStatus; $this->saveBookingDto($request, $bookingCreateDto, BookingDto::MODE_CREATE); diff --git a/src/Service/ParticipantCardDataService.php b/src/Service/ParticipantCardDataService.php index 1165870..fc7edf6 100644 --- a/src/Service/ParticipantCardDataService.php +++ b/src/Service/ParticipantCardDataService.php @@ -36,7 +36,7 @@ class ParticipantCardDataService } // Extract participant name with fallback - $name = $this->getParticipantName($participant, $index); + $name = $this->getParticipantName($bookingDto, $participant, $index); // Extract email $email = $participant->email ?? ''; @@ -78,7 +78,7 @@ class ParticipantCardDataService /** * Get participant name with fallback to generic label. */ - private function getParticipantName(object $participant, int $index): string + private function getParticipantName(BookingDto $bookingDto, object $participant, int $index): string { $firstName = $participant->firstName ?? ''; $lastName = $participant->lastName ?? ''; @@ -86,7 +86,12 @@ class ParticipantCardDataService $name = trim($firstName.' '.$lastName); if ('' === $name) { - return 0 === $index ? 'Anmelder:in' : 'Teilnehmer:in'; + // For internal agency bookings, first participant is not the applicant + if (0 === $index && false === $bookingDto->isInternalAgencyBooking()) { + return 'Anmelder:in'; + } + + return 'Teilnehmer:in'; } return $name; diff --git a/templates/booking/_participant_form.html.twig b/templates/booking/_participant_form.html.twig index b071f08..eba5faa 100644 --- a/templates/booking/_participant_form.html.twig +++ b/templates/booking/_participant_form.html.twig @@ -155,7 +155,7 @@ {{ participantIndex + 1 }}
- {{ participantIndex == 0 ? 'Anmelder:in' : 'Teilnehmer:in ' ~ (participantIndex + 1) }} + {{ participantIndex == 0 and not bookingDto.isInternalAgencyBooking() ? 'Anmelder:in' : 'Teilnehmer:in ' ~ (participantIndex + 1) }}
@@ -445,7 +445,7 @@
- Reiseversicherung* – wie Anmelder + Reiseversicherung* – wie {{ bookingDto.isInternalAgencyBooking() ? 'Teilnehmer:in 1' : 'Anmelder:in' }} {% if applicantInsurance %} @@ -480,7 +480,7 @@ {% else %}
- + diff --git a/templates/booking/create/step_2.html.twig b/templates/booking/create/step_2.html.twig index 15a0002..b057df7 100644 --- a/templates/booking/create/step_2.html.twig +++ b/templates/booking/create/step_2.html.twig @@ -32,7 +32,7 @@

- Teilnehmer + Teilnehmer:innen

{# Display form-level validation errors #} diff --git a/templates/booking/create/step_4.html.twig b/templates/booking/create/step_4.html.twig index d7900fd..85501f3 100644 --- a/templates/booking/create/step_4.html.twig +++ b/templates/booking/create/step_4.html.twig @@ -191,7 +191,7 @@
{{ participant.firstName }} {{ participant.lastName }} - {% if loop.first %}(Anmelder){% endif %} + {% if loop.first and not bookingCreateDto.isInternalAgencyBooking() %}(Anmelder:in){% endif %} {% if participantPrices is defined and participantPrices[loop.index0] is defined %} {{ participantPrices[loop.index0]|format_currency('EUR') }} diff --git a/templates/booking/edit/index.html.twig b/templates/booking/edit/index.html.twig index 1893aea..176609d 100644 --- a/templates/booking/edit/index.html.twig +++ b/templates/booking/edit/index.html.twig @@ -44,7 +44,7 @@ {# Scrollable content #}
{% block participant_cards %} -

Teilnehmer

+

Teilnehmer:innen

{% if isDirty %} {% include '_partials/_alert.html.twig' with { diff --git a/tests/Service/BookingServiceBabyTest.php b/tests/Service/BookingServiceBabyTest.php index 853dbf4..576f9b3 100644 --- a/tests/Service/BookingServiceBabyTest.php +++ b/tests/Service/BookingServiceBabyTest.php @@ -7,6 +7,7 @@ namespace App\Tests\Service; use App\BusProNet\Constants; use App\BusProNet\Model\Service; use App\BusProNet\Model\Travel; +use App\BusProNet\XmlLoader\AgencyLoader; use App\Form\Model\BookingDto; use App\Form\Model\ParticipantDto; use App\Service\BookingPriceCalculatorService; @@ -25,11 +26,14 @@ class BookingServiceBabyTest extends TestCase $travelDataService = $this->createMock(TravelDataService::class); $priceCalculator = $this->createMock(BookingPriceCalculatorService::class); $this->participantEligibilityService = $this->createMock(ParticipantEligibilityService::class); + $agencyLoader = $this->createMock(AgencyLoader::class); $this->bookingService = new BookingService( $travelDataService, $priceCalculator, - $this->participantEligibilityService + $this->participantEligibilityService, + $agencyLoader, + 'F' // default booking status ); } diff --git a/tests/Service/BookingServiceStatusTest.php b/tests/Service/BookingServiceStatusTest.php index df8e539..7266177 100644 --- a/tests/Service/BookingServiceStatusTest.php +++ b/tests/Service/BookingServiceStatusTest.php @@ -7,6 +7,7 @@ namespace App\Tests\Service; use App\BusProNet\Constants; use App\BusProNet\Model\Room; use App\BusProNet\Model\Travel; +use App\BusProNet\XmlLoader\AgencyLoader; use App\Exception\NoRoomsAvailableException; use App\Service\BookingPriceCalculatorService; use App\Service\BookingService; @@ -27,11 +28,14 @@ class BookingServiceStatusTest extends TestCase $this->travelDataService = $this->createMock(TravelDataService::class); $priceCalculator = $this->createMock(BookingPriceCalculatorService::class); $participantEligibility = $this->createMock(ParticipantEligibilityService::class); + $agencyLoader = $this->createMock(AgencyLoader::class); $this->bookingService = new BookingService( $this->travelDataService, $priceCalculator, - $participantEligibility + $participantEligibility, + $agencyLoader, + 'F' // default booking status ); }
wie Anmelderwie {{ bookingDto.isInternalAgencyBooking() ? 'Teilnehmer:in 1' : 'Anmelder:in' }}