Files
myep/src/Form/Model/BookingDto.php
T

262 lines
7.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 App\Validator\Constraints as AppAssert;
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.
*/
#[AppAssert\RoomSelection(groups: ['booking_create_step_1'])]
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;
}
/**
* Gets the label of the single selected room type.
*
* Returns the room label when exactly one room type is selected (auto-assignment scenario).
* Used by templates to display the room assignment when the dropdown is hidden.
*
* @return string|null The room label or null if not a single room type scenario
*/
public function getSingleRoomLabel(): ?string
{
$selectedRooms = $this->getSelectedRooms();
// Only return label when exactly one room type selected
if (1 !== count($selectedRooms)) {
return null;
}
$roomSelection = reset($selectedRooms);
$availableRooms = $this->travel->getAvailableRooms();
$room = $availableRooms[$roomSelection->roomId] ?? null;
return $room?->label;
}
#[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();
}
}
/**
* Checks if this is an inquiry booking (status='A').
*
* Inquiry bookings occur when a travel is fully booked but still accepts inquiries.
*/
public function isInquiryBooking(): bool
{
return 'A' === $this->bookingStatus;
}
/**
* Checks if any participants have entered voucher codes.
*
* @return bool True if any promotional, purchase, or goodwill vouchers are present
*/
public function hasVouchers(): bool
{
foreach ($this->participants as $participant) {
if (null !== $participant->promoVoucherCode && '' !== trim($participant->promoVoucherCode)) {
return true;
}
if (null !== $participant->purchaseVoucherCode && '' !== trim($participant->purchaseVoucherCode)) {
return true;
}
}
return false;
}
/**
* Checks if any participants have entered goodwill vouchers.
*
* Goodwill vouchers (Kulanz) are a special type of purchase voucher that cannot be
* redeemed in inquiry bookings. This method is used to display appropriate warnings.
*
* @return bool True if any goodwill vouchers are present
*/
public function hasGoodwillVouchers(): bool
{
foreach ($this->participants as $participant) {
if (true === $participant->hasGoodwillVoucher) {
return true;
}
}
return false;
}
}