wip: modernized edit flow, fix insurance tier calculation

This commit is contained in:
Björn Fromme
2026-03-16 11:59:10 +01:00
parent 28831a3b06
commit 5c0814ef9b
31 changed files with 272 additions and 536 deletions
+6 -8
View File
@@ -15,9 +15,7 @@ use App\BusProNet\Model\PersonalData;
use App\BusProNet\Model\Travel;
use App\BusProNet\Traits\ApiClientTrait;
use App\BusProNet\XmlParser\ApiResponseParser;
use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingDto;
use App\Form\Model\BookingEditDto;
use App\Form\Model\RegistrationDto;
use League\Flysystem\FilesystemException;
use League\Flysystem\FilesystemOperator;
@@ -215,14 +213,14 @@ class ApiClient
* First phase of the two-phase booking process. Validates all booking data
* and returns pricing information without creating an actual booking.
*
* @param BookingCreateDto $bookingDto The booking creation form data
* @param bool $debug Enable debug mode (XML dumps)
* @param BookingDto $bookingDto The booking creation form data
* @param bool $debug Enable debug mode (XML dumps)
*
* @return Notification|BookingResponse Notification on error, BookingResponse on success
*
* @throws ApiClientException If the API request fails
*/
public function createBookingInquiry(BookingCreateDto $bookingDto, bool $debug = false): Notification|BookingResponse
public function createBookingInquiry(BookingDto $bookingDto, bool $debug = false): Notification|BookingResponse
{
$payload = $this->bookingDataProcessor->createBookingRequestPayload($bookingDto, 'Anfrage');
@@ -242,14 +240,14 @@ class ApiClient
* Second phase of the two-phase booking process. Creates the actual booking
* after successful inquiry validation.
*
* @param BookingCreateDto $bookingDto The booking creation form data
* @param bool $debug Enable debug mode (XML dumps)
* @param BookingDto $bookingDto The booking creation form data
* @param bool $debug Enable debug mode (XML dumps)
*
* @return Notification|BookingResponse Notification on error, BookingResponse with booking number on success
*
* @throws ApiClientException If the API request fails
*/
public function createBooking(BookingCreateDto $bookingDto, bool $debug = false): Notification|BookingResponse
public function createBooking(BookingDto $bookingDto, bool $debug = false): Notification|BookingResponse
{
$payload = $this->bookingDataProcessor->createBookingRequestPayload($bookingDto, 'Buchung');
@@ -12,9 +12,7 @@ use App\BusProNet\Model\PersonalData;
use App\BusProNet\Model\Travel;
use App\BusProNet\Utility\DirectionMapper;
use App\Form\Model\BankAccountDto;
use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingDto;
use App\Form\Model\BookingEditDto;
use App\Form\Model\ParticipantDto;
use App\Service\InsuranceMatchingService;
@@ -39,6 +37,12 @@ class BookingDataProcessor
*
* Extracts all service selections from the booking entity and assigns them
* to participant DTOs, creating a unified data structure identical to create mode.
*
* 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
*/
public function createBookingDtoFromBooking(Booking $booking, Travel $travel): BookingDto
{
@@ -351,8 +355,8 @@ class BookingDataProcessor
* Insurance can be either individual or package-based, with automatic price-tier adjustment.
*
* @param ParticipantDto $participant The participant data from the form
* @param Booking $bookingData The booking data object to update
* @param Travel $travelData The travel data containing available insurances
* @param Booking $bookingData The booking data object to update
* @param Travel $travelData The travel data containing available insurances
*/
private function processInsurance(ParticipantDto $participant, Booking $bookingData, Travel $travelData): void
{
@@ -654,7 +658,7 @@ class BookingDataProcessor
* ('Anfrage') or a final booking commit ('Buchung').
*
* @param BookingDto $bookingDto The booking creation form data
* @param string $bookingType Either 'Anfrage' (inquiry) or 'Buchung' (final booking)
* @param string $bookingType Either 'Anfrage' (inquiry) or 'Buchung' (final booking)
*
* @return array The structured payload array for BusProNet API submission
*/
@@ -822,11 +826,11 @@ class BookingDataProcessor
* - abreise (departure date)
* - anzahl (number of rooms of this type booked)
*
* @param array $payload The payload array to modify
* @param array $roomMap Map of room ID to participant IDs
* @param BookingCreateDto $bookingDto The booking data for accessing room details and quantities
* @param array $payload The payload array to modify
* @param array $roomMap Map of room ID to participant IDs
* @param BookingDto $bookingDto The booking data for accessing room details and quantities
*/
private function addRoomMappingsToPayload(array &$payload, array $roomMap, BookingCreateDto $bookingDto): void
private function addRoomMappingsToPayload(array &$payload, array $roomMap, BookingDto $bookingDto): void
{
if (empty($roomMap)) {
return;
@@ -870,7 +874,7 @@ class BookingDataProcessor
*
* @return array<string, array<int>> Map of room ID to participant IDs
*/
private function collectRoomMappings(BookingCreateDto $bookingDto): array
private function collectRoomMappings(BookingDto $bookingDto): array
{
$roomMap = [];
@@ -893,7 +897,7 @@ class BookingDataProcessor
*
* @return array<string, array<int>> Map of service ID to participant IDs
*/
private function collectServiceMappings(BookingCreateDto $bookingDto): array
private function collectServiceMappings(BookingDto $bookingDto): array
{
$serviceMap = [];
@@ -944,7 +948,7 @@ class BookingDataProcessor
*
* @return array<string, array<int>> Map of transportation service ID to participant IDs
*/
private function collectTransportationMappings(BookingCreateDto $bookingDto): array
private function collectTransportationMappings(BookingDto $bookingDto): array
{
$transportationMap = [];
@@ -971,7 +975,7 @@ class BookingDataProcessor
*
* @return array<string, array<int>> Map of pickup ID to participant IDs
*/
private function collectPickupMappings(BookingCreateDto $bookingDto): array
private function collectPickupMappings(BookingDto $bookingDto): array
{
$pickupMap = [];
@@ -993,7 +997,7 @@ class BookingDataProcessor
*
* @return array<string, array<int>> Map of insurance ID to participant IDs
*/
private function collectInsuranceMappings(BookingCreateDto $bookingDto): array
private function collectInsuranceMappings(BookingDto $bookingDto): array
{
$insuranceMap = [];
@@ -1033,7 +1037,12 @@ class BookingDataProcessor
}
// Get all available insurances from travel data
$availableInsurances = array_values($bookingDto->travel->insurances);
$availableInsurances = $bookingDto->travel->insurances;
// Exclude complementary insurances from bulk assignment logic
// They are only available as part of packages and cannot be directly selected
$availableInsurances = array_filter($availableInsurances, fn ($insurance) => !$insurance->complementary);
$availableInsurances = array_values($availableInsurances);
// Use InsuranceMatchingService for proper type-based assignment with price tier matching
$assignments = $this->insuranceMatchingService->batchAssignInsuranceToParticipants(
@@ -1055,7 +1064,7 @@ class BookingDataProcessor
// In edit mode: only apply bulk insurance if participant has no insurance
// This respects the rule that once assigned, insurance cannot be changed
if ($bookingDto instanceof BookingEditDto && null !== $participant->insurance) {
if (BookingDto::MODE_EDIT === $bookingDto->getMode() && null !== $participant->insurance) {
continue; // Skip participants with existing insurance assignment
}
+10
View File
@@ -96,4 +96,14 @@ class Service
#[Groups(['api:single', 'api:list'])]
public ?string $description = null;
/**
* Indicates whether this service should be included in insurance eligibility price calculation.
*
* Maps to XML attribute 'versicherungsberechnung' (J=true, N=false).
* When false, this service's price is excluded when determining which insurance
* tier is appropriate for a participant.
*/
#[Groups(['api:single', 'api:list'])]
public bool $includeInInsuranceCalculation = true;
}
+6
View File
@@ -145,6 +145,9 @@ class TravelParser extends AbstractParser
// Parse age constraints
$this->parseServiceAgeConstraints($serviceNode, $service);
// Parse insurance calculation flag
$service->includeInInsuranceCalculation = $this->stringToBool($serviceNode->attr('versicherungsberechnung'));
$additionalServices[$serviceId] = $service;
});
@@ -192,6 +195,9 @@ class TravelParser extends AbstractParser
$service->dayTime = (new DayTimeUtility())->mapTime($timeFrom);
}
// Parse insurance calculation flag
$service->includeInInsuranceCalculation = $this->stringToBool($serviceNode->attr('versicherungsberechnung'));
$transportationServices[$serviceId] = $service;
});
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Controller\Booking;
use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingDto;
use Symfony\Component\HttpFoundation\RedirectResponse;
/**
@@ -21,7 +21,7 @@ trait BookingCreateTrait
*
* @return RedirectResponse|null Returns redirect response if validation fails, null if access is allowed
*/
private function validateStepAccess(BookingCreateDto $bookingCreateDto, int $expectedStep): ?RedirectResponse
private function validateStepAccess(BookingDto $bookingCreateDto, int $expectedStep): ?RedirectResponse
{
// Allow access to current step or any previous step
if ($expectedStep > $bookingCreateDto->currentStep) {
@@ -36,7 +36,7 @@ trait BookingCreateTrait
/**
* Redirects to the current step based on the DTO's currentStep.
*/
private function redirectToCurrentStep(BookingCreateDto $bookingCreateDto): RedirectResponse
private function redirectToCurrentStep(BookingDto $bookingCreateDto): RedirectResponse
{
$route = match ($bookingCreateDto->currentStep) {
2 => 'app_booking_create_step_2',
@@ -51,7 +51,7 @@ trait BookingCreateTrait
/**
* Returns the total number of participants based on room selections.
*/
private function getParticipantsCount(BookingCreateDto $bookingCreateDto): int
private function getParticipantsCount(BookingDto $bookingCreateDto): int
{
return $this
->bookingService
@@ -63,7 +63,7 @@ trait BookingCreateTrait
*
* @return array<string, mixed> Array containing all variables needed for the summary partial
*/
private function getSummaryVariables(BookingCreateDto $bookingCreateDto): array
private function getSummaryVariables(BookingDto $bookingCreateDto): array
{
$participantsCount = $this->getParticipantsCount($bookingCreateDto);
$summary = $this->bookingService->getRoomSummaryAndParticipantCount($bookingCreateDto);
@@ -9,7 +9,9 @@ use App\Exception\HotelNotFoundException;
use App\Exception\HotelNotInTravelException;
use App\Exception\NoRoomsAvailableException;
use App\Exception\TravelNotFoundException;
use App\Form\Model\BookingDto;
use App\Service\BookingService;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
@@ -27,7 +29,7 @@ trait BookingExceptionHandlerTrait
* Handles all booking-related exceptions and provides appropriate user feedback
* by redirecting to the error page with flash messages.
*/
protected function getOrCreateBookingCreateDto(BookingService $bookingService, Request $request): mixed
protected function getOrCreateBookingCreateDto(BookingService $bookingService, Request $request): BookingDto|RedirectResponse
{
try {
return $bookingService->getOrCreateBookingCreateDto($request);
@@ -6,7 +6,7 @@ namespace App\Controller\Booking;
use App\Controller\Traits\HtmxControllerTrait;
use App\Form\BookingCreateStep2Type;
use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingDto;
use App\Form\Model\ParticipantDto;
use App\Service\BookingPriceCalculatorService;
use App\Service\BookingService;
@@ -67,7 +67,7 @@ class CreateStep2Controller extends AbstractController
// Pre-select mandatory services for participants with birth dates
$this->bookingService->preselectMandatoryServices($bookingCreateDto);
$this->bookingService->saveBookingCreateDto($request, $bookingCreateDto);
$this->bookingService->saveBookingDto($request, $bookingCreateDto, BookingDto::MODE_CREATE);
$form = $this->createForm(BookingCreateStep2Type::class, $bookingCreateDto, [
'attr' => ['novalidate' => 'novalidate'],
@@ -78,7 +78,7 @@ class CreateStep2Controller extends AbstractController
if ($form->isSubmitted() && $form->isValid()) {
$bookingCreateDto->currentStep = 3;
$this->bookingService->saveBookingCreateDto($request, $bookingCreateDto);
$this->bookingService->saveBookingDto($request, $bookingCreateDto, BookingDto::MODE_CREATE);
return $this->redirectToRoute('app_booking_create_step_3');
}
@@ -129,7 +129,7 @@ class CreateStep2Controller extends AbstractController
// Pre-select mandatory services after form processing but before pricing calculation
$this->bookingService->preselectMandatoryServices($bookingCreateDto);
$this->bookingService->saveBookingCreateDto($request, $bookingCreateDto);
$this->bookingService->saveBookingDto($request, $bookingCreateDto, BookingDto::MODE_CREATE);
// Collect notifications from all participants
$notifications = $this->collectParticipantNotifications($bookingCreateDto);
@@ -173,9 +173,9 @@ class CreateStep2Controller extends AbstractController
* based on room selections. Preserves existing participant data when possible
* and assigns proper index values.
*
* @param BookingCreateDto $bookingCreateDto The booking DTO to update
* @param BookingDto $bookingCreateDto The booking DTO to update
*/
private function ensureCorrectNumberOfParticipants(BookingCreateDto $bookingCreateDto): void
private function ensureCorrectNumberOfParticipants(BookingDto $bookingCreateDto): void
{
$participantsCount = $this->getParticipantsCount($bookingCreateDto);
@@ -195,13 +195,13 @@ class CreateStep2Controller extends AbstractController
* to ensure service availability is reasonably up-to-date while reducing API calls.
* This is essential for accurate pricing and service selection during the booking process.
*
* @param BookingCreateDto $bookingCreateDto The booking DTO containing travel data to enrich
* @param BookingDto $bookingCreateDto The booking DTO containing travel data to enrich
*/
private function enrichWithFreshAvailabilities(BookingCreateDto $bookingCreateDto): void
private function enrichWithFreshAvailabilities(BookingDto $bookingCreateDto): void
{
$dateId = $bookingCreateDto->travel->id;
$availabilities = $this->travelDataService->getAvailabilityDataCached($dateId);
$availabilities = $this->travelDataService->getAvailabilityData($dateId, true);
if (null !== $availabilities) {
$this->travelDataService->patchAvailabilities($bookingCreateDto->travel, $availabilities);
@@ -214,9 +214,9 @@ class CreateStep2Controller extends AbstractController
* This is called when entering Step 2 to ensure all participants have room assignments
* based on the selected rooms from Step 1. Only assigns if participants are unassigned.
*
* @param BookingCreateDto $bookingCreateDto The booking DTO with participants and room selections
* @param BookingDto $bookingCreateDto The booking DTO with participants and room selections
*/
private function autoAssignRoomsIfNeeded(BookingCreateDto $bookingCreateDto): void
private function autoAssignRoomsIfNeeded(BookingDto $bookingCreateDto): void
{
// Check if any participants need room assignment
$needsAssignment = false;
@@ -235,11 +235,11 @@ class CreateStep2Controller extends AbstractController
/**
* Collects all notifications from participants and clears them.
*
* @param BookingCreateDto $bookingCreateDto The booking DTO containing participants
* @param BookingDto $bookingCreateDto The booking DTO containing participants
*
* @return array<array{type: string, message: string}> Array of notification messages
*/
private function collectParticipantNotifications(BookingCreateDto $bookingCreateDto): array
private function collectParticipantNotifications(BookingDto $bookingCreateDto): array
{
$notifications = [];
+2 -2
View File
@@ -2,7 +2,7 @@
namespace App\Form;
use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingDto;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\FormBuilderInterface;
@@ -30,7 +30,7 @@ class BookingCreateStep1Type extends AbstractType
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => BookingCreateDto::class,
'data_class' => BookingDto::class,
]);
}
}
+5 -5
View File
@@ -2,7 +2,7 @@
namespace App\Form;
use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingDto;
use App\Form\Service\ParticipantFieldHandlerRegistry;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
@@ -31,7 +31,7 @@ class BookingCreateStep2Type extends AbstractType
*/
public function onPreSetData(FormEvent $event): void
{
/** @var BookingCreateDto|null $data */
/** @var BookingDto|null $data */
$data = $event->getData();
if (null === $data) {
return;
@@ -43,7 +43,7 @@ class BookingCreateStep2Type extends AbstractType
/**
* Handles dynamic participant form field updates on POST requests (e.g., from HTMX).
*
* This listener synchronizes the BookingCreateDto with the submitted participant data *before*
* This listener synchronizes the BookingDto with the submitted participant data *before*
* the form's children are processed. It then rebuilds the participants
* field to ensure choice loaders are created with the fresh state.
*/
@@ -52,7 +52,7 @@ class BookingCreateStep2Type extends AbstractType
$form = $event->getForm();
$submittedData = $event->getData();
/** @var BookingCreateDto $bookingDto */
/** @var BookingDto $bookingDto */
$bookingDto = $form->getData();
// Process field handlers and synchronize submitted data with cleaned DTO state
@@ -81,7 +81,7 @@ class BookingCreateStep2Type extends AbstractType
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => BookingCreateDto::class,
'data_class' => BookingDto::class,
]);
}
}
+3 -3
View File
@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Form;
use App\BusProNet\Constants;
use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingDto;
use Carbon\CarbonImmutable;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
@@ -69,7 +69,7 @@ class BookingCreateStep3Type extends AbstractType
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => BookingCreateDto::class,
'data_class' => BookingDto::class,
]);
}
@@ -88,7 +88,7 @@ class BookingCreateStep3Type extends AbstractType
*
* Direct debit is only available if the travel starts at least 14 days from now.
*/
private function isDebitAvailable(BookingCreateDto $bookingDto): bool
private function isDebitAvailable(BookingDto $bookingDto): bool
{
$travelStartDate = CarbonImmutable::instance($bookingDto->travel->dateFrom);
$now = CarbonImmutable::now();
+3 -3
View File
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Form;
use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingDto;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\FormBuilderInterface;
@@ -31,7 +31,7 @@ class BookingCreateStep4Type extends AbstractType
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => BookingCreateDto::class,
'data_class' => BookingDto::class,
]);
}
}
}
-151
View File
@@ -1,151 +0,0 @@
<?php
namespace App\Form\Model;
use App\BusProNet\Constants;
use App\BusProNet\Model\Travel;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
class BookingCreateDto implements BookingDtoInterface
{
public int $currentStep = 1;
/**
* @var array<int, RoomSelectionDto>
*/
#[Assert\Valid]
public array $roomSelections = [];
/**
* @var array<int, ParticipantDto>
*/
#[Assert\Valid]
public array $participants = [];
#[Assert\Choice(
choices: [Constants::PAYMENT_METHOD_TRANSFER, Constants::PAYMENT_METHOD_DEBIT],
message: 'Bitte wählen Sie eine gültige Zahlungsart.'
)]
public ?string $paymentMethod = Constants::PAYMENT_METHOD_TRANSFER;
public ?BankAccountDto $bankAccount = null;
public ?int $agencyId = null;
public function __construct(public Travel $travel, public int $hotelId)
{
}
public function getMode(): string
{
return BookingDtoInterface::MODE_CREATE;
}
/**
* @return array<int, RoomSelectionDto>
*/
public function getSelectedRooms(): array
{
return array_filter($this->roomSelections, function (RoomSelectionDto $roomSelection) {
return 0 < $roomSelection->quantity;
});
}
public function getParticipants(): array
{
return $this->participants;
}
public function hasParticipant(int $index): bool
{
return isset($this->participants[$index]);
}
public function getParticipant(int $index): ?ParticipantDto
{
return $this->participants[$index] ?? null;
}
/**
* Determines if this is a family booking based on participant age distribution.
*
* A family booking is defined as:
* - 1 or 2 participants aged 18 or older (adults)
* - At least 1 participant younger than 18 (children)
*
* @return bool True if this qualifies as a family booking
*/
public function isFamilyBooking(): bool
{
$adults = 0; // Count of participants >= 18 years
$children = 0; // Count of participants < 18 years
// Use travel start date for age calculation
$travelStartDate = $this->travel->dateFrom;
foreach ($this->participants as $participant) {
$age = $participant->getAge($travelStartDate);
if (null === $age) {
continue; // Skip participants without birth date
}
if ($age >= 18) {
++$adults;
} else {
++$children;
}
}
return ($adults >= 1 && $adults <= 2) && ($children >= 1);
}
#[Assert\Callback(callback: 'validateRoomSelection', groups: ['booking_create_step_1'])]
public function validateRoomSelection(ExecutionContextInterface $context): void
{
$selectedRooms = $this->getSelectedRooms();
if (0 === count($selectedRooms)) {
$context->buildViolation('Bitte mindestens ein Zimmer/Bett auswählen')
->addViolation();
}
}
#[Assert\Callback]
public function validateBankAccount(ExecutionContextInterface $context): void
{
if (Constants::PAYMENT_METHOD_DEBIT !== $this->paymentMethod) {
return;
}
if (null === $this->bankAccount) {
$context->buildViolation('Bitte geben Sie Ihre Bankverbindung an.')
->atPath('bankAccount')
->addViolation();
return;
}
// Validate IBAN
if (null === $this->bankAccount->iban || '' === trim($this->bankAccount->iban)) {
$context->buildViolation('Bitte geben Sie Ihre IBAN ein.')
->atPath('bankAccount.iban')
->addViolation();
}
// Validate account holder
if (null === $this->bankAccount->accountHolder || '' === trim($this->bankAccount->accountHolder)) {
$context->buildViolation('Bitte geben Sie den Kontoinhaber ein.')
->atPath('bankAccount.accountHolder')
->addViolation();
}
// Validate SEPA mandate
if (false === $this->bankAccount->sepaMandateAccepted) {
$context->buildViolation('Bitte akzeptieren Sie das SEPA-Mandat.')
->atPath('bankAccount.sepaMandateAccepted')
->addViolation();
}
}
}
-51
View File
@@ -1,51 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Form\Model;
/**
* Interface for unified access to booking data across create and edit workflows.
*
* This interface provides a consistent API for accessing common booking data
* regardless of whether the data comes from a creation workflow (BookingCreateDto)
* or an edit workflow (BookingEditDto). This enables code reuse and simplifies
* form field handlers and state management systems.
*/
interface BookingDtoInterface
{
public const MODE_CREATE = 'create';
public const MODE_EDIT = 'edit';
/**
* Gets the booking mode (create or edit).
*
* @return string One of MODE_CREATE or MODE_EDIT constants
*/
public function getMode(): string;
/**
* Gets all participants in the booking.
*
* @return array<int, ParticipantDto> Array of participant DTOs indexed by participant index
*/
public function getParticipants(): array;
/**
* Checks if a participant exists at the given index.
*
* @param int $index The participant index to check
*
* @return bool True if a participant exists at the given index
*/
public function hasParticipant(int $index): bool;
/**
* Gets a participant by index.
*
* @param int $index The participant index
*
* @return ParticipantDto|null The participant DTO or null if not found
*/
public function getParticipant(int $index): ?ParticipantDto;
}
-121
View File
@@ -1,121 +0,0 @@
<?php
namespace App\Form\Model;
use App\BusProNet\Constants;
use App\BusProNet\Model\Booking;
use App\BusProNet\Model\PersonalData;
use App\BusProNet\Model\Travel;
use App\BusProNet\Utility\DirectionMapper;
use Symfony\Component\Validator\Constraints as Assert;
class BookingEditDto implements BookingDtoInterface
{
/**
* @var array<int, ParticipantDto>
*/
#[Assert\Valid]
public array $participants = [];
public function __construct(public Booking $booking, public Travel $travel)
{
}
public function getMode(): string
{
return BookingDtoInterface::MODE_EDIT;
}
public static function fromBooking(Booking $booking, Travel $travel): static
{
$instance = new static($booking, $travel);
$instance->booking = $booking;
$instance->travel = $travel;
foreach ($booking->participants as $index => $participant) {
/** @var PersonalData $participant */
// For the first participant (applicant), use applicant data instead of participant data
$personalData = 0 === $index && null !== $booking->applicant ? $booking->applicant : $participant;
$participantData = ParticipantDto::fromPersonalData($personalData);
$participantData->index = $index;
$participantData->courses = $booking
->getAdditionalServicesForParticipantByGroup($index, Constants::TOKEN_COURSES);
// Skipass is single selection - use dedicated method
$participantData->skiPass = $booking->getSkiPassForParticipant($index);
$participantData->additionalServices = $booking
->getAdditionalServicesForParticipantByGroup($index, Constants::TOKEN_ADDITIONAL);
$participantData->board = $booking
->getAdditionalServicesForParticipantByGroup($index, Constants::TOKEN_BOARD);
$participantData->rentals = $booking
->getAdditionalServicesForParticipantByGroup($index, Constants::TOKEN_RENTALS);
// Transportation services using improved direction mapping
// Direction mapping handles BusProNet's inconsistent codes (H <=> HIN, R <=> RUECK)
$outboundTransportation = $booking
->getTransportationServiceForParticipantAndDirection($index, DirectionMapper::OUTBOUND_BOOKING);
$inboundTransportation = $booking
->getTransportationServiceForParticipantAndDirection($index, DirectionMapper::INBOUND_BOOKING);
// Set new improved property names
$participantData->transportationOutbound = $outboundTransportation;
$participantData->transportationInbound = $inboundTransportation;
// Pickup handling (currently only supports outbound pickup)
$pickup = $booking->getPickupForParticipant($index);
$participantData->pickup = $pickup;
// Insurance - get insurance for participant
$insurance = $booking->getInsuranceForParticipant($index);
$participantData->insurance = $insurance;
// Room assignment - extract from booking room mappings
$room = $booking->getRoomForParticipant($index);
$participantData->assignedRoomId = $room?->id;
$instance->participants[$index] = $participantData;
}
return $instance;
}
public function isCanceled(): bool
{
return 'S' === $this->booking->status;
}
public function isOption(): bool
{
return 'O' === $this->booking->status;
}
public function getParticipants(): array
{
return $this->participants;
}
public function hasParticipant(int $index): bool
{
return isset($this->participants[$index]);
}
public function getParticipant(int $index): ?ParticipantDto
{
return $this->participants[$index] ?? null;
}
/**
* Gets all selected rooms for the booking (edit context).
*
* In edit mode, rooms are fixed and not selectable - returns empty array.
* Participant count should be derived from actual participants, not room selections.
*
* @return array<int, RoomSelectionDto>
*/
public function getSelectedRooms(): array
{
return [];
}
}
+2 -2
View File
@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Form;
use App\BusProNet\Constants;
use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingDto;
use Carbon\CarbonImmutable;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
@@ -95,7 +95,7 @@ class PaymentType extends AbstractType
*
* Direct debit is only available if the travel starts at least 14 days from now.
*/
private function isDebitAvailable(BookingCreateDto $bookingDto): bool
private function isDebitAvailable(BookingDto $bookingDto): bool
{
$travelStartDate = CarbonImmutable::instance($bookingDto->travel->dateFrom);
$now = CarbonImmutable::now();
@@ -194,6 +194,16 @@ class CreateFieldStateProvider extends AbstractFieldStateProvider
'hidden' => FieldValueCondition::equals('parking', false),
];
// Make mobile field required for applicant (participant index 0)
$this->fieldStateConditions['mobile'] = [
'required' => new ApplicantCondition(),
];
// Make address field required for applicant (participant index 0)
$this->fieldStateConditions['address'] = [
'required' => new ApplicantCondition(),
];
// Example field state conditions would be registered here
// For demonstration purposes, here are some example patterns:
@@ -8,9 +8,7 @@ use App\BusProNet\Constants;
use App\BusProNet\Model\Pickup;
use App\BusProNet\Model\Service;
use App\BusProNet\Utility\DirectionMapper;
use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingDto;
use App\Form\Model\BookingEditDto;
use App\Form\Service\Abstract\AbstractFieldOptionsProvider;
use App\Form\Service\Factory\ParticipantRoomChoiceLoaderFactory;
use App\Service\InsuranceMatchingService;
@@ -462,9 +460,9 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
* as the participant's selected skipass. This ensures rental equipment
* is only available for the exact duration of the skipass.
*
* @param array $rentals Array of rental Service objects to filter
* @param array $rentals Array of rental Service objects to filter
* @param BookingDto $bookingDto The booking DTO containing participant data
* @param int $participantIndex Index of the participant to evaluate
* @param int $participantIndex Index of the participant to evaluate
*
* @return array Filtered array of rentals matching skipass duration
*/
@@ -587,15 +585,15 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
/**
* Checks if a service should be rendered as read-only due to unavailability.
*
* @param Service $service The service to check
* @param Service $service The service to check
* @param BookingDto $bookingDto The booking DTO containing participant data
* @param int $participantIndex Index of the participant currently selecting services
* @param int $participantIndex Index of the participant currently selecting services
*
* @return bool True if the service should be read-only due to unavailability
*/
private function isServiceUnavailableForParticipant(Service $service, BookingDto $bookingDto, int $participantIndex): bool
{
if (!$bookingDto instanceof BookingCreateDto) {
if (BookingDto::MODE_EDIT === $bookingDto->getMode()) {
// For non-create workflows, don't apply availability restrictions
return false;
}
@@ -610,9 +608,9 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
* If no age evaluator is configured or participant has no birth date,
* returns empty array to be handled by field visibility conditions.
*
* @param array $services Array of Service objects to filter
* @param array $services Array of Service objects to filter
* @param BookingDto $bookingDto The booking DTO containing participant data
* @param int $participantIndex Index of the participant to evaluate
* @param int $participantIndex Index of the participant to evaluate
*
* @return array Filtered array of available services
*/
@@ -667,7 +665,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
* Gets eligible insurances for a participant based on eligibility criteria.
*
* @param BookingDto $bookingDto The booking DTO containing travel and participant data
* @param int $participantIndex The index of the participant to get eligible insurances for
* @param int $participantIndex The index of the participant to get eligible insurances for
*
* @return array Array of eligible insurance objects filtered by age, family status, and other constraints
*/
@@ -125,6 +125,10 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler
$currentInsurance = $participant->insurance;
$availableInsurances = $bookingDto->travel->insurances ?? [];
// Exclude complementary insurances from reassignment logic
// They are only available as part of packages and cannot be directly selected
$availableInsurances = array_filter($availableInsurances, fn ($insurance) => !$insurance->complementary);
// Determine if this is a new user selection or just form resubmission
$isNewSelection = null !== $selectedInsuranceId
&& (null === $currentInsurance || !$this->isSameInsurance($selectedInsuranceId, $currentInsurance));
+35 -34
View File
@@ -8,7 +8,6 @@ use App\BusProNet\Constants;
use App\BusProNet\Model\Insurance;
use App\BusProNet\Model\Room;
use App\BusProNet\Model\Service;
use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingDto;
use App\Form\Model\ParticipantDto;
@@ -57,10 +56,6 @@ class BookingPriceCalculatorService
{
$roomPricing = [];
if (false === $bookingDto instanceof BookingCreateDto) {
return $roomPricing;
}
$selectedRooms = $bookingDto->getSelectedRooms();
if (true === empty($selectedRooms)) {
return $roomPricing;
@@ -196,16 +191,12 @@ class BookingPriceCalculatorService
* including their room allocation (full room price) and all selected services.
*
* @param BookingDto $bookingDto The booking data containing all participants
* @param int $participantIndex The index of the participant to calculate for
* @param int $participantIndex The index of the participant to calculate for
*
* @return float The total price for the specified participant
*/
public function calculateIndividualParticipantPrice(BookingDto $bookingDto, int $participantIndex): float
{
if (false === $bookingDto instanceof BookingCreateDto) {
return 0.0;
}
$participant = $bookingDto->getParticipant($participantIndex);
if (null === $participant) {
return 0.0;
@@ -237,10 +228,6 @@ class BookingPriceCalculatorService
*/
public function calculateAllParticipantIndividualPrices(BookingDto $bookingDto): array
{
if (false === $bookingDto instanceof BookingCreateDto) {
return [];
}
$participantPrices = [];
$participants = $bookingDto->getParticipants();
@@ -257,17 +244,17 @@ class BookingPriceCalculatorService
* This method is used for insurance eligibility filtering to avoid circular dependency
* where insurance selection affects travel price which affects insurance eligibility.
*
* @param BookingDto $bookingDto The booking data containing all participants
* @param int $participantIndex The index of the participant to calculate for
* Only includes services where versicherungsberechnung='J' in the XML. Services with
* versicherungsberechnung='N' (like CO² compensation) are excluded from the calculation
* as per BPN API requirements for insurance tier determination.
*
* @return float The total price for the specified participant excluding insurance
* @param BookingDto $bookingDto The booking data containing all participants
* @param int $participantIndex The index of the participant to calculate for
*
* @return float The total price for the specified participant excluding insurance and non-calculated services
*/
public function calculateIndividualParticipantPriceExcludingInsurance(BookingDto $bookingDto, int $participantIndex): float
{
if (false === $bookingDto instanceof BookingCreateDto) {
return 0.0;
}
$participant = $bookingDto->getParticipant($participantIndex);
if (null === $participant) {
return 0.0;
@@ -285,7 +272,8 @@ class BookingPriceCalculatorService
}
// Add service prices for this participant (excluding insurance)
$totalPrice += $this->calculateParticipantServiceTotal($participant, false);
// For insurance eligibility calculation, only include services marked with versicherungsberechnung='J'
$totalPrice += $this->calculateParticipantServiceTotal($participant, false, null, true);
return $totalPrice;
}
@@ -528,23 +516,28 @@ class BookingPriceCalculatorService
/**
* Calculates the total service cost for a single participant.
*
* @param ParticipantDto $participant The participant to calculate services for
* @param bool $includeInsurance Whether to include insurance pricing (default: true)
* @param BookingDto|null $bookingDto Optional booking DTO for bulk insurance resolution
* @param ParticipantDto $participant The participant to calculate services for
* @param bool $includeInsurance Whether to include insurance pricing (default: true)
* @param BookingDto|null $bookingDto Optional booking DTO for bulk insurance resolution
* @param bool $onlyInsuranceCalculationServices Only include services with includeInInsuranceCalculation=true (default: false)
*
* @return float The total service cost for this participant
*/
private function calculateParticipantServiceTotal(ParticipantDto $participant, bool $includeInsurance = true, ?BookingDto $bookingDto = null): float
private function calculateParticipantServiceTotal(ParticipantDto $participant, bool $includeInsurance = true, ?BookingDto $bookingDto = null, bool $onlyInsuranceCalculationServices = false): float
{
$serviceTotal = 0.0;
// Single service selections
if (null !== $participant->skiPass && null !== $participant->skiPass->price) {
$serviceTotal += $participant->skiPass->price;
if (false === $onlyInsuranceCalculationServices || true === $participant->skiPass->includeInInsuranceCalculation) {
$serviceTotal += $participant->skiPass->price;
}
}
if (null !== $participant->rentalInsurance && null !== $participant->rentalInsurance->price) {
$serviceTotal += $participant->rentalInsurance->price;
if (false === $onlyInsuranceCalculationServices || true === $participant->rentalInsurance->includeInInsuranceCalculation) {
$serviceTotal += $participant->rentalInsurance->price;
}
}
// Get effective insurance (considering bulk insurance for dependent participants)
@@ -555,11 +548,15 @@ class BookingPriceCalculatorService
// Transportation services
if (null !== $participant->transportationOutbound && null !== $participant->transportationOutbound->price) {
$serviceTotal += $participant->transportationOutbound->price;
if (false === $onlyInsuranceCalculationServices || true === $participant->transportationOutbound->includeInInsuranceCalculation) {
$serviceTotal += $participant->transportationOutbound->price;
}
}
if (null !== $participant->transportationInbound && null !== $participant->transportationInbound->price) {
$serviceTotal += $participant->transportationInbound->price;
if (false === $onlyInsuranceCalculationServices || true === $participant->transportationInbound->includeInInsuranceCalculation) {
$serviceTotal += $participant->transportationInbound->price;
}
}
if (null !== $participant->pickup && null !== $participant->pickup->price) {
@@ -567,7 +564,9 @@ class BookingPriceCalculatorService
}
if (null !== $participant->parkingService && null !== $participant->parkingService->price) {
$serviceTotal += $participant->parkingService->price;
if (false === $onlyInsuranceCalculationServices || true === $participant->parkingService->includeInInsuranceCalculation) {
$serviceTotal += $participant->parkingService->price;
}
}
// Multiple service selections
@@ -582,7 +581,9 @@ class BookingPriceCalculatorService
if (true === is_array($serviceArray)) {
foreach ($serviceArray as $service) {
if ($service instanceof Service && null !== $service->price) {
$serviceTotal += $service->price;
if (false === $onlyInsuranceCalculationServices || true === $service->includeInInsuranceCalculation) {
$serviceTotal += $service->price;
}
}
}
}
@@ -594,7 +595,7 @@ class BookingPriceCalculatorService
/**
* Retrieves a room by ID from the booking's travel data.
*/
private function getRoomById(BookingCreateDto $bookingDto, ?int $roomId): ?Room
private function getRoomById(BookingDto $bookingDto, ?int $roomId): ?Room
{
if (null === $roomId) {
return null;
@@ -644,7 +645,7 @@ class BookingPriceCalculatorService
* This method is used for pricing calculations to show correct prices when bulk
* insurance is enabled, even though the actual assignment happens in the processor.
*
* @param ParticipantDto $participant The participant to get insurance for
* @param ParticipantDto $participant The participant to get insurance for
* @param BookingDto|null $bookingDto Optional booking DTO for bulk insurance check
*
* @return Insurance|null The effective insurance for pricing purposes
+15 -16
View File
@@ -6,7 +6,6 @@ use App\BusProNet\Model\Room;
use App\BusProNet\Model\Travel;
use App\Exception\BookingSessionNotFoundException;
use App\Exception\NoRoomsAvailableException;
use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingDto;
use App\Form\Model\RoomSelectionDto;
use Symfony\Component\HttpFoundation\Request;
@@ -32,7 +31,7 @@ class BookingService
* is first loaded, before any HTMX modifications. This ensures accurate change
* detection for room assignment resets.
*/
public function getOrCreateBaselineSnapshot(Request $request, BookingCreateDto $bookingCreateDto): array
public function getOrCreateBaselineSnapshot(Request $request, BookingDto $bookingCreateDto): array
{
$baselineKey = self::BOOKING_CREATE_BASELINE_KEY;
@@ -66,11 +65,11 @@ class BookingService
*
* @param Request $request The HTTP request containing session data
*
* @return BookingCreateDto The booking DTO from session
* @return BookingDto The booking DTO from session
*
* @throws BookingSessionNotFoundException When no valid booking session exists
*/
public function getOrCreateBookingCreateDto(Request $request): BookingCreateDto
public function getOrCreateBookingCreateDto(Request $request): BookingDto
{
$bookingCreateDto = $request->getSession()->get(self::BOOKING_CREATE_KEY);
@@ -130,9 +129,9 @@ class BookingService
* across multiple HTTP requests during the booking flow.
*
* @param Request $request The HTTP request with session
* @param BookingCreateDto $bookingCreateDto The booking DTO to persist
* @param BookingDto $bookingCreateDto The booking DTO to persist
*/
public function saveBookingCreateDto(Request $request, BookingCreateDto $bookingCreateDto): void
public function saveBookingCreateDto(Request $request, BookingDto $bookingCreateDto): void
{
$this->saveBookingDto($request, $bookingCreateDto, BookingDto::MODE_CREATE);
}
@@ -164,11 +163,11 @@ class BookingService
/**
* Creates a fresh booking session with the provided travel parameters.
*
* This method initializes a new BookingCreateDto with empty room selections
* This method initializes a new BookingDto with empty room selections
* and saves it to the session. It's designed to be called from the clean
* booking entry point without requiring UID parameters.
*/
public function startFreshBooking(Request $request, int $dateId, int $hotelId, ?int $agencyId = null): BookingCreateDto
public function startFreshBooking(Request $request, int $dateId, int $hotelId, ?int $agencyId = null): BookingDto
{
$travelData = $this->travelDataService->getTravelData($dateId, $hotelId);
if (null === $travelData) {
@@ -188,7 +187,7 @@ class BookingService
$availableRooms
);
$bookingCreateDto = new BookingCreateDto($travelData, $hotelId);
$bookingCreateDto = new BookingDto($travelData, $hotelId);
$bookingCreateDto->roomSelections = $roomSelections;
$bookingCreateDto->currentStep = 1;
$bookingCreateDto->agencyId = $agencyId;
@@ -344,9 +343,9 @@ class BookingService
* invalid assignments. Called when users modify their room selections
* in step 1 to ensure participants are reassigned appropriately.
*
* @param BookingCreateDto $dto The booking DTO to reset assignments for
* @param BookingDto $dto The booking DTO to reset assignments for
*/
public function resetParticipantAssignments(BookingCreateDto $dto): void
public function resetParticipantAssignments(BookingDto $dto): void
{
foreach ($dto->participants as $participant) {
$participant->assignedRoomId = null;
@@ -358,7 +357,7 @@ class BookingService
*
* @return array<int, array{int, int}> Array of [roomId, quantity] pairs
*/
public function createRoomSelectionSnapshot(BookingCreateDto $dto): array
public function createRoomSelectionSnapshot(BookingDto $dto): array
{
return array_map(
fn ($roomSelection) => [(int) $roomSelection->roomId, (int) $roomSelection->quantity],
@@ -373,11 +372,11 @@ class BookingService
* to detect changes that would require participant reassignment.
*
* @param array $oldSnapshot The baseline room selection snapshot
* @param BookingCreateDto $newDto The current booking DTO
* @param BookingDto $newDto The current booking DTO
*
* @return bool True if room selections have changed, false otherwise
*/
public function hasRoomSelectionChanged(array $oldSnapshot, BookingCreateDto $newDto): bool
public function hasRoomSelectionChanged(array $oldSnapshot, BookingDto $newDto): bool
{
$newSnapshot = $this->createRoomSelectionSnapshot($newDto);
@@ -395,9 +394,9 @@ class BookingService
* have at least one skipass available for their age. Ineligible participants are
* skipped to prevent their mandatory services from being included in pricing.
*
* @param BookingCreateDto $bookingDto The booking DTO to update with mandatory services
* @param BookingDto $bookingDto The booking DTO to update with mandatory services
*/
public function preselectMandatoryServices(BookingCreateDto $bookingDto): void
public function preselectMandatoryServices(BookingDto $bookingDto): void
{
$additionalServices = $bookingDto->travel->getAdditionalServicesBySubTypes(\App\BusProNet\Constants::TOKEN_ADDITIONAL);
$mandatoryServices = array_filter($additionalServices, fn ($service) => true === $service->mandatory);
+34 -15
View File
@@ -6,7 +6,6 @@ namespace App\Service;
use App\BusProNet\Model\Insurance;
use App\BusProNet\Traits\SortByPriceTrait;
use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingDto;
use App\Form\Model\ParticipantDto;
use App\Model\InsuranceEligibilityCriteria;
@@ -31,9 +30,9 @@ class InsuranceMatchingService
/**
* Filters insurances based on participant and booking criteria.
*
* @param array<Insurance> $insurances Available insurances to filter
* @param ParticipantDto $participant The participant to match insurances for
* @param BookingDto $booking The booking context for additional criteria
* @param array<Insurance> $insurances Available insurances to filter
* @param ParticipantDto $participant The participant to match insurances for
* @param BookingDto $booking The booking context for additional criteria
*
* @return array<Insurance> Filtered array of eligible insurances
*/
@@ -60,10 +59,10 @@ class InsuranceMatchingService
* current insurance is no longer eligible. It finds the same insurance type
* (subType + familyInsurance) with the correct price tier.
*
* @param array<Insurance> $availableInsurances All available insurances
* @param Insurance $currentInsurance The currently selected insurance
* @param ParticipantDto $participant The participant to reassign for
* @param BookingDto $booking The booking context
* @param array<Insurance> $availableInsurances All available insurances
* @param Insurance $currentInsurance The currently selected insurance
* @param ParticipantDto $participant The participant to reassign for
* @param BookingDto $booking The booking context
*
* @return Insurance|null The reassigned insurance or null if no suitable match found
*/
@@ -90,9 +89,9 @@ class InsuranceMatchingService
* (subType + familyInsurance) to all participants, but selects the appropriate price tier
* based on each participant's individual travel price.
*
* @param array<Insurance> $availableInsurances All available insurances
* @param Insurance $selectedInsurance The insurance selected by the applicant
* @param BookingDto $booking The booking with all participants
* @param array<Insurance> $availableInsurances All available insurances
* @param Insurance $selectedInsurance The insurance selected by the applicant
* @param BookingDto $booking The booking with all participants
*
* @return array<int, Insurance|null> Array indexed by participant index with assigned insurances
*
@@ -187,8 +186,8 @@ class InsuranceMatchingService
*/
private function checkFamilyInsuranceConstraints(Insurance $insurance, BookingDto $booking): bool
{
// Family booking detection only available in BookingCreateDto
if (!$booking instanceof BookingCreateDto) {
// Family booking detection only available in create mode
if (BookingDto::MODE_EDIT === $booking->getMode()) {
return true; // Skip family constraints for edit mode
}
@@ -316,14 +315,34 @@ class InsuranceMatchingService
* affects travel price which then affects insurance eligibility.
*
* @param BookingDto $booking The booking to calculate price for
* @param int $participantIndex The participant index to calculate for
* @param int $participantIndex The participant index to calculate for
*
* @return float The total travel price for the participant excluding insurance
*/
private function calculateTravelPrice(BookingDto $booking, int $participantIndex): float
{
$participant = $booking->getParticipant($participantIndex);
// Debug logging to understand price calculation
if (null !== $participant && null !== $participant->insurance) {
error_log(sprintf(
'[InsuranceMatching] Participant %d: calculating travel price WITH insurance=%d (€%.2f) currently selected',
$participantIndex,
$participant->insurance->id,
$participant->insurance->price ?? 0.0
));
}
// Use the price calculator to get the participant's individual price excluding insurance
return $this->priceCalculatorService->calculateIndividualParticipantPriceExcludingInsurance($booking, $participantIndex);
$travelPrice = $this->priceCalculatorService->calculateIndividualParticipantPriceExcludingInsurance($booking, $participantIndex);
error_log(sprintf(
'[InsuranceMatching] Participant %d: calculated travel price (excluding insurance) = €%.2f',
$participantIndex,
$travelPrice
));
return $travelPrice;
}
/**
+3 -3
View File
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Service;
use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingDto;
/**
* Handles automatic room assignment for booking participants.
@@ -26,9 +26,9 @@ class RoomAssignmentService
* - 2x "Doppelzimmer" (capacity 2) = participants 0-1 → room A, participants 2-3 → room A
* - 1x "3-Bett-Zimmer" (capacity 3) = participants 4-6 → room B
*
* @param BookingCreateDto $dto The booking DTO containing room selections and participants
* @param BookingDto $dto The booking DTO containing room selections and participants
*/
public function assignParticipantsToRooms(BookingCreateDto $dto): void
public function assignParticipantsToRooms(BookingDto $dto): void
{
$participantIndex = 0;
$selectedRooms = $dto->getSelectedRooms();
+17 -17
View File
@@ -7,7 +7,7 @@ namespace App\Service;
use App\BusProNet\Constants;
use App\BusProNet\Model\Service;
use App\BusProNet\Utility\DirectionMapper;
use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingDto;
/**
* Calculates dynamic service availability based on current booking selections.
@@ -22,12 +22,12 @@ class ServiceAvailabilityCalculator
/**
* Calculate remaining availability for all services based on current participant selections.
*
* @param BookingCreateDto $bookingDto The booking data with participant selections
* @param int $currentParticipantIndex The index of the participant currently filling the form
* @param BookingDto $bookingDto The booking data with participant selections
* @param int $currentParticipantIndex The index of the participant currently filling the form
*
* @return array<int, int> Array mapping service IDs to remaining availability counts
*/
public function calculateRemainingAvailability(BookingCreateDto $bookingDto, int $currentParticipantIndex): array
public function calculateRemainingAvailability(BookingDto $bookingDto, int $currentParticipantIndex): array
{
$serviceUsage = $this->calculateServiceUsage($bookingDto, $currentParticipantIndex);
$remainingAvailability = [];
@@ -52,13 +52,13 @@ class ServiceAvailabilityCalculator
/**
* Filter services array to only include those with remaining availability.
*
* @param array $services Array of Service objects to filter
* @param BookingCreateDto $bookingDto The booking data with participant selections
* @param int $participantIndex The index of the participant currently filling the form
* @param array $services Array of Service objects to filter
* @param BookingDto $bookingDto The booking data with participant selections
* @param int $participantIndex The index of the participant currently filling the form
*
* @return array Filtered array containing only available services
*/
public function filterAvailableServices(array $services, BookingCreateDto $bookingDto, int $participantIndex): array
public function filterAvailableServices(array $services, BookingDto $bookingDto, int $participantIndex): array
{
$remainingAvailability = $this->calculateRemainingAvailability($bookingDto, $participantIndex);
@@ -76,13 +76,13 @@ class ServiceAvailabilityCalculator
/**
* Check if a specific service is unavailable (sold out) for the current participant.
*
* @param int $serviceId The ID of the service to check
* @param BookingCreateDto $bookingDto The booking data with participant selections
* @param int $participantIndex The index of the participant currently filling the form
* @param int $serviceId The ID of the service to check
* @param BookingDto $bookingDto The booking data with participant selections
* @param int $participantIndex The index of the participant currently filling the form
*
* @return bool True if the service is unavailable (has availability limit and remaining is 0)
*/
public function isServiceUnavailable(int $serviceId, BookingCreateDto $bookingDto, int $participantIndex): bool
public function isServiceUnavailable(int $serviceId, BookingDto $bookingDto, int $participantIndex): bool
{
$allServices = $this->getAllServicesFromTravel($bookingDto);
$service = null;
@@ -107,12 +107,12 @@ class ServiceAvailabilityCalculator
/**
* Calculate how many times each service has been selected by other participants.
*
* @param BookingCreateDto $bookingDto The booking data with participant selections
* @param int $currentParticipantIndex The index of the participant currently filling the form
* @param BookingDto $bookingDto The booking data with participant selections
* @param int $currentParticipantIndex The index of the participant currently filling the form
*
* @return array<int, int> Array mapping service IDs to usage counts
*/
private function calculateServiceUsage(BookingCreateDto $bookingDto, int $currentParticipantIndex): array
private function calculateServiceUsage(BookingDto $bookingDto, int $currentParticipantIndex): array
{
$serviceUsage = [];
@@ -187,11 +187,11 @@ class ServiceAvailabilityCalculator
/**
* Get all services from the travel data for availability calculation.
*
* @param BookingCreateDto $bookingDto The booking data containing travel information
* @param BookingDto $bookingDto The booking data containing travel information
*
* @return array<Service> Array of all available services
*/
private function getAllServicesFromTravel(BookingCreateDto $bookingDto): array
private function getAllServicesFromTravel(BookingDto $bookingDto): array
{
$allServices = [];
@@ -5,7 +5,7 @@ namespace App\Validator\Constraints;
use App\BusProNet\ApiClient;
use App\BusProNet\Model\BookingUpdate;
use App\BusProNet\Model\Notification;
use App\Form\Model\BookingEditDto;
use App\Form\Model\BookingDto;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
@@ -17,7 +17,7 @@ class BookingValidator extends ConstraintValidator
public function validate(mixed $value, Constraint $constraint): void
{
/** @var BookingEditDto $booking */
/** @var BookingDto $booking */
$bookingData = $value;
$result = $this->apiClient->updateBooking($bookingData);
+2 -3
View File
@@ -216,9 +216,8 @@
{% set applicantInsurance = form.vars.data.participants[0].insurance %}
{% if applicantInsurance %}
{{ applicantInsurance.label }}
{% set participantPrice = participantPrices[loop.index0] %}
{% if participantPrice.insurance and participantPrice.insurance > 0 %}
<span class="text-gray-500">(€{{ participantPrice.insurance|number_format(2, ',', '.') }})</span>
{% if applicantInsurance.price and applicantInsurance.price > 0 %}
<span class="text-gray-500">(€{{ applicantInsurance.price|number_format(2, ',', '.') }})</span>
{% endif %}
<span class="italic text-gray-500 ml-2"> wie Anmelder</span>
{% else %}
@@ -14,7 +14,7 @@ use App\BusProNet\Model\Pickup;
use App\BusProNet\Model\Room;
use App\BusProNet\Model\Service;
use App\BusProNet\Model\Travel;
use App\Form\Model\BookingEditDto;
use App\Form\Model\BookingDto;
use App\Form\Model\ParticipantDto;
use PHPUnit\Framework\TestCase;
@@ -210,29 +210,31 @@ class BookingDataProcessorTest extends TestCase
$this->assertArrayNotHasKey('zustiege', $result);
}
private function createCompleteFormData(): BookingEditDto
private function createCompleteFormData(): BookingDto
{
$formData = new BookingEditDto($this->createMockBooking(), $this->createMockTravel());
$formData->participants = [
$bookingDto = new BookingDto($this->createMockTravel(), 1);
$bookingDto->booking = $this->createMockBooking();
$bookingDto->participants = [
$this->createMockParticipantDto(0, 'F'),
$this->createMockParticipantDto(1, 'F'),
];
return $formData;
return $bookingDto;
}
private function createFormDataWithCanceledParticipant(): BookingEditDto
private function createFormDataWithCanceledParticipant(): BookingDto
{
$formData = new BookingEditDto($this->createMockBooking(), $this->createMockTravel());
$formData->participants = [
$bookingDto = new BookingDto($this->createMockTravel(), 1);
$bookingDto->booking = $this->createMockBooking();
$bookingDto->participants = [
$this->createMockParticipantDto(0, 'F'),
$this->createMockParticipantDto(1, 'S'), // Canceled
];
return $formData;
return $bookingDto;
}
private function createFormDataWithAdditionalServices(): BookingEditDto
private function createFormDataWithAdditionalServices(): BookingDto
{
$formData = $this->createCompleteFormData();
@@ -243,7 +245,7 @@ class BookingDataProcessorTest extends TestCase
return $formData;
}
private function createFormDataWithTransportation(): BookingEditDto
private function createFormDataWithTransportation(): BookingDto
{
$formData = $this->createCompleteFormData();
@@ -254,7 +256,7 @@ class BookingDataProcessorTest extends TestCase
return $formData;
}
private function createFormDataWithBusPickup(): BookingEditDto
private function createFormDataWithBusPickup(): BookingDto
{
$formData = $this->createCompleteFormData();
@@ -267,7 +269,7 @@ class BookingDataProcessorTest extends TestCase
return $formData;
}
private function createFormDataWithNonBusTransportation(): BookingEditDto
private function createFormDataWithNonBusTransportation(): BookingDto
{
$formData = $this->createCompleteFormData();
@@ -279,7 +281,7 @@ class BookingDataProcessorTest extends TestCase
return $formData;
}
private function createFormDataWithUnusedServices(): BookingEditDto
private function createFormDataWithUnusedServices(): BookingDto
{
$formData = $this->createCompleteFormData();
@@ -289,7 +291,7 @@ class BookingDataProcessorTest extends TestCase
return $formData;
}
private function createFormDataWithUpdatedPersonalData(): BookingEditDto
private function createFormDataWithUpdatedPersonalData(): BookingDto
{
$formData = $this->createCompleteFormData();
@@ -302,7 +304,7 @@ class BookingDataProcessorTest extends TestCase
return $formData;
}
private function createFormDataWithInactiveParticipant(): BookingEditDto
private function createFormDataWithInactiveParticipant(): BookingDto
{
$formData = $this->createCompleteFormData();
@@ -318,7 +320,7 @@ class BookingDataProcessorTest extends TestCase
return $formData;
}
private function createFormDataForApplicantSync(): BookingEditDto
private function createFormDataForApplicantSync(): BookingDto
{
$formData = $this->createCompleteFormData();
@@ -330,7 +332,7 @@ class BookingDataProcessorTest extends TestCase
return $formData;
}
private function createFormDataWithBankAccount(): BookingEditDto
private function createFormDataWithBankAccount(): BookingDto
{
$formData = $this->createCompleteFormData();
@@ -345,7 +347,7 @@ class BookingDataProcessorTest extends TestCase
return $formData;
}
private function createFormDataWithoutBankAccount(): BookingEditDto
private function createFormDataWithoutBankAccount(): BookingDto
{
$formData = $this->createCompleteFormData();
$formData->booking->bankAccount = null;
@@ -353,7 +355,7 @@ class BookingDataProcessorTest extends TestCase
return $formData;
}
private function createFormDataWithRooms(): BookingEditDto
private function createFormDataWithRooms(): BookingDto
{
$formData = $this->createCompleteFormData();
@@ -371,7 +373,7 @@ class BookingDataProcessorTest extends TestCase
return $formData;
}
private function createFormDataWithoutExistingCommunication(): BookingEditDto
private function createFormDataWithoutExistingCommunication(): BookingDto
{
$formData = $this->createCompleteFormData();
@@ -384,7 +386,7 @@ class BookingDataProcessorTest extends TestCase
return $formData;
}
private function createFormDataWithoutPickups(): BookingEditDto
private function createFormDataWithoutPickups(): BookingDto
{
$formData = $this->createCompleteFormData();
$formData->booking->pickupsOutbound = [];
@@ -6,8 +6,7 @@ namespace App\Tests\Form\Service\Condition;
use App\BusProNet\Model\Booking;
use App\BusProNet\Model\Travel;
use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingEditDto;
use App\Form\Model\BookingDto;
use App\Form\Service\Condition\InsuranceMutabilityCondition;
use Carbon\Carbon;
use PHPUnit\Framework\TestCase;
@@ -35,7 +34,7 @@ class InsuranceMutabilityConditionTest extends TestCase
$travel = new Travel();
$travel->dateFrom = new \DateTimeImmutable('+10 days');
$bookingDto = new BookingCreateDto($travel, 123);
$bookingDto = new BookingDto($travel, 123);
$result = $this->condition->evaluate($bookingDto, 0, []);
@@ -154,7 +153,7 @@ class InsuranceMutabilityConditionTest extends TestCase
$this->assertStringContainsString('3', $result);
}
private function createEditDto(\DateTimeImmutable $travelDate, \DateTimeImmutable $bookingDate): BookingEditDto
private function createEditDto(\DateTimeImmutable $travelDate, \DateTimeImmutable $bookingDate): BookingDto
{
$booking = new Booking();
$booking->bookingDate = $bookingDate;
@@ -162,6 +161,9 @@ class InsuranceMutabilityConditionTest extends TestCase
$travel = new Travel();
$travel->dateFrom = $travelDate;
return new BookingEditDto($booking, $travel);
$bookingDto = new BookingDto($travel, 1);
$bookingDto->booking = $booking;
return $bookingDto;
}
}
@@ -6,7 +6,7 @@ namespace App\Tests\Form\Service;
use App\BusProNet\Model\Insurance;
use App\BusProNet\Model\Travel;
use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingDto;
use App\Form\Model\ParticipantDto;
use App\Form\Service\ParticipantInsuranceFieldHandler;
use App\Service\InsuranceMatchingService;
@@ -229,8 +229,8 @@ class ParticipantInsuranceFieldHandlerTest extends TestCase
return $insurance;
}
private function createMockBookingDto(): BookingCreateDto
private function createMockBookingDto(): BookingDto
{
return $this->createMock(BookingCreateDto::class);
return $this->createMock(BookingDto::class);
}
}
@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Tests\Form\Service;
use App\BusProNet\Model\Travel;
use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingDto;
use App\Form\Model\ParticipantDto;
use App\Form\Service\ParticipantLicensePlateFieldHandler;
use PHPUnit\Framework\TestCase;
@@ -38,7 +38,7 @@ class ParticipantLicensePlateFieldHandlerTest extends TestCase
public function testProcessFieldWithoutParticipant(): void
{
$travel = new Travel();
$bookingDto = new BookingCreateDto($travel, 1);
$bookingDto = new BookingDto($travel, 1);
$submittedData = ['licensePlate' => 'AB-CD 123'];
$this->handler->processField($submittedData, $bookingDto, 0);
@@ -54,7 +54,7 @@ class ParticipantLicensePlateFieldHandlerTest extends TestCase
$participant->licensePlate = 'AB-CD 123'; // Should be cleared
$travel = new Travel();
$bookingDto = new BookingCreateDto($travel, 1);
$bookingDto = new BookingDto($travel, 1);
$bookingDto->participants = [$participant];
$submittedData = ['licensePlate' => 'XY-ZZ 999'];
@@ -70,7 +70,7 @@ class ParticipantLicensePlateFieldHandlerTest extends TestCase
$participant->parking = true; // Parking selected
$travel = new Travel();
$bookingDto = new BookingCreateDto($travel, 1);
$bookingDto = new BookingDto($travel, 1);
$bookingDto->participants = [$participant];
$submittedData = ['licensePlate' => 'AB-CD 123'];
@@ -86,7 +86,7 @@ class ParticipantLicensePlateFieldHandlerTest extends TestCase
$participant->parking = true; // Parking selected
$travel = new Travel();
$bookingDto = new BookingCreateDto($travel, 1);
$bookingDto = new BookingDto($travel, 1);
$bookingDto->participants = [$participant];
$submittedData = ['licensePlate' => ''];
@@ -102,7 +102,7 @@ class ParticipantLicensePlateFieldHandlerTest extends TestCase
$participant->parking = true; // Parking selected
$travel = new Travel();
$bookingDto = new BookingCreateDto($travel, 1);
$bookingDto = new BookingDto($travel, 1);
$bookingDto->participants = [$participant];
$submittedData = ['licensePlate' => null];
@@ -118,7 +118,7 @@ class ParticipantLicensePlateFieldHandlerTest extends TestCase
$participant->parking = true; // Parking selected
$travel = new Travel();
$bookingDto = new BookingCreateDto($travel, 1);
$bookingDto = new BookingDto($travel, 1);
$bookingDto->participants = [$participant];
$submittedData = ['licensePlate' => ' AB-CD 123 '];
@@ -134,7 +134,7 @@ class ParticipantLicensePlateFieldHandlerTest extends TestCase
$participant->parking = true; // Parking selected
$travel = new Travel();
$bookingDto = new BookingCreateDto($travel, 1);
$bookingDto = new BookingDto($travel, 1);
$bookingDto->participants = [$participant];
$submittedData = ['licensePlate' => ' '];
@@ -150,7 +150,7 @@ class ParticipantLicensePlateFieldHandlerTest extends TestCase
$participant->parking = true; // Parking selected
$travel = new Travel();
$bookingDto = new BookingCreateDto($travel, 1);
$bookingDto = new BookingDto($travel, 1);
$bookingDto->participants = [$participant];
// Test with integer value
@@ -170,7 +170,7 @@ class ParticipantLicensePlateFieldHandlerTest extends TestCase
$participant->parking = true; // Parking selected
$travel = new Travel();
$bookingDto = new BookingCreateDto($travel, 1);
$bookingDto = new BookingDto($travel, 1);
$bookingDto->participants = [$participant];
$submittedData = []; // No licensePlate field
@@ -189,7 +189,7 @@ class ParticipantLicensePlateFieldHandlerTest extends TestCase
$participant2->parking = true;
$travel = new Travel();
$bookingDto = new BookingCreateDto($travel, 1);
$bookingDto = new BookingDto($travel, 1);
$bookingDto->participants = [$participant1, $participant2];
$submittedData = ['licensePlate' => 'AB-CD 123'];
@@ -7,7 +7,7 @@ namespace App\Tests\Service;
use App\BusProNet\Model\Room;
use App\BusProNet\Model\Service;
use App\BusProNet\Model\Travel;
use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingDto;
use App\Form\Model\ParticipantDto;
use App\Form\Model\RoomSelectionDto;
use App\Service\BookingPriceCalculatorService;
@@ -38,7 +38,7 @@ class BookingPriceCalculatorServiceTest extends TestCase
$roomSelection->roomId = 1;
$roomSelection->quantity = 2; // 2 rooms selected
$bookingDto = new BookingCreateDto($travel, 1);
$bookingDto = new BookingDto($travel, 1);
$bookingDto->roomSelections = [$roomSelection];
// Test the calculation
@@ -70,7 +70,7 @@ class BookingPriceCalculatorServiceTest extends TestCase
$roomSelection->roomId = 2;
$roomSelection->quantity = 1; // 1 room selected
$bookingDto = new BookingCreateDto($travel, 1);
$bookingDto = new BookingDto($travel, 1);
$bookingDto->roomSelections = [$roomSelection];
// Test the calculation
@@ -112,7 +112,7 @@ class BookingPriceCalculatorServiceTest extends TestCase
$doubleRoomSelection->roomId = 2;
$doubleRoomSelection->quantity = 2; // 2 double rooms
$bookingDto = new BookingCreateDto($travel, 1);
$bookingDto = new BookingDto($travel, 1);
$bookingDto->roomSelections = [$singleRoomSelection, $doubleRoomSelection];
// Test the calculation
@@ -155,7 +155,7 @@ class BookingPriceCalculatorServiceTest extends TestCase
$roomSelection->roomId = 1;
$roomSelection->quantity = 1;
$bookingDto = new BookingCreateDto($travel, 1);
$bookingDto = new BookingDto($travel, 1);
$bookingDto->roomSelections = [$roomSelection];
$result = $this->service->calculateRoomPricing($bookingDto);
@@ -179,7 +179,7 @@ class BookingPriceCalculatorServiceTest extends TestCase
$roomSelection->roomId = 1;
$roomSelection->quantity = 0; // Zero quantity - not selected
$bookingDto = new BookingCreateDto($travel, 1);
$bookingDto = new BookingDto($travel, 1);
$bookingDto->roomSelections = [$roomSelection];
$result = $this->service->calculateRoomPricing($bookingDto);
@@ -212,7 +212,7 @@ class BookingPriceCalculatorServiceTest extends TestCase
$participant->skiPass = $skiPass;
$participant->courses = [$course];
$bookingDto = new BookingCreateDto($travel, 1);
$bookingDto = new BookingDto($travel, 1);
$bookingDto->participants = [$participant];
$result = $this->service->calculateIndividualParticipantPrice($bookingDto, 0);
@@ -234,7 +234,7 @@ class BookingPriceCalculatorServiceTest extends TestCase
$participant->assignedRoomId = null; // No room assigned
$participant->skiPass = $skiPass;
$bookingDto = new BookingCreateDto($travel, 1);
$bookingDto = new BookingDto($travel, 1);
$bookingDto->participants = [$participant];
$result = $this->service->calculateIndividualParticipantPrice($bookingDto, 0);
@@ -246,7 +246,7 @@ class BookingPriceCalculatorServiceTest extends TestCase
public function testCalculateIndividualParticipantPriceForNonExistentParticipant(): void
{
$travel = new Travel();
$bookingDto = new BookingCreateDto($travel, 1);
$bookingDto = new BookingDto($travel, 1);
$bookingDto->participants = [];
$result = $this->service->calculateIndividualParticipantPrice($bookingDto, 0);
@@ -290,7 +290,7 @@ class BookingPriceCalculatorServiceTest extends TestCase
$participant3 = new ParticipantDto();
$participant3->assignedRoomId = null; // No room assigned
$bookingDto = new BookingCreateDto($travel, 1);
$bookingDto = new BookingDto($travel, 1);
$bookingDto->participants = [$participant1, $participant2, $participant3];
$result = $this->service->calculateAllParticipantIndividualPrices($bookingDto);
@@ -308,7 +308,7 @@ class BookingPriceCalculatorServiceTest extends TestCase
public function testCalculateAllParticipantIndividualPricesWithEmptyBooking(): void
{
$travel = new Travel();
$bookingDto = new BookingCreateDto($travel, 1);
$bookingDto = new BookingDto($travel, 1);
$bookingDto->participants = [];
$result = $this->service->calculateAllParticipantIndividualPrices($bookingDto);
@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Tests\Service;
use App\BusProNet\Model\Insurance;
use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingDto;
use App\Form\Model\ParticipantDto;
use App\Service\BookingPriceCalculatorService;
use App\Service\InsuranceMatchingService;
@@ -254,10 +254,10 @@ class InsuranceMatchingServiceTest extends TestCase
return $participant;
}
private function createBooking(string $travelDateFrom, string $travelDateTo): BookingCreateDto
private function createBooking(string $travelDateFrom, string $travelDateTo): BookingDto
{
$travel = $this->createTravel($travelDateFrom, $travelDateTo);
$booking = new BookingCreateDto($travel, 1);
$booking = new BookingDto($travel, 1);
return $booking;
}