199 lines
5.7 KiB
PHP
199 lines
5.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Form\Model;
|
|
|
|
use App\BusProNet\Constants;
|
|
use App\BusProNet\Model\Booking;
|
|
use App\BusProNet\Model\Travel;
|
|
use Symfony\Component\Validator\Constraints as Assert;
|
|
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
|
|
|
/**
|
|
* Unified DTO for both booking creation and editing workflows.
|
|
*
|
|
* This DTO consolidates the previously separate BookingCreateDto and BookingEditDto
|
|
* into a single class that handles both modes. All service selections are stored
|
|
* in participant DTOs regardless of mode, ensuring consistent data structure and
|
|
* simplifying pricing calculations, field handlers, and template rendering.
|
|
*/
|
|
class BookingDto
|
|
{
|
|
public const MODE_CREATE = 'create';
|
|
public const MODE_EDIT = 'edit';
|
|
|
|
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;
|
|
|
|
/**
|
|
* Booking status code for API submission.
|
|
* Values: 'F' (Final/Frei), 'A' (Anfrage/Inquiry).
|
|
*/
|
|
public string $bookingStatus = 'F';
|
|
|
|
/**
|
|
* Reference to booking entity (only populated in edit mode).
|
|
*/
|
|
public ?Booking $booking = null;
|
|
|
|
/**
|
|
* Timestamp of last session update (for staleness detection).
|
|
* Updated automatically by BookingService::saveBookingDto().
|
|
*/
|
|
public ?\DateTimeImmutable $lastSessionUpdate = null;
|
|
|
|
/**
|
|
* Fingerprint of the booking state when loaded from API (edit mode only).
|
|
* This property stores the original state and is never updated after initial load.
|
|
* Used to detect unsaved changes in edit mode by comparing with current state.
|
|
*/
|
|
public ?string $originalFingerprint = null;
|
|
|
|
public function __construct(public Travel $travel, public int $hotelId)
|
|
{
|
|
}
|
|
|
|
public function getMode(): string
|
|
{
|
|
return null !== $this->booking ? self::MODE_EDIT : self::MODE_CREATE;
|
|
}
|
|
|
|
/**
|
|
* @return array<int, RoomSelectionDto>
|
|
*/
|
|
public function getSelectedRooms(): array
|
|
{
|
|
// In edit mode, rooms are fixed - return empty array
|
|
if (self::MODE_EDIT === $this->getMode()) {
|
|
return [];
|
|
}
|
|
|
|
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)
|
|
*/
|
|
public function isFamilyBooking(): bool
|
|
{
|
|
$adults = 0;
|
|
$children = 0;
|
|
|
|
$travelStartDate = $this->travel->dateFrom;
|
|
|
|
foreach ($this->participants as $participant) {
|
|
$age = $participant->getAge($travelStartDate);
|
|
|
|
if (null === $age) {
|
|
continue;
|
|
}
|
|
|
|
if ($age >= 18) {
|
|
++$adults;
|
|
} else {
|
|
++$children;
|
|
}
|
|
}
|
|
|
|
return ($adults >= 1 && $adults <= 2) && ($children >= 1);
|
|
}
|
|
|
|
public function isCanceled(): bool
|
|
{
|
|
return null !== $this->booking && 'S' === $this->booking->status;
|
|
}
|
|
|
|
public function isOption(): bool
|
|
{
|
|
return null !== $this->booking && 'O' === $this->booking->status;
|
|
}
|
|
|
|
#[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;
|
|
}
|
|
|
|
if (null === $this->bankAccount->iban || '' === trim($this->bankAccount->iban)) {
|
|
$context->buildViolation('Bitte geben Sie Ihre IBAN ein.')
|
|
->atPath('bankAccount.iban')
|
|
->addViolation();
|
|
}
|
|
|
|
if (null === $this->bankAccount->accountHolder || '' === trim($this->bankAccount->accountHolder)) {
|
|
$context->buildViolation('Bitte geben Sie den Kontoinhaber ein.')
|
|
->atPath('bankAccount.accountHolder')
|
|
->addViolation();
|
|
}
|
|
|
|
if (false === $this->bankAccount->sepaMandateAccepted) {
|
|
$context->buildViolation('Bitte akzeptieren Sie das SEPA-Mandat.')
|
|
->atPath('bankAccount.sepaMandateAccepted')
|
|
->addViolation();
|
|
}
|
|
}
|
|
}
|