374 lines
12 KiB
PHP
374 lines
12 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\BusProNet\Model;
|
|
|
|
use App\BusProNet\Constants;
|
|
use App\BusProNet\Traits\SortByPriceTrait;
|
|
use Symfony\Component\Serializer\Attribute\Context;
|
|
use Symfony\Component\Serializer\Attribute\Groups;
|
|
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
|
|
|
|
/**
|
|
* Represents a travel package with services, accommodation, and pricing information.
|
|
*
|
|
* This class handles travel data including dates, hotel information, available services,
|
|
* transportation options, and participant configuration. It provides methods to filter
|
|
* and retrieve services by various criteria and manage included services.
|
|
*/
|
|
class Travel
|
|
{
|
|
use SortByPriceTrait;
|
|
|
|
/**
|
|
* CRM selection that offers the "I am not travelling myself" choice on a travel date.
|
|
*/
|
|
private const NON_TRAVELLING_APPLICANT_SELECTION_ID = 1488;
|
|
|
|
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
|
public ?int $id = null;
|
|
|
|
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
|
public ?string $code = null;
|
|
|
|
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
|
public ?string $productCode = null;
|
|
|
|
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
|
public ?int $productId = null;
|
|
|
|
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
|
public ?string $label = null;
|
|
|
|
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
|
public ?int $hotelId = null;
|
|
|
|
#[Groups(['api:single', 'snapshot'])]
|
|
public ?Hotel $hotel = null;
|
|
|
|
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
|
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
|
|
public ?\DateTimeImmutable $dateFrom = null;
|
|
|
|
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
|
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
|
|
public ?\DateTimeImmutable $dateTo = null;
|
|
|
|
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
|
public ?string $type = null;
|
|
|
|
/**
|
|
* Travel booking status from BusProNet API.
|
|
* Possible values: 'Frei', 'Anfrage', 'Buchungsstop'.
|
|
*/
|
|
#[Groups(['api:single', 'snapshot'])]
|
|
public ?string $status = null;
|
|
|
|
/**
|
|
* Allowed booking status codes from buchungstatusmoeglich attribute.
|
|
* Array of status codes (F, A, O) indicating which booking types are permitted.
|
|
*
|
|
* @var array<string>
|
|
*/
|
|
#[Groups(['api:single', 'snapshot'])]
|
|
public array $allowedBookingStatus = [];
|
|
|
|
#[Groups(['api:list', 'api:single', 'snapshot'])]
|
|
public ?float $priceFrom = null;
|
|
|
|
/**
|
|
* @var array<int, CrmSelectionGroup>
|
|
*/
|
|
#[Groups(['api:single', 'snapshot'])]
|
|
public array $selectionGroups = [];
|
|
|
|
/**
|
|
* @var array<int, Service>
|
|
*/
|
|
#[Groups(['api:single', 'snapshot'])]
|
|
public array $additionalServices = [];
|
|
|
|
#[Groups(['api:single', 'snapshot'])]
|
|
public bool $additionalServicesMutable = true;
|
|
|
|
/**
|
|
* @var array<int, Service>
|
|
*/
|
|
#[Groups(['api:single', 'snapshot'])]
|
|
public array $transportationServices = [];
|
|
|
|
#[Groups(['api:single', 'snapshot'])]
|
|
public bool $transportationServicesMutable = true;
|
|
|
|
/**
|
|
* @var array<int, Pickup>
|
|
*/
|
|
#[Groups(['api:single', 'snapshot'])]
|
|
public array $pickups = [];
|
|
|
|
/**
|
|
* @var array<int, Pickup>
|
|
*/
|
|
#[Groups(['api:single', 'snapshot'])]
|
|
public array $dropOffs = [];
|
|
|
|
#[Groups(['api:single', 'snapshot'])]
|
|
public bool $pickupsMutable = true;
|
|
|
|
/**
|
|
* @var array<int, Room>
|
|
*/
|
|
#[Groups(['api:single', 'snapshot'])]
|
|
public array $rooms = [];
|
|
|
|
#[Groups(['api:single', 'snapshot'])]
|
|
public bool $roomsMutable = true;
|
|
|
|
#[Groups(['api:single', 'snapshot'])]
|
|
public bool $participantDataMutable = true;
|
|
|
|
#[Groups(['api:single', 'snapshot'])]
|
|
public bool $participantCountMutable = true;
|
|
|
|
#[Groups(['api:single', 'snapshot'])]
|
|
public ?Guide $guide = null;
|
|
|
|
/**
|
|
* @var array<string|int, Insurance> Available insurances for this travel package
|
|
*/
|
|
#[Groups(['api:single'])]
|
|
public array $insurances = [];
|
|
|
|
/**
|
|
* Retrieves additional services filtered by subtype and optionally by travel date range.
|
|
*
|
|
* Filters additional services based on the provided subtype(s) and optionally whether
|
|
* their date range overlaps with the travel dates. Services with null dates are
|
|
* considered always available when date filtering is enabled.
|
|
* Services are sorted by price in ascending order.
|
|
*
|
|
* Availability filtering is NOT applied here - it happens at the form/UI level where
|
|
* unavailable services are shown as readonly with appropriate tooltips.
|
|
*
|
|
* @param mixed $subTypes The service subtype(s) to filter by
|
|
* @param bool $filterByTravelDateRange Whether to filter by travel date range overlap
|
|
*
|
|
* @return array<int, Service> The filtered and sorted services array
|
|
*/
|
|
public function getAdditionalServicesBySubTypes(mixed $subTypes, bool $filterByTravelDateRange = false): array
|
|
{
|
|
$subTypes = (array) $subTypes;
|
|
|
|
$services = array_filter($this->additionalServices, function (Service $service) use ($subTypes, $filterByTravelDateRange) {
|
|
// Check subtype
|
|
if (false === in_array($service->subType, $subTypes, true)) {
|
|
return false;
|
|
}
|
|
|
|
// Check date range overlap if filtering by travel dates is enabled
|
|
if (true === $filterByTravelDateRange) {
|
|
if (null !== $service->dateFrom && null !== $service->dateTo
|
|
&& null !== $this->dateFrom && null !== $this->dateTo) {
|
|
// Service is available if its date range overlaps with travel dates:
|
|
// service.dateFrom <= travel.dateTo AND service.dateTo >= travel.dateFrom
|
|
return $service->dateFrom <= $this->dateTo && $service->dateTo >= $this->dateFrom;
|
|
}
|
|
// Service is available if it has no date constraints or travel has no dates
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
return $this->sortByPrice($services);
|
|
}
|
|
|
|
/**
|
|
* Retrieves transportation services filtered by direction.
|
|
*
|
|
* Filters transportation services based on travel direction. Services are sorted by subtype.
|
|
* Availability filtering is NOT applied here - it happens at the form/UI level where
|
|
* unavailable services are shown as readonly with appropriate tooltips.
|
|
*
|
|
* @param string $direction The travel direction to filter by
|
|
*
|
|
* @return array<int, Service> The filtered and sorted transportation services
|
|
*/
|
|
public function getTransportationServicesByDirection(string $direction): array
|
|
{
|
|
$services = array_filter($this->transportationServices, fn (Service $service) => $direction === $service->direction);
|
|
|
|
usort($services, fn (Service $a, Service $b) => $a->subType <=> $b->subType);
|
|
|
|
return $services;
|
|
}
|
|
|
|
/**
|
|
* Retrieves included services from selection groups.
|
|
*
|
|
* Maps selection group IDs to their corresponding services based on
|
|
* the predefined included services mapping.
|
|
*
|
|
* @return array<int, CrmSelectionGroup> The included services from selection groups
|
|
*/
|
|
public function getIncludedServices(): array
|
|
{
|
|
$services = [];
|
|
|
|
foreach (CrmSelectionGroup::$includedServicesMapping as $id => $label) {
|
|
if (true === isset($this->selectionGroups[$id])) {
|
|
$services[] = $this->selectionGroups[$id];
|
|
}
|
|
}
|
|
|
|
return $services;
|
|
}
|
|
|
|
/**
|
|
* Checks whether any CRM selection group contains the given selection ID.
|
|
*
|
|
* This scans all parsed groups and their selections, regardless of group.
|
|
*/
|
|
public function hasSelectionId(int $selectionId): bool
|
|
{
|
|
foreach ($this->selectionGroups as $selectionGroup) {
|
|
if (null === $selectionGroup->selections) {
|
|
continue;
|
|
}
|
|
|
|
foreach ($selectionGroup->selections as $selection) {
|
|
if ($selectionId === $selection->id) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Whether a booking for this travel date may be placed by an applicant who does not travel
|
|
* along, such as a parent booking for their children.
|
|
*/
|
|
public function allowsNonTravellingApplicant(): bool
|
|
{
|
|
return $this->hasSelectionId(self::NON_TRAVELLING_APPLICANT_SELECTION_ID);
|
|
}
|
|
|
|
/**
|
|
* Retrieves rooms filtered by their IDs.
|
|
*
|
|
* Filters the rooms array to return only rooms whose IDs match
|
|
* the provided array of IDs. Returns an empty array if no matching
|
|
* rooms are found.
|
|
*
|
|
* @param array<int> $ids The array of room IDs to filter by
|
|
*
|
|
* @return array<int, Room> The filtered rooms array
|
|
*/
|
|
public function getRoomsByIds(array $ids): array
|
|
{
|
|
if (true === empty($ids)) {
|
|
return [];
|
|
}
|
|
|
|
return array_filter($this->rooms, function (Room $room) use ($ids) {
|
|
return in_array($room->id, $ids, true);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Retrieves all bookable rooms (both regular and inquiry).
|
|
*
|
|
* Returns rooms that have availability greater than zero and a bookable status
|
|
* (either 'Frei' for regular booking or 'Anfrage' for inquiry booking).
|
|
*
|
|
* @return array<int, Room> The filtered array of bookable rooms, indexed by room ID
|
|
*/
|
|
public function getAvailableRooms(): array
|
|
{
|
|
$result = [];
|
|
foreach ($this->rooms as $room) {
|
|
if ($room->available > 0 && $this->isBookableRoomStatus($room->status)) {
|
|
$result[$room->id] = $room;
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Checks if the travel has any bookable rooms.
|
|
*
|
|
* @return bool True if at least one room is bookable
|
|
*/
|
|
public function hasBookableRooms(): bool
|
|
{
|
|
return [] !== $this->getAvailableRooms();
|
|
}
|
|
|
|
/**
|
|
* Checks if all bookable rooms require inquiry booking.
|
|
*
|
|
* Returns true if there are bookable rooms but none with 'Frei' status.
|
|
* This indicates the booking must be submitted as an inquiry.
|
|
*
|
|
* @return bool True if only inquiry rooms are available
|
|
*/
|
|
public function requiresInquiryBooking(): bool
|
|
{
|
|
$bookableRooms = $this->getAvailableRooms();
|
|
|
|
if ([] === $bookableRooms) {
|
|
return false;
|
|
}
|
|
|
|
foreach ($bookableRooms as $room) {
|
|
if (Constants::STATUS_AVAILABLE === $room->status) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Checks if a room status allows booking.
|
|
*/
|
|
private function isBookableRoomStatus(?string $status): bool
|
|
{
|
|
return Constants::STATUS_AVAILABLE === $status
|
|
|| Constants::STATUS_ON_REQUEST === $status;
|
|
}
|
|
|
|
/**
|
|
* Retrieves a room by its ID.
|
|
*
|
|
* @param int|null $roomId The room ID to search for
|
|
*
|
|
* @return Room|null The room with the matching ID, or null if not found
|
|
*/
|
|
public function getRoomById(?int $roomId): ?Room
|
|
{
|
|
if (null === $roomId) {
|
|
return null;
|
|
}
|
|
|
|
$rooms = $this->getRoomsByIds([$roomId]);
|
|
|
|
return false === empty($rooms) ? reset($rooms) : null;
|
|
}
|
|
|
|
/**
|
|
* Checks if a specific booking status code is allowed for this travel.
|
|
*
|
|
* @param string $statusCode The booking status code to check (F, A, or O)
|
|
*
|
|
* @return bool True if the status is allowed, false otherwise
|
|
*/
|
|
public function isBookingStatusAllowed(string $statusCode): bool
|
|
{
|
|
return in_array($statusCode, $this->allowedBookingStatus, true);
|
|
}
|
|
}
|