wip: modernized edit flow, fix insurance tier calculation

This commit is contained in:
Björn Fromme
2025-10-09 17:42:39 +02:00
parent cba0f747cd
commit 67c642bc2f
31 changed files with 272 additions and 536 deletions
-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 [];
}
}