wip: booking process, refactoring
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user