'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 */ #[Groups(['api:single', 'snapshot'])] public array $allowedBookingStatus = []; #[Groups(['api:list', 'api:single', 'snapshot'])] public ?float $priceFrom = null; /** * @var array */ #[Groups(['api:single', 'snapshot'])] public array $selectionGroups = []; /** * @var array */ #[Groups(['api:single', 'snapshot'])] public array $additionalServices = []; #[Groups(['api:single', 'snapshot'])] public bool $additionalServicesMutable = true; /** * @var array */ #[Groups(['api:single', 'snapshot'])] public array $transportationServices = []; #[Groups(['api:single', 'snapshot'])] public bool $transportationServicesMutable = true; /** * @var array */ #[Groups(['api:single', 'snapshot'])] public array $pickups = []; /** * @var array */ #[Groups(['api:single', 'snapshot'])] public array $dropOffs = []; #[Groups(['api:single', 'snapshot'])] public bool $pickupsMutable = true; /** * @var array */ #[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 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 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 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 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 $ids The array of room IDs to filter by * * @return array 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 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); } }