wip: booking process, refactoring
This commit is contained in:
@@ -48,6 +48,10 @@ APP_BPN_DEBUG=false
|
||||
|
||||
APP_TRAVEL_INFO_BASE_URL=https://www.ep-reisen.de/reiseinformationen/
|
||||
|
||||
# Travel Data Service Configuration
|
||||
APP_TRAVEL_PREFER_REMOTE=false
|
||||
APP_TRAVEL_ENABLE_FALLBACK=true
|
||||
|
||||
API_KEYS=
|
||||
|
||||
XML_EXPORT_PATH="%kernel.project_dir%/var/xmlexport"
|
||||
|
||||
@@ -54,7 +54,7 @@ Accept: application/json
|
||||
Authorization: Bearer {{$auth.token("oauth2_api")}}
|
||||
|
||||
### API travel remote
|
||||
GET {{base_url}}/api/travels/1277/remote
|
||||
GET {{base_url}}/api/travels/11603
|
||||
Accept: application/json
|
||||
Authorization: Bearer {{$auth.token("oauth2_api")}}
|
||||
|
||||
@@ -63,3 +63,8 @@ GET {{base_url}}/api/travels/11603/152546/2026-01-03/availability
|
||||
Accept: application/json
|
||||
Authorization: Bearer {{$auth.token("oauth2_api")}}
|
||||
|
||||
### API products remote
|
||||
GET {{base_url}}/api/products
|
||||
Accept: application/json
|
||||
Authorization: Bearer {{$auth.token("oauth2_api")}}
|
||||
|
||||
|
||||
@@ -56,3 +56,9 @@ services:
|
||||
App\Security\Crypt:
|
||||
arguments:
|
||||
$path: '%path_to_keys%'
|
||||
|
||||
App\Service\TravelDataService:
|
||||
arguments:
|
||||
$logger: '@monolog.logger.core'
|
||||
$preferRemote: '%env(bool:APP_TRAVEL_PREFER_REMOTE)%'
|
||||
$enableFallback: '%env(bool:APP_TRAVEL_ENABLE_FALLBACK)%'
|
||||
|
||||
@@ -34,6 +34,7 @@ class ApiClient
|
||||
public const TYPE_AVAILABILITY = 'VERFUEGBARKEIT';
|
||||
public const TYPE_AVAILABILITY_HOTEL = 'VERFUEGBARKEITHOTEL';
|
||||
public const TYPE_BOOKING_UPDATE = 'BUCHUNGAENDERUNG';
|
||||
public const TYPE_PRODUCTS = 'PRODUKTE';
|
||||
public const TYPE_PRODUCT_DATA = 'PRODUKTDATEN';
|
||||
|
||||
private array $config;
|
||||
@@ -205,13 +206,13 @@ class ApiClient
|
||||
/**
|
||||
* @throws ApiClientException
|
||||
*/
|
||||
public function getMutableData(int $travelId): Notification|BaseData
|
||||
public function getMutableData(int $dateId): Notification|BaseData
|
||||
{
|
||||
$data = [
|
||||
'user' => $this->config['bpn_username'],
|
||||
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_MUTABLE_DATA),
|
||||
'satz' => ['@typ' => static::TYPE_MUTABLE_DATA],
|
||||
'idreise' => $travelId,
|
||||
'idreise' => $dateId,
|
||||
];
|
||||
|
||||
return $this->sendRequest(static::TYPE_MUTABLE_DATA, $data);
|
||||
@@ -220,13 +221,13 @@ class ApiClient
|
||||
/**
|
||||
* @throws ApiClientException
|
||||
*/
|
||||
public function getAvailabilities(int $travelId): Notification|BaseData
|
||||
public function getAvailabilities(int $dateId): Notification|BaseData
|
||||
{
|
||||
$data = [
|
||||
'user' => $this->config['bpn_username'],
|
||||
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_AVAILABILITY),
|
||||
'satz' => ['@typ' => static::TYPE_AVAILABILITY],
|
||||
'idreise' => $travelId,
|
||||
'idreise' => $dateId,
|
||||
];
|
||||
|
||||
return $this->sendRequest(static::TYPE_AVAILABILITY, $data);
|
||||
@@ -235,13 +236,13 @@ class ApiClient
|
||||
/**
|
||||
* @throws ApiClientException
|
||||
*/
|
||||
public function getHotelAvailability(int $travelId, int $hotelId, \DateTimeInterface $dateTo): Notification|BaseData
|
||||
public function getHotelAvailability(int $dateId, int $hotelId, \DateTimeInterface $dateTo): Notification|BaseData
|
||||
{
|
||||
$data = [
|
||||
'user' => $this->config['bpn_username'],
|
||||
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_AVAILABILITY_HOTEL),
|
||||
'satz' => ['@typ' => static::TYPE_AVAILABILITY_HOTEL],
|
||||
'idreise' => $travelId,
|
||||
'idreise' => $dateId,
|
||||
'idpartner' => $hotelId,
|
||||
'terminbis' => $dateTo->format('d.m.Y'),
|
||||
];
|
||||
@@ -313,6 +314,20 @@ class ApiClient
|
||||
return $this->sendRequest(static::TYPE_PRODUCT_DATA, $data, ['hotelId' => $hotelId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ApiClientException
|
||||
*/
|
||||
public function getProducts(): Notification|BaseData
|
||||
{
|
||||
$data = [
|
||||
'user' => $this->config['bpn_username'],
|
||||
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_PRODUCTS),
|
||||
'satz' => ['@typ' => static::TYPE_PRODUCTS],
|
||||
];
|
||||
|
||||
return $this->sendRequest(static::TYPE_PRODUCTS, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ApiClientException
|
||||
*/
|
||||
|
||||
@@ -19,4 +19,4 @@ final class Constants
|
||||
public const STATUS_AVAILABLE = 'Frei';
|
||||
public const STATUS_BLOCKED = 'Buchungsstop';
|
||||
public const STATUS_ON_REQUEST = 'Anfrage';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,97 +1,236 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\DataProcessor;
|
||||
|
||||
use App\BusProNet\Model\Communication;
|
||||
use App\Form\Model\BookingEditDto;
|
||||
|
||||
/**
|
||||
* Processes booking form data and converts it into BusProNet API payload format.
|
||||
*
|
||||
* This processor handles the complex transformation of booking edit form data into the structured
|
||||
* array format required by the BusProNet API. It manages service mappings between participants
|
||||
* and various booking components like additional services, transportation, and accommodations.
|
||||
* The processor ensures data consistency by resetting and rebuilding participant-to-service
|
||||
* mappings based on form selections, while maintaining proper API payload structure.
|
||||
*/
|
||||
class BookingDataProcessor
|
||||
{
|
||||
/**
|
||||
* Creates an update request payload for the BusProNet API from booking form data.
|
||||
*
|
||||
* This method processes booking edit form data and transforms it into the structured array
|
||||
* format expected by the BusProNet XML API. It handles service mappings, participant data
|
||||
* updates, and generates the complete payload structure including booking metadata,
|
||||
* participant information, services, transportation, and accommodation details.
|
||||
*
|
||||
* The process involves:
|
||||
* - Resetting existing service-to-participant mappings
|
||||
* - Rebuilding mappings based on current form selections
|
||||
* - Adding new services from travel data when participants select them
|
||||
* - Removing services with no participant mappings
|
||||
* - Updating participant personal data from form input
|
||||
* - Synchronizing applicant data with first participant details
|
||||
* - Building the final API payload structure
|
||||
*
|
||||
* @param BookingEditDto|null $formData The booking edit form data containing updated participant and service selections
|
||||
*
|
||||
* @return array The structured payload array ready for BusProNet API submission
|
||||
*/
|
||||
public function createUpdateRequestPayload(?BookingEditDto $formData): array
|
||||
{
|
||||
$bookingData = $formData->booking;
|
||||
$travelData = $formData->travel;
|
||||
|
||||
// Reset mappings
|
||||
$this->resetServiceMappings($bookingData);
|
||||
|
||||
foreach ($formData->participants as $participant) {
|
||||
$this->processParticipantServices($participant, $bookingData, $travelData);
|
||||
}
|
||||
|
||||
$this->removeUnusedServices($bookingData);
|
||||
$this->updateParticipantPersonalData($formData->participants, $bookingData);
|
||||
$this->syncApplicantData($bookingData);
|
||||
|
||||
$payload = $this->buildBasePayload($bookingData);
|
||||
$this->addBankAccountToPayload($payload, $bookingData);
|
||||
$this->buildParticipantPayload($payload, $bookingData);
|
||||
$this->buildServicesPayload($payload, $bookingData);
|
||||
$this->buildPickupPayload($payload, $bookingData);
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets all existing participant-to-service mappings to start with a clean slate.
|
||||
*
|
||||
* This ensures that service assignments are rebuilt from scratch based on current form selections.
|
||||
*
|
||||
* @param object $bookingData The booking data object containing services to reset
|
||||
*/
|
||||
private function resetServiceMappings(object $bookingData): void
|
||||
{
|
||||
$servicesToReset = [
|
||||
...$bookingData->additionalServices,
|
||||
...$bookingData->transportationServices,
|
||||
...$bookingData->pickupsTo,
|
||||
...$bookingData->pickupsFro,
|
||||
];
|
||||
|
||||
foreach ($servicesToReset as $service) {
|
||||
$service->mapping = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Update mappings, add services and pickups
|
||||
foreach ($formData->participants as $participant) {
|
||||
// skip canceled participants
|
||||
if (true === $participant->isCanceled()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$servicesToMap = [
|
||||
...$participant->courses,
|
||||
...$participant->additionalServices,
|
||||
...$participant->skiPass,
|
||||
...$participant->board,
|
||||
...$participant->rentals,
|
||||
];
|
||||
foreach ($servicesToMap as $service) {
|
||||
if (false === isset($this->additionalServices[$service->id])) {
|
||||
$serviceToAdd = $travelData->additionalServices[$service->id];
|
||||
if (null !== $serviceToAdd) {
|
||||
$bookingData->additionalServices[$service->id] = $serviceToAdd;
|
||||
$bookingData->additionalServices[$service->id]->individualPrice[$participant->index] = $serviceToAdd->price;
|
||||
}
|
||||
}
|
||||
$bookingData->additionalServices[$service->id]->mapping[] = $participant->index;
|
||||
}
|
||||
foreach ([$participant->transportationServiceTo, $participant->transportationServiceFro] as $service) {
|
||||
if (false === isset($this->transportationServices[$service->id])) {
|
||||
$serviceToAdd = $travelData->transportationServices[$service->id];
|
||||
if (null !== $serviceToAdd) {
|
||||
$bookingData->transportationServices[$service->id] = $serviceToAdd;
|
||||
$bookingData->transportationServices[$service->id]->individualPrice[$participant->index] = $serviceToAdd->price;
|
||||
}
|
||||
}
|
||||
$bookingData->transportationServices[$service->id]->mapping[] = $participant->index;
|
||||
}
|
||||
if ('BUS' === $participant->transportationServiceTo->subType && null !== $selectedPickup = $participant->pickup) {
|
||||
if (false === isset($bookingData->pickupsTo[$selectedPickup->id])) {
|
||||
$bookingData->pickupsTo[$selectedPickup->id] = $selectedPickup;
|
||||
}
|
||||
$bookingData->pickupsTo[$selectedPickup->id]->mapping[] = $participant->index;
|
||||
}
|
||||
/**
|
||||
* Processes all services for a single participant.
|
||||
*
|
||||
* This orchestrator method handles the complete service assignment workflow for one participant,
|
||||
* including additional services, transportation services, and pickup locations.
|
||||
*
|
||||
* @param object $participant The participant data from the form
|
||||
* @param object $bookingData The booking data object to update
|
||||
* @param object $travelData The travel data containing available services
|
||||
*/
|
||||
private function processParticipantServices(object $participant, object $bookingData, object $travelData): void
|
||||
{
|
||||
if (true === $participant->isCanceled()) {
|
||||
return;
|
||||
}
|
||||
// Remove services/pickups with empty mappings
|
||||
|
||||
$this->processAdditionalServices($participant, $bookingData, $travelData);
|
||||
$this->processTransportationServices($participant, $bookingData, $travelData);
|
||||
$this->processPickupLocations($participant, $bookingData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes additional services for a participant.
|
||||
*
|
||||
* Maps additional services (courses, ski passes, board options, rentals) to the participant.
|
||||
* Adds new services to the booking if they don't already exist and sets individual pricing.
|
||||
*
|
||||
* @param object $participant The participant data from the form
|
||||
* @param object $bookingData The booking data object to update
|
||||
* @param object $travelData The travel data containing available services
|
||||
*/
|
||||
private function processAdditionalServices(object $participant, object $bookingData, object $travelData): void
|
||||
{
|
||||
$servicesToMap = [
|
||||
...$participant->courses,
|
||||
...$participant->additionalServices,
|
||||
...$participant->skiPass,
|
||||
...$participant->board,
|
||||
...$participant->rentals,
|
||||
];
|
||||
|
||||
foreach ($servicesToMap as $service) {
|
||||
if (false === isset($bookingData->additionalServices[$service->id])) {
|
||||
$serviceToAdd = $travelData->additionalServices[$service->id] ?? null;
|
||||
if (null !== $serviceToAdd) {
|
||||
$bookingData->additionalServices[$service->id] = $serviceToAdd;
|
||||
$bookingData->additionalServices[$service->id]->individualPrice[$participant->index] = $serviceToAdd->price;
|
||||
}
|
||||
}
|
||||
$bookingData->additionalServices[$service->id]->mapping[] = $participant->index;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes transportation services for a participant.
|
||||
*
|
||||
* Maps transportation services (both directions: to and from destination) to the participant.
|
||||
* Adds new transportation services to the booking if they don't already exist.
|
||||
*
|
||||
* @param object $participant The participant data from the form
|
||||
* @param object $bookingData The booking data object to update
|
||||
* @param object $travelData The travel data containing available services
|
||||
*/
|
||||
private function processTransportationServices(object $participant, object $bookingData, object $travelData): void
|
||||
{
|
||||
foreach ([$participant->transportationServiceTo, $participant->transportationServiceFro] as $service) {
|
||||
if (false === isset($bookingData->transportationServices[$service->id])) {
|
||||
$serviceToAdd = $travelData->transportationServices[$service->id] ?? null;
|
||||
if (null !== $serviceToAdd) {
|
||||
$bookingData->transportationServices[$service->id] = $serviceToAdd;
|
||||
$bookingData->transportationServices[$service->id]->individualPrice[$participant->index] = $serviceToAdd->price;
|
||||
}
|
||||
}
|
||||
$bookingData->transportationServices[$service->id]->mapping[] = $participant->index;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes pickup locations for participants using bus transportation.
|
||||
*
|
||||
* Only processes pickup locations for bus transportation services and maps the participant
|
||||
* to their selected pickup location.
|
||||
*
|
||||
* @param object $participant The participant data from the form
|
||||
* @param object $bookingData The booking data object to update
|
||||
*/
|
||||
private function processPickupLocations(object $participant, object $bookingData): void
|
||||
{
|
||||
if ('BUS' === $participant->transportationServiceTo->subType && null !== $selectedPickup = $participant->pickup) {
|
||||
if (false === isset($bookingData->pickupsTo[$selectedPickup->id])) {
|
||||
$bookingData->pickupsTo[$selectedPickup->id] = $selectedPickup;
|
||||
}
|
||||
$bookingData->pickupsTo[$selectedPickup->id]->mapping[] = $participant->index;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes services and pickups with no participant mappings.
|
||||
*
|
||||
* Cleans up unused services to prevent empty services from being sent to the API.
|
||||
* This includes additional services, transportation services, and pickup locations.
|
||||
*
|
||||
* @param object $bookingData The booking data object to clean up
|
||||
*/
|
||||
private function removeUnusedServices(object $bookingData): void
|
||||
{
|
||||
foreach ($bookingData->additionalServices as $service) {
|
||||
if (0 === count($service->mapping)) {
|
||||
unset($bookingData->additionalServices[$service->id]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($bookingData->transportationServices as $service) {
|
||||
if (0 === count($service->mapping)) {
|
||||
unset($bookingData->transportationServices[$service->id]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($bookingData->pickupsTo as $pickup) {
|
||||
if (0 === count($pickup->mapping)) {
|
||||
unset($bookingData->pickupsTo[$pickup->id]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($bookingData->pickupsFro as $pickup) {
|
||||
if (0 === count($pickup->mapping)) {
|
||||
unset($bookingData->pickupsFro[$pickup->id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update participants' personal data
|
||||
foreach ($formData->participants as $participant) {
|
||||
// skip canceled participants
|
||||
/**
|
||||
* Updates participant personal data from form input.
|
||||
*
|
||||
* Only processes participants with status 'F' (active/confirmed participants).
|
||||
* Updates all personal data fields and communication information.
|
||||
*
|
||||
* @param array $participants The participants array from the form
|
||||
* @param object $bookingData The booking data object to update
|
||||
*/
|
||||
private function updateParticipantPersonalData(array $participants, object $bookingData): void
|
||||
{
|
||||
foreach ($participants as $participant) {
|
||||
if ('F' !== $participant->status) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$bookingData->participants[$participant->index]->firstName = $participant->firstName;
|
||||
$bookingData->participants[$participant->index]->name = $participant->lastName;
|
||||
$bookingData->participants[$participant->index]->dateOfBirth = $participant->dateOfBirth;
|
||||
@@ -109,19 +248,40 @@ class BookingDataProcessor
|
||||
$bookingData->participants[$participant->index]->communication->mobile = $participant->mobile;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update applicant's data with personal data of first participant
|
||||
/**
|
||||
* Synchronizes applicant data with first participant's physical characteristics.
|
||||
*
|
||||
* The applicant (booking holder) inherits physical data from the first participant.
|
||||
*
|
||||
* @param object $bookingData The booking data object to update
|
||||
*/
|
||||
private function syncApplicantData(object $bookingData): void
|
||||
{
|
||||
if (false !== $firstParticipant = reset($bookingData->participants)) {
|
||||
$bookingData->applicant->height = $firstParticipant->height;
|
||||
$bookingData->applicant->weight = $firstParticipant->weight;
|
||||
$bookingData->applicant->shoeSize = $firstParticipant->shoeSize;
|
||||
}
|
||||
}
|
||||
|
||||
$payload = [
|
||||
/**
|
||||
* Builds the base API payload structure with booking information.
|
||||
*
|
||||
* Creates the main structure that will be populated with detailed data sections.
|
||||
*
|
||||
* @param object $bookingData The booking data object
|
||||
*
|
||||
* @return array The base payload structure
|
||||
*/
|
||||
private function buildBasePayload(object $bookingData): array
|
||||
{
|
||||
return [
|
||||
'idbuchung' => $bookingData->id,
|
||||
'status' => $bookingData->status,
|
||||
'idagentur' => $bookingData->agencyId,
|
||||
'idreise' => $bookingData->travelId,
|
||||
'idreise' => $bookingData->dateId,
|
||||
'idpartner' => $bookingData->hotelId,
|
||||
'anmelder' => $bookingData->applicant->toPayload(),
|
||||
'zahlung' => [
|
||||
@@ -142,7 +302,18 @@ class BookingDataProcessor
|
||||
'ferienzielunterbringung' => [],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds bank account information to the payload if present.
|
||||
*
|
||||
* Bank account information is required for direct debit payments.
|
||||
*
|
||||
* @param array $payload The payload array to modify
|
||||
* @param object $bookingData The booking data object
|
||||
*/
|
||||
private function addBankAccountToPayload(array &$payload, object $bookingData): void
|
||||
{
|
||||
if (null !== $bookingData->bankAccount) {
|
||||
$payload['zahlung']['bankverbindung'] = [
|
||||
'@kreditinstitut' => $bookingData->bankAccount->bankName,
|
||||
@@ -151,7 +322,18 @@ class BookingDataProcessor
|
||||
'@kontoinhaber' => $bookingData->bankAccount->holder,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the participant list section of the payload.
|
||||
*
|
||||
* Includes status and personal data for each participant.
|
||||
*
|
||||
* @param array $payload The payload array to modify
|
||||
* @param object $bookingData The booking data object
|
||||
*/
|
||||
private function buildParticipantPayload(array &$payload, object $bookingData): void
|
||||
{
|
||||
foreach ($bookingData->participants as $index => $participant) {
|
||||
$payload['teilnehmerliste']['teilnehmer'][] = [
|
||||
'@id' => $index,
|
||||
@@ -159,7 +341,19 @@ class BookingDataProcessor
|
||||
...$participant->toPayload(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the services sections of the payload.
|
||||
*
|
||||
* Includes additional services, transportation services, and accommodation details
|
||||
* with participant mappings and quantities.
|
||||
*
|
||||
* @param array $payload The payload array to modify
|
||||
* @param object $bookingData The booking data object
|
||||
*/
|
||||
private function buildServicesPayload(array &$payload, object $bookingData): void
|
||||
{
|
||||
foreach ($bookingData->additionalServices as $service) {
|
||||
$payload['zusatzleistungen']['zusatzleistung'][] = [
|
||||
'@idleistung' => $service->id,
|
||||
@@ -187,7 +381,18 @@ class BookingDataProcessor
|
||||
'@zuordnung' => implode(',', $room->mapping),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the pickup locations section of the payload.
|
||||
*
|
||||
* Only included in payload if there are actual pickup assignments for bus transportation.
|
||||
*
|
||||
* @param array $payload The payload array to modify
|
||||
* @param object $bookingData The booking data object
|
||||
*/
|
||||
private function buildPickupPayload(array &$payload, object $bookingData): void
|
||||
{
|
||||
if (0 < count($bookingData->pickupsTo)) {
|
||||
$payload['zustiege']['zustieg'] = [];
|
||||
foreach ($bookingData->pickupsTo as $pickup) {
|
||||
@@ -198,7 +403,5 @@ class BookingDataProcessor
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,18 +16,18 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
class Address
|
||||
{
|
||||
#[Assert\NotBlank(message: 'Bitte angeben', groups: ['personal_data'])]
|
||||
public string $street = '';
|
||||
public ?string $street = null;
|
||||
|
||||
#[Assert\NotBlank(message: 'Bitte angeben', groups: ['personal_data'])]
|
||||
public string $postCode = '';
|
||||
public ?string $postCode = null;
|
||||
|
||||
#[Assert\NotBlank(message: 'Bitte angeben', groups: ['personal_data'])]
|
||||
public string $city = '';
|
||||
public ?string $city = null;
|
||||
|
||||
public string $district = '';
|
||||
public ?string $district = null;
|
||||
|
||||
#[Assert\NotBlank(message: 'Bitte angeben', groups: ['personal_data'])]
|
||||
public string $country = '';
|
||||
public ?string $country = null;
|
||||
|
||||
/**
|
||||
* Converts the address to API payload format.
|
||||
|
||||
@@ -22,7 +22,7 @@ class Booking
|
||||
public ?float $price = null;
|
||||
public ?\DateTimeImmutable $bookingDate = null;
|
||||
public ?string $travelName = null;
|
||||
public ?int $travelId = null;
|
||||
public ?int $dateId = null;
|
||||
public ?string $travelCode = null;
|
||||
public ?\DateTimeImmutable $travelDate = null;
|
||||
public ?Travel $travelData = null;
|
||||
@@ -88,8 +88,8 @@ class Booking
|
||||
* Filters additional services by group and participant index mapping.
|
||||
* Returns services that are assigned to the specified participant.
|
||||
*
|
||||
* @param int $participantIndex The participant index to filter by
|
||||
* @param mixed $group The service group(s) to filter by
|
||||
* @param int $participantIndex The participant index to filter by
|
||||
* @param mixed $group The service group(s) to filter by
|
||||
*
|
||||
* @return array<Service> The filtered services for the participant
|
||||
*/
|
||||
@@ -108,14 +108,14 @@ class Booking
|
||||
* Finds the transportation service that matches the participant index
|
||||
* and travel direction (e.g., 'H' for outbound, 'R' for return).
|
||||
*
|
||||
* @param int $participantIndex The participant index to search for
|
||||
* @param string $direction The travel direction ('H' or 'R')
|
||||
* @param int $participantIndex The participant index to search for
|
||||
* @param string $direction The travel direction ('H' or 'R')
|
||||
*
|
||||
* @return Service|null The matching transportation service or null if not found
|
||||
*/
|
||||
public function getTransportationServiceForParticipantAndDirection(
|
||||
int $participantIndex,
|
||||
string $direction
|
||||
string $direction,
|
||||
): ?Service {
|
||||
foreach ($this->transportationServices as $service) {
|
||||
if ($service->direction !== $direction) {
|
||||
|
||||
@@ -15,14 +15,14 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
*/
|
||||
class Communication
|
||||
{
|
||||
public string $phone = '';
|
||||
public ?string $phone = null;
|
||||
|
||||
#[Assert\NotBlank(message: 'Bitte angeben', groups: ['personal_data'])]
|
||||
public string $mobile = '';
|
||||
public ?string $mobile = null;
|
||||
|
||||
#[Assert\NotBlank(message: 'Bitte angeben', groups: ['personal_data'])]
|
||||
#[Assert\Email(message: 'Bitte eine gültige Adresse angeben', mode: 'strict', groups: ['personal_data'])]
|
||||
public string $email = '';
|
||||
public ?string $email = null;
|
||||
|
||||
public bool $newsletter = false;
|
||||
|
||||
|
||||
@@ -24,20 +24,20 @@ class PersonalData
|
||||
public ?string $status = null;
|
||||
|
||||
#[Assert\NotBlank(message: 'Bitte angeben', groups: ['Default', 'personal_data'])]
|
||||
public string $name = '';
|
||||
public ?string $name = null;
|
||||
|
||||
#[Assert\NotBlank(message: 'Bitte angeben', groups: ['Default', 'personal_data'])]
|
||||
public string $firstName = '';
|
||||
|
||||
public string $salutation = '';
|
||||
public string $title = '';
|
||||
public string $gender = '';
|
||||
public string $nationality = '';
|
||||
public string $height = '';
|
||||
public string $shoeSize = '';
|
||||
public string $weight = '';
|
||||
public ?string $salutation = null;
|
||||
public ?string $title = null;
|
||||
public ?string $gender = null;
|
||||
public ?string $nationality = null;
|
||||
public ?string $height = null;
|
||||
public ?string $shoeSize = null;
|
||||
public ?string $weight = null;
|
||||
public ?\DateTimeImmutable $dateOfBirth = null;
|
||||
public string $remarks = '';
|
||||
public ?string $remarks = null;
|
||||
|
||||
#[Assert\Valid(groups: ['personal_data'])]
|
||||
public Address $address;
|
||||
@@ -63,7 +63,7 @@ class PersonalData
|
||||
{
|
||||
// Ensure date of birth is populated
|
||||
if (null === $dob = $this->dateOfBirth) {
|
||||
$dob = new \DateTimeImmutable(self::DEFAULT_AGE_YEARS . ' years ago');
|
||||
$dob = new \DateTimeImmutable(self::DEFAULT_AGE_YEARS.' years ago');
|
||||
}
|
||||
|
||||
return [
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\Model;
|
||||
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
class Product
|
||||
{
|
||||
#[Groups(['api:list'])]
|
||||
public ?int $id = null;
|
||||
|
||||
#[Groups(['api:list'])]
|
||||
public ?string $code = null;
|
||||
|
||||
#[Groups(['api:list'])]
|
||||
public ?string $name = null;
|
||||
}
|
||||
@@ -92,8 +92,8 @@ class Travel
|
||||
* Filters additional services based on the provided group(s) and optionally
|
||||
* by availability. Services are sorted alphabetically by label.
|
||||
*
|
||||
* @param mixed $group The service group(s) to filter by
|
||||
* @param bool $availableOnly Whether to include only available services
|
||||
* @param mixed $group The service group(s) to filter by
|
||||
* @param bool $availableOnly Whether to include only available services
|
||||
*
|
||||
* @return array<Service> The filtered and sorted services array
|
||||
*/
|
||||
@@ -118,8 +118,8 @@ class Travel
|
||||
* Filters transportation services based on travel direction and optionally
|
||||
* by availability. Services are sorted by subtype.
|
||||
*
|
||||
* @param string $direction The travel direction to filter by
|
||||
* @param bool $availableOnly Whether to include only available services
|
||||
* @param string $direction The travel direction to filter by
|
||||
* @param bool $availableOnly Whether to include only available services
|
||||
*
|
||||
* @return array<Service> The filtered and sorted transportation services
|
||||
*/
|
||||
@@ -193,7 +193,7 @@ class Travel
|
||||
public function getAvailableRooms(): array
|
||||
{
|
||||
return array_filter($this->rooms, function (Room $room) {
|
||||
return $room->available > 0 && $room->status === Constants::STATUS_AVAILABLE;
|
||||
return $room->available > 0 && Constants::STATUS_AVAILABLE === $room->status;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\Utility;
|
||||
|
||||
class DateCodeUtility
|
||||
{
|
||||
/**
|
||||
* Sanitizes the travel code by removing dashes and slashes, and converting to uppercase.
|
||||
*
|
||||
* @param string $travelCode the travel code to sanitize
|
||||
*
|
||||
* @return string the sanitized travel code
|
||||
*/
|
||||
public function sanitize(string $travelCode): string
|
||||
{
|
||||
return str_replace(['-', '/'], '', strtoupper($travelCode));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the base code (without the date part) from the travel code.
|
||||
*
|
||||
* @param string $travelCode the travel code
|
||||
*
|
||||
* @return string the base code in uppercase
|
||||
*/
|
||||
public function getBaseCode(string $travelCode): string
|
||||
{
|
||||
return strtoupper(substr($travelCode, 0, -6));
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the date from the travel code and returns it as a DateTimeImmutable object.
|
||||
*
|
||||
* @param string $travelCode the travel code containing the date
|
||||
*
|
||||
* @return \DateTimeImmutable the extracted date, or null if invalid
|
||||
*/
|
||||
public function getDate(string $travelCode): \DateTimeImmutable
|
||||
{
|
||||
$datePart = substr($travelCode, -6);
|
||||
|
||||
return \DateTimeImmutable::createFromFormat('dmy', $datePart);
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\Utility;
|
||||
|
||||
class TravelCodeUtility
|
||||
{
|
||||
public function sanitize(string $travelCode): string
|
||||
{
|
||||
return str_replace(['-', '/'], '', strtoupper($travelCode));
|
||||
}
|
||||
|
||||
public function getBaseCode(string $travelCode): string
|
||||
{
|
||||
return strtoupper(substr($travelCode, 0, -6));
|
||||
}
|
||||
|
||||
public function getDate(string $travelCode): \DateTimeImmutable
|
||||
{
|
||||
$datePart = substr($travelCode, -6);
|
||||
|
||||
return \DateTimeImmutable::createFromFormat('dmy', $datePart);
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,9 @@ namespace App\BusProNet\XmlLoader;
|
||||
use App\BusProNet\Model\BaseData;
|
||||
use App\BusProNet\Model\MutableData;
|
||||
use App\BusProNet\Model\Travel;
|
||||
use App\BusProNet\Utility\TravelCodeUtility;
|
||||
use App\BusProNet\Utility\DateCodeUtility;
|
||||
use App\BusProNet\XmlParser\TravelParser;
|
||||
use League\Flysystem\FilesystemException;
|
||||
use League\Flysystem\FilesystemOperator;
|
||||
use League\Flysystem\StorageAttributes;
|
||||
use Psr\Cache\InvalidArgumentException;
|
||||
@@ -24,11 +25,11 @@ use Symfony\Contracts\Cache\ItemInterface;
|
||||
class TravelLoader extends AbstractLoader
|
||||
{
|
||||
/**
|
||||
* @param HotelLoader $hotelDataLoader Hotel data loader for hotel information
|
||||
* @param TravelParser $travelParser Parser for XML travel data
|
||||
* @param string $travelInfoBaseUrl Base URL for travel information pages
|
||||
* @param CacheInterface $cache Cache interface for performance optimization
|
||||
* @param FilesystemOperator $xmlExport Filesystem operator for XML file access
|
||||
* @param HotelLoader $hotelDataLoader Hotel data loader for hotel information
|
||||
* @param TravelParser $travelParser Parser for XML travel data
|
||||
* @param string $travelInfoBaseUrl Base URL for travel information pages
|
||||
* @param CacheInterface $cache Cache interface for performance optimization
|
||||
* @param FilesystemOperator $xmlExport Filesystem operator for XML file access
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly HotelLoader $hotelDataLoader,
|
||||
@@ -103,25 +104,55 @@ class TravelLoader extends AbstractLoader
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a travel code to its corresponding travel ID.
|
||||
* Map a date code to its corresponding date ID.
|
||||
*
|
||||
* Uses the cached files mapping to find the travel ID associated with
|
||||
* the given travel code. Returns null if no matching travel is found.
|
||||
* Uses the cached files mapping to find the date ID associated with
|
||||
* the given date code. Returns null if no matching date is found.
|
||||
*
|
||||
* @param string $travelCode The travel code to look up
|
||||
* @return int|null The travel ID or null if not found
|
||||
* @param string $dateCode The date code to look up
|
||||
*
|
||||
* @return int|null The date ID or null if not found
|
||||
*/
|
||||
public function mapCodeToId(string $travelCode): ?int
|
||||
public function mapCodeToId(string $dateCode): ?int
|
||||
{
|
||||
$mapping = $this->generateFilesMap();
|
||||
|
||||
$travelCodes = array_column($mapping, 'code', 'id');
|
||||
$dateCodes = array_column($mapping, 'code', 'id');
|
||||
|
||||
if (false === $travelId = array_search($travelCode, $travelCodes)) {
|
||||
if (false === $dateId = array_search($dateCode, $dateCodes)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $travelId;
|
||||
return $dateId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a date ID to its corresponding product ID.
|
||||
*
|
||||
* Finds the product ID by looking up which XML file contains the given date ID.
|
||||
* Since XML files are named with the product ID pattern (Ziel_x.xml), this method
|
||||
* extracts the product ID from the file path.
|
||||
*
|
||||
* @param int $dateId The date/term ID to look up
|
||||
*
|
||||
* @return int|null The product ID or null if not found
|
||||
*/
|
||||
public function mapDateIdToProductId(int $dateId): ?int
|
||||
{
|
||||
$mapping = $this->generateFilesMap();
|
||||
|
||||
if (!isset($mapping[$dateId])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$filename = $mapping[$dateId]['file'];
|
||||
|
||||
// Extract product ID from filename (e.g., "Ziel_12345.xml" -> 12345)
|
||||
if (preg_match('/Ziel_(\d+)\.xml$/', $filename, $matches)) {
|
||||
return (int) $matches[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -130,26 +161,27 @@ class TravelLoader extends AbstractLoader
|
||||
* Retrieves travel data from XML exports. If filename is provided, loads directly
|
||||
* from that file. Otherwise, uses the cached mapping to find the appropriate file.
|
||||
*
|
||||
* @param int $travelId The travel ID to load
|
||||
* @param int|null $hotelId Optional hotel ID for specific hotel data
|
||||
* @param int $dateId The travel date ID to load
|
||||
* @param int|null $hotelId Optional hotel ID for specific hotel data
|
||||
* @param string|null $filename Optional filename to load from directly
|
||||
*
|
||||
* @return Travel|null The loaded travel object or null if not found
|
||||
*/
|
||||
public function loadById(int $travelId, ?int $hotelId = null, ?string $filename = null): ?Travel
|
||||
public function loadById(int $dateId, ?int $hotelId = null, ?string $filename = null): ?Travel
|
||||
{
|
||||
if (null !== $filename) {
|
||||
return $this->loadXml($travelId, $hotelId, $filename);
|
||||
return $this->loadXml($dateId, $hotelId, $filename);
|
||||
}
|
||||
|
||||
$mapping = $this->generateFilesMap();
|
||||
|
||||
if (false === isset($mapping[$travelId])) {
|
||||
if (false === isset($mapping[$dateId])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$filename = $mapping[$travelId]['file'];
|
||||
$filename = $mapping[$dateId]['file'];
|
||||
|
||||
return $this->loadXml($travelId, $hotelId, $filename);
|
||||
return $this->loadXml($dateId, $hotelId, $filename);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,23 +190,28 @@ class TravelLoader extends AbstractLoader
|
||||
* Reads and parses XML file content to extract travel information for
|
||||
* the specified travel ID and optional hotel ID.
|
||||
*
|
||||
* @param int $travelId The travel ID to load
|
||||
* @param int|null $hotelId Optional hotel ID for specific hotel data
|
||||
* @param string $filename The XML filename to load from
|
||||
* @param int $dateId The travel date ID to load
|
||||
* @param int|null $hotelId Optional hotel ID for specific hotel data
|
||||
* @param string $filename The XML filename to load from
|
||||
*
|
||||
* @return Travel|null The loaded travel object or null if not found
|
||||
*/
|
||||
private function loadXml(int $travelId, ?int $hotelId, string $filename): ?Travel
|
||||
private function loadXml(int $dateId, ?int $hotelId, string $filename): ?Travel
|
||||
{
|
||||
$xml = $this->xmlExport->read($filename);
|
||||
$crawler = new Crawler($xml);
|
||||
try {
|
||||
$xml = $this->xmlExport->read($filename);
|
||||
$crawler = new Crawler($xml);
|
||||
|
||||
$travelNode = $crawler->filterXPath(sprintf('//reise/termin[@idbuspro="%d"]', $travelId));
|
||||
$travelNode = $crawler->filterXPath(sprintf('//reise/termin[@idbuspro="%d"]', $dateId));
|
||||
|
||||
if (0 === $travelNode->count()) {
|
||||
if (0 === $travelNode->count()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->travelParser->parse($travelNode->first(), $hotelId);
|
||||
} catch (FilesystemException $e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->travelParser->parse($travelNode->first(), $hotelId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -184,7 +221,7 @@ class TravelLoader extends AbstractLoader
|
||||
* mutable data configuration. This controls which travel components
|
||||
* can be modified during booking.
|
||||
*
|
||||
* @param Travel $travel The travel object to update
|
||||
* @param Travel $travel The travel object to update
|
||||
* @param BaseData $mutableData The mutability configuration data
|
||||
*/
|
||||
public function patchMutability(Travel $travel, BaseData $mutableData): void
|
||||
@@ -220,7 +257,7 @@ class TravelLoader extends AbstractLoader
|
||||
* Updates the availability status of additional and transportation
|
||||
* services based on the provided availability data.
|
||||
*
|
||||
* @param Travel $travel The travel object to update
|
||||
* @param Travel $travel The travel object to update
|
||||
* @param BaseData $availabilities The availability data for services
|
||||
*/
|
||||
public function patchAvailabilities(Travel $travel, BaseData $availabilities): void
|
||||
@@ -244,10 +281,10 @@ class TravelLoader extends AbstractLoader
|
||||
*/
|
||||
public function patchBookings(BaseData $bookings): void
|
||||
{
|
||||
$travelCodeUtility = new TravelCodeUtility();
|
||||
$travelCodeUtility = new DateCodeUtility();
|
||||
|
||||
foreach ($bookings->getItems() as $booking) {
|
||||
if (null === $travelId = $booking->travelId) {
|
||||
if (null === $travelId = $booking->dateId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -59,8 +59,11 @@ class ApiResponseParser extends AbstractParser
|
||||
return (new AvailabilitiesParser())->parseRooms($resultNode);
|
||||
case ApiClient::TYPE_BOOKING_UPDATE:
|
||||
return (new BookingUpdateParser())->parse($resultNode);
|
||||
case ApiClient::TYPE_PRODUCTS:
|
||||
return (new ProductsParser())->parse($resultNode);
|
||||
case ApiClient::TYPE_PRODUCT_DATA:
|
||||
$travelNode = $crawler->filterXPath('//reise/termin');
|
||||
|
||||
return (new TravelParser())->parse($travelNode->first(), ...$additionalArgs);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ use App\BusProNet\Model\BankAccount;
|
||||
use App\BusProNet\Model\Booking;
|
||||
use App\BusProNet\Model\Communication;
|
||||
use App\BusProNet\Model\PersonalData;
|
||||
use App\BusProNet\Model\Service;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
|
||||
class BookingParser extends AbstractParser
|
||||
@@ -40,7 +39,7 @@ class BookingParser extends AbstractParser
|
||||
$travelData = $node->filterXPath('//reise');
|
||||
|
||||
$booking->travelName = $travelData->attr('bezeichnung');
|
||||
$booking->travelId = $this->getIntOrNullValue($node->filterXPath('//idreise'));
|
||||
$booking->dateId = $this->getIntOrNullValue($node->filterXPath('//idreise'));
|
||||
$booking->travelCode = $travelData->attr('code');
|
||||
$booking->travelDate = $this->stringToDate($travelData->attr('termin'));
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ class BookingsParser extends AbstractParser
|
||||
$booking->price = $this->getFloatOrNullValue($node->filterXPath('//preis'));
|
||||
$booking->bookingDate = $this->getDateTimeOrNullValue($node->filterXPath('//buchungsdatum'));
|
||||
$booking->travelName = $this->getStringOrNullValue($node->filterXPath('//reise'));
|
||||
$booking->travelId = $this->getIntOrNullValue($node->filterXPath('//idreise'));
|
||||
$booking->dateId = $this->getIntOrNullValue($node->filterXPath('//idreise'));
|
||||
$booking->travelDate = $this->getDateOrNullValue($node->filterXPath('//reisedatum'));
|
||||
$booking->hasDocuments = $this->getBoolValue($node->filterXPath('//reisedokument'));
|
||||
$booking->payment = $this->getFloatOrNullValue($node->filterXPath('//zahlung'));
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\XmlParser;
|
||||
|
||||
use App\BusProNet\Model\BaseData;
|
||||
use App\BusProNet\Model\Product;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
|
||||
class ProductsParser extends AbstractParser
|
||||
{
|
||||
public function parse(Crawler $result): BaseData
|
||||
{
|
||||
$products = [];
|
||||
|
||||
$result
|
||||
->filterXPath('//produkte/produkt')
|
||||
->each(function (Crawler $node) use (&$products) {
|
||||
if (false === empty($node->attr('code'))) {
|
||||
$product = new Product();
|
||||
$product->id = (int) $node->attr('id');
|
||||
$product->code = (string) $node->attr('code');
|
||||
$product->name = (string) $node->attr('bezeichnung');
|
||||
|
||||
$products[$product->id] = $product;
|
||||
}
|
||||
})
|
||||
;
|
||||
|
||||
return new BaseData($products);
|
||||
}
|
||||
}
|
||||
@@ -28,8 +28,9 @@ class TravelParser extends AbstractParser
|
||||
* Extracts all travel-related data from the XML node including dates,
|
||||
* pricing, services, rooms, pickups, and guide information.
|
||||
*
|
||||
* @param Crawler $node The XML node containing travel data
|
||||
* @param Crawler $node The XML node containing travel data
|
||||
* @param int|null $hotelId Optional hotel ID for specific hotel data
|
||||
*
|
||||
* @return Travel The parsed travel object
|
||||
*/
|
||||
public function parse(Crawler $node, ?int $hotelId = null): Travel
|
||||
@@ -67,6 +68,7 @@ class TravelParser extends AbstractParser
|
||||
* the XML structure. Each group contains multiple selection options.
|
||||
*
|
||||
* @param Crawler $node The XML node containing selection group data
|
||||
*
|
||||
* @return array<int, CrmSelectionGroup> Array of selection groups indexed by ID
|
||||
*/
|
||||
public function getSelectionGroups(Crawler $node): array
|
||||
@@ -104,6 +106,7 @@ class TravelParser extends AbstractParser
|
||||
* from the XML structure with pricing and availability information.
|
||||
*
|
||||
* @param Crawler $node The XML node containing additional service data
|
||||
*
|
||||
* @return array<int, Service> Array of additional services indexed by ID
|
||||
*/
|
||||
public function getAdditionalServices(Crawler $node): array
|
||||
@@ -139,6 +142,7 @@ class TravelParser extends AbstractParser
|
||||
* with scheduling, pricing, and direction information.
|
||||
*
|
||||
* @param Crawler $node The XML node containing transportation service data
|
||||
*
|
||||
* @return array<int, Service> Array of transportation services indexed by ID
|
||||
*/
|
||||
public function getTransportationServices(Crawler $node): array
|
||||
@@ -177,6 +181,7 @@ class TravelParser extends AbstractParser
|
||||
* and extracts name and phone contact details.
|
||||
*
|
||||
* @param Crawler $travelNode The XML node containing travel data
|
||||
*
|
||||
* @return Guide|null The guide object or null if no guide found
|
||||
*/
|
||||
public function getGuide(Crawler $travelNode): ?Guide
|
||||
@@ -204,8 +209,9 @@ class TravelParser extends AbstractParser
|
||||
* Date and time parsing is only performed for outbound journeys when
|
||||
* a default date is provided.
|
||||
*
|
||||
* @param Crawler $node The XML node containing pickup data
|
||||
* @param Crawler $node The XML node containing pickup data
|
||||
* @param \DateTimeImmutable|null $defaultDate Default date for time parsing (outbound only)
|
||||
*
|
||||
* @return array<int, Pickup> Array of pickup locations indexed by ID
|
||||
*/
|
||||
public function getPickups(Crawler $node, ?\DateTimeImmutable $defaultDate = null): array
|
||||
@@ -237,8 +243,9 @@ class TravelParser extends AbstractParser
|
||||
* Retrieves the hotel node either by specific hotel ID or returns
|
||||
* the first hotel node if no ID is specified.
|
||||
*
|
||||
* @param Crawler $node The XML node containing hotel data
|
||||
* @param Crawler $node The XML node containing hotel data
|
||||
* @param int|null $hotelId Optional hotel ID to filter by
|
||||
*
|
||||
* @return Crawler The hotel XML node
|
||||
*/
|
||||
public function getHotelNode(Crawler $node, ?int $hotelId): Crawler
|
||||
@@ -260,6 +267,7 @@ class TravelParser extends AbstractParser
|
||||
* and board options from the hotel XML structure.
|
||||
*
|
||||
* @param Crawler $node The XML node containing room data
|
||||
*
|
||||
* @return array<int, Room> Array of rooms indexed by room ID
|
||||
*/
|
||||
public function getRooms(Crawler $node): array
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Api;
|
||||
|
||||
use App\BusProNet\ApiClient;
|
||||
use App\BusProNet\Exception\ApiClientException;
|
||||
use Psr\Cache\InvalidArgumentException;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
|
||||
#[Route('/api')]
|
||||
#[IsGranted('ROLE_OAUTH2_API')]
|
||||
class ProductController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ApiClient $apiClient,
|
||||
private readonly CacheInterface $cache,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route(path: '/products', name: 'api_products')]
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
try {
|
||||
$products = $this->cache->get('api_products', function (ItemInterface $item) {
|
||||
$item->expiresAfter(3600); // 1 hour cache
|
||||
|
||||
return $this->apiClient->getProducts();
|
||||
});
|
||||
|
||||
return $this->json($products);
|
||||
} catch (InvalidArgumentException|ApiClientException $e) {
|
||||
return $this->json(['error' => $e->getMessage(), 'type' => 'api_error']);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,116 +5,91 @@ namespace App\Controller\Api;
|
||||
use App\BusProNet\ApiClient;
|
||||
use App\BusProNet\Exception\ApiClientException;
|
||||
use App\BusProNet\Model\Travel;
|
||||
use App\BusProNet\Utility\TravelCodeUtility;
|
||||
use App\BusProNet\XmlLoader\HotelLoader;
|
||||
use App\BusProNet\XmlLoader\PickupLoader;
|
||||
use App\BusProNet\XmlLoader\TravelLoader;
|
||||
use Psr\Cache\InvalidArgumentException;
|
||||
use App\BusProNet\Utility\DateCodeUtility;
|
||||
use App\Service\TravelDataService;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Attribute\MapDateTime;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
|
||||
#[Route('/api')]
|
||||
#[IsGranted('ROLE_OAUTH2_API')]
|
||||
class TravelController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TravelLoader $travelXmlLoader,
|
||||
private readonly HotelLoader $hotelXmlLoader,
|
||||
private readonly PickupLoader $pickupXmlLoader,
|
||||
private readonly ApiClient $apiClient,
|
||||
private readonly CacheInterface $cache,
|
||||
private readonly TravelDataService $travelDataService,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route(path: '/travels', name: 'api_travel_mapping')]
|
||||
public function mapping(): JsonResponse
|
||||
{
|
||||
$mapping = $this->travelXmlLoader->generateFilesMap();
|
||||
$mapping = $this->travelDataService->generateFilesMap();
|
||||
|
||||
return $this->json($mapping);
|
||||
}
|
||||
|
||||
#[Route(
|
||||
path: '/travels/{travelId}/{hotelId}',
|
||||
path: '/travels/{dateId}/{hotelId}',
|
||||
name: 'api_travel_single_id',
|
||||
requirements: ['travelId' => '\d+', 'hotelId' => '\d+'],
|
||||
requirements: ['dateId' => '\d+', 'hotelId' => '\d+'],
|
||||
defaults: ['hotelId' => null],
|
||||
)]
|
||||
public function singleById(int $travelId, ?int $hotelId = null): JsonResponse
|
||||
public function byId(Request $request, int $dateId, ?int $hotelId = null): JsonResponse
|
||||
{
|
||||
$travel = $this->loadCached($travelId, $hotelId);
|
||||
$source = $request->query->get('source');
|
||||
$preferRemote = $request->query->getBoolean('prefer_remote');
|
||||
|
||||
$travel = $this->loadTravelData($dateId, $hotelId, $source, $preferRemote);
|
||||
|
||||
if (null === $travel) {
|
||||
return $this->json(['message' => 'Travel not found'], Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
return $this->json($travel, Response::HTTP_OK, [], ['groups' => ['api:list', 'api:single']]);
|
||||
}
|
||||
|
||||
#[Route(
|
||||
path: '/travels/{travelId}/remote',
|
||||
name: 'api_travel_single_id_remote',
|
||||
requirements: ['travelId' => '\d+'],
|
||||
)]
|
||||
public function singleByIdRemote(int $travelId): JsonResponse
|
||||
{
|
||||
$cacheKey = sprintf('bpn_travel_remote_%d', $travelId);
|
||||
|
||||
try {
|
||||
$result = $this->cache->get($cacheKey, function (ItemInterface $item) use ($travelId) {
|
||||
$item->expiresAfter(300); // 5 minutes cache for remote API calls
|
||||
|
||||
try {
|
||||
return $this->apiClient->getTravelData($travelId);
|
||||
} catch (ApiClientException $e) {
|
||||
// Return error info instead of throwing to avoid cache wrapping issues
|
||||
return ['error' => $e->getMessage(), 'type' => 'api_error'];
|
||||
}
|
||||
});
|
||||
|
||||
// Check if result is an error
|
||||
if (is_array($result) && isset($result['error'], $result['type'])) {
|
||||
return $this->json($result['error'], Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
return $this->json($result, Response::HTTP_OK, [], ['groups' => ['api:list', 'api:single']]);
|
||||
} catch (InvalidArgumentException) {
|
||||
return $this->json('Cache error occurred', Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
#[Route(
|
||||
path: '/travels/{travelCode}/{hotelCode}',
|
||||
path: '/travels/{dateCode}/{hotelCode}',
|
||||
name: 'api_travel_single_code',
|
||||
defaults: ['hotelCode' => null],
|
||||
)]
|
||||
public function singleByCode(string $travelCode, ?string $hotelCode = null): JsonResponse
|
||||
public function byCode(Request $request, $dateCode, ?string $hotelCode = null): JsonResponse
|
||||
{
|
||||
// sanitize travel code by removing potential dividers
|
||||
$travelCode = (new TravelCodeUtility())->sanitize($travelCode);
|
||||
// sanitize date code by removing potential dividers
|
||||
$dateCode = (new DateCodeUtility())->sanitize($dateCode);
|
||||
|
||||
$travelId = $this->travelXmlLoader->mapCodeToId($travelCode);
|
||||
$hotelId = $hotelCode ? $this->hotelXmlLoader->mapCodeToId($travelCode) : null;
|
||||
$dateId = $this->travelDataService->mapDateCodeToId($dateCode);
|
||||
$hotelId = $hotelCode ? $this->travelDataService->mapHotelCodeToId($hotelCode) : null;
|
||||
|
||||
if (null === $travelId) {
|
||||
if (null === $dateId) {
|
||||
return $this->json(['message' => 'Not found'], Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$travel = $this->loadCached($travelId, $hotelId);
|
||||
$source = $request->query->get('source');
|
||||
$preferRemote = $request->query->getBoolean('prefer_remote');
|
||||
|
||||
$travel = $this->loadTravelData($dateId, $hotelId, $source, $preferRemote);
|
||||
|
||||
if (null === $travel) {
|
||||
return $this->json(['message' => 'Travel not found'], Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
return $this->json($travel, Response::HTTP_OK, [], ['groups' => ['api:list', 'api:single']]);
|
||||
}
|
||||
|
||||
#[Route('/travels/{travelId}/{hotelId}/{dateTo}/availability', name: 'api_travel_hotel_availability')]
|
||||
#[Route('/travels/{dateId}/{hotelId}/{dateTo}/availability', name: 'api_travel_hotel_availability')]
|
||||
public function hotelAvailability(
|
||||
int $travelId,
|
||||
int $dateId,
|
||||
int $hotelId,
|
||||
#[MapDateTime(format: 'Y-m-d')] \DateTimeImmutable $dateTo
|
||||
#[MapDateTime(format: 'Y-m-d')] \DateTimeImmutable $dateTo,
|
||||
): JsonResponse {
|
||||
try {
|
||||
$result = $this->apiClient->getHotelAvailability($travelId, $hotelId, $dateTo);
|
||||
$result = $this->apiClient->getHotelAvailability($dateId, $hotelId, $dateTo);
|
||||
|
||||
return $this->json($result);
|
||||
} catch (ApiClientException $e) {
|
||||
@@ -122,32 +97,26 @@ class TravelController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
private function loadCached(int $travelId, ?int $hotelId = null): ?Travel
|
||||
{
|
||||
$cacheKey = sprintf('bpn_travel_%d_%d', $travelId, $hotelId ?? 0);
|
||||
|
||||
try {
|
||||
return $this->cache->get($cacheKey, function (ItemInterface $item) use ($travelId, $hotelId) {
|
||||
$item->expiresAfter(60);
|
||||
|
||||
try {
|
||||
$travel = $this->travelXmlLoader->loadById($travelId, $hotelId);
|
||||
|
||||
if (null === $travel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->pickupXmlLoader->patchPickupsDetails($travel);
|
||||
$this->hotelXmlLoader->patchHotelDetails($travel);
|
||||
|
||||
return $travel;
|
||||
} catch (\Exception) {
|
||||
// Return null for any loader exceptions to avoid cache wrapping issues
|
||||
return null;
|
||||
}
|
||||
});
|
||||
} catch (InvalidArgumentException) {
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Loads travel data based on the provided parameters.
|
||||
*
|
||||
* @param int $dateId the ID of the date for which travel data is requested
|
||||
* @param int|null $hotelId the ID of the hotel for which travel data is requested (optional)
|
||||
* @param string|null $source the source of the travel data ('local', 'remote', or null for default behavior)
|
||||
* @param bool $preferRemote whether to prefer remote data when the source is not explicitly specified
|
||||
*
|
||||
* @return Travel|null returns a Travel object if data is found, or null if no data is available
|
||||
*/
|
||||
private function loadTravelData(
|
||||
int $dateId,
|
||||
?int $hotelId = null,
|
||||
?string $source = null,
|
||||
bool $preferRemote = false,
|
||||
): ?Travel {
|
||||
return match ($source) {
|
||||
'local' => $this->travelDataService->getTravelDataFromXml($dateId, $hotelId),
|
||||
'remote' => $this->travelDataService->getTravelDataFromApi($dateId, $hotelId),
|
||||
default => $this->travelDataService->getTravelData($dateId, $hotelId, $preferRemote),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,19 +2,20 @@
|
||||
|
||||
namespace App\Controller\Booking;
|
||||
|
||||
use App\Service\BookingService;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use App\Form\BookingCreateStep1Type;
|
||||
use App\Form\BookingCreateStep2Type;
|
||||
use App\Form\Model\ParticipantDto;
|
||||
use App\Service\BookingService;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
class CreateController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly BookingService $bookingCreateService,
|
||||
) {
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route('/bookings/create', name: 'app_booking_create_step_1')]
|
||||
@@ -25,14 +26,16 @@ class CreateController extends AbstractController
|
||||
// Validate step access - allow step 1 or redirect to current step
|
||||
$this->validateStepAccess($bookingCreateDto, 1);
|
||||
|
||||
$form = $this->createForm(BookingCreateStep1Type::class, $bookingCreateDto);
|
||||
$form = $this->createForm(BookingCreateStep1Type::class, $bookingCreateDto, [
|
||||
'validation_groups' => ['booking_create_step_1'],
|
||||
]);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$bookingCreateDto->currentStep = 2;
|
||||
$this->bookingCreateService->saveBookingCreateDto($request, $bookingCreateDto);
|
||||
|
||||
|
||||
return $this->redirectToRoute('app_booking_create_step_2');
|
||||
}
|
||||
|
||||
@@ -46,18 +49,30 @@ class CreateController extends AbstractController
|
||||
public function participants(Request $request): Response
|
||||
{
|
||||
$bookingCreateDto = $this->bookingCreateService->getOrCreateBookingCreateDto($request);
|
||||
|
||||
|
||||
// Validate step access
|
||||
$this->validateStepAccess($bookingCreateDto, 2);
|
||||
|
||||
$form = $this->createForm(BookingCreateStep2Type::class, $bookingCreateDto);
|
||||
// Ensure correct number of participants
|
||||
if ($bookingCreateDto->getParticipantsCount() !== count($bookingCreateDto->participants)) {
|
||||
$participants = $bookingCreateDto->participants;
|
||||
$bookingCreateDto->participants = [];
|
||||
for ($i = 0; $i < $bookingCreateDto->getParticipantsCount(); ++$i) {
|
||||
$bookingCreateDto->participants[] = $participants[$i] ?? new ParticipantDto();
|
||||
}
|
||||
}
|
||||
|
||||
$form = $this->createForm(BookingCreateStep2Type::class, $bookingCreateDto, [
|
||||
'attr' => ['novalidate' => 'novalidate'],
|
||||
'validation_groups' => ['booking_create_step_2'],
|
||||
]);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$bookingCreateDto->currentStep = 3;
|
||||
$this->bookingCreateService->saveBookingCreateDto($request, $bookingCreateDto);
|
||||
|
||||
|
||||
return $this->redirectToRoute('app_booking_create_step_3');
|
||||
}
|
||||
|
||||
@@ -74,7 +89,7 @@ class CreateController extends AbstractController
|
||||
|
||||
// Validate step access
|
||||
$this->validateStepAccess($bookingCreateDto, 3);
|
||||
|
||||
|
||||
return $this->render('booking/confirm.html.twig', [
|
||||
'bookingCreateDto' => $bookingCreateDto,
|
||||
]);
|
||||
@@ -84,14 +99,13 @@ class CreateController extends AbstractController
|
||||
* Validates step access and redirects if necessary.
|
||||
*
|
||||
* @param \App\Form\Model\BookingCreateDto $bookingCreateDto
|
||||
* @param int $expectedStep
|
||||
*/
|
||||
private function validateStepAccess($bookingCreateDto, int $expectedStep): void
|
||||
{
|
||||
// Allow access to current step or any previous step
|
||||
if ($expectedStep > $bookingCreateDto->currentStep) {
|
||||
$this->addFlash('error', 'Bitte erst die vorherigen Schritte abschließen.');
|
||||
|
||||
|
||||
$this->redirectToCurrentStep($bookingCreateDto);
|
||||
}
|
||||
}
|
||||
@@ -102,17 +116,17 @@ class CreateController extends AbstractController
|
||||
private function redirectToCurrentStep($bookingCreateDto): void
|
||||
{
|
||||
$routeParams = [
|
||||
'travel_id' => $bookingCreateDto->travelData->id,
|
||||
'date_id' => $bookingCreateDto->travelData->id,
|
||||
'hotel_id' => $bookingCreateDto->travelData->hotelId,
|
||||
];
|
||||
|
||||
$route = match ($bookingCreateDto->currentStep) {
|
||||
1 => 'app_booking_create_step_1',
|
||||
2 => 'app_booking_create_step_2',
|
||||
2 => 'app_booking_create_step_2',
|
||||
3 => 'app_booking_create_step_3',
|
||||
default => 'app_booking_create_step_1',
|
||||
};
|
||||
|
||||
$this->redirectToRoute($route, $routeParams);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
|
||||
use function Symfony\Component\String\u;
|
||||
|
||||
class DownloadController extends AbstractController
|
||||
|
||||
@@ -6,12 +6,12 @@ use App\BusProNet\ApiClient;
|
||||
use App\BusProNet\Exception\ApiClientException;
|
||||
use App\BusProNet\Model\Notification;
|
||||
use App\BusProNet\XmlLoader\PickupLoader;
|
||||
use App\BusProNet\XmlLoader\TravelLoader;
|
||||
use App\Controller\Traits\BookingDataTrait;
|
||||
use App\Entity\User;
|
||||
use App\Form\BookingEditType;
|
||||
use App\Form\Model\BookingEditDto;
|
||||
use App\Security\Crypt;
|
||||
use App\Service\TravelDataService;
|
||||
use Psr\Cache\InvalidArgumentException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
@@ -28,7 +28,7 @@ class EditController extends AbstractController
|
||||
|
||||
public function __construct(
|
||||
private readonly ApiClient $apiClient,
|
||||
private readonly TravelLoader $travelDataLoader,
|
||||
private readonly TravelDataService $travelDataService,
|
||||
private readonly PickupLoader $pickupDataLoader,
|
||||
private readonly CacheInterface $cache,
|
||||
private readonly Security $security,
|
||||
@@ -58,7 +58,7 @@ class EditController extends AbstractController
|
||||
$this->denyAccessUnlessGranted('EDIT', $bookingData);
|
||||
|
||||
// Load according travel data
|
||||
$travelData = $this->travelDataLoader->loadById($bookingData->travelId);
|
||||
$travelData = $this->travelDataService->getTravelData($bookingData->dateId);
|
||||
|
||||
if (null === $travelData) {
|
||||
$this->addFlash('error', 'Reisedaten nicht (mehr) verfügbar');
|
||||
@@ -66,34 +66,26 @@ class EditController extends AbstractController
|
||||
return $this->redirectToRoute('app_bookings');
|
||||
}
|
||||
|
||||
// Fetch mutability information via API
|
||||
try {
|
||||
$mutableData = $this->apiClient->getMutableData($bookingData->travelId);
|
||||
} catch (ApiClientException $e) {
|
||||
$this->addFlash('error', 'Reisedaten nicht (mehr) verfügbar');
|
||||
// Fetch mutability and availability information via service
|
||||
$mutableData = $this->travelDataService->getMutabilityData($bookingData->dateId);
|
||||
$availabilities = $this->travelDataService->getAvailabilityData($bookingData->dateId);
|
||||
|
||||
return $this->redirectToRoute('app_bookings');
|
||||
}
|
||||
|
||||
// Fetch availability information via API
|
||||
try {
|
||||
$availabilities = $this->apiClient->getAvailabilities($bookingData->travelId);
|
||||
} catch (ApiClientException $e) {
|
||||
if (null === $mutableData || null === $availabilities) {
|
||||
$this->addFlash('error', 'Reisedaten nicht (mehr) verfügbar');
|
||||
|
||||
return $this->redirectToRoute('app_bookings');
|
||||
}
|
||||
|
||||
// Patch travel data with additional information from above
|
||||
$this->travelDataLoader->patchAvailabilities($travelData, $availabilities);
|
||||
$this->travelDataLoader->patchMutability($travelData, $mutableData);
|
||||
$this->pickupDataLoader->patchPickupsDetails($travelData);
|
||||
$this->travelDataService->patchAvailabilities($travelData, $availabilities);
|
||||
$this->travelDataService->patchMutability($travelData, $mutableData);
|
||||
|
||||
// Create DTO for form
|
||||
$formData = BookingEditDto::fromBooking($bookingData, $travelData);
|
||||
|
||||
$form = $this->createForm(BookingEditType::class, $formData, [
|
||||
'attr' => ['novalidate' => 'novalidate'],
|
||||
'validation_groups' => ['booking_edit'],
|
||||
]);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
@@ -26,9 +26,9 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
class PersonalDataController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @param ApiClient $apiClient BusProNet API client for data operations
|
||||
* @param Crypt $crypt Encryption service for password handling
|
||||
* @param LoggerInterface $logger Logger for audit trails and debugging
|
||||
* @param ApiClient $apiClient BusProNet API client for data operations
|
||||
* @param Crypt $crypt Encryption service for password handling
|
||||
* @param LoggerInterface $logger Logger for audit trails and debugging
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly ApiClient $apiClient,
|
||||
@@ -45,6 +45,7 @@ class PersonalDataController extends AbstractController
|
||||
* communication details. Uses the Post-Redirect-Get pattern for form processing.
|
||||
*
|
||||
* @param Request $request The HTTP request containing form data
|
||||
*
|
||||
* @return Response The rendered personal data page or redirect response
|
||||
*
|
||||
* @throws ApiClientException When BusProNet API communication fails
|
||||
|
||||
@@ -2,15 +2,57 @@
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\BusProNet\Form\CountryType;
|
||||
use App\Form\Model\ParticipantDto;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\BirthdayType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use App\Form\Model\ParticipantDto;
|
||||
|
||||
class BookingCreateParticipantType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('firstName', TextType::class, [
|
||||
'label' => 'Vorname',
|
||||
])
|
||||
->add('lastName', TextType::class, [
|
||||
'label' => 'Nachname',
|
||||
])
|
||||
->add('dateOfBirth', BirthdayType::class, [
|
||||
'label' => 'Geburtsdatum',
|
||||
'html5' => true,
|
||||
'widget' => 'single_text',
|
||||
'input' => 'datetime_immutable',
|
||||
])
|
||||
->add('gender', ChoiceType::class, [
|
||||
'label' => 'Geschlecht',
|
||||
'required' => false,
|
||||
'placeholder' => 'keine Angabe',
|
||||
'choices' => [
|
||||
'männlich' => 'M',
|
||||
'weiblich' => 'W',
|
||||
'divers' => 'D',
|
||||
],
|
||||
])
|
||||
->add('nationality', CountryType::class, [
|
||||
'label' => 'Nationalität',
|
||||
'property' => 'nationality',
|
||||
'preferred_choices' => ['D', 'A', 'CH'],
|
||||
])
|
||||
->add('email', EmailType::class, [
|
||||
'label' => 'E-Mail',
|
||||
'required' => false,
|
||||
])
|
||||
->add('mobile', TextType::class, [
|
||||
'label' => 'Telefon (mobil)',
|
||||
'required' => false,
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
@@ -19,4 +61,4 @@ class BookingCreateParticipantType extends AbstractType
|
||||
'data_class' => ParticipantDto::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use App\Form\Model\BookingCreateDto;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
|
||||
use App\Form\RoomSelectType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class BookingCreateStep1Type extends AbstractType
|
||||
{
|
||||
@@ -34,4 +33,4 @@ class BookingCreateStep1Type extends AbstractType
|
||||
'data_class' => BookingCreateDto::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Form\Model\BookingCreateDto;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
|
||||
use App\Form\Model\BookingCreateDto;
|
||||
|
||||
class BookingCreateStep2Type extends AbstractType
|
||||
{
|
||||
@@ -15,6 +15,9 @@ class BookingCreateStep2Type extends AbstractType
|
||||
$builder
|
||||
->add('participants', CollectionType::class, [
|
||||
'entry_type' => BookingCreateParticipantType::class,
|
||||
'allow_add' => false,
|
||||
'allow_delete' => false,
|
||||
'by_reference' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -24,4 +27,4 @@ class BookingCreateStep2Type extends AbstractType
|
||||
'data_class' => BookingCreateDto::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
class BookingCreateDto
|
||||
{
|
||||
public int $currentStep = 1;
|
||||
|
||||
|
||||
#[Assert\Valid]
|
||||
/**
|
||||
* @var array<int, RoomSelectionDto>
|
||||
@@ -26,7 +26,7 @@ class BookingCreateDto
|
||||
{
|
||||
}
|
||||
|
||||
#[Assert\Callback]
|
||||
#[Assert\Callback(groups: ['booking_create_step_1'])]
|
||||
public function assertValidRoomSelections(ExecutionContextInterface $context): void
|
||||
{
|
||||
$participantsCount = $this->getParticipantsCount();
|
||||
@@ -57,8 +57,15 @@ class BookingCreateDto
|
||||
foreach ($this->roomSelections as $roomSelection) {
|
||||
$room = $rooms[$roomSelection->roomId];
|
||||
$participantsCount += $room->minPax * $roomSelection->quantity;
|
||||
}
|
||||
}
|
||||
|
||||
return $participantsCount;
|
||||
}
|
||||
}
|
||||
|
||||
public function getSelectedRooms(): array
|
||||
{
|
||||
return array_filter($this->roomSelections, function (RoomSelectionDto $roomSelection) {
|
||||
return 0 < $roomSelection->quantity;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use App\BusProNet\Model\Service;
|
||||
use App\Validator\Constraints as AppAssert;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[AppAssert\Participant]
|
||||
#[AppAssert\Participant(groups: ['booking_edit'])]
|
||||
class ParticipantDto
|
||||
{
|
||||
public ?int $index = null;
|
||||
@@ -17,10 +17,10 @@ class ParticipantDto
|
||||
public ?string $status = null;
|
||||
public bool $mutable = false;
|
||||
|
||||
#[Assert\NotBlank(message: 'Bitte angeben')]
|
||||
#[Assert\NotBlank(message: 'Bitte angeben', groups: ['booking_edit', 'booking_create_step_2'])]
|
||||
public ?string $firstName = null;
|
||||
|
||||
#[Assert\NotBlank(message: 'Bitte angeben')]
|
||||
#[Assert\NotBlank(message: 'Bitte angeben', groups: ['booking_edit', 'booking_create_step_2'])]
|
||||
public ?string $lastName = null;
|
||||
public ?string $title = null;
|
||||
public ?string $gender = null;
|
||||
@@ -29,11 +29,11 @@ class ParticipantDto
|
||||
public ?string $shoeSize = null;
|
||||
public ?string $weight = null;
|
||||
|
||||
#[Assert\NotNull(message: 'Bitte angeben')]
|
||||
#[Assert\NotNull(message: 'Bitte angeben', groups: ['booking_edit', 'booking_create_step_2'])]
|
||||
public ?\DateTimeImmutable $dateOfBirth = null;
|
||||
|
||||
#[Assert\NotBlank(message: 'Bitte angeben')]
|
||||
#[Assert\Email(message: 'Bitte eine gültige E-Mail Adresse angeben', mode: 'strict')]
|
||||
#[Assert\NotBlank(message: 'Bitte angeben', groups: ['booking_edit', 'booking_create_step_2'])]
|
||||
#[Assert\Email(message: 'Bitte eine gültige E-Mail Adresse angeben', mode: 'strict', groups: ['booking_edit', 'booking_create_step_2'])]
|
||||
public ?string $email = null;
|
||||
|
||||
public ?string $mobile = null;
|
||||
|
||||
@@ -6,7 +6,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
class RoomSelectionDto
|
||||
{
|
||||
#[Assert\NotNull(message: 'Bitte eine Zimmerkategorie auswählen')]
|
||||
#[Assert\NotNull(message: 'Bitte eine Zimmerkategorie auswählen', groups: ['booking_create_step_1'])]
|
||||
public ?int $roomId = null;
|
||||
|
||||
public ?string $roomLabel = null;
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use App\Form\Model\RoomSelectionDto;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class RoomSelectType extends AbstractType
|
||||
{
|
||||
@@ -37,4 +37,4 @@ class RoomSelectType extends AbstractType
|
||||
'data_class' => RoomSelectionDto::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,18 +3,16 @@
|
||||
namespace App\Service;
|
||||
|
||||
use App\BusProNet\Model\Room;
|
||||
use App\BusProNet\XmlLoader\HotelLoader;
|
||||
use App\BusProNet\XmlLoader\TravelLoader;
|
||||
use App\Form\Model\BookingCreateDto;
|
||||
use App\Form\Model\RoomSelectionDto;
|
||||
use App\Service\TravelDataService;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class BookingService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TravelLoader $travelDataLoader,
|
||||
private readonly HotelLoader $hotelDataLoader,
|
||||
private readonly TravelDataService $travelDataService,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -23,30 +21,23 @@ class BookingService
|
||||
$bookingUuid = $request->query->get('uid');
|
||||
$bookingCreateDto = $request->getSession()->get('booking_create');
|
||||
|
||||
// No UID parameter - try to get existing DTO from session
|
||||
if (null === $bookingUuid) {
|
||||
return $bookingCreateDto ?? throw new NotFoundHttpException('No booking data found. Please start from the beginning.');
|
||||
// No UID parameter - return existing DTO from session if available
|
||||
if (null === $bookingUuid && null !== $bookingCreateDto) {
|
||||
return $bookingCreateDto;
|
||||
}
|
||||
|
||||
// Create a new DTO - we need travel_id and hotel_id for this
|
||||
$travelId = $request->query->getInt('travel_id');
|
||||
// Create a new DTO - we need date_id and hotel_id for this
|
||||
$dateId = $request->query->getInt('date_id');
|
||||
$hotelId = $request->query->getInt('hotel_id');
|
||||
|
||||
if (0 === $travelId || 0 === $hotelId) {
|
||||
throw new NotFoundHttpException('Missing travel_id or hotel_id parameters');
|
||||
if (0 === $dateId || 0 === $hotelId) {
|
||||
throw new NotFoundHttpException('Missing date_id or hotel_id parameters');
|
||||
}
|
||||
|
||||
$travelData = $this->travelDataLoader->loadById($travelId, $hotelId);
|
||||
$travelData = $this->travelDataService->getTravelData($dateId, $hotelId);
|
||||
if (null === $travelData) {
|
||||
throw new NotFoundHttpException('Travel data not found');
|
||||
throw new NotFoundHttpException(sprintf('Travel data not found for date ID %d and hotel ID %d', $dateId, $hotelId));
|
||||
}
|
||||
|
||||
$hotelData = $this->hotelDataLoader->loadById($hotelId);
|
||||
if (null === $hotelData) {
|
||||
throw new NotFoundHttpException('Hotel data not found');
|
||||
}
|
||||
|
||||
$travelData->hotel = $hotelData;
|
||||
$roomsIdsAndQuantities = $this->processRoomQuantities($request);
|
||||
$availableRooms = $travelData->getAvailableRooms();
|
||||
|
||||
|
||||
@@ -0,0 +1,552 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\BusProNet\ApiClient;
|
||||
use App\BusProNet\Exception\ApiClientException;
|
||||
use App\BusProNet\Model\BaseData;
|
||||
use App\BusProNet\Model\Notification;
|
||||
use App\BusProNet\Model\Travel;
|
||||
use App\BusProNet\XmlLoader\HotelLoader;
|
||||
use App\BusProNet\XmlLoader\PickupLoader;
|
||||
use App\BusProNet\XmlLoader\TravelLoader;
|
||||
use Psr\Cache\InvalidArgumentException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
|
||||
/**
|
||||
* Unified service for retrieving travel data from both local XML files and remote API.
|
||||
*
|
||||
* This service provides a unified interface for accessing travel data regardless of source,
|
||||
* supporting automatic fallback between local XML files and remote API calls. It handles caching,
|
||||
* error recovery, and data enrichment for both data sources.
|
||||
*/
|
||||
class TravelDataService
|
||||
{
|
||||
public const SOURCE_LOCAL = 'local';
|
||||
public const SOURCE_REMOTE = 'remote';
|
||||
|
||||
public function __construct(
|
||||
private readonly TravelLoader $travelLoader,
|
||||
private readonly HotelLoader $hotelLoader,
|
||||
private readonly PickupLoader $pickupLoader,
|
||||
private readonly ApiClient $apiClient,
|
||||
private readonly CacheInterface $cache,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly bool $preferRemote = false,
|
||||
private readonly bool $enableFallback = true,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve travel data with automatic source selection and fallback.
|
||||
*
|
||||
* Attempts to load travel data from the preferred source first, then falls back
|
||||
* to the alternative source if the primary fails. Handles caching and enrichment
|
||||
* of data from both sources.
|
||||
*
|
||||
* @param int $dateId The travel date ID to retrieve
|
||||
* @param int|null $hotelId Optional hotel ID for specific hotel data
|
||||
* @param bool $preferRemote Whether to prefer remote API over XML for this call
|
||||
* @param bool $enableCache Whether to use caching for this request
|
||||
*
|
||||
* @return Travel|null The travel data or null if not found in any source
|
||||
*/
|
||||
public function getTravelData(
|
||||
int $dateId,
|
||||
?int $hotelId = null,
|
||||
?bool $preferRemote = null,
|
||||
bool $enableCache = true,
|
||||
): ?Travel {
|
||||
$preferRemote = $preferRemote ?? $this->preferRemote;
|
||||
$cacheKey = sprintf('travel_unified_%d_%d_%s', $dateId, $hotelId ?? 0, $preferRemote ? 'remote' : 'local');
|
||||
|
||||
if (!$enableCache) {
|
||||
return $this->loadTravelDataUncached($dateId, $hotelId, $preferRemote);
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->cache->get($cacheKey, function (ItemInterface $item) use ($dateId, $hotelId, $preferRemote) {
|
||||
$item->expiresAfter(300); // 5 minutes cache
|
||||
|
||||
return $this->loadTravelDataUncached($dateId, $hotelId, $preferRemote);
|
||||
});
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$this->logger->error('Cache error in TravelDataService', [
|
||||
'dateId' => $dateId,
|
||||
'hotelId' => $hotelId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return $this->loadTravelDataUncached($dateId, $hotelId, $preferRemote);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve travel data specifically from XML files.
|
||||
*
|
||||
* Loads travel data from local XML files with full data enrichment including
|
||||
* hotel details and pickup information.
|
||||
*
|
||||
* @param int $dateId The travel date ID to retrieve
|
||||
* @param int|null $hotelId Optional hotel ID for specific hotel data
|
||||
*
|
||||
* @return Travel|null The travel data or null if not found in XML
|
||||
*/
|
||||
public function getTravelDataFromXml(int $dateId, ?int $hotelId = null): ?Travel
|
||||
{
|
||||
try {
|
||||
$travel = $this->travelLoader->loadById($dateId, $hotelId);
|
||||
|
||||
if (null === $travel) {
|
||||
$this->logger->debug('Travel not found in XML', [
|
||||
'dateId' => $dateId,
|
||||
'hotelId' => $hotelId,
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->enrichTravelData($travel);
|
||||
$this->logger->debug('Travel data loaded from XML', [
|
||||
'dateId' => $dateId,
|
||||
'hotelId' => $hotelId,
|
||||
'travelId' => $travel->id,
|
||||
]);
|
||||
|
||||
return $travel;
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error('Failed to load travel data from XML', [
|
||||
'dateId' => $dateId,
|
||||
'hotelId' => $hotelId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve travel data specifically from remote API.
|
||||
*
|
||||
* Loads travel data from the remote BusProNet API. Note that the API uses
|
||||
* product IDs rather than date IDs, so mapping is performed internally.
|
||||
*
|
||||
* @param int $dateId The travel date ID to retrieve
|
||||
* @param int|null $hotelId Optional hotel ID for specific hotel data
|
||||
*
|
||||
* @return Travel|null The travel data or null if not found via API
|
||||
*/
|
||||
public function getTravelDataFromApi(int $dateId, ?int $hotelId = null): ?Travel
|
||||
{
|
||||
try {
|
||||
// Map dateId to productId for API call
|
||||
$productId = $this->mapDateIdToProductId($dateId);
|
||||
if (null === $productId) {
|
||||
$this->logger->debug('Cannot map dateId to productId for API call', [
|
||||
'dateId' => $dateId,
|
||||
'hotelId' => $hotelId,
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$result = $this->apiClient->getTravelData($productId, $hotelId);
|
||||
|
||||
if (!$result instanceof Travel) {
|
||||
$this->logger->debug('API returned non-travel result', [
|
||||
'dateId' => $dateId,
|
||||
'hotelId' => $hotelId,
|
||||
'productId' => $productId,
|
||||
'resultType' => get_class($result),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->logger->debug('Travel data loaded from API', [
|
||||
'dateId' => $dateId,
|
||||
'hotelId' => $hotelId,
|
||||
'productId' => $productId,
|
||||
'travelId' => $result->id,
|
||||
]);
|
||||
|
||||
return $result;
|
||||
} catch (ApiClientException $e) {
|
||||
$this->logger->error('Failed to load travel data from API', [
|
||||
'dateId' => $dateId,
|
||||
'hotelId' => $hotelId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if travel data exists in XML files.
|
||||
*
|
||||
* Performs a lightweight check to determine if travel data exists in XML
|
||||
* files without loading the full travel object.
|
||||
*
|
||||
* @param int $dateId The travel date ID to check
|
||||
* @param int|null $hotelId Optional hotel ID for specific hotel data
|
||||
*
|
||||
* @return bool True if travel data exists in XML files
|
||||
*/
|
||||
public function existsInXml(int $dateId, ?int $hotelId = null): bool
|
||||
{
|
||||
$mapping = $this->generateFilesMap();
|
||||
|
||||
if (false === isset($mapping[$dateId])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If hotelId is specified, check if it exists in the travel's hotels
|
||||
if (null !== $hotelId && false === isset($mapping[$dateId]['hotels'][$hotelId])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get information about available data sources for a travel.
|
||||
*
|
||||
* Returns information about which data sources (local XML, remote API, or both) have
|
||||
* data available for the specified travel.
|
||||
*
|
||||
* @param int $dateId The travel date ID to check
|
||||
* @param int|null $hotelId Optional hotel ID for specific hotel data
|
||||
*
|
||||
* @return array<string, bool> Array with 'local' and 'remote' keys indicating availability
|
||||
*/
|
||||
public function getAvailableSources(int $dateId, ?int $hotelId = null): array
|
||||
{
|
||||
return [
|
||||
static::SOURCE_LOCAL => $this->existsInXml($dateId, $hotelId),
|
||||
static::SOURCE_REMOTE => null !== $this->mapDateIdToProductId($dateId),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Load travel data directly without caching.
|
||||
*
|
||||
* Internal method that handles the actual loading logic with fallback support.
|
||||
* Tries the preferred source first, then falls back to the alternative if enabled.
|
||||
*
|
||||
* @param int $dateId The travel date ID to retrieve
|
||||
* @param int|null $hotelId Optional hotel ID for specific hotel data
|
||||
* @param bool $preferRemote Whether to prefer remote API over XML
|
||||
*
|
||||
* @return Travel|null The travel data or null if not found
|
||||
*/
|
||||
private function loadTravelDataUncached(int $dateId, ?int $hotelId = null, bool $preferRemote = false): ?Travel
|
||||
{
|
||||
$primarySource = $preferRemote ? self::SOURCE_REMOTE : self::SOURCE_LOCAL;
|
||||
$fallbackSource = $preferRemote ? self::SOURCE_LOCAL : self::SOURCE_REMOTE;
|
||||
|
||||
// Try primary source first
|
||||
$travel = $this->loadFromSource($dateId, $hotelId, $primarySource);
|
||||
|
||||
if (null !== $travel) {
|
||||
return $travel;
|
||||
}
|
||||
|
||||
// Try fallback source if enabled
|
||||
if (true === $this->enableFallback) {
|
||||
$this->logger->debug('Fallback to alternative source', [
|
||||
'dateId' => $dateId,
|
||||
'hotelId' => $hotelId,
|
||||
'primarySource' => $primarySource,
|
||||
'fallbackSource' => $fallbackSource,
|
||||
]);
|
||||
|
||||
$travel = $this->loadFromSource($dateId, $hotelId, $fallbackSource);
|
||||
}
|
||||
|
||||
return $travel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load travel data from a specific source.
|
||||
*
|
||||
* Internal method that routes to the appropriate loader based on source type.
|
||||
*
|
||||
* @param int $dateId The travel date ID to retrieve
|
||||
* @param int|null $hotelId Optional hotel ID for specific hotel data
|
||||
* @param string $source The source type (SOURCE_LOCAL or SOURCE_REMOTE)
|
||||
*
|
||||
* @return Travel|null The travel data or null if not found
|
||||
*/
|
||||
private function loadFromSource(int $dateId, ?int $hotelId, string $source): ?Travel
|
||||
{
|
||||
return match ($source) {
|
||||
self::SOURCE_LOCAL => $this->getTravelDataFromXml($dateId, $hotelId),
|
||||
self::SOURCE_REMOTE => $this->getTravelDataFromApi($dateId, $hotelId),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Map date code to date ID.
|
||||
*
|
||||
* Converts a date code string to its corresponding date ID using the
|
||||
* date loader's mapping functionality.
|
||||
*
|
||||
* @param string $dateCode The date code to map
|
||||
*
|
||||
* @return int|null The corresponding date ID or null if not found
|
||||
*/
|
||||
public function mapDateCodeToId(string $dateCode): ?int
|
||||
{
|
||||
try {
|
||||
$dateId = $this->travelLoader->mapCodeToId($dateCode);
|
||||
|
||||
$this->logger->debug('Date code mapping', [
|
||||
'dateCode' => $dateCode,
|
||||
'dateId' => $dateId,
|
||||
]);
|
||||
|
||||
return $dateId;
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error('Failed to map date code to ID', [
|
||||
'dateCode' => $dateCode,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map hotel code to hotel ID.
|
||||
*
|
||||
* Converts a hotel code string to its corresponding hotel ID using the
|
||||
* hotel loader's mapping functionality.
|
||||
*
|
||||
* @param string $hotelCode The hotel code to map
|
||||
*
|
||||
* @return int|null The corresponding hotel ID or null if not found
|
||||
*/
|
||||
public function mapHotelCodeToId(string $hotelCode): ?int
|
||||
{
|
||||
try {
|
||||
$hotelId = $this->hotelLoader->mapCodeToId($hotelCode);
|
||||
|
||||
$this->logger->debug('Hotel code mapping', [
|
||||
'hotelCode' => $hotelCode,
|
||||
'hotelId' => $hotelId,
|
||||
]);
|
||||
|
||||
return $hotelId;
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error('Failed to map hotel code to ID', [
|
||||
'hotelCode' => $hotelCode,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map date ID to product ID for API calls.
|
||||
*
|
||||
* Converts a date ID to its corresponding product ID for use with the
|
||||
* remote API. This method exposes the existing loader functionality
|
||||
* through the service layer.
|
||||
*
|
||||
* @param int $dateId The date ID to map
|
||||
*
|
||||
* @return int|null The corresponding product ID or null if not found
|
||||
*/
|
||||
public function mapDateIdToProductId(int $dateId): ?int
|
||||
{
|
||||
try {
|
||||
$productId = $this->travelLoader->mapDateIdToProductId($dateId);
|
||||
|
||||
$this->logger->debug('Date ID to product ID mapping', [
|
||||
'dateId' => $dateId,
|
||||
'productId' => $productId,
|
||||
]);
|
||||
|
||||
return $productId;
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error('Failed to map date ID to product ID', [
|
||||
'dateId' => $dateId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate files mapping for available travel data.
|
||||
*
|
||||
* Creates a mapping of all available travel data files with their
|
||||
* corresponding travel and hotel information. This method exposes
|
||||
* the existing loader functionality through the service layer.
|
||||
*
|
||||
* @return array<int, array<string, mixed>> Array mapping of travel data files
|
||||
*/
|
||||
public function generateFilesMap(): array
|
||||
{
|
||||
try {
|
||||
$mapping = $this->travelLoader->generateFilesMap();
|
||||
|
||||
$this->logger->debug('Generated files mapping', [
|
||||
'count' => count($mapping),
|
||||
]);
|
||||
|
||||
return $mapping;
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error('Failed to generate files mapping', [
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch mutability data from API.
|
||||
*
|
||||
* Retrieves mutability configuration data from the API for a specific travel date.
|
||||
* Handles API errors and notification responses gracefully.
|
||||
*
|
||||
* @param int $dateId The travel date ID for API call
|
||||
*
|
||||
* @return BaseData|null The mutability data or null if not available or error occurred
|
||||
*/
|
||||
public function getMutabilityData(int $dateId): ?BaseData
|
||||
{
|
||||
try {
|
||||
$mutableData = $this->apiClient->getMutableData($dateId);
|
||||
|
||||
if ($mutableData instanceof Notification) {
|
||||
$this->logger->warning('API returned notification for mutability data', [
|
||||
'dateId' => $dateId,
|
||||
'message' => $mutableData->message,
|
||||
'isError' => $mutableData->isError(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->logger->debug('Successfully fetched mutability data', [
|
||||
'dateId' => $dateId,
|
||||
]);
|
||||
|
||||
return $mutableData;
|
||||
} catch (ApiClientException $e) {
|
||||
$this->logger->error('Failed to fetch mutability data from API', [
|
||||
'dateId' => $dateId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply mutability data to travel services.
|
||||
*
|
||||
* Updates the mutability status of various travel services based on the
|
||||
* provided mutability configuration data.
|
||||
*
|
||||
* @param Travel $travel The travel object to update
|
||||
* @param BaseData $mutableData The mutability configuration data
|
||||
*/
|
||||
public function patchMutability(Travel $travel, BaseData $mutableData): void
|
||||
{
|
||||
$this->travelLoader->patchMutability($travel, $mutableData);
|
||||
|
||||
$this->logger->debug('Successfully patched mutability data', [
|
||||
'travelId' => $travel->id,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch availability data from API.
|
||||
*
|
||||
* Retrieves availability information from the API for a specific travel date.
|
||||
* Handles API errors and notification responses gracefully.
|
||||
*
|
||||
* @param int $dateId The travel date ID for API call
|
||||
*
|
||||
* @return BaseData|null The availability data or null if not available or error occurred
|
||||
*/
|
||||
public function getAvailabilityData(int $dateId): ?BaseData
|
||||
{
|
||||
try {
|
||||
$availabilities = $this->apiClient->getAvailabilities($dateId);
|
||||
|
||||
if ($availabilities instanceof Notification) {
|
||||
$this->logger->warning('API returned notification for availability data', [
|
||||
'dateId' => $dateId,
|
||||
'message' => $availabilities->message,
|
||||
'isError' => $availabilities->isError(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->logger->debug('Successfully fetched availability data', [
|
||||
'dateId' => $dateId,
|
||||
]);
|
||||
|
||||
return $availabilities;
|
||||
} catch (ApiClientException $e) {
|
||||
$this->logger->error('Failed to fetch availability data from API', [
|
||||
'dateId' => $dateId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply availability data to travel services.
|
||||
*
|
||||
* Updates the availability status of additional and transportation
|
||||
* services based on the provided availability data.
|
||||
*
|
||||
* @param Travel $travel The travel object to update
|
||||
* @param BaseData $availabilities The availability data for services
|
||||
*/
|
||||
public function patchAvailabilities(Travel $travel, BaseData $availabilities): void
|
||||
{
|
||||
$this->travelLoader->patchAvailabilities($travel, $availabilities);
|
||||
|
||||
$this->logger->debug('Successfully patched availability data', [
|
||||
'travelId' => $travel->id,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enrich travel data with additional information.
|
||||
*
|
||||
* Adds hotel details and pickup information to travel data loaded from XML.
|
||||
* This enrichment is necessary for complete travel information.
|
||||
*
|
||||
* @param Travel $travel The travel object to enrich
|
||||
*/
|
||||
private function enrichTravelData(Travel $travel): void
|
||||
{
|
||||
try {
|
||||
$this->pickupLoader->patchPickupsDetails($travel);
|
||||
$this->hotelLoader->patchHotelDetails($travel);
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->warning('Failed to enrich travel data', [
|
||||
'travelId' => $travel->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,9 @@
|
||||
<h2>Zimmerauswahl</h2>
|
||||
{{ form_start(form) }}
|
||||
{{ form_row(form.roomSelections) }}
|
||||
<button type="submit" class="button bg-button bg-button--secondary">Weiter</button>
|
||||
<div class="flex justify-end">
|
||||
<button type="submit" class="button bg-button bg-button--secondary">Weiter</button>
|
||||
</div>
|
||||
{{ form_rest(form) }}
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
@@ -22,4 +24,4 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -7,9 +7,27 @@
|
||||
<div class="col-span-2">
|
||||
<h2>Teilnehmer</h2>
|
||||
{{ form_start(form) }}
|
||||
{% for participant in form.participants %}
|
||||
{{ form_row(participant) }}
|
||||
{% endfor %}
|
||||
{% do form.participants.setRendered %}
|
||||
<div class="space-y-8 pb-8">
|
||||
{% for participant in form.participants %}
|
||||
<fieldset class="border rounded-md p-8 pt-4">
|
||||
<legend class="font-bold text-xl px-2">
|
||||
Teilnehmer:in {{ loop.index }}
|
||||
</legend>
|
||||
<div class="grid grid-cols-2 gap-4 pb-4">
|
||||
{{ form_row(participant.firstName) }}
|
||||
{{ form_row(participant.lastName) }}
|
||||
{{ form_row(participant.dateOfBirth) }}
|
||||
{{ form_row(participant.gender) }}
|
||||
{{ form_row(participant.nationality) }}
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
{{ form_row(participant.email) }}
|
||||
{{ form_row(participant.mobile) }}
|
||||
</div>
|
||||
</fieldset>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<a href="{{ path('app_booking_create_step_1') }}" class="button bg-button bg-button--secondary">Zurück</a>
|
||||
<button type="submit" class="button bg-button bg-button--secondary">Weiter</button>
|
||||
@@ -19,13 +37,42 @@
|
||||
</div>
|
||||
<div>
|
||||
<h3>Zusammenfassung</h3>
|
||||
<p>Reise: {{ bookingCreateDto.travelData.label }}</p>
|
||||
<p>Datum: {{ bookingCreateDto.travelData.dateFrom | date('d.m.Y') }} - {{ bookingCreateDto.travelData.dateTo | date('d.m.Y') }}</p>
|
||||
<p>Hotel: {{ bookingCreateDto.travelData.hotel.name }}</p>
|
||||
<h4>
|
||||
Reise
|
||||
</h4>
|
||||
<p>
|
||||
{{ bookingCreateDto.travelData.label }}
|
||||
</p>
|
||||
<h4>
|
||||
Datum
|
||||
</h4>
|
||||
<p>
|
||||
{{ bookingCreateDto.travelData.dateFrom | date('d.m.Y') }} - {{ bookingCreateDto.travelData.dateTo | date('d.m.Y') }}
|
||||
</p>
|
||||
<h4>
|
||||
Unterkunft
|
||||
</h4>
|
||||
<p>
|
||||
{{ bookingCreateDto.travelData.hotel.name }}
|
||||
</p>
|
||||
{% if bookingCreateDto.participantsCount > 0 %}
|
||||
<p>Teilnehmerzahl: {{ bookingCreateDto.participantsCount }}</p>
|
||||
<h4>
|
||||
Anzahl Teilnehmer
|
||||
</h4>
|
||||
<p>
|
||||
{{ bookingCreateDto.participantsCount }}
|
||||
</p>
|
||||
{% endif %}
|
||||
<p>Zimmer: </p>
|
||||
<h4>
|
||||
Zimmer
|
||||
</h4>
|
||||
<ul>
|
||||
{% for roomSelection in bookingCreateDto.selectedRooms %}
|
||||
<li>
|
||||
{{ roomSelection.quantity }}x {{ roomSelection.roomLabel }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -4,22 +4,45 @@ namespace App\Tests\BusProNet\DataLoader;
|
||||
|
||||
use App\BusProNet\Model\Hotel;
|
||||
use App\BusProNet\XmlLoader\HotelLoader;
|
||||
use League\Flysystem\FilesystemOperator;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Cache\Adapter\ArrayAdapter;
|
||||
|
||||
class HotelDataLoaderTest extends TestCase
|
||||
{
|
||||
public function testParse(): void
|
||||
public function testLoadAll(): void
|
||||
{
|
||||
$loader = new HotelLoader(__DIR__.'/../../Resources');
|
||||
$hotelId = 163113;
|
||||
$xmlContent = '<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--Export für dreipunktnull büro für mediengestaltung-->
|
||||
<hotels erstellt_am="14.07.2025 15:04:46">
|
||||
<hotel id="1" idbuspro="163113" code="SBWOXA">
|
||||
<name>L\'Oxalys</name>
|
||||
<ort>Val Thorens</ort>
|
||||
<land>F</land>
|
||||
<strasse>Rue des Lacs</strasse>
|
||||
<telefon></telefon>
|
||||
<art>Hotel</art>
|
||||
<internetbuchbar>True</internetbuchbar>
|
||||
</hotel>
|
||||
</hotels>';
|
||||
|
||||
$xml = $loader->loadById($hotelId, 'hotels_data.xml');
|
||||
$this->assertInstanceOf(\SimpleXMLElement::class, $xml);
|
||||
$filesystem = $this->createMock(FilesystemOperator::class);
|
||||
$filesystem->expects($this->once())
|
||||
->method('read')
|
||||
->with('hotel.xml')
|
||||
->willReturn($xmlContent);
|
||||
|
||||
$hotel = $loader->parseXml($xml);
|
||||
$cache = new ArrayAdapter();
|
||||
$loader = new HotelLoader($cache, $filesystem);
|
||||
|
||||
$hotels = $loader->loadAll();
|
||||
|
||||
$this->assertIsArray($hotels);
|
||||
$this->assertArrayHasKey(163113, $hotels);
|
||||
|
||||
$hotel = $hotels[163113];
|
||||
$this->assertInstanceOf(Hotel::class, $hotel);
|
||||
$this->assertEquals($hotelId, $hotel->id);
|
||||
$this->assertEquals(163113, $hotel->id);
|
||||
$this->assertEquals('L\'Oxalys', $hotel->name);
|
||||
$this->assertEquals('SBWOXA', $hotel->code);
|
||||
$this->assertEquals('Val Thorens', $hotel->city);
|
||||
|
||||
@@ -4,22 +4,41 @@ namespace App\Tests\BusProNet\DataLoader;
|
||||
|
||||
use App\BusProNet\Model\Pickup;
|
||||
use App\BusProNet\XmlLoader\PickupLoader;
|
||||
use League\Flysystem\FilesystemOperator;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Cache\Adapter\ArrayAdapter;
|
||||
|
||||
class PickupDataLoaderTest extends TestCase
|
||||
{
|
||||
public function testParse(): void
|
||||
public function testLoadById(): void
|
||||
{
|
||||
$loader = new PickupLoader();
|
||||
$pickupId = 1;
|
||||
$xmlContent = '<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--Export für dreipunktnull büro für mediengestaltung-->
|
||||
<zustiege erstellt_am="14.07.2025 15:15:09">
|
||||
<zustieg id="1" idbuspro="1" code="MS-Hbf">
|
||||
<ort>Münster</ort>
|
||||
<plz>48143</plz>
|
||||
<strasse>Hafenstr/Ecke Friedrich-Ebert-Str</strasse>
|
||||
<art>BUS</art>
|
||||
<hausabholung>False</hausabholung>
|
||||
<crsbuchbar>True</crsbuchbar>
|
||||
<internetbuchbar>True</internetbuchbar>
|
||||
</zustieg>
|
||||
</zustiege>';
|
||||
|
||||
$xml = $loader->loadById($pickupId, 'pickups_data.xml');
|
||||
$this->assertInstanceOf(\SimpleXMLElement::class, $xml);
|
||||
$filesystem = $this->createMock(FilesystemOperator::class);
|
||||
$filesystem->expects($this->once())
|
||||
->method('read')
|
||||
->with('zustiege.xml')
|
||||
->willReturn($xmlContent);
|
||||
|
||||
$pickup = $loader->parseXml($xml);
|
||||
$cache = new ArrayAdapter();
|
||||
$loader = new PickupLoader($cache, $filesystem);
|
||||
|
||||
$pickup = $loader->loadById(1);
|
||||
|
||||
$this->assertInstanceOf(Pickup::class, $pickup);
|
||||
$this->assertEquals($pickupId, $pickup->id);
|
||||
$this->assertEquals(1, $pickup->id);
|
||||
$this->assertEquals('MS-Hbf', $pickup->code);
|
||||
$this->assertEquals('Münster', $pickup->city);
|
||||
$this->assertEquals('48143', $pickup->postalCode);
|
||||
|
||||
@@ -3,21 +3,56 @@
|
||||
namespace App\Tests\BusProNet\DataLoader;
|
||||
|
||||
use App\BusProNet\Model\Travel;
|
||||
use App\BusProNet\XmlLoader\HotelLoader;
|
||||
use App\BusProNet\XmlLoader\TravelLoader;
|
||||
use App\BusProNet\XmlParser\TravelParser;
|
||||
use League\Flysystem\FilesystemOperator;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Cache\Adapter\ArrayAdapter;
|
||||
|
||||
class TravelDataLoaderTest extends TestCase
|
||||
{
|
||||
public function testParse(): void
|
||||
public function testLoadById(): void
|
||||
{
|
||||
$loader = new TravelLoader('');
|
||||
$filename = __DIR__.'/../../Resources/travel_data.xml';
|
||||
$travelId = 11478;
|
||||
$xmlContent = '<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--Export für dreipunktnull büro für mediengestaltung-->
|
||||
<reisen>
|
||||
<reise id="1" idbuspro="1234" code="DPWMP" erstellt_am="14.07.2025 15:02:01">
|
||||
<termin id="1" idbuspro="11478" idprodukt="1234" termin="06.01.2025" bis="25.01.2025" code="DPWMP060125" reiseart="F">
|
||||
<text>Davos - Sportclub Waldschlössli</text>
|
||||
<abpreis>534.2</abpreis>
|
||||
<hotel idbuspro="123">
|
||||
<text>Test Hotel</text>
|
||||
</hotel>
|
||||
</termin>
|
||||
</reise>
|
||||
</reisen>';
|
||||
|
||||
$xml = $loader->loadById($travelId, $filename);
|
||||
$this->assertInstanceOf(\SimpleXMLElement::class, $xml);
|
||||
$filesystem = $this->createMock(FilesystemOperator::class);
|
||||
$filesystem->expects($this->once())
|
||||
->method('read')
|
||||
->with('test_file.xml')
|
||||
->willReturn($xmlContent);
|
||||
|
||||
$travel = $loader->parseXml($xml);
|
||||
$hotelLoader = $this->createMock(HotelLoader::class);
|
||||
$travelParser = $this->createMock(TravelParser::class);
|
||||
|
||||
$expectedTravel = new Travel();
|
||||
$expectedTravel->code = 'DPWMP060125';
|
||||
$expectedTravel->type = 'F';
|
||||
$expectedTravel->dateFrom = new \DateTimeImmutable('2025-01-06');
|
||||
$expectedTravel->dateTo = new \DateTimeImmutable('2025-01-25');
|
||||
$expectedTravel->label = 'Davos - Sportclub Waldschlössli';
|
||||
$expectedTravel->priceFrom = 534.2;
|
||||
|
||||
$travelParser->expects($this->once())
|
||||
->method('parse')
|
||||
->willReturn($expectedTravel);
|
||||
|
||||
$cache = new ArrayAdapter();
|
||||
$loader = new TravelLoader($hotelLoader, $travelParser, 'http://example.com', $cache, $filesystem);
|
||||
|
||||
$travel = $loader->loadById(11478, null, 'test_file.xml');
|
||||
|
||||
$this->assertInstanceOf(Travel::class, $travel);
|
||||
$this->assertEquals('DPWMP060125', $travel->code);
|
||||
@@ -26,11 +61,5 @@ class TravelDataLoaderTest extends TestCase
|
||||
$this->assertInstanceOf(\DateTimeImmutable::class, $travel->dateTo);
|
||||
$this->assertEquals('Davos - Sportclub Waldschlössli', $travel->label);
|
||||
$this->assertEquals(534.2, $travel->priceFrom);
|
||||
|
||||
$this->assertCount(8, $travel->selectionGroups);
|
||||
$this->assertCount(27, $travel->additionalServices);
|
||||
$this->assertCount(5, $travel->transportationServices);
|
||||
$this->assertCount(7, $travel->pickupsTo);
|
||||
$this->assertCount(12, $travel->rooms);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,507 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\BusProNet\DataProcessor;
|
||||
|
||||
use App\BusProNet\DataProcessor\BookingDataProcessor;
|
||||
use App\BusProNet\Model\Address;
|
||||
use App\BusProNet\Model\BankAccount;
|
||||
use App\BusProNet\Model\Booking;
|
||||
use App\BusProNet\Model\Communication;
|
||||
use App\BusProNet\Model\PersonalData;
|
||||
use App\BusProNet\Model\Pickup;
|
||||
use App\BusProNet\Model\Room;
|
||||
use App\BusProNet\Model\Service;
|
||||
use App\BusProNet\Model\Travel;
|
||||
use App\Form\Model\BookingEditDto;
|
||||
use App\Form\Model\ParticipantDto;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Comprehensive test suite for BookingDataProcessor.
|
||||
*
|
||||
* Tests all aspects of booking data processing including service mappings,
|
||||
* participant data updates, and API payload generation.
|
||||
*/
|
||||
class BookingDataProcessorTest extends TestCase
|
||||
{
|
||||
private BookingDataProcessor $processor;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->processor = new BookingDataProcessor();
|
||||
}
|
||||
|
||||
public function testCreateUpdateRequestPayloadWithCompleteData(): void
|
||||
{
|
||||
$formData = $this->createCompleteFormData();
|
||||
|
||||
$result = $this->processor->createUpdateRequestPayload($formData);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
$this->assertArrayHasKey('idbuchung', $result);
|
||||
$this->assertArrayHasKey('teilnehmerliste', $result);
|
||||
$this->assertArrayHasKey('zusatzleistungen', $result);
|
||||
$this->assertArrayHasKey('beförderungen', $result);
|
||||
$this->assertArrayHasKey('ferienzielunterbringungen', $result);
|
||||
|
||||
$this->assertEquals(123, $result['idbuchung']);
|
||||
$this->assertEquals('ACTIVE', $result['status']);
|
||||
$this->assertCount(2, $result['teilnehmerliste']['teilnehmer']);
|
||||
}
|
||||
|
||||
public function testCanceledParticipantsAreSkipped(): void
|
||||
{
|
||||
$formData = $this->createFormDataWithCanceledParticipant();
|
||||
|
||||
$result = $this->processor->createUpdateRequestPayload($formData);
|
||||
|
||||
$this->assertCount(2, $result['teilnehmerliste']['teilnehmer']);
|
||||
$this->assertEquals(0, $result['teilnehmerliste']['teilnehmer'][0]['@id']);
|
||||
$this->assertEquals(1, $result['teilnehmerliste']['teilnehmer'][1]['@id']);
|
||||
}
|
||||
|
||||
public function testAdditionalServicesProcessing(): void
|
||||
{
|
||||
$formData = $this->createFormDataWithAdditionalServices();
|
||||
|
||||
$result = $this->processor->createUpdateRequestPayload($formData);
|
||||
|
||||
$this->assertNotEmpty($result['zusatzleistungen']['zusatzleistung']);
|
||||
$this->assertCount(2, $result['zusatzleistungen']['zusatzleistung']);
|
||||
|
||||
$service = $result['zusatzleistungen']['zusatzleistung'][0];
|
||||
$this->assertEquals(1, $service['@idleistung']);
|
||||
$this->assertEquals(1, $service['@anzahl']);
|
||||
$this->assertEquals('0', $service['@zuordnung']);
|
||||
}
|
||||
|
||||
public function testTransportationServicesProcessing(): void
|
||||
{
|
||||
$formData = $this->createFormDataWithTransportation();
|
||||
|
||||
$result = $this->processor->createUpdateRequestPayload($formData);
|
||||
|
||||
$this->assertNotEmpty($result['beförderungen']['beförderung']);
|
||||
$this->assertCount(2, $result['beförderungen']['beförderung']);
|
||||
}
|
||||
|
||||
public function testBusPickupLocationsProcessing(): void
|
||||
{
|
||||
$formData = $this->createFormDataWithBusPickup();
|
||||
|
||||
$result = $this->processor->createUpdateRequestPayload($formData);
|
||||
|
||||
$this->assertArrayHasKey('zustiege', $result);
|
||||
$this->assertNotEmpty($result['zustiege']['zustieg']);
|
||||
$this->assertEquals(1, $result['zustiege']['zustieg'][0]['@idzustieg']);
|
||||
}
|
||||
|
||||
public function testNonBusTransportationSkipsPickup(): void
|
||||
{
|
||||
$formData = $this->createFormDataWithNonBusTransportation();
|
||||
|
||||
$result = $this->processor->createUpdateRequestPayload($formData);
|
||||
|
||||
$this->assertArrayNotHasKey('zustiege', $result);
|
||||
}
|
||||
|
||||
public function testUnusedServicesAreRemoved(): void
|
||||
{
|
||||
$formData = $this->createFormDataWithUnusedServices();
|
||||
|
||||
$result = $this->processor->createUpdateRequestPayload($formData);
|
||||
|
||||
$this->assertEmpty($result['zusatzleistungen']['zusatzleistung']);
|
||||
$this->assertEmpty($result['beförderungen']['beförderung']);
|
||||
}
|
||||
|
||||
public function testParticipantPersonalDataUpdate(): void
|
||||
{
|
||||
$formData = $this->createFormDataWithUpdatedPersonalData();
|
||||
|
||||
$this->processor->createUpdateRequestPayload($formData);
|
||||
|
||||
$participant = $formData->booking->participants[0];
|
||||
$this->assertEquals('Updated', $participant->firstName);
|
||||
$this->assertEquals('Participant', $participant->name);
|
||||
$this->assertEquals('[email protected]', $participant->communication->email);
|
||||
$this->assertEquals('+49123456789', $participant->communication->mobile);
|
||||
}
|
||||
|
||||
public function testInactiveParticipantsPersonalDataNotUpdated(): void
|
||||
{
|
||||
$formData = $this->createFormDataWithInactiveParticipant();
|
||||
|
||||
$this->processor->createUpdateRequestPayload($formData);
|
||||
|
||||
$participant = $formData->booking->participants[0];
|
||||
$this->assertEquals('Original', $participant->firstName);
|
||||
$this->assertEquals('Name', $participant->name);
|
||||
}
|
||||
|
||||
public function testApplicantDataSyncWithFirstParticipant(): void
|
||||
{
|
||||
$formData = $this->createFormDataForApplicantSync();
|
||||
|
||||
$this->processor->createUpdateRequestPayload($formData);
|
||||
|
||||
$applicant = $formData->booking->applicant;
|
||||
$firstParticipant = $formData->booking->participants[0];
|
||||
|
||||
$this->assertEquals($firstParticipant->height, $applicant->height);
|
||||
$this->assertEquals($firstParticipant->weight, $applicant->weight);
|
||||
$this->assertEquals($firstParticipant->shoeSize, $applicant->shoeSize);
|
||||
}
|
||||
|
||||
public function testBankAccountIncludedInPayload(): void
|
||||
{
|
||||
$formData = $this->createFormDataWithBankAccount();
|
||||
|
||||
$result = $this->processor->createUpdateRequestPayload($formData);
|
||||
|
||||
$this->assertArrayHasKey('bankverbindung', $result['zahlung']);
|
||||
$this->assertEquals('Test Bank', $result['zahlung']['bankverbindung']['@kreditinstitut']);
|
||||
$this->assertEquals('DE89370400440532013000', $result['zahlung']['bankverbindung']['@iban']);
|
||||
}
|
||||
|
||||
public function testBankAccountNotIncludedWhenNull(): void
|
||||
{
|
||||
$formData = $this->createFormDataWithoutBankAccount();
|
||||
|
||||
$result = $this->processor->createUpdateRequestPayload($formData);
|
||||
|
||||
$this->assertArrayNotHasKey('bankverbindung', $result['zahlung']);
|
||||
}
|
||||
|
||||
public function testAccommodationRoomsProcessing(): void
|
||||
{
|
||||
$formData = $this->createFormDataWithRooms();
|
||||
|
||||
$result = $this->processor->createUpdateRequestPayload($formData);
|
||||
|
||||
$this->assertNotEmpty($result['ferienzielunterbringungen']['ferienzielunterbringung']);
|
||||
|
||||
$room = $result['ferienzielunterbringungen']['ferienzielunterbringung'][0];
|
||||
$this->assertEquals(1, $room['@idzimmer']);
|
||||
$this->assertEquals('DOUBLE', $room['@kategorie']);
|
||||
$this->assertEquals('01.01.2024', $room['@anreise']);
|
||||
$this->assertEquals('07.01.2024', $room['@abreise']);
|
||||
}
|
||||
|
||||
public function testCommunicationObjectCreation(): void
|
||||
{
|
||||
$formData = $this->createFormDataWithoutExistingCommunication();
|
||||
|
||||
$this->processor->createUpdateRequestPayload($formData);
|
||||
|
||||
$participant = $formData->booking->participants[0];
|
||||
$this->assertInstanceOf(Communication::class, $participant->communication);
|
||||
$this->assertEquals('[email protected]', $participant->communication->email);
|
||||
}
|
||||
|
||||
public function testEmptyPickupsToDoesNotCreateZustiegeSection(): void
|
||||
{
|
||||
$formData = $this->createFormDataWithoutPickups();
|
||||
|
||||
$result = $this->processor->createUpdateRequestPayload($formData);
|
||||
|
||||
$this->assertArrayNotHasKey('zustiege', $result);
|
||||
}
|
||||
|
||||
private function createCompleteFormData(): BookingEditDto
|
||||
{
|
||||
$formData = new BookingEditDto();
|
||||
|
||||
$formData->booking = $this->createMockBooking();
|
||||
$formData->travel = $this->createMockTravel();
|
||||
$formData->participants = [
|
||||
$this->createMockParticipantDto(0, 'F'),
|
||||
$this->createMockParticipantDto(1, 'F'),
|
||||
];
|
||||
|
||||
return $formData;
|
||||
}
|
||||
|
||||
private function createFormDataWithCanceledParticipant(): BookingEditDto
|
||||
{
|
||||
$formData = new BookingEditDto();
|
||||
|
||||
$formData->booking = $this->createMockBooking();
|
||||
$formData->travel = $this->createMockTravel();
|
||||
$formData->participants = [
|
||||
$this->createMockParticipantDto(0, 'F'),
|
||||
$this->createMockParticipantDto(1, 'S'), // Canceled
|
||||
];
|
||||
|
||||
return $formData;
|
||||
}
|
||||
|
||||
private function createFormDataWithAdditionalServices(): BookingEditDto
|
||||
{
|
||||
$formData = $this->createCompleteFormData();
|
||||
|
||||
$participant = $formData->participants[0];
|
||||
$participant->courses = [$this->createMockService(1)];
|
||||
$participant->additionalServices = [$this->createMockService(2)];
|
||||
|
||||
return $formData;
|
||||
}
|
||||
|
||||
private function createFormDataWithTransportation(): BookingEditDto
|
||||
{
|
||||
$formData = $this->createCompleteFormData();
|
||||
|
||||
$participant = $formData->participants[0];
|
||||
$participant->transportationServiceTo = $this->createMockService(1);
|
||||
$participant->transportationServiceFro = $this->createMockService(2);
|
||||
|
||||
return $formData;
|
||||
}
|
||||
|
||||
private function createFormDataWithBusPickup(): BookingEditDto
|
||||
{
|
||||
$formData = $this->createCompleteFormData();
|
||||
|
||||
$participant = $formData->participants[0];
|
||||
$busService = $this->createMockService(1);
|
||||
$busService->subType = 'BUS';
|
||||
$participant->transportationServiceTo = $busService;
|
||||
$participant->pickup = $this->createMockPickup(1);
|
||||
|
||||
return $formData;
|
||||
}
|
||||
|
||||
private function createFormDataWithNonBusTransportation(): BookingEditDto
|
||||
{
|
||||
$formData = $this->createCompleteFormData();
|
||||
|
||||
$participant = $formData->participants[0];
|
||||
$trainService = $this->createMockService(1);
|
||||
$trainService->subType = 'TRAIN';
|
||||
$participant->transportationServiceTo = $trainService;
|
||||
|
||||
return $formData;
|
||||
}
|
||||
|
||||
private function createFormDataWithUnusedServices(): BookingEditDto
|
||||
{
|
||||
$formData = $this->createCompleteFormData();
|
||||
|
||||
// Remove all participants so services become unused
|
||||
$formData->participants = [];
|
||||
|
||||
return $formData;
|
||||
}
|
||||
|
||||
private function createFormDataWithUpdatedPersonalData(): BookingEditDto
|
||||
{
|
||||
$formData = $this->createCompleteFormData();
|
||||
|
||||
$participant = $formData->participants[0];
|
||||
$participant->firstName = 'Updated';
|
||||
$participant->lastName = 'Participant';
|
||||
$participant->email = '[email protected]';
|
||||
$participant->mobile = '+49123456789';
|
||||
|
||||
return $formData;
|
||||
}
|
||||
|
||||
private function createFormDataWithInactiveParticipant(): BookingEditDto
|
||||
{
|
||||
$formData = $this->createCompleteFormData();
|
||||
|
||||
$participant = $formData->participants[0];
|
||||
$participant->status = 'C'; // Inactive status
|
||||
$participant->firstName = 'Updated';
|
||||
$participant->lastName = 'Participant';
|
||||
|
||||
// Ensure original data remains unchanged
|
||||
$formData->booking->participants[0]->firstName = 'Original';
|
||||
$formData->booking->participants[0]->name = 'Name';
|
||||
|
||||
return $formData;
|
||||
}
|
||||
|
||||
private function createFormDataForApplicantSync(): BookingEditDto
|
||||
{
|
||||
$formData = $this->createCompleteFormData();
|
||||
|
||||
$firstParticipant = $formData->booking->participants[0];
|
||||
$firstParticipant->height = '180';
|
||||
$firstParticipant->weight = '75';
|
||||
$firstParticipant->shoeSize = '42';
|
||||
|
||||
return $formData;
|
||||
}
|
||||
|
||||
private function createFormDataWithBankAccount(): BookingEditDto
|
||||
{
|
||||
$formData = $this->createCompleteFormData();
|
||||
|
||||
$bankAccount = new BankAccount();
|
||||
$bankAccount->bankName = 'Test Bank';
|
||||
$bankAccount->iban = 'DE89370400440532013000';
|
||||
$bankAccount->bic = 'COBADEFFXXX';
|
||||
$bankAccount->holder = 'Test Holder';
|
||||
|
||||
$formData->booking->bankAccount = $bankAccount;
|
||||
|
||||
return $formData;
|
||||
}
|
||||
|
||||
private function createFormDataWithoutBankAccount(): BookingEditDto
|
||||
{
|
||||
$formData = $this->createCompleteFormData();
|
||||
$formData->booking->bankAccount = null;
|
||||
|
||||
return $formData;
|
||||
}
|
||||
|
||||
private function createFormDataWithRooms(): BookingEditDto
|
||||
{
|
||||
$formData = $this->createCompleteFormData();
|
||||
|
||||
$room = new Room();
|
||||
$room->id = 1;
|
||||
$room->category = 'DOUBLE';
|
||||
$room->boardId = 1;
|
||||
$room->dateFrom = new \DateTimeImmutable('2024-01-01');
|
||||
$room->dateTo = new \DateTimeImmutable('2024-01-07');
|
||||
$room->totalCount = 2;
|
||||
$room->mapping = [0, 1];
|
||||
|
||||
$formData->booking->rooms = [$room];
|
||||
|
||||
return $formData;
|
||||
}
|
||||
|
||||
private function createFormDataWithoutExistingCommunication(): BookingEditDto
|
||||
{
|
||||
$formData = $this->createCompleteFormData();
|
||||
|
||||
$participant = $formData->participants[0];
|
||||
$participant->email = '[email protected]';
|
||||
$participant->mobile = '+49987654321';
|
||||
|
||||
$formData->booking->participants[0]->communication = new Communication();
|
||||
|
||||
return $formData;
|
||||
}
|
||||
|
||||
private function createFormDataWithoutPickups(): BookingEditDto
|
||||
{
|
||||
$formData = $this->createCompleteFormData();
|
||||
$formData->booking->pickupsTo = [];
|
||||
|
||||
return $formData;
|
||||
}
|
||||
|
||||
private function createMockBooking(): Booking
|
||||
{
|
||||
$booking = new Booking();
|
||||
$booking->id = 123;
|
||||
$booking->status = 'ACTIVE';
|
||||
$booking->agencyId = 1;
|
||||
$booking->dateId = 1;
|
||||
$booking->hotelId = 1;
|
||||
$booking->paymentId = '1';
|
||||
$booking->paymentLabel = 'Credit Card';
|
||||
$booking->paymentType = 'CC';
|
||||
$booking->additionalServices = [];
|
||||
$booking->transportationServices = [];
|
||||
$booking->pickupsTo = [];
|
||||
$booking->pickupsFro = [];
|
||||
$booking->participants = [
|
||||
$this->createMockPersonalData('Participant0'),
|
||||
$this->createMockPersonalData('Participant1'),
|
||||
];
|
||||
$booking->participantsStatus = ['F', 'F'];
|
||||
$booking->applicant = $this->createMockPersonalData('Applicant');
|
||||
$booking->bankAccount = null;
|
||||
$booking->rooms = [];
|
||||
|
||||
return $booking;
|
||||
}
|
||||
|
||||
private function createMockTravel(): Travel
|
||||
{
|
||||
$travel = new Travel();
|
||||
$travel->additionalServices = [
|
||||
1 => $this->createMockService(1),
|
||||
2 => $this->createMockService(2),
|
||||
];
|
||||
$travel->transportationServices = [
|
||||
1 => $this->createMockService(1),
|
||||
2 => $this->createMockService(2),
|
||||
];
|
||||
|
||||
return $travel;
|
||||
}
|
||||
|
||||
private function createMockParticipantDto(int $index, string $status): ParticipantDto
|
||||
{
|
||||
$participant = new ParticipantDto();
|
||||
$participant->index = $index;
|
||||
$participant->status = $status;
|
||||
$participant->firstName = "Participant{$index}";
|
||||
$participant->lastName = 'LastName';
|
||||
$participant->dateOfBirth = new \DateTimeImmutable('1990-01-01');
|
||||
$participant->gender = 'M';
|
||||
$participant->nationality = 'DE';
|
||||
$participant->height = '175';
|
||||
$participant->weight = '70';
|
||||
$participant->shoeSize = '40';
|
||||
$participant->email = null;
|
||||
$participant->mobile = null;
|
||||
$participant->courses = [];
|
||||
$participant->additionalServices = [];
|
||||
$participant->skiPass = [];
|
||||
$participant->board = [];
|
||||
$participant->rentals = [];
|
||||
$participant->transportationServiceTo = $this->createMockService(1);
|
||||
$participant->transportationServiceFro = $this->createMockService(2);
|
||||
$participant->pickup = null;
|
||||
|
||||
return $participant;
|
||||
}
|
||||
|
||||
private function createMockPersonalData(string $firstName): PersonalData
|
||||
{
|
||||
$personalData = new PersonalData();
|
||||
$personalData->firstName = $firstName;
|
||||
$personalData->name = 'LastName';
|
||||
$personalData->dateOfBirth = new \DateTimeImmutable('1990-01-01');
|
||||
$personalData->gender = 'M';
|
||||
$personalData->nationality = 'DE';
|
||||
$personalData->height = '175';
|
||||
$personalData->weight = '70';
|
||||
$personalData->shoeSize = '40';
|
||||
$personalData->address = new Address();
|
||||
$personalData->communication = new Communication();
|
||||
|
||||
return $personalData;
|
||||
}
|
||||
|
||||
private function createMockService(int $id): Service
|
||||
{
|
||||
$service = new Service();
|
||||
$service->id = $id;
|
||||
$service->price = 50.0;
|
||||
$service->mapping = [];
|
||||
$service->individualPrice = [];
|
||||
$service->subType = 'STANDARD';
|
||||
|
||||
return $service;
|
||||
}
|
||||
|
||||
private function createMockPickup(int $id): Pickup
|
||||
{
|
||||
$pickup = new Pickup();
|
||||
$pickup->id = $id;
|
||||
$pickup->mapping = [];
|
||||
|
||||
return $pickup;
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
namespace BusProNet\Utility;
|
||||
|
||||
use App\BusProNet\Utility\TravelCodeUtility;
|
||||
use App\BusProNet\Utility\DateCodeUtility;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class TravelCodeUtilityTest extends TestCase
|
||||
{
|
||||
public function testSanitizeCode(): void
|
||||
{
|
||||
$utility = new TravelCodeUtility();
|
||||
$utility = new DateCodeUtility();
|
||||
|
||||
$travelCode = 'ABCDEF/010125';
|
||||
$sanitized = $utility->sanitize($travelCode);
|
||||
@@ -26,7 +26,7 @@ class TravelCodeUtilityTest extends TestCase
|
||||
|
||||
public function testGetBaseCode(): void
|
||||
{
|
||||
$utility = new TravelCodeUtility();
|
||||
$utility = new DateCodeUtility();
|
||||
|
||||
$travelCode = 'ABCDEF010125';
|
||||
$baseCode = $utility->getBaseCode($travelCode);
|
||||
@@ -39,7 +39,7 @@ class TravelCodeUtilityTest extends TestCase
|
||||
|
||||
public function testGetDate(): void
|
||||
{
|
||||
$utility = new TravelCodeUtility();
|
||||
$utility = new DateCodeUtility();
|
||||
|
||||
$travelCode = 'ABCDEF010125';
|
||||
$date = $utility->getDate($travelCode);
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Service;
|
||||
|
||||
use App\BusProNet\ApiClient;
|
||||
use App\BusProNet\Model\Travel;
|
||||
use App\BusProNet\XmlLoader\HotelLoader;
|
||||
use App\BusProNet\XmlLoader\PickupLoader;
|
||||
use App\BusProNet\XmlLoader\TravelLoader;
|
||||
use App\Service\TravelDataService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
|
||||
class TravelDataServiceTest extends TestCase
|
||||
{
|
||||
private TravelDataService $service;
|
||||
private TravelLoader $travelLoader;
|
||||
private HotelLoader $hotelLoader;
|
||||
private PickupLoader $pickupLoader;
|
||||
private ApiClient $apiClient;
|
||||
private CacheInterface $cache;
|
||||
private LoggerInterface $logger;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->travelLoader = $this->createMock(TravelLoader::class);
|
||||
$this->hotelLoader = $this->createMock(HotelLoader::class);
|
||||
$this->pickupLoader = $this->createMock(PickupLoader::class);
|
||||
$this->apiClient = $this->createMock(ApiClient::class);
|
||||
$this->cache = $this->createMock(CacheInterface::class);
|
||||
$this->logger = $this->createMock(LoggerInterface::class);
|
||||
|
||||
$this->service = new TravelDataService(
|
||||
$this->travelLoader,
|
||||
$this->hotelLoader,
|
||||
$this->pickupLoader,
|
||||
$this->apiClient,
|
||||
$this->cache,
|
||||
$this->logger,
|
||||
false, // preferRemote
|
||||
true // enableFallback
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetTravelDataFromXmlSuccess(): void
|
||||
{
|
||||
$dateId = 12345;
|
||||
$hotelId = 67890;
|
||||
$travel = new Travel();
|
||||
$travel->id = $dateId;
|
||||
$travel->hotelId = $hotelId;
|
||||
|
||||
$this->travelLoader
|
||||
->expects($this->once())
|
||||
->method('loadById')
|
||||
->with($dateId, $hotelId)
|
||||
->willReturn($travel);
|
||||
|
||||
$this->pickupLoader
|
||||
->expects($this->once())
|
||||
->method('patchPickupsDetails')
|
||||
->with($travel);
|
||||
|
||||
$this->hotelLoader
|
||||
->expects($this->once())
|
||||
->method('patchHotelDetails')
|
||||
->with($travel);
|
||||
|
||||
$result = $this->service->getTravelDataFromXml($dateId, $hotelId);
|
||||
|
||||
$this->assertSame($travel, $result);
|
||||
}
|
||||
|
||||
public function testGetTravelDataFromXmlNotFound(): void
|
||||
{
|
||||
$dateId = 12345;
|
||||
$hotelId = 67890;
|
||||
|
||||
$this->travelLoader
|
||||
->expects($this->once())
|
||||
->method('loadById')
|
||||
->with($dateId, $hotelId)
|
||||
->willReturn(null);
|
||||
|
||||
$this->pickupLoader
|
||||
->expects($this->never())
|
||||
->method('patchPickupsDetails');
|
||||
|
||||
$this->hotelLoader
|
||||
->expects($this->never())
|
||||
->method('patchHotelDetails');
|
||||
|
||||
$result = $this->service->getTravelDataFromXml($dateId, $hotelId);
|
||||
|
||||
$this->assertNull($result);
|
||||
}
|
||||
|
||||
public function testGetTravelDataFromApiSuccess(): void
|
||||
{
|
||||
$dateId = 12345;
|
||||
$hotelId = 67890;
|
||||
$productId = 555;
|
||||
$travel = new Travel();
|
||||
$travel->id = $dateId;
|
||||
$travel->hotelId = $hotelId;
|
||||
|
||||
$this->travelLoader
|
||||
->expects($this->once())
|
||||
->method('mapDateIdToProductId')
|
||||
->with($dateId)
|
||||
->willReturn($productId);
|
||||
|
||||
$this->apiClient
|
||||
->expects($this->once())
|
||||
->method('getTravelData')
|
||||
->with($productId, $hotelId)
|
||||
->willReturn($travel);
|
||||
|
||||
$result = $this->service->getTravelDataFromApi($dateId, $hotelId);
|
||||
|
||||
$this->assertSame($travel, $result);
|
||||
}
|
||||
|
||||
public function testGetTravelDataFromApiCannotMapDateId(): void
|
||||
{
|
||||
$dateId = 12345;
|
||||
$hotelId = 67890;
|
||||
|
||||
$this->travelLoader
|
||||
->expects($this->once())
|
||||
->method('mapDateIdToProductId')
|
||||
->with($dateId)
|
||||
->willReturn(null);
|
||||
|
||||
$this->apiClient
|
||||
->expects($this->never())
|
||||
->method('getTravelData');
|
||||
|
||||
$result = $this->service->getTravelDataFromApi($dateId, $hotelId);
|
||||
|
||||
$this->assertNull($result);
|
||||
}
|
||||
|
||||
public function testExistsInXmlTrue(): void
|
||||
{
|
||||
$dateId = 12345;
|
||||
$hotelId = 67890;
|
||||
$mapping = [
|
||||
$dateId => [
|
||||
'id' => $dateId,
|
||||
'hotels' => [
|
||||
$hotelId => ['id' => $hotelId, 'name' => 'Test Hotel']
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
$this->travelLoader
|
||||
->expects($this->once())
|
||||
->method('generateFilesMap')
|
||||
->willReturn($mapping);
|
||||
|
||||
$result = $this->service->existsInXml($dateId, $hotelId);
|
||||
|
||||
$this->assertTrue($result);
|
||||
}
|
||||
|
||||
public function testExistsInXmlFalseNoTravel(): void
|
||||
{
|
||||
$dateId = 12345;
|
||||
$hotelId = 67890;
|
||||
$mapping = [];
|
||||
|
||||
$this->travelLoader
|
||||
->expects($this->once())
|
||||
->method('generateFilesMap')
|
||||
->willReturn($mapping);
|
||||
|
||||
$result = $this->service->existsInXml($dateId, $hotelId);
|
||||
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
|
||||
public function testExistsInXmlFalseNoHotel(): void
|
||||
{
|
||||
$dateId = 12345;
|
||||
$hotelId = 67890;
|
||||
$mapping = [
|
||||
$dateId => [
|
||||
'id' => $dateId,
|
||||
'hotels' => []
|
||||
]
|
||||
];
|
||||
|
||||
$this->travelLoader
|
||||
->expects($this->once())
|
||||
->method('generateFilesMap')
|
||||
->willReturn($mapping);
|
||||
|
||||
$result = $this->service->existsInXml($dateId, $hotelId);
|
||||
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
|
||||
public function testGetAvailableSources(): void
|
||||
{
|
||||
$dateId = 12345;
|
||||
$hotelId = 67890;
|
||||
$productId = 555;
|
||||
$mapping = [
|
||||
$dateId => [
|
||||
'id' => $dateId,
|
||||
'hotels' => [
|
||||
$hotelId => ['id' => $hotelId, 'name' => 'Test Hotel']
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
$this->travelLoader
|
||||
->expects($this->once())
|
||||
->method('generateFilesMap')
|
||||
->willReturn($mapping);
|
||||
|
||||
$this->travelLoader
|
||||
->expects($this->once())
|
||||
->method('mapDateIdToProductId')
|
||||
->with($dateId)
|
||||
->willReturn($productId);
|
||||
|
||||
$result = $this->service->getAvailableSources($dateId, $hotelId);
|
||||
|
||||
$this->assertEquals([
|
||||
'local' => true,
|
||||
'remote' => true,
|
||||
], $result);
|
||||
}
|
||||
|
||||
public function testGetTravelDataWithCaching(): void
|
||||
{
|
||||
$dateId = 12345;
|
||||
$hotelId = 67890;
|
||||
$travel = new Travel();
|
||||
$travel->id = $dateId;
|
||||
$travel->hotelId = $hotelId;
|
||||
|
||||
$cacheItem = $this->createMock(ItemInterface::class);
|
||||
$cacheItem
|
||||
->expects($this->once())
|
||||
->method('expiresAfter')
|
||||
->with(300);
|
||||
|
||||
$this->cache
|
||||
->expects($this->once())
|
||||
->method('get')
|
||||
->with('travel_unified_12345_67890_local')
|
||||
->willReturnCallback(function (string $key, callable $callback) use ($cacheItem, $travel) {
|
||||
return $callback($cacheItem);
|
||||
});
|
||||
|
||||
$this->travelLoader
|
||||
->expects($this->once())
|
||||
->method('loadById')
|
||||
->with($dateId, $hotelId)
|
||||
->willReturn($travel);
|
||||
|
||||
$this->pickupLoader
|
||||
->expects($this->once())
|
||||
->method('patchPickupsDetails')
|
||||
->with($travel);
|
||||
|
||||
$this->hotelLoader
|
||||
->expects($this->once())
|
||||
->method('patchHotelDetails')
|
||||
->with($travel);
|
||||
|
||||
$result = $this->service->getTravelData($dateId, $hotelId, false, true);
|
||||
|
||||
$this->assertSame($travel, $result);
|
||||
}
|
||||
|
||||
public function testGetTravelDataWithoutCaching(): void
|
||||
{
|
||||
$dateId = 12345;
|
||||
$hotelId = 67890;
|
||||
$travel = new Travel();
|
||||
$travel->id = $dateId;
|
||||
$travel->hotelId = $hotelId;
|
||||
|
||||
$this->cache
|
||||
->expects($this->never())
|
||||
->method('get');
|
||||
|
||||
$this->travelLoader
|
||||
->expects($this->once())
|
||||
->method('loadById')
|
||||
->with($dateId, $hotelId)
|
||||
->willReturn($travel);
|
||||
|
||||
$this->pickupLoader
|
||||
->expects($this->once())
|
||||
->method('patchPickupsDetails')
|
||||
->with($travel);
|
||||
|
||||
$this->hotelLoader
|
||||
->expects($this->once())
|
||||
->method('patchHotelDetails')
|
||||
->with($travel);
|
||||
|
||||
$result = $this->service->getTravelData($dateId, $hotelId, false, false);
|
||||
|
||||
$this->assertSame($travel, $result);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user