wip: initial commit
This commit is contained in:
@@ -0,0 +1,309 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet;
|
||||
|
||||
use App\BusProNet\ApiResponseParser\ResponseParser;
|
||||
use App\BusProNet\Exception\ApiClientException;
|
||||
use App\BusProNet\Model\BaseData;
|
||||
use App\BusProNet\Model\Booking;
|
||||
use App\BusProNet\Model\CrmAttributes;
|
||||
use App\BusProNet\Model\File;
|
||||
use App\BusProNet\Model\Notification;
|
||||
use App\BusProNet\Model\PersonalData;
|
||||
use App\Form\Model\BookingData;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class ApiClient
|
||||
{
|
||||
public const TYPE_NOTIFICATION = 'HINWEIS';
|
||||
public const TYPE_CUSTOMER_DATA = 'KUNDENKONTO';
|
||||
public const TYPE_BASE_DATA_COUNTRIES = 'STAMMLAENDER';
|
||||
public const TYPE_MUTABLE_FIELDS = 'MOEGLICHEAENDERUNGEN';
|
||||
public const TYPE_AVAILABILITY = 'VERFUEGBARKEIT';
|
||||
public const TYPE_BOOKING_UPDATE = 'BUCHUNGAENDERUNG';
|
||||
|
||||
private array $config;
|
||||
|
||||
public function __construct(
|
||||
private readonly HttpClientInterface $httpClient,
|
||||
private readonly SerializerInterface $serializer,
|
||||
private readonly ResponseParser $responseParser,
|
||||
private readonly LoggerInterface $logger,
|
||||
array $options
|
||||
) {
|
||||
$this->config = $this->resolveOptions($options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ApiClientException
|
||||
*/
|
||||
public function getPersonalData(string $email, string $password): Notification|PersonalData
|
||||
{
|
||||
$data = [
|
||||
'anfrage' => [
|
||||
'user' => $this->config['bpn_username'],
|
||||
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_CUSTOMER_DATA),
|
||||
'satz' => ['@typ' => static::TYPE_CUSTOMER_DATA],
|
||||
'art' => 'Adressdaten',
|
||||
'email' => $email,
|
||||
'passwort' => $password,
|
||||
],
|
||||
];
|
||||
|
||||
return $this->sendRequest(static::TYPE_CUSTOMER_DATA, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ApiClientException
|
||||
*/
|
||||
public function updatePersonalData(string $email, string $password, PersonalData $personalData): Notification|PersonalData
|
||||
{
|
||||
$data = [
|
||||
'anfrage' => [
|
||||
'user' => $this->config['bpn_username'],
|
||||
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_CUSTOMER_DATA),
|
||||
'satz' => ['@typ' => static::TYPE_CUSTOMER_DATA],
|
||||
'art' => 'Adressdaten_Ändern',
|
||||
'email' => $email,
|
||||
'passwort' => $password,
|
||||
'idadresse' => $personalData->addressId,
|
||||
'adressdaten' => $personalData->toPayload(),
|
||||
],
|
||||
];
|
||||
|
||||
return $this->sendRequest(static::TYPE_CUSTOMER_DATA, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ApiClientException
|
||||
*/
|
||||
public function getBookings(string $email, string $password): Notification|BaseData
|
||||
{
|
||||
$data = [
|
||||
'anfrage' => [
|
||||
'user' => $this->config['bpn_username'],
|
||||
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_CUSTOMER_DATA),
|
||||
'satz' => ['@typ' => static::TYPE_CUSTOMER_DATA],
|
||||
'art' => 'Vorgänge',
|
||||
'email' => $email,
|
||||
'passwort' => $password,
|
||||
],
|
||||
];
|
||||
|
||||
return $this->sendRequest(static::TYPE_CUSTOMER_DATA, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ApiClientException
|
||||
*/
|
||||
public function getBooking(string $email, string $password, int $id): Notification|Booking
|
||||
{
|
||||
$data = [
|
||||
'anfrage' => [
|
||||
'user' => $this->config['bpn_username'],
|
||||
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_CUSTOMER_DATA),
|
||||
'satz' => ['@typ' => static::TYPE_CUSTOMER_DATA],
|
||||
'art' => 'Vorgang_Details',
|
||||
'email' => $email,
|
||||
'passwort' => $password,
|
||||
'idbuchung' => $id,
|
||||
],
|
||||
];
|
||||
|
||||
return $this->sendRequest(static::TYPE_CUSTOMER_DATA, $data);
|
||||
}
|
||||
|
||||
public function updateBooking(string $email, string $password, BookingData $formData): Notification
|
||||
{
|
||||
$booking = $formData->booking;
|
||||
|
||||
$data = [
|
||||
'anfrage' => [
|
||||
'user' => $this->config['bpn_username'],
|
||||
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_BOOKING_UPDATE),
|
||||
'satz' => ['@typ' => static::TYPE_BOOKING_UPDATE],
|
||||
'buchungsart' => 'Buchung',
|
||||
'idbuchung' => $booking->id,
|
||||
...$booking->toPayload($formData),
|
||||
],
|
||||
];
|
||||
|
||||
return $this->sendRequest(static::TYPE_BOOKING_UPDATE, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ApiClientException
|
||||
*/
|
||||
public function getMutableFields(int $id): Notification|BaseData
|
||||
{
|
||||
$data = [
|
||||
'anfrage' => [
|
||||
'user' => $this->config['bpn_username'],
|
||||
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_MUTABLE_FIELDS),
|
||||
'satz' => ['@typ' => static::TYPE_MUTABLE_FIELDS],
|
||||
'art' => 'Vorgang_Details',
|
||||
'idreise' => $id,
|
||||
],
|
||||
];
|
||||
|
||||
return $this->sendRequest(static::TYPE_MUTABLE_FIELDS, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ApiClientException
|
||||
*/
|
||||
public function getAvailabilities(int $id): Notification|BaseData
|
||||
{
|
||||
$data = [
|
||||
'anfrage' => [
|
||||
'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' => $id,
|
||||
],
|
||||
];
|
||||
|
||||
return $this->sendRequest(static::TYPE_AVAILABILITY, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ApiClientException
|
||||
*/
|
||||
public function resetPassword(string $email): Notification
|
||||
{
|
||||
$data = [
|
||||
'anfrage' => [
|
||||
'user' => $this->config['bpn_username'],
|
||||
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_CUSTOMER_DATA),
|
||||
'satz' => ['@typ' => static::TYPE_CUSTOMER_DATA],
|
||||
'art' => 'Passwort_Anfrage',
|
||||
'email' => $email,
|
||||
],
|
||||
];
|
||||
|
||||
return $this->sendRequest(static::TYPE_CUSTOMER_DATA, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ApiClientException
|
||||
*/
|
||||
public function getCrmAttributes(string $email, string $password): Notification|CrmAttributes
|
||||
{
|
||||
$data = [
|
||||
'anfrage' => [
|
||||
'user' => $this->config['bpn_username'],
|
||||
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_CUSTOMER_DATA),
|
||||
'satz' => ['@typ' => static::TYPE_CUSTOMER_DATA],
|
||||
'art' => 'SelektionCRM',
|
||||
'email' => $email,
|
||||
'passwort' => $password,
|
||||
],
|
||||
];
|
||||
|
||||
return $this->sendRequest(static::TYPE_CUSTOMER_DATA, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ApiClientException
|
||||
*/
|
||||
public function getBaseData(string $type): Notification|BaseData
|
||||
{
|
||||
$data = [
|
||||
'anfrage' => [
|
||||
'user' => $this->config['bpn_username'],
|
||||
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], $type),
|
||||
'satz' => ['@typ' => $type],
|
||||
],
|
||||
];
|
||||
|
||||
return $this->sendRequest($type, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Notification|File|null
|
||||
* @throws ApiClientException
|
||||
*/
|
||||
public function getDocuments(string $email, string $password, int $id, string $type): mixed
|
||||
{
|
||||
$data = [
|
||||
'anfrage' => [
|
||||
'user' => $this->config['bpn_username'],
|
||||
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_CUSTOMER_DATA),
|
||||
'satz' => ['@typ' => static::TYPE_CUSTOMER_DATA],
|
||||
'art' => $type,
|
||||
'email' => $email,
|
||||
'passwort' => $password,
|
||||
'idbuchung' => $id,
|
||||
],
|
||||
];
|
||||
|
||||
return $this->sendRequest(static::TYPE_CUSTOMER_DATA, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ApiClientException
|
||||
*/
|
||||
private function sendRequest(string $type, array $data): mixed
|
||||
{
|
||||
$requestId = (string) Uuid::v7();
|
||||
|
||||
$body = $this
|
||||
->serializer
|
||||
->serialize($data, 'xml')
|
||||
;
|
||||
|
||||
if (true === $this->config['debug']) {
|
||||
$this->logger->info('Request sent', [
|
||||
'id' => $requestId,
|
||||
'request' => $body,
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $this->httpClient->request('GET', $this->config['bpn_url'], [
|
||||
'query' => [
|
||||
'operation' => $body,
|
||||
],
|
||||
'verify_peer' => false,
|
||||
'verify_host' => false,
|
||||
]);
|
||||
|
||||
$xml = $response->getContent();
|
||||
|
||||
if (true === $this->config['debug']) {
|
||||
$this->logger->info('Response received', [
|
||||
'id' => $requestId,
|
||||
'response' => $xml,
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->responseParser->parseXmlString($type, $xml);
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
|
||||
$this->logger->error('API error', ['error' => $e->getMessage()]);
|
||||
throw new ApiClientException($e->getMessage());
|
||||
}
|
||||
|
||||
private function createKey(string $username, string $password, string $type): string
|
||||
{
|
||||
$date = (new \DateTimeImmutable())->format('Ymd');
|
||||
|
||||
return md5($username.$password.$date.$type);
|
||||
}
|
||||
|
||||
private function resolveOptions(array $options): array
|
||||
{
|
||||
$optionsResolver = new OptionsResolver();
|
||||
$optionsResolver->setRequired(['bpn_url', 'bpn_username', 'bpn_password']);
|
||||
$optionsResolver->setDefaults([
|
||||
'debug' => false,
|
||||
]);
|
||||
|
||||
return $optionsResolver->resolve($options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\ApiResponseParser;
|
||||
|
||||
use App\BusProNet\Model\Availability;
|
||||
use App\BusProNet\Model\BaseData;
|
||||
|
||||
class AvailabilitiesResponseParser
|
||||
{
|
||||
use ResponseParserTrait;
|
||||
|
||||
public function parse(\SimpleXMLElement $xml): BaseData
|
||||
{
|
||||
$availabilities = [];
|
||||
|
||||
foreach ($xml->leistungen->leistung as $item) {
|
||||
$attributes = $item->attributes();
|
||||
$id = (int) $attributes['id'];
|
||||
|
||||
$availability = new Availability();
|
||||
$availability->serviceId = $id;
|
||||
$availability->status = (string) $attributes['status'];
|
||||
$availability->available = (int) $attributes['frei'];
|
||||
$availability->price = $this->stringToFloat((string) $attributes['preis']);
|
||||
|
||||
$availabilities[$id] = $availability;
|
||||
}
|
||||
|
||||
return new BaseData($availabilities);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\ApiResponseParser;
|
||||
|
||||
use App\BusProNet\Model\Address;
|
||||
use App\BusProNet\Model\Booking;
|
||||
use App\BusProNet\Model\Communication;
|
||||
use App\BusProNet\Model\PersonalData;
|
||||
use App\BusProNet\Model\Room;
|
||||
use App\BusProNet\Model\Service;
|
||||
|
||||
class BookingResponseParser
|
||||
{
|
||||
use ResponseParserTrait;
|
||||
|
||||
public function parse(\SimpleXMLElement $xml): Booking
|
||||
{
|
||||
$travelData = $xml->reise;
|
||||
|
||||
$booking = new Booking();
|
||||
|
||||
$booking->id = (int) $xml->idbuchung;
|
||||
$booking->bookingNumber = (int) $xml->vorgang;
|
||||
$booking->invoiceNumber = (int) $xml->zahlungsdaten->rechnung;
|
||||
$booking->totalPrice = $this->stringToFloat((string) $xml->zahlungsdaten->gesamtbetrag);
|
||||
$booking->status = (string) $xml->status;
|
||||
|
||||
$booking->travel = (string) $travelData->attributes()['bezeichnung'];
|
||||
$booking->travelId = (int) $xml->idreise;
|
||||
$booking->travelCode = (string) $travelData->attributes()['code'];
|
||||
$booking->travelDate = $this->stringToDate((string) $travelData->attributes()['termin']);
|
||||
|
||||
$booking->hotelId = (int) $xml->idpartner;
|
||||
$booking->hotelName = (string) $xml->partner;
|
||||
|
||||
$booking->applicant = $this->parsePersonalData($xml->anmelder);
|
||||
|
||||
$booking->participantsStatus = $this->stringToArray((string) $xml->status_teilnehmer, '/');
|
||||
$booking->participants = $this->parseParticipants($xml);
|
||||
|
||||
if ($xml->beförderungen) {
|
||||
$booking->transportationServices = $this
|
||||
->parseServices($xml->beförderungen->beförderung, Service::TYPE_TRANSPORTATION);
|
||||
}
|
||||
if ($xml->zusatzleistungen) {
|
||||
$booking->additionalServices = $this
|
||||
->parseServices($xml->zusatzleistungen->zusatzleistung, Service::TYPE_ADDITIONAL);
|
||||
}
|
||||
if ($xml->ferienzielunterbringungen) {
|
||||
$booking->rooms = $this->parseRooms($xml->ferienzielunterbringungen->ferienzielunterbringung);
|
||||
}
|
||||
|
||||
return $booking;
|
||||
}
|
||||
|
||||
private function parseParticipants(\SimpleXMLElement $xml): array
|
||||
{
|
||||
$participants = [];
|
||||
|
||||
foreach ($xml->teilnehmerliste->teilnehmer as $participant) {
|
||||
$id = (int) $participant->attributes()['id'];
|
||||
$participants[$id] = $this->parsePersonalData($participant);
|
||||
}
|
||||
|
||||
return $participants;
|
||||
}
|
||||
|
||||
private function parsePersonalData(\SimpleXMLElement $xml): PersonalData
|
||||
{
|
||||
$personalData = new PersonalData();
|
||||
$personalData->addressId = $xml->idadresse ? (int) $xml->idadresse : null;
|
||||
$personalData->personId = (int) $xml->idadresseperson;
|
||||
$personalData->name = (string) $xml->name;
|
||||
$personalData->firstName = (string) $xml->vorname;
|
||||
$personalData->salutation = (string) $xml->anrede;
|
||||
$personalData->title = (string) $xml->titel;
|
||||
$personalData->gender = $xml->geschlecht ? strtoupper((string) $xml->geschlecht) : null;
|
||||
$personalData->dateOfBirth = $xml->geburtsdatum ? $this->stringToDate((string) $xml->geburtsdatum) : null;
|
||||
$personalData->nationality = (string) $xml->nationalitaet;
|
||||
$personalData->height = $xml->sonstiges1 ? (int) $xml->sonstiges1 : null;
|
||||
$personalData->weight = $xml->sonstiges2 ? (int) $xml->sonstiges2 : null;
|
||||
$personalData->shoeSize = $xml->sonstiges3 ? (int) $xml->sonstiges3 : null;
|
||||
|
||||
if ($xml->anschrift) {
|
||||
$address = new Address();
|
||||
$address->street = (string) $xml->anschrift->strasse;
|
||||
$address->postCode = (string) $xml->anschrift->plz;
|
||||
$address->city = (string) $xml->anschrift->ort;
|
||||
$address->district = (string) $xml->anschrift->ortsteil;
|
||||
$address->country = (string) $xml->anschrift->land;
|
||||
|
||||
$personalData->address = $address;
|
||||
}
|
||||
|
||||
if ($xml->kommunikation) {
|
||||
$communication = new Communication();
|
||||
$communication->email = (string) $xml->kommunikation->email;
|
||||
$communication->phone = (string) $xml->kommunikation->telefonprivat;
|
||||
$communication->mobile = (string) $xml->kommunikation->telefonmobil;
|
||||
|
||||
$personalData->communication = $communication;
|
||||
}
|
||||
|
||||
return $personalData;
|
||||
}
|
||||
|
||||
private function parseServices(\SimpleXMLElement $xml, string $type): array
|
||||
{
|
||||
$services = [];
|
||||
|
||||
foreach ($xml as $service) {
|
||||
$attributes = $service->attributes();
|
||||
$id = (int) $attributes['idleistung'];
|
||||
|
||||
$service = new Service();
|
||||
$service->id = $id;
|
||||
$service->label = (string) $attributes['leistung'];
|
||||
$service->dateFrom = $attributes['termin'] ? $this->stringToDate((string) $attributes['termin']) : null;
|
||||
$service->dateTo = $attributes['terminbis'] ? $this->stringToDate((string) $attributes['terminbis']) : null;
|
||||
$service->subType = (string) $attributes['unterart'];
|
||||
$service->totalCount = (int) $attributes['anzahl'];
|
||||
$mapping = $this->stringToArray((string) $attributes['zuordnung']);
|
||||
$service->mapping = array_map('intval', $mapping);
|
||||
$service->totalPrice = $this->stringToFloat((string) $attributes['gesamtpreis']);
|
||||
$individualPrices = array_map(
|
||||
function ($price) {
|
||||
return $this->stringToFloat((string) $price);
|
||||
}, $this->stringToArray((string) $attributes['einzelpreis'], '/')
|
||||
);
|
||||
$service->individualPrice = $this->arrayToOneBased($individualPrices);
|
||||
if (Service::TYPE_TRANSPORTATION === $type) {
|
||||
$service->direction = (string) $attributes['richtung'];
|
||||
}
|
||||
|
||||
$services[$id] = $service;
|
||||
}
|
||||
|
||||
return $services;
|
||||
}
|
||||
|
||||
private function parseRooms(\SimpleXMLElement $xml): array
|
||||
{
|
||||
$rooms = [];
|
||||
|
||||
foreach ($xml as $item) {
|
||||
$attributes = $item->attributes();
|
||||
$id = (int) $attributes['idzimmer'];
|
||||
|
||||
$room = new Room();
|
||||
$room->id = $id;
|
||||
$room->label = (string) $attributes['zimmer'];
|
||||
$room->dateFrom = $attributes['termin'] ? $this->stringToDate((string) $attributes['anreise']) : null;
|
||||
$room->dateTo = $attributes['terminbis'] ? $this->stringToDate((string) $attributes['abreise']) : null;
|
||||
$room->totalCount = (int) $attributes['anzahl'];
|
||||
$room->minPax = (int) $attributes['minpax'];
|
||||
$room->maxPax = (int) $attributes['maxpax'];
|
||||
$room->board = (string) $attributes['verpflegung'];
|
||||
$mapping = $this->stringToArray((string) $attributes['zuordnung']);
|
||||
$room->mapping = array_map('intval', $mapping);
|
||||
$room->totalPrice = $this->stringToFloat((string) $attributes['gesamtpreis']);
|
||||
$individualPrices = array_map(
|
||||
function ($price) {
|
||||
return $this->stringToFloat((string) $price);
|
||||
}, $this->stringToArray((string) $attributes['einzelpreis'], '/')
|
||||
);
|
||||
$room->individualPrice = $this->arrayToOneBased($individualPrices);
|
||||
$rooms[$id] = $room;
|
||||
}
|
||||
|
||||
return $rooms;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\ApiResponseParser;
|
||||
|
||||
use App\BusProNet\Model\BaseData;
|
||||
use App\BusProNet\Model\Booking;
|
||||
|
||||
class BookingsResponseParser
|
||||
{
|
||||
use ResponseParserTrait;
|
||||
|
||||
public function parse(\SimpleXMLElement $xml): BaseData
|
||||
{
|
||||
$bookings = [];
|
||||
|
||||
foreach ($xml->vorgaenge->vorgang as $item) {
|
||||
$booking = new Booking();
|
||||
|
||||
$booking->id = (int) $item->id;
|
||||
$booking->bookingNumber = (int) $item->vorgangsnummer;
|
||||
$booking->status = (string) $item->status;
|
||||
$booking->participantCount = (int) $item->personen;
|
||||
$booking->price = $this->stringToFloat((string) $item->preis);
|
||||
$booking->bookingDate = $this->stringToDateTime((string) $item->buchungsdatum);
|
||||
$booking->travel = (string) $item->reise;
|
||||
$booking->travelDate = $this->stringToDate((string) $item->reisedatum);
|
||||
$booking->document = $this->stringToBool((string) $item->reisedokument);
|
||||
$booking->payment = $this->stringToFloat((string) $item->zahlung);
|
||||
|
||||
$bookings[] = $booking;
|
||||
}
|
||||
|
||||
return new BaseData($bookings);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\ApiResponseParser;
|
||||
|
||||
use App\BusProNet\Model\BaseData;
|
||||
use App\BusProNet\Model\Country;
|
||||
|
||||
class CountriesResponseParser
|
||||
{
|
||||
public function parse(\SimpleXMLElement $xml): BaseData
|
||||
{
|
||||
$countries = [];
|
||||
|
||||
foreach ($xml->laender->land as $item) {
|
||||
$itemAttributes = $item->attributes();
|
||||
$token = (string) $itemAttributes['kuerzel'];
|
||||
$country = new Country();
|
||||
$country->id = (int) $itemAttributes['id'];
|
||||
$country->name = (string) $itemAttributes['bezeichnung'];
|
||||
$country->token = (string) $itemAttributes['kuerzel'];
|
||||
$country->nationality = (string) $itemAttributes['nationalitaet'];
|
||||
|
||||
$countries[$token] = $country;
|
||||
}
|
||||
|
||||
return new BaseData($countries);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\ApiResponseParser;
|
||||
|
||||
use App\BusProNet\Model\CrmAttribute;
|
||||
use App\BusProNet\Model\CrmAttributeGroup;
|
||||
use App\BusProNet\Model\CrmAttributes;
|
||||
|
||||
class CrmAttributesResponseParser
|
||||
{
|
||||
use ResponseParserTrait;
|
||||
|
||||
private const BPN_CRM_ID_ADMIN = 1292;
|
||||
private const BPN_CRM_ID_MANAGER = 1293;
|
||||
private const BPN_CRM_ID_TEAMER = 1070;
|
||||
|
||||
public function parse(\SimpleXMLElement $xml): CrmAttributes
|
||||
{
|
||||
$groups = [];
|
||||
$isAdmin = $isManager = $isHouseManager = $isTeamer = false;
|
||||
$hotelCode = null;
|
||||
|
||||
foreach ($xml->selektionsmerkmale->selektionsgruppe as $item) {
|
||||
$group = new CrmAttributeGroup();
|
||||
$group->label = (string) $item->attributes()['bezeichnung'];
|
||||
$attributes = [];
|
||||
|
||||
foreach ($item->selektion as $subItem) {
|
||||
$subItemAttributes = $subItem->attributes();
|
||||
$attributeLabel = (string) $subItemAttributes['bezeichnung'];
|
||||
$attribute = new CrmAttribute();
|
||||
$attribute->id = (int) $subItemAttributes['id'];
|
||||
$attribute->label = $attributeLabel;
|
||||
$attribute->selected = $this->stringToBool((string) $subItemAttributes['auswahl']);
|
||||
|
||||
if (1 === preg_match('/^Hausleitung ([A-Z0-9]+)$/', $attributeLabel, $matches) && true === $attribute->selected) {
|
||||
$isHouseManager = true;
|
||||
$hotelCode = $matches[1];
|
||||
}
|
||||
if (static::BPN_CRM_ID_ADMIN === $attribute->id && true === $attribute->selected) {
|
||||
$isAdmin = true;
|
||||
}
|
||||
if (static::BPN_CRM_ID_MANAGER === $attribute->id && true === $attribute->selected) {
|
||||
$isManager = true;
|
||||
}
|
||||
if (static::BPN_CRM_ID_TEAMER === $attribute->id && true === $attribute->selected) {
|
||||
$isTeamer = true;
|
||||
}
|
||||
|
||||
$attributes[] = $attribute;
|
||||
}
|
||||
$group->attributes = $attributes;
|
||||
$groups[] = $group;
|
||||
}
|
||||
|
||||
$response = new CrmAttributes();
|
||||
$response->attributeGroups = $groups;
|
||||
$response->admin = $isAdmin;
|
||||
$response->manager = $isManager;
|
||||
$response->houseManager = $isHouseManager;
|
||||
$response->teamer = $isTeamer;
|
||||
$response->hotelCode = $hotelCode;
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\ApiResponseParser;
|
||||
|
||||
use App\BusProNet\Model\File;
|
||||
use PhpZip\Exception\ZipException;
|
||||
use PhpZip\ZipFile;
|
||||
|
||||
class DocumentsResponseParser
|
||||
{
|
||||
public function parseConfirmation(\SimpleXMLElement $xml): ?File
|
||||
{
|
||||
$item = (string) $xml->bestaetigungpdf;
|
||||
[, $pdfData] = explode(',', $item);
|
||||
|
||||
return new File('bestaetigung.pdf', base64_decode($pdfData), 'application/pdf');
|
||||
}
|
||||
|
||||
public function parseDocuments(\SimpleXMLElement $xml): ?File
|
||||
{
|
||||
if (1 === count($xml->reisedokumente->reisedokument)) {
|
||||
$item = $xml->reisedokumente->reisedokument[0];
|
||||
[, $pdfData] = explode(',', $item->pdf);
|
||||
|
||||
return new File((string) $item->datei.'.pdf', base64_decode($pdfData), 'application/pdf');
|
||||
}
|
||||
|
||||
// In case of multiple documents create ZIP file
|
||||
try {
|
||||
$zip = new ZipFile();
|
||||
|
||||
foreach ($xml->reisedokumente->reisedokument as $item) {
|
||||
[, $pdfData] = explode(',', $item->pdf);
|
||||
$zip->addFromString($item->datei.'.pdf', base64_decode($pdfData));
|
||||
}
|
||||
|
||||
return new File('reisedokumente.zip', $zip->outputAsString(), 'application/zip');
|
||||
}
|
||||
catch (ZipException $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\ApiResponseParser;
|
||||
|
||||
use App\BusProNet\Model\BaseData;
|
||||
use App\BusProNet\Model\MutableField;
|
||||
|
||||
class MutableFieldsResponseParser
|
||||
{
|
||||
use ResponseParserTrait;
|
||||
|
||||
public function parse(\SimpleXMLElement $xml): BaseData
|
||||
{
|
||||
$mutableFields = [];
|
||||
|
||||
foreach ($xml->änderungen->änderung as $item) {
|
||||
$attributes = $item->attributes();
|
||||
|
||||
$type = match ((string) $attributes['art']) {
|
||||
'anzahl_teilnehmer' => 'participant_count',
|
||||
'beförderung' => 'transportation',
|
||||
'zustieg' => 'pickup',
|
||||
'unterbringung' => 'accommodation',
|
||||
'zusatzleistung' => 'services',
|
||||
'teilnehmerdaten' => 'participant_data',
|
||||
default => false,
|
||||
};
|
||||
|
||||
if (false === $type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$mutableField = new MutableField($type, $this->stringToBool((string) $attributes['möglich']));
|
||||
|
||||
if ($attributes['möglichbiszum']) {
|
||||
$mutableField->mutableBefore = $this->stringToDate((string) $attributes['möglichbiszum']);
|
||||
}
|
||||
|
||||
$mutableFields[$type] = $mutableField;
|
||||
}
|
||||
|
||||
return new BaseData($mutableFields);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\ApiResponseParser;
|
||||
|
||||
use App\BusProNet\Model\Notification;
|
||||
|
||||
class NotificationResponseParser
|
||||
{
|
||||
public function parse(\SimpleXMLElement $xml): Notification
|
||||
{
|
||||
$recordXml = $xml->satz;
|
||||
|
||||
return new Notification((int) $recordXml->nr, (string) $recordXml->text);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\ApiResponseParser;
|
||||
|
||||
use App\BusProNet\Model\Address;
|
||||
use App\BusProNet\Model\Communication;
|
||||
use App\BusProNet\Model\PersonalData;
|
||||
|
||||
class PersonalDataResponseParser
|
||||
{
|
||||
public function parse(\SimpleXMLElement $xml): PersonalData
|
||||
{
|
||||
$addressXml = $xml->adressdaten;
|
||||
|
||||
$dateString = (string) $addressXml->geburtsdatum;
|
||||
$dateOfBirth = empty($dateString) ? null : \DateTimeImmutable::createFromFormat('d.m.Y', $dateString);
|
||||
|
||||
$personalData = new PersonalData();
|
||||
|
||||
$personalData->addressId = (int) $xml->idadresse;
|
||||
$personalData->personId = (int) $xml->idperson;
|
||||
$personalData->firstName = (string) $addressXml->vorname;
|
||||
$personalData->name = (string) $addressXml->name;
|
||||
$personalData->salutation = (string) $addressXml->anrede;
|
||||
$personalData->title = (string) $addressXml->titel;
|
||||
$personalData->gender = strtoupper((string) $addressXml->geschlecht);
|
||||
$personalData->dateOfBirth = $dateOfBirth;
|
||||
|
||||
$personalData->height = $xml->sonstiges1 ? (int) $xml->sonstiges1 : null;
|
||||
$personalData->weight = $xml->sonstiges2 ? (int) $xml->sonstiges2 : null;
|
||||
$personalData->shoeSize = $xml->sonstiges3 ? (int) $xml->sonstiges3 : null;
|
||||
|
||||
$postalXml = $addressXml->anschrift;
|
||||
|
||||
$address = new Address();
|
||||
$address->street = (string) $postalXml->strasse;
|
||||
$address->postCode = (string) $postalXml->plz;
|
||||
$address->city = (string) $postalXml->ort;
|
||||
$address->country = (string) $postalXml->land;
|
||||
|
||||
$personalData->address = $address;
|
||||
|
||||
$contactXml = $addressXml->kommunikation;
|
||||
|
||||
$communication = new Communication();
|
||||
$communication->phone = (string) $contactXml->telefonprivat;
|
||||
$communication->mobile = (string) $contactXml->telefonmobil;
|
||||
$communication->email = (string) $contactXml->email;
|
||||
|
||||
$personalData->communication = $communication;
|
||||
|
||||
return $personalData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\ApiResponseParser;
|
||||
|
||||
use App\BusProNet\ApiClient;
|
||||
use App\BusProNet\Exception\ResponseParserException;
|
||||
|
||||
class ResponseParser
|
||||
{
|
||||
/**
|
||||
* @throws ResponseParserException
|
||||
*/
|
||||
public function parseXmlString(string $type, string $content): mixed
|
||||
{
|
||||
$xml = simplexml_load_string($content);
|
||||
|
||||
// Override type when present in XML to catch error responses
|
||||
$responseType = $type;
|
||||
$responseTypeXml = $xml->xpath('satz/@typ');
|
||||
if (0 < count($responseTypeXml)) {
|
||||
$responseType = (string) $xml->xpath('satz/@typ')[0];
|
||||
}
|
||||
|
||||
switch ($responseType) {
|
||||
case ApiClient::TYPE_NOTIFICATION:
|
||||
return (new NotificationResponseParser())->parse($xml);
|
||||
case ApiClient::TYPE_CUSTOMER_DATA:
|
||||
$subType = (string) $xml->art;
|
||||
switch ($subType) {
|
||||
case 'Adressdaten':
|
||||
case 'Adressdaten_Ändern':
|
||||
return (new PersonalDataResponseParser())->parse($xml);
|
||||
case 'SelektionCRM':
|
||||
case 'SelektionCRM_Ändern':
|
||||
return (new CrmAttributesResponseParser())->parse($xml);
|
||||
case 'Vorgänge':
|
||||
return (new BookingsResponseParser())->parse($xml);
|
||||
case 'Vorgang_Details':
|
||||
return (new BookingResponseParser())->parse($xml);
|
||||
case 'Dokumentdruck':
|
||||
return (new DocumentsResponseParser())->parseDocuments($xml);
|
||||
case 'Vorgangdruck':
|
||||
return (new DocumentsResponseParser())->parseConfirmation($xml);
|
||||
}
|
||||
break;
|
||||
case ApiClient::TYPE_BASE_DATA_COUNTRIES:
|
||||
return (new CountriesResponseParser())->parse($xml);
|
||||
case ApiClient::TYPE_MUTABLE_FIELDS:
|
||||
return (new MutableFieldsResponseParser())->parse($xml);
|
||||
case ApiClient::TYPE_AVAILABILITY:
|
||||
return (new AvailabilitiesResponseParser())->parse($xml);
|
||||
}
|
||||
|
||||
throw new ResponseParserException('Unable to parse XML response');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\ApiResponseParser;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Carbon\Exceptions\InvalidFormatException;
|
||||
|
||||
trait ResponseParserTrait
|
||||
{
|
||||
private function arrayToOneBased(array $data): array
|
||||
{
|
||||
$keys = range(1, count($data));
|
||||
|
||||
return array_combine($keys, $data);
|
||||
}
|
||||
|
||||
protected function stringToArray(string $string, string $separator = ','): array
|
||||
{
|
||||
return explode($separator, $string);
|
||||
}
|
||||
|
||||
protected function stringToFloat(string $string): float
|
||||
{
|
||||
return (float) str_replace(['.', ','], ['', '.'], $string);
|
||||
}
|
||||
|
||||
protected function stringToBool(string $string): bool
|
||||
{
|
||||
return 'True' === $string;
|
||||
}
|
||||
|
||||
protected function stringToDate(string $string): ?\DateTimeImmutable
|
||||
{
|
||||
try {
|
||||
$date = Carbon::createFromFormat('d.m.Y', $string)->startOfDay();
|
||||
return $date->toDateTimeImmutable();
|
||||
} catch (InvalidFormatException $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected function stringToDateTime(string $string): ?\DateTimeImmutable
|
||||
{
|
||||
try {
|
||||
$dateTime = Carbon::createFromFormat('d.m.Y H:i', substr($string, 0, 16))->startOfDay();
|
||||
return $dateTime->toDateTimeImmutable();
|
||||
} catch (InvalidFormatException $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\DataLoader;
|
||||
|
||||
use App\BusProNet\ApiResponseParser\ResponseParserTrait;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
|
||||
abstract class AbstractDataLoader
|
||||
{
|
||||
use ResponseParserTrait;
|
||||
|
||||
public function __construct(protected readonly CacheInterface $cache, protected readonly ?string $xmlPath = null)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\DataLoader;
|
||||
|
||||
use App\BusProNet\Model\Hotel;
|
||||
use Psr\Cache\InvalidArgumentException;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
|
||||
class HotelDataLoader extends AbstractDataLoader
|
||||
{
|
||||
public function loadAll(?string $filename = 'hotel.xml'): array
|
||||
{
|
||||
try {
|
||||
return $this->cache->get('bpn_hotels', function (ItemInterface $item) use ($filename) {
|
||||
$item->expiresAfter(3600);
|
||||
|
||||
$xml = $this->loadXml($filename);
|
||||
|
||||
$hotels = [];
|
||||
|
||||
foreach ($xml->hotel as $hotel) {
|
||||
$id = (int)$hotel->attributes()['idbuspro'];
|
||||
$hotels[$id] = $this->parseXml($hotel);
|
||||
}
|
||||
|
||||
return $hotels;
|
||||
});
|
||||
} catch (InvalidArgumentException $e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public function loadById(int $id, ?string $filename = 'hotel.xml'): ?Hotel
|
||||
{
|
||||
$hotels = $this->loadAll($filename);
|
||||
|
||||
return $hotels[$id] ?? null;
|
||||
}
|
||||
|
||||
public function loadByCode(string $code, ?string $filename = 'hotel.xml'): ?Hotel
|
||||
{
|
||||
$hotels = $this->loadAll($filename);
|
||||
|
||||
foreach ($hotels as $hotel) {
|
||||
if ($code === $hotel->code) {
|
||||
return $hotel;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function loadXml(?string $filename = 'hotel.xml'): \SimpleXMLElement
|
||||
{
|
||||
$filepath = $this->xmlPath.'/'.$filename;
|
||||
|
||||
return simplexml_load_file($filepath);
|
||||
}
|
||||
|
||||
public function parseXml(\SimpleXMLElement $xml): Hotel
|
||||
{
|
||||
$attributes = $xml->attributes();
|
||||
|
||||
$hotel = new Hotel();
|
||||
$hotel->id = (int) $attributes['idbuspro'];
|
||||
$hotel->code = (string) $attributes['code'];
|
||||
$hotel->name = (string) $xml->name;
|
||||
$hotel->city = $xml->ort ? (string) $xml->ort : null;
|
||||
$hotel->country = (string) $xml->land;
|
||||
$hotel->street = (string) $xml->strasse;
|
||||
$hotel->phone = $xml->telefon ? (string) $xml->telefon : null;
|
||||
|
||||
return $hotel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\DataLoader;
|
||||
|
||||
use App\BusProNet\Model\Pickup;
|
||||
use App\BusProNet\Model\Travel;
|
||||
use Psr\Cache\InvalidArgumentException;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
|
||||
class PickupDataLoader extends AbstractDataLoader
|
||||
{
|
||||
public function loadAll(?string $filename = 'zustiege.xml'): array
|
||||
{
|
||||
try {
|
||||
return $this->cache->get('bpn_pickups', function (ItemInterface $item) use ($filename) {
|
||||
$item->expiresAfter(3600);
|
||||
|
||||
$xml = simplexml_load_file($this->xmlPath . '/' . $filename);
|
||||
|
||||
$pickups = [];
|
||||
|
||||
foreach ($xml->zustieg as $pickup) {
|
||||
$id = (int)$pickup->attributes()['idbuspro'];
|
||||
$pickups[$id] = $this->parseXml($pickup);
|
||||
}
|
||||
|
||||
return $pickups;
|
||||
});
|
||||
} catch (InvalidArgumentException $e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public function loadById(int $id, ?string $filename = 'zustiege.xml'): ?Pickup
|
||||
{
|
||||
$pickups = $this->loadAll($filename);
|
||||
|
||||
return $pickups[$id] ?? null;
|
||||
}
|
||||
|
||||
public function parseXml(\SimpleXMLElement $xml): Pickup
|
||||
{
|
||||
$attributes = $xml->attributes();
|
||||
|
||||
$pickup = new Pickup();
|
||||
$pickup->id = (int) $attributes['idbuspro'];
|
||||
$pickup->code = (string) $attributes['code'];
|
||||
$pickup->city = (string) $xml->ort;
|
||||
$pickup->postalCode = (string) $xml->plz;
|
||||
$pickup->street = (string) $xml->strasse;
|
||||
|
||||
return $pickup;
|
||||
}
|
||||
|
||||
public function enrichPickupsData(Travel $travel): void
|
||||
{
|
||||
foreach ($travel->pickups as $pickupId => $pickup) {
|
||||
$pickupData = $this->loadById($pickupId);
|
||||
|
||||
$pickup->code = $pickupData->code;
|
||||
$pickup->postalCode = $pickupData->postalCode;
|
||||
$pickup->city = $pickupData->city;
|
||||
$pickup->street = $pickupData->street;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\DataLoader;
|
||||
|
||||
use App\BusProNet\Model\BaseData;
|
||||
use App\BusProNet\Model\CrmAttribute;
|
||||
use App\BusProNet\Model\CrmAttributeGroup;
|
||||
use App\BusProNet\Model\Pickup;
|
||||
use App\BusProNet\Model\Room;
|
||||
use App\BusProNet\Model\Service;
|
||||
use App\BusProNet\Model\Travel;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
|
||||
class TravelDataLoader extends AbstractDataLoader
|
||||
{
|
||||
public function loadById(int $id, ?string $filename = null): ?Travel
|
||||
{
|
||||
if (null !== $filename) {
|
||||
return $this->loadXml($id, $filename);
|
||||
}
|
||||
|
||||
$finder = new Finder();
|
||||
$finder->files()->in($this->xmlPath)->name('Ziel_*.xml');
|
||||
|
||||
foreach ($finder as $file) {
|
||||
if (null !== $xml = $this->loadXml($id, $file->getRealPath())) {
|
||||
return $xml;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function loadXml(int $id, string $filename): ?Travel
|
||||
{
|
||||
$xml = simplexml_load_file($filename);
|
||||
|
||||
foreach ($xml->reise->termin as $travel) {
|
||||
if ($id === (int) $travel->attributes()['idbuspro']) {
|
||||
return $this->parseXml($travel);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function parseXml(\SimpleXMLElement $xml): Travel
|
||||
{
|
||||
$attributes = $xml->attributes();
|
||||
|
||||
$travel = new Travel();
|
||||
$travel->id = (int) $attributes['idbuspro'];
|
||||
$travel->label = (string) $xml->text;
|
||||
$travel->dateFrom = $this->stringToDate((string) $attributes['termin']);
|
||||
$travel->dateTo = $this->stringToDate((string) $attributes['bis']);
|
||||
$travel->code = (string) $attributes['code'];
|
||||
$travel->type = (string) $attributes['reiseart'];
|
||||
$travel->priceFrom = $this->stringToFloat((string) $xml->abpreis);
|
||||
$travel->selectionGroups = $this->getSelectionGroups($xml);
|
||||
$travel->additionalServices = $this->getAdditionalServices($xml);
|
||||
$travel->transportationServices = $this->getTransportationServices($xml);
|
||||
$travel->rooms = $this->getRooms($xml);
|
||||
$travel->pickups = $this->getPickups($xml);
|
||||
|
||||
return $travel;
|
||||
}
|
||||
|
||||
public function getSelectionGroups(\SimpleXMLElement $xml): array
|
||||
{
|
||||
$selectionGroups = [];
|
||||
|
||||
foreach ($xml->selektiongruppe as $item) {
|
||||
$groupId = (int) $item->attributes()['idbuspro'];
|
||||
$selectionGroup = new CrmAttributeGroup();
|
||||
$selectionGroup->id = $groupId;
|
||||
$selectionGroup->label = (string) $item->attributes()['bezeichnung'];
|
||||
|
||||
foreach ($item->selektion as $subItem) {
|
||||
$selectionId = (int) $subItem->attributes()['idbuspro'];
|
||||
$selection = new CrmAttribute();
|
||||
$selection->id = $selectionId;
|
||||
$selection->label = (string) $subItem->attributes()['bezeichnung'];
|
||||
$selectionGroups[$groupId]['selections'][$selectionId] = (string) $subItem->attributes()['bezeichnung'];
|
||||
$selectionGroup->attributes[] = $selection;
|
||||
}
|
||||
|
||||
$selectionGroups[$groupId] = $selectionGroup;
|
||||
}
|
||||
|
||||
return $selectionGroups;
|
||||
}
|
||||
|
||||
public function getAdditionalServices(\SimpleXMLElement $xml): array
|
||||
{
|
||||
$additionalServices = [];
|
||||
|
||||
foreach ($xml->lei_sonstiges->leistung as $item) {
|
||||
$attributes = $item->attributes();
|
||||
$serviceId = (int) $attributes['idbuspro'];
|
||||
|
||||
$service = new Service();
|
||||
$service->id = $serviceId;
|
||||
$service->subType = (string) $attributes['unterart'];
|
||||
$service->mandatory = $this->stringToBool((string) $attributes['pflicht']);
|
||||
$service->dateFrom = $this->stringToDate((string) $attributes['termin']);
|
||||
$service->dateTo = $this->stringToDate((string) $attributes['bis']);
|
||||
$service->label = (string) $item->text;
|
||||
$service->price = $this->stringToFloat((string) $item->preis);
|
||||
$service->status = (string) $item->status;
|
||||
|
||||
$additionalServices[$serviceId] = $service;
|
||||
}
|
||||
|
||||
return $additionalServices;
|
||||
}
|
||||
|
||||
public function getTransportationServices(\SimpleXMLElement $xml): array
|
||||
{
|
||||
$transportationServices = [];
|
||||
|
||||
foreach ($xml->lei_befoerderung->leistung as $item) {
|
||||
$attributes = $item->attributes();
|
||||
$serviceId = (int) $attributes['idbuspro'];
|
||||
|
||||
$service = new Service();
|
||||
$service->id = $serviceId;
|
||||
$service->subType = (string) $attributes['unterart'];
|
||||
$service->dateFrom = $this->stringToDate((string) $attributes['termin']);
|
||||
$service->dateTo = $this->stringToDate((string) $attributes['bis']);
|
||||
$service->label = (string) $item->text;
|
||||
$service->price = $this->stringToFloat((string) $item->preis);
|
||||
$service->direction = (string) $item->richtung;
|
||||
|
||||
$transportationServices[$serviceId] = $service;
|
||||
}
|
||||
|
||||
return $transportationServices;
|
||||
}
|
||||
|
||||
public function getPickups(\SimpleXMLElement $xml): array
|
||||
{
|
||||
$pickups = [];
|
||||
|
||||
foreach ($xml->zustiege->zustieg as $item) {
|
||||
$attributes = $item->attributes();
|
||||
$pickupId = (int) $attributes['idbuspro'];
|
||||
|
||||
$pickup = new Pickup();
|
||||
$pickup->id = $pickupId;
|
||||
$pickup->time = $this->stringToDateTime((string) $attributes['zeit']);
|
||||
$pickup->price = $attributes['preis'] ? $this->stringToFloat((string) $attributes['preis']) : null;
|
||||
|
||||
$pickups[$pickupId] = $pickup;
|
||||
}
|
||||
|
||||
return $pickups;
|
||||
}
|
||||
|
||||
public function getRooms(\SimpleXMLElement $xml): array
|
||||
{
|
||||
$rooms = [];
|
||||
|
||||
foreach ($xml->hotel->zimmer->preis as $item) {
|
||||
$attributes = $item->attributes();
|
||||
$roomId = (int) $attributes['idbuspro_zimmer'];
|
||||
|
||||
$room = new Room();
|
||||
$room->id = $roomId;
|
||||
$room->code = (string) $item->attributes()['zimmercode'];
|
||||
$room->label = (string) $item->attributes()['zimmertext'];
|
||||
$room->minPax = (int) $item->attributes()['MinPax'];
|
||||
$room->maxPax = (int) $item->attributes()['MaxPax'];
|
||||
$room->nights = (int) $item->attributes()['naechte'];
|
||||
$room->price = $attributes['preis'] ? $this->stringToFloat((string) $attributes['preis']) : null;
|
||||
$room->status = (string) $item->status;
|
||||
$room->available = (int) $item->attributes()['verfuegbar'];
|
||||
|
||||
$rooms[$roomId] = $room;
|
||||
}
|
||||
|
||||
return $rooms;
|
||||
}
|
||||
|
||||
public function enrichServicesData(Travel $travel, BaseData $availabilities): void
|
||||
{
|
||||
$serviceAvailabilities = $availabilities->getItems();
|
||||
|
||||
foreach ([...$travel->additionalServices, ...$travel->transportationServices] as $service) {
|
||||
if (array_key_exists($service->id, $serviceAvailabilities)) {
|
||||
$service->available = $serviceAvailabilities[$service->id]->available;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\DataProvider;
|
||||
|
||||
use App\BusProNet\ApiClient;
|
||||
use App\BusProNet\Exception\ApiClientException;
|
||||
use App\BusProNet\Model\Country;
|
||||
use Psr\Cache\InvalidArgumentException;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
|
||||
class CountryDataProvider
|
||||
{
|
||||
public function __construct(private readonly ApiClient $apiClient, private readonly CacheInterface $cache)
|
||||
{}
|
||||
|
||||
public function getAll(): array
|
||||
{
|
||||
try {
|
||||
return $this->cache->get('bpn_countries', function (ItemInterface $item) {
|
||||
$item->expiresAfter(3600);
|
||||
|
||||
return $this
|
||||
->apiClient
|
||||
->getBaseData(ApiClient::TYPE_BASE_DATA_COUNTRIES)
|
||||
->getItems()
|
||||
;
|
||||
});
|
||||
} catch (InvalidArgumentException $e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public function get(?string $token): ?Country
|
||||
{
|
||||
if (null === $token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->getAll()[$token] ?? null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\Exception;
|
||||
|
||||
class ApiClientException extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\Exception;
|
||||
|
||||
class ResponseParserException extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\Form\ChoiceLoader;
|
||||
|
||||
use App\BusProNet\DataProvider\CountryDataProvider;
|
||||
use App\BusProNet\Model\Country;
|
||||
use Symfony\Component\Form\ChoiceList\ArrayChoiceList;
|
||||
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
|
||||
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
|
||||
|
||||
class CountryChoiceLoader implements ChoiceLoaderInterface
|
||||
{
|
||||
public function __construct(private readonly CountryDataProvider $countries, private readonly string $property)
|
||||
{}
|
||||
|
||||
public function loadChoiceList(callable $value = null): ChoiceListInterface
|
||||
{
|
||||
$choices = [];
|
||||
/** @var Country[] $countries */
|
||||
$countries = $this->countries->getAll();
|
||||
|
||||
foreach ($countries as $country) {
|
||||
$key = 'nationality' === $this->property ? $country->nationality : $country->name;
|
||||
$choices[$key] = $country->token;
|
||||
}
|
||||
|
||||
return new ArrayChoiceList($choices);
|
||||
|
||||
}
|
||||
|
||||
public function loadChoicesForValues(array $values, callable $value = null): array
|
||||
{
|
||||
return $values;
|
||||
}
|
||||
|
||||
public function loadValuesForChoices(array $choices, callable $value = null): array
|
||||
{
|
||||
return $choices;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\Form;
|
||||
|
||||
use App\BusProNet\DataProvider\CountryDataProvider;
|
||||
use App\BusProNet\Form\ChoiceLoader\CountryChoiceLoader;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\ChoiceList\ChoiceList;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\OptionsResolver\Options;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class CountryType extends AbstractType
|
||||
{
|
||||
public function __construct(private readonly CountryDataProvider $countries)
|
||||
{}
|
||||
|
||||
public function getParent(): string
|
||||
{
|
||||
return ChoiceType::class;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefined(['property']);
|
||||
$resolver->setAllowedValues('property', ['country', 'nationality']);
|
||||
$resolver->setDefaults([
|
||||
'property' => 'country',
|
||||
'choice_loader' => function (Options $options) {
|
||||
return ChoiceList::loader(
|
||||
$this,
|
||||
new CountryChoiceLoader($this->countries, $options['property']),
|
||||
[$options['property']]
|
||||
);
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\Model;
|
||||
|
||||
class Address
|
||||
{
|
||||
public ?string $street = null;
|
||||
public ?string $postCode = null;
|
||||
public ?string $city = null;
|
||||
public ?string $district = null;
|
||||
public ?string $country = null;
|
||||
|
||||
public function toPayload(): array
|
||||
{
|
||||
return [
|
||||
'strasse' => $this->street,
|
||||
'plz' => $this->postCode,
|
||||
'ort' => $this->city,
|
||||
'ortsteil' => $this->district,
|
||||
'land' => $this->country,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\Model;
|
||||
|
||||
class Availability
|
||||
{
|
||||
public ?int $serviceId = null;
|
||||
public ?string $status = null;
|
||||
public ?int $available = null;
|
||||
public ?float $price = null;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\Model;
|
||||
|
||||
class BaseData
|
||||
{
|
||||
public function __construct(private readonly array $items)
|
||||
{}
|
||||
|
||||
public function getItems(): array
|
||||
{
|
||||
return $this->items;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\Model;
|
||||
|
||||
use App\Form\Model\BookingData;
|
||||
|
||||
class Booking
|
||||
{
|
||||
public ?int $id = null;
|
||||
public ?int $bookingNumber = null;
|
||||
public ?Travel $travelData = null;
|
||||
public ?string $status = null;
|
||||
public ?PersonalData $applicant = null;
|
||||
public ?int $participantCount = null;
|
||||
public ?float $price = null;
|
||||
public ?\DateTimeImmutable $bookingDate = null;
|
||||
public ?string $travel = null;
|
||||
public ?int $travelId = null;
|
||||
public ?string $travelCode = null;
|
||||
public ?\DateTimeImmutable $travelDate = null;
|
||||
public ?int $hotelId = null;
|
||||
public ?string $hotelName = null;
|
||||
public ?bool $document = null;
|
||||
public ?float $payment = null;
|
||||
public array $participantsStatus = [];
|
||||
public array $participants = [];
|
||||
public array $transportationServices = [];
|
||||
public array $additionalServices = [];
|
||||
public array $rooms = [];
|
||||
public ?int $invoiceNumber = null;
|
||||
public ?float $totalPrice = null;
|
||||
|
||||
public function getBalance(): float
|
||||
{
|
||||
if (null === $this->payment) {
|
||||
return $this->price;
|
||||
}
|
||||
|
||||
return $this->price - $this->payment;
|
||||
}
|
||||
|
||||
public function getAdditionalServicesByGroup(mixed $group): array
|
||||
{
|
||||
$group = (array) $group;
|
||||
return array_filter($this->additionalServices, function (Service $service) use ($group) {
|
||||
return in_array($service->subType, $group);
|
||||
});
|
||||
}
|
||||
|
||||
public function getAdditionalServicesForParticipantByGroup(int $participantIndex, mixed $group): array
|
||||
{
|
||||
$services = $this->getAdditionalServicesByGroup($group);
|
||||
|
||||
return array_filter($services, function (Service $service) use ($participantIndex) {
|
||||
return in_array($participantIndex, $service->mapping);
|
||||
});
|
||||
}
|
||||
|
||||
public function getTransportationServiceForParticipantAndDirection(int $participantIndex, string $direction): ?Service
|
||||
{
|
||||
foreach ($this->transportationServices as $service) {
|
||||
if ($service->direction !== $direction) {
|
||||
continue;
|
||||
}
|
||||
if (in_array($participantIndex, $service->mapping)) {
|
||||
return $service;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getRoomForParticipant(int $participantIndex): ?Room
|
||||
{
|
||||
foreach ($this->rooms as $room) {
|
||||
if (in_array($participantIndex, $room->mapping)) {
|
||||
return $room;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function getPriceForParticipant(int $participantIndex): float
|
||||
{
|
||||
$price = 0.0;
|
||||
|
||||
foreach ([...$this->transportationServices, ...$this->additionalServices] as $service) {
|
||||
if (in_array($participantIndex, $service->mapping) && isset($service->individualPrice[$participantIndex])) {
|
||||
$price += $service->individualPrice[$participantIndex];
|
||||
} elseif ($service->price) {
|
||||
$price += $service->price;
|
||||
}
|
||||
}
|
||||
|
||||
$room = $this->getRoomForParticipant($participantIndex);
|
||||
|
||||
if (isset($room->individualPrice[$participantIndex])) {
|
||||
$price += $room->individualPrice[$participantIndex];
|
||||
} elseif ($room->price) {
|
||||
$price += $room->price;
|
||||
}
|
||||
|
||||
return $price;
|
||||
}
|
||||
|
||||
public function isEditable(): bool
|
||||
{
|
||||
return $this->travelDate > new \DateTimeImmutable() && false === in_array($this->status, ['S', 'U']);
|
||||
}
|
||||
|
||||
public function toPayload(?BookingData $formData): array
|
||||
{
|
||||
// Reset services to participants mappings
|
||||
foreach ([...$this->additionalServices, ...$this->transportationServices] as $service) {
|
||||
$service->mapping = [];
|
||||
}
|
||||
// Update mappings and add previously unselected services
|
||||
foreach ($formData->participants as $participant) {
|
||||
$servicesToMap = [
|
||||
...$participant->courses,
|
||||
...$participant->additionalServices,
|
||||
...$participant->skiPass,
|
||||
...$participant->board,
|
||||
...$participant->rentals,
|
||||
];
|
||||
foreach ($servicesToMap as $service) {
|
||||
if (false === isset($this->additionalServices[$service->id])) {
|
||||
$serviceToAdd = $this->travelData->additionalServices[$service->id];
|
||||
$this->additionalServices[$service->id] = $serviceToAdd;
|
||||
$this->additionalServices[$service->id]->individualPrice[$participant->index] = $serviceToAdd->price;
|
||||
}
|
||||
$this->additionalServices[$service->id]->mapping[] = $participant->index;
|
||||
}
|
||||
foreach ([$participant->transportationServiceTo, $participant->transportationServiceFro] as $service) {
|
||||
if (false === isset($this->transportationServices[$service->id])) {
|
||||
$serviceToAdd = $this->travelData->transportationServices[$service->id];
|
||||
$this->transportationServices[$service->id] = $serviceToAdd;
|
||||
$this->transportationServices[$service->id]->individualPrice[$participant->index] = $serviceToAdd->price;
|
||||
}
|
||||
$this->transportationServices[$service->id]->mapping[] = $participant->index;
|
||||
}
|
||||
}
|
||||
// Remove services with empty mappings
|
||||
foreach ($this->additionalServices as $service) {
|
||||
if (0 === count($service->mapping)) {
|
||||
unset($this->additionalServices[$service->id]);
|
||||
}
|
||||
}
|
||||
foreach ($this->transportationServices as $service) {
|
||||
if (0 === count($service->mapping)) {
|
||||
unset($this->transportationServices[$service->id]);
|
||||
}
|
||||
}
|
||||
// Update participants' personal data
|
||||
foreach ($formData->participants as $participant) {
|
||||
$this->participants[$participant->index]->firstName = $participant->firstName;
|
||||
$this->participants[$participant->index]->lastName = $participant->lastName;
|
||||
$this->participants[$participant->index]->dateOfBirth = $participant->dateOfBirth;
|
||||
$this->participants[$participant->index]->gender = $participant->gender;
|
||||
$this->participants[$participant->index]->nationality = $participant->nationality;
|
||||
$this->participants[$participant->index]->height = $participant->height;
|
||||
$this->participants[$participant->index]->weight = $participant->weight;
|
||||
$this->participants[$participant->index]->shoeSize = $participant->shoeSize;
|
||||
$this->participants[$participant->index]->communication->email = $participant->email;
|
||||
$this->participants[$participant->index]->communication->mobile = $participant->mobile;
|
||||
}
|
||||
|
||||
return [
|
||||
'status' => $this->status,
|
||||
'idreise' => $this->travelId,
|
||||
'anmelder' => $this->applicant->toPayload(),
|
||||
'teilnehmerliste' => [],
|
||||
'beförderungen' => [],
|
||||
'unterbringungen' => [],
|
||||
'zusatzleistungen' => [],
|
||||
'zustiege' => [],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\Model;
|
||||
|
||||
class Communication
|
||||
{
|
||||
public ?string $phone = null;
|
||||
public ?string $mobile = null;
|
||||
public ?string $email = null;
|
||||
|
||||
public function toPayload(): array
|
||||
{
|
||||
return [
|
||||
'email' => $this->email,
|
||||
'telefonmobil' => $this->mobile,
|
||||
'telefonprivat' => $this->phone,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\Model;
|
||||
|
||||
class Country
|
||||
{
|
||||
public ?int $id = null;
|
||||
public ?string $name = null;
|
||||
public ?string $token = null;
|
||||
public ?string $nationality = null;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\Model;
|
||||
|
||||
class CrmAttribute
|
||||
{
|
||||
public ?int $id = null;
|
||||
public ?string $label = null;
|
||||
public bool $selected = false;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\Model;
|
||||
|
||||
class CrmAttributeGroup
|
||||
{
|
||||
public ?int $id = null;
|
||||
public ?string $label = null;
|
||||
public ?array $attributes = null;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\Model;
|
||||
|
||||
class CrmAttributes
|
||||
{
|
||||
public ?array $attributeGroups = null;
|
||||
public bool $admin = false;
|
||||
public bool $manager = false;
|
||||
public bool $houseManager = false;
|
||||
public bool $teamer = false;
|
||||
public ?string $hotelCode = null;
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
$crmSelections = [];
|
||||
|
||||
foreach ($this->attributeGroups as $group) {
|
||||
/** @var CrmAttributeGroup $group */
|
||||
if (false === isset($crmSelections[$group->label])) {
|
||||
$crmSelections[$group->label] = [];
|
||||
}
|
||||
foreach ($group->attributes as $attribute) {
|
||||
/** @var CrmAttribute $attribute */
|
||||
if (false === $attribute->selected) {
|
||||
continue;
|
||||
}
|
||||
$crmSelections[$group->label][] = $attribute->label;
|
||||
}
|
||||
}
|
||||
|
||||
return $crmSelections;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\Model;
|
||||
|
||||
class Error
|
||||
{
|
||||
private static array $errors = [
|
||||
100 => 'Anfrageknoten fehlt',
|
||||
101 => 'Satzknoten fehlt in Anfrageknoten',
|
||||
102 => 'Ungültiger Satztyp [#1#]',
|
||||
103 => 'User fehlt',
|
||||
104 => 'User fehlerhaft',
|
||||
105 => 'Key fehlt',
|
||||
106 => 'Key fehlerhaft',
|
||||
200 => 'Keine Einträge vorhanden',
|
||||
201 => 'Keine Partner zur Auswahl gefunden',
|
||||
202 => 'Keinen Partner mit der ID [#1#] gefunden',
|
||||
203 => 'Ungültiger Termin',
|
||||
300 => 'IDReise fehlt',
|
||||
301 => 'Reise nicht gefunden',
|
||||
302 => 'IDLeistung (Hin & Rück) fehlt',
|
||||
303 => 'IDLeistung passt nicht zu IDReise',
|
||||
304 => 'Es konnte nicht für alle Teilnehmer ein Sitzplatz ermittelt werden',
|
||||
305 => 'Anzahl Personen fehlt',
|
||||
306 => 'Leistungen mit ID #1# nicht gefunden',
|
||||
307 => 'Leistungen mit ID #1# passen nicht zur Reise mit ID #2#',
|
||||
308 => 'Keine Zahlungsarten gefunden',
|
||||
400 => 'IDPartner fehlt',
|
||||
401 => 'Partner nicht gefunden',
|
||||
402 => 'Bis-Termin fehlt',
|
||||
403 => 'Keine Unterbringungsleistungen gefunden',
|
||||
500 => 'Art fehlt',
|
||||
501 => 'Reise- und Buchungszeitraum fehlen',
|
||||
502 => 'Keine Kunden gefunden',
|
||||
600 => 'Keine Gutscheine gefunden',
|
||||
601 => 'Gutschein-IDs fehlen',
|
||||
602 => 'Gutscheine mit ID #1# nicht gefunden',
|
||||
610 => 'Buchungsart fehlt',
|
||||
611 => 'Buchungsart falsch',
|
||||
612 => 'Gutscheinart fehlt',
|
||||
613 => 'Gutscheinart falsch',
|
||||
614 => 'Gutscheinstamm-ID fehlt',
|
||||
615 => 'Gutschein (Stamm) mit ID #1# nicht gefunden',
|
||||
616 => 'Kapazität beim Gutschein #1# nicht ausreichend',
|
||||
617 => 'Rechnungsempfänger fehlt',
|
||||
618 => 'Zahlungsart fehlt',
|
||||
619 => 'Agentur-ID fehlt',
|
||||
620 => 'Agentur mit ID #1# nicht gefunden',
|
||||
650 => '#1#',
|
||||
700 => 'Keine Agenturen gefunden',
|
||||
701 => 'Agentur mit ID #1# nicht gefunden',
|
||||
800 => 'Produkt-ID fehlt',
|
||||
801 => 'Produkt mit ID #1# nicht gefunden',
|
||||
805 => 'Es wurden keine Produkte gefunden',
|
||||
810 => 'Stammdaten (#1#) nicht gefunden',
|
||||
900 => 'Buchungsart fehlt',
|
||||
901 => 'Buchungsart falsch',
|
||||
902 => 'Status fehlt',
|
||||
903 => 'Status falsch',
|
||||
904 => 'Agentur-ID fehlt',
|
||||
905 => 'Agentur mit ID #1# nicht gefunden',
|
||||
906 => 'Reise-ID fehlt',
|
||||
907 => 'Reise mit ID #1# nicht gefunden',
|
||||
908 => 'Reise mit ID #1# ist storniert',
|
||||
909 => 'Beförderungen fehlen',
|
||||
910 => 'Leistung mit ID #1# gehört nicht zur Reise',
|
||||
911 => 'Leistung mit ID #1# gehört nicht zum Produkt',
|
||||
912 => 'Beförderungsleistung für die #1# fehlt',
|
||||
913 => 'Unterbringungen fehlen',
|
||||
914 => 'Partner-ID fehlt',
|
||||
915 => 'Partner mit ID #1# nicht gefunden',
|
||||
916 => 'Ferienziel-Unterbringungen fehlen',
|
||||
917 => 'Ferienziel: Zimmer-IDZ fehlt',
|
||||
918 => 'Ferienziel: Kategorie fehlt',
|
||||
919 => 'Ferienziel: Verpflegungs-ID fehlt',
|
||||
920 => 'Ferienziel: Anreise fehlt',
|
||||
921 => 'Ferienziel: Anreise passt nicht zur Beförderungsleistung (Hinfahrt)',
|
||||
922 => 'Ferienziel: Abreise fehlt',
|
||||
923 => 'Ferienziel: Abreise passt nicht zur Beförderungsleistung (Rückfahrt)',
|
||||
924 => 'Ferienziel: Keine Preise zu den Daten gefunden',
|
||||
925 => 'Ferienziel: Preis zu den Daten nicht gefunden',
|
||||
930 => 'Zustiege fehlen',
|
||||
931 => 'Zustieg mit ID #1# nicht gefunden',
|
||||
932 => 'Zustieg mit ID #1# bei Leistung #2# nicht freigegeben',
|
||||
933 => 'Sitzplan mit ID #1# bei Leistung #2# nicht freigegeben',
|
||||
935 => 'Versicherung mit ID #1# nicht gefunden',
|
||||
940 => 'Zahlungsart fehlt',
|
||||
941 => 'Zahlungsart-ID fehlt',
|
||||
942 => 'Zahlungsart mit ID #1# nicht gültig',
|
||||
950 => 'Anmelder fehlt',
|
||||
951 => 'Teilnehmerliste fehlt',
|
||||
952 => 'Teilnehmer-ID fehlt',
|
||||
960 => 'Preisfehler: Struct #1# / Obj #2#',
|
||||
961 => 'Buchung konnte nicht gespeichert werden',
|
||||
980 => 'Reise ist fürs Internet gesperrt',
|
||||
981 => 'Reise ist nicht mehr buchbar (#1#)',
|
||||
982 => 'Optionsbuchungen nicht zugelassen',
|
||||
983 => 'Anfragebuchung nicht zugelassen',
|
||||
984 => 'Leistung mit ID #1# nicht fürs Internet buchbar',
|
||||
985 => 'Zustieg mit ID #1# nicht fürs Internet buchbar',
|
||||
999 => 'Systemfehler: #1#',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\Model;
|
||||
|
||||
class File
|
||||
{
|
||||
public function __construct(string $filename, string $content, string $mimeType)
|
||||
{
|
||||
$this->filename = $filename;
|
||||
$this->content = $content;
|
||||
$this->mimeType = $mimeType;
|
||||
}
|
||||
|
||||
public ?string $filename = null;
|
||||
public ?string $content = null;
|
||||
public ?string $mimeType = null;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\Model;
|
||||
|
||||
class Hotel
|
||||
{
|
||||
public ?int $id = null;
|
||||
public ?string $code = null;
|
||||
public ?string $name = null;
|
||||
public ?string $city = null;
|
||||
public ?string $street = null;
|
||||
public ?string $phone = null;
|
||||
public ?string $country = null;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\Model;
|
||||
|
||||
class MutableField
|
||||
{
|
||||
public function __construct(string $type, bool $mutable, ?\DateTimeImmutable $mutableUntil = null)
|
||||
{
|
||||
$this->type = $type;
|
||||
$this->mutable = $mutable;
|
||||
$this->mutableBefore = $mutableUntil;
|
||||
}
|
||||
|
||||
public ?string $type = null;
|
||||
public bool $mutable = false;
|
||||
public ?\DateTimeImmutable $mutableBefore = null;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\Model;
|
||||
|
||||
class Notification
|
||||
{
|
||||
public function __construct(int $code, string $message)
|
||||
{
|
||||
$this->code = $code;
|
||||
$this->message = $message;
|
||||
}
|
||||
|
||||
public ?int $code;
|
||||
public ?string $message;
|
||||
|
||||
public function isSuccessful(): bool
|
||||
{
|
||||
// Successful responses don't carry codes and messages
|
||||
if (null === $this->code && null === $this->message) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// These weird and contradictory looking assertions are required because
|
||||
// of the stupid API implementation that doesn't distinguish between error
|
||||
// and success responses.
|
||||
$responseIsError = 100 <= $this->code || $this->message === 'Daten konnten nicht gesendet werden.';
|
||||
$responseIsSuccess = 650 === $this->code && false === stripos($this->message, 'fehler');
|
||||
|
||||
return false === $responseIsError && true === $responseIsSuccess;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\Model;
|
||||
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
class PersonalData
|
||||
{
|
||||
public ?int $addressId = null;
|
||||
public ?int $personId = null;
|
||||
#[Assert\NotBlank(message: 'Bitte angeben')]
|
||||
public ?string $name = null;
|
||||
#[Assert\NotBlank(message: 'Bitte angeben')]
|
||||
public ?string $firstName = null;
|
||||
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;
|
||||
#[Assert\Valid()]
|
||||
public ?Address $address = null;
|
||||
#[Assert\Valid()]
|
||||
public ?Communication $communication = null;
|
||||
|
||||
public function toPayload(): array
|
||||
{
|
||||
// Ensure date of birth is populated
|
||||
if (null === $dob = $this->dateOfBirth) {
|
||||
$dob = new \DateTimeImmutable('18 years ago');
|
||||
}
|
||||
|
||||
return [
|
||||
'idadresse' => $this->addressId,
|
||||
'idadresseperson' => $this->personId,
|
||||
'geburtsdatum' => $dob->format('d.m.Y'),
|
||||
'anrede' => $this->salutation,
|
||||
'geschlecht' => $this->gender,
|
||||
'nationalitaet' => $this->nationality,
|
||||
'titel' => $this->title,
|
||||
'vorname' => $this->firstName,
|
||||
'name' => $this->name,
|
||||
'anschrift' => $this->address->toPayload(),
|
||||
'kommunikation' => $this->communication->toPayload(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\Model;
|
||||
|
||||
class Pickup
|
||||
{
|
||||
public ?int $id = null;
|
||||
public ?string $code = null;
|
||||
public ?string $city = null;
|
||||
public ?string $postalCode = null;
|
||||
public ?string $street = null;
|
||||
public ?\DateTimeImmutable $time = null;
|
||||
public ?float $price = null;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\Model;
|
||||
|
||||
class Room
|
||||
{
|
||||
public ?int $id = null;
|
||||
public ?string $code = null;
|
||||
public ?string $label = null;
|
||||
public ?\DateTimeImmutable $dateFrom = null;
|
||||
public ?\DateTimeImmutable $dateTo = null;
|
||||
public ?int $totalCount = null;
|
||||
public ?int $minPax = null;
|
||||
public ?int $maxPax = null;
|
||||
public ?int $nights = null;
|
||||
public ?string $status = null;
|
||||
public ?int $available = null;
|
||||
public ?string $board = null;
|
||||
public array $mapping = [];
|
||||
public ?float $price = null;
|
||||
public ?float $totalPrice = null;
|
||||
public array $individualPrice = [];
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\Model;
|
||||
|
||||
class Service
|
||||
{
|
||||
public const TYPE_INCLUDED = 'included';
|
||||
public const TYPE_ADDITIONAL = 'additional';
|
||||
public const TYPE_TRANSPORTATION = 'transportation';
|
||||
|
||||
public const TOKEN_COURSES = 'KUR';
|
||||
public const TOKEN_SKI_PASS = 'SPA';
|
||||
public const TOKEN_ADDITIONAL = 'SON';
|
||||
public const TOKEN_BOARD = 'VPF';
|
||||
public const TOKEN_RENTALS = ['VER', 'VE2', 'VE3', 'VE4', 'VE5', 'VE6', 'VE7', 'VE8'];
|
||||
|
||||
public ?int $id = null;
|
||||
public ?string $status = null;
|
||||
public bool $mandatory = false;
|
||||
public ?string $label = null;
|
||||
public ?\DateTimeImmutable $dateFrom = null;
|
||||
public ?\DateTimeImmutable $dateTo = null;
|
||||
public ?string $subType = null;
|
||||
public ?int $totalCount = null;
|
||||
public array $mapping = [];
|
||||
public ?float $price = null;
|
||||
public ?float $totalPrice = null;
|
||||
public array $individualPrice = [];
|
||||
public ?string $direction = null;
|
||||
public ?int $available = null;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\Model;
|
||||
|
||||
class Travel
|
||||
{
|
||||
public ?int $id = null;
|
||||
public ?string $code = null;
|
||||
public ?string $label = null;
|
||||
public ?\DateTimeImmutable $dateFrom = null;
|
||||
public ?\DateTimeImmutable $dateTo = null;
|
||||
public ?string $type = null;
|
||||
public ?float $priceFrom = null;
|
||||
public array $selectionGroups = [];
|
||||
public array $additionalServices = [];
|
||||
public array $transportationServices = [];
|
||||
public array $pickups = [];
|
||||
public array $rooms = [];
|
||||
|
||||
public function getAdditionalServicesByGroup(mixed $group, bool $availableOnly = true): array
|
||||
{
|
||||
$group = (array) $group;
|
||||
|
||||
return array_filter($this->additionalServices, function (Service $service) use ($group, $availableOnly) {
|
||||
return in_array($service->subType, $group) && (false === $availableOnly || $service->available > 0);
|
||||
});
|
||||
}
|
||||
|
||||
public function getTransportationServicesByDirection(string $direction, bool $availableOnly = true): array
|
||||
{
|
||||
return array_filter($this->transportationServices, function (Service $service) use ($direction, $availableOnly) {
|
||||
return $direction === $service->direction
|
||||
&& $service->price >= 0
|
||||
&& (false === $availableOnly || $service->available > 0);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\Security;
|
||||
|
||||
use App\BusProNet\ApiClient;
|
||||
use App\BusProNet\Exception\ApiClientException;
|
||||
use App\BusProNet\Model\CrmAttributes;
|
||||
use App\BusProNet\Model\CrmAttributesResponse;
|
||||
use App\BusProNet\Model\PersonalData;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
|
||||
use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
|
||||
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
|
||||
|
||||
class Authenticator extends AbstractLoginFormAuthenticator implements AuthenticationEntryPointInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UrlGeneratorInterface $urlGenerator,
|
||||
private readonly ApiClient $apiClient,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
protected function getLoginUrl(Request $request): string
|
||||
{
|
||||
return $this->urlGenerator->generate('app_login');
|
||||
}
|
||||
|
||||
public function authenticate(Request $request): Passport
|
||||
{
|
||||
$email = trim($request->request->getString('_username'));
|
||||
$passwordPlain = trim($request->request->getString('_password'));
|
||||
|
||||
// Very lame hashing applied here as required by BPN
|
||||
$password = md5($passwordPlain);
|
||||
|
||||
try {
|
||||
$response = $this->apiClient->getPersonalData($email, $password);
|
||||
} catch (ApiClientException $e) {
|
||||
throw new CustomUserMessageAuthenticationException($e->getMessage());
|
||||
}
|
||||
|
||||
if (false === $response instanceof PersonalData) {
|
||||
throw new CustomUserMessageAuthenticationException('User not found');
|
||||
}
|
||||
|
||||
$csrfToken = $request->request->getString('_csrf_token');
|
||||
|
||||
return new SelfValidatingPassport(
|
||||
new UserBadge($email, function () use ($email, $password, $response, $request) {
|
||||
$crmAttributes = $this->apiClient->getCrmAttributes($email, $password);
|
||||
$roles = $this->collectRoles($crmAttributes);
|
||||
$user = new User($email, $response->personId, $response->addressId, $password, $roles);
|
||||
|
||||
$request->getSession()->set('bpn_user', $user);
|
||||
|
||||
return $user;
|
||||
}),
|
||||
[
|
||||
new CsrfTokenBadge('authenticate', $csrfToken),
|
||||
new RememberMeBadge(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
|
||||
{
|
||||
$this->logger->info('Login', [
|
||||
'email' => $token->getUserIdentifier(),
|
||||
]);
|
||||
|
||||
return new RedirectResponse($this->urlGenerator->generate('app_personal_data'));
|
||||
}
|
||||
|
||||
private function collectRoles(CrmAttributes $crmAttributes): array
|
||||
{
|
||||
// Collect user's roles from CRM attributes
|
||||
$roles = [];
|
||||
|
||||
if ($crmAttributes->admin) {
|
||||
$roles[] = 'ROLE_ADMIN';
|
||||
} elseif ($crmAttributes->manager) {
|
||||
$roles[] = 'ROLE_MANAGER';
|
||||
} elseif ($crmAttributes->houseManager) {
|
||||
$roles[] = 'ROLE_HOUSE_MANAGER';
|
||||
}
|
||||
|
||||
if ($crmAttributes->teamer) {
|
||||
$roles[] = 'ROLE_TEAMER';
|
||||
}
|
||||
|
||||
return $roles;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\Security;
|
||||
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
class User implements UserInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $email,
|
||||
private readonly int $personId,
|
||||
private readonly int $addressId,
|
||||
private readonly ?string $password = null,
|
||||
private readonly array $roles = [],
|
||||
) {
|
||||
}
|
||||
|
||||
public function getEmail(): ?string
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
public function getPassword(): ?string
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
public function getPersonId(): ?int
|
||||
{
|
||||
return $this->personId;
|
||||
}
|
||||
|
||||
public function getAddressId(): int
|
||||
{
|
||||
return $this->addressId;
|
||||
}
|
||||
|
||||
public function getRoles(): array
|
||||
{
|
||||
return ['ROLE_USER', ...$this->roles];
|
||||
}
|
||||
|
||||
public function eraseCredentials()
|
||||
{
|
||||
}
|
||||
|
||||
public function getUserIdentifier(): string
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet\Security;
|
||||
|
||||
use App\BusProNet\ApiClient;
|
||||
use App\BusProNet\Exception\ApiClientException;
|
||||
use App\BusProNet\Model\PersonalData;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
|
||||
class UserProvider implements UserProviderInterface
|
||||
{
|
||||
public function __construct(private readonly ApiClient $apiClient, private readonly RequestStack $requestStack)
|
||||
{
|
||||
}
|
||||
|
||||
public function refreshUser(UserInterface $user): UserInterface
|
||||
{
|
||||
if (null !== $activeUuser = $this->requestStack->getSession()->get('bpn_user')) {
|
||||
return $activeUuser;
|
||||
}
|
||||
|
||||
return $this->loadUserByIdentifier($user->getUserIdentifier());
|
||||
}
|
||||
|
||||
public function supportsClass(string $class): bool
|
||||
{
|
||||
return User::class === $class;
|
||||
}
|
||||
|
||||
public function loadUserByIdentifier(string $identifier): UserInterface
|
||||
{
|
||||
if (null === $activeUser = $this->requestStack->getSession()->get('bpn_user')) {
|
||||
throw new UserNotFoundException();
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $this->apiClient->getPersonalData($activeUser->getEmail(), $activeUser->getPassword());
|
||||
} catch (ApiClientException $e) {
|
||||
throw new UserNotFoundException();
|
||||
}
|
||||
|
||||
if (false === $response instanceof PersonalData) {
|
||||
throw new UserNotFoundException();
|
||||
}
|
||||
|
||||
return $activeUser;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\BusProNet\ApiClient;
|
||||
use App\BusProNet\DataLoader\PickupDataLoader;
|
||||
use App\BusProNet\DataLoader\TravelDataLoader;
|
||||
use App\BusProNet\Model\File;
|
||||
use App\BusProNet\Model\Notification;
|
||||
use App\Form\BookingType;
|
||||
use App\Form\Model\BookingData;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use function Symfony\Component\String\u;
|
||||
|
||||
class BookingController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ApiClient $apiClient,
|
||||
private readonly TravelDataLoader $travelDataLoader,
|
||||
private readonly PickupDataLoader $pickupDataLoader,
|
||||
private readonly Security $security,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route('/bookings', name: 'app_bookings')]
|
||||
#[IsGranted("ROLE_USER")]
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$bpnUser = $request->getSession()->get('bpn_user');
|
||||
|
||||
if (null === $bpnUser) {
|
||||
return $this->security->logout();
|
||||
}
|
||||
|
||||
$bookings = $this->apiClient->getBookings($bpnUser->getEmail(), $bpnUser->getPassword());
|
||||
|
||||
return $this->render('booking/index.html.twig', [
|
||||
'bookings' => $bookings->getItems(),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/bookings/{id}/edit', name: 'app_booking_edit', requirements: ['id' => '\d+'])]
|
||||
#[IsGranted("ROLE_USER")]
|
||||
public function edit(int $id, Request $request): Response
|
||||
{
|
||||
$bpnUser = $request->getSession()->get('bpn_user');
|
||||
|
||||
if (null === $bpnUser) {
|
||||
return $this->security->logout();
|
||||
}
|
||||
|
||||
$booking = $this->apiClient->getBooking($bpnUser->getEmail(), $bpnUser->getPassword(), $id);
|
||||
|
||||
//$this->denyAccessUnlessGranted('VIEW', $booking);
|
||||
|
||||
$travelData = $this->travelDataLoader->loadById($booking->travelId);
|
||||
|
||||
if (null === $travelData) {
|
||||
$this->addFlash('error', 'Reisedaten nicht (mehr) verfügbar');
|
||||
|
||||
return $this->redirectToRoute('app_bookings');
|
||||
}
|
||||
|
||||
$mutableFields = $this->apiClient->getMutableFields($booking->travelId);
|
||||
$availabilities = $this->apiClient->getAvailabilities($booking->travelId);
|
||||
|
||||
$this->travelDataLoader->enrichServicesData($travelData, $availabilities);
|
||||
$this->pickupDataLoader->enrichPickupsData($travelData);
|
||||
|
||||
$formData = BookingData::fromBooking($booking);
|
||||
|
||||
$form = $this->createForm(BookingType::class, $formData, [
|
||||
'hx_post' => $this->generateUrl('app_booking_edit', ['id' => $id]),
|
||||
'hx_target' => '#app',
|
||||
'hx_swap' => 'innerHTML show:top',
|
||||
'attr' => ['novalidate' => 'novalidate'],
|
||||
'travel' => $travelData,
|
||||
'mutable_fields' => $mutableFields,
|
||||
]);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$this->apiClient->updateBooking($bpnUser->getEmail(), $bpnUser->getPassword(), $formData);
|
||||
}
|
||||
|
||||
return $this->render('booking/edit.html.twig', [
|
||||
'booking' => $booking,
|
||||
'travelData' => $travelData,
|
||||
'mutableFields' => $mutableFields,
|
||||
'availabilities' => $availabilities,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/bookings/{id}/documents', name: 'app_booking_documents', requirements: ['id' => '\d+'])]
|
||||
#[IsGranted("ROLE_USER")]
|
||||
public function documents(int $id, Request $request): Response
|
||||
{
|
||||
$bpnUser = $request->getSession()->get('bpn_user');
|
||||
|
||||
if (null === $bpnUser) {
|
||||
return $this->security->logout();
|
||||
}
|
||||
|
||||
$file = $this
|
||||
->apiClient
|
||||
->getDocuments($bpnUser->getEmail(), $bpnUser->getPassword(), $id, 'Dokumentdruck')
|
||||
;
|
||||
|
||||
if (null === $file || $file instanceof Notification) {
|
||||
$this->addFlash('error', 'Keine Dokumente vorhanden');
|
||||
|
||||
return $this->redirectToRoute('app_bookings');
|
||||
}
|
||||
|
||||
return $this->createDownloadResponse($file);
|
||||
}
|
||||
|
||||
#[Route('/bookings/{id}/confirmation', name: 'app_booking_confirmation', requirements: ['id' => '\d+'])]
|
||||
#[IsGranted("ROLE_USER")]
|
||||
public function confirmation(int $id, Request $request): Response
|
||||
{
|
||||
$bpnUser = $request->getSession()->get('bpn_user');
|
||||
|
||||
if (null === $bpnUser) {
|
||||
return $this->security->logout();
|
||||
}
|
||||
|
||||
$file = $this
|
||||
->apiClient
|
||||
->getDocuments($bpnUser->getEmail(), $bpnUser->getPassword(), $id, 'Vorgangdruck')
|
||||
;
|
||||
|
||||
return $this->createDownloadResponse($file);
|
||||
}
|
||||
|
||||
private function createDownloadResponse(File $file): Response
|
||||
{
|
||||
$filename = u($file->filename)->ascii();
|
||||
|
||||
$response = new Response($file->content);
|
||||
|
||||
$disposition = $response->headers->makeDisposition(
|
||||
ResponseHeaderBag::DISPOSITION_ATTACHMENT,
|
||||
$filename,
|
||||
md5($filename)
|
||||
);
|
||||
|
||||
$response->headers->set('Content-Disposition', $disposition);
|
||||
$response->headers->set('Content-Type', $file->mimeType);
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\BusProNet\ApiClient;
|
||||
use App\BusProNet\Exception\ApiClientException;
|
||||
use App\Form\PersonalDataType;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
class PersonalDataController extends AbstractController
|
||||
{
|
||||
public function __construct(private readonly ApiClient $apiClient, private readonly Security $security)
|
||||
{}
|
||||
|
||||
#[Route('/personal-data', name: 'app_personal_data')]
|
||||
#[IsGranted('ROLE_USER')]
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$bpnUser = $request->getSession()->get('bpn_user');
|
||||
|
||||
if (null === $bpnUser) {
|
||||
return $this->security->logout();
|
||||
}
|
||||
|
||||
$personalData = $this
|
||||
->apiClient
|
||||
->getPersonalData($bpnUser->getEmail(), $bpnUser->getPassword())
|
||||
;
|
||||
|
||||
$form = $this->createForm(PersonalDataType::class, $personalData, [
|
||||
'attr' => ['novalidate' => 'novalidate'],
|
||||
'hx_post' => $this->generateUrl('app_personal_data'),
|
||||
'hx_target' => '#app',
|
||||
]);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
try {
|
||||
$this->apiClient->updatePersonalData($bpnUser->getEmail(), $bpnUser->getPassword(), $personalData);
|
||||
$this->addFlash('success', 'Personal data updated.');
|
||||
} catch (ApiClientException $e) {
|
||||
$this->addFlash('error', $e->getMessage());
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('app_personal_data');
|
||||
}
|
||||
|
||||
return $this->render('personal_data/index.html.twig', [
|
||||
'personalData' => $personalData,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
||||
|
||||
class SecurityController extends AbstractController
|
||||
{
|
||||
#[Route('/', name: 'app_login')]
|
||||
#[IsGranted('PUBLIC_ACCESS')]
|
||||
public function login(AuthenticationUtils $authenticationUtils): Response
|
||||
{
|
||||
if (null !== $this->getUser()) {
|
||||
return $this->redirectToRoute('app_personal_data');
|
||||
}
|
||||
|
||||
$error = $authenticationUtils->getLastAuthenticationError();
|
||||
$lastUsername = $authenticationUtils->getLastUsername();
|
||||
|
||||
return $this->render('security/login.html.twig', [
|
||||
'last_username' => $lastUsername,
|
||||
'error' => $error,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/logout', name: 'app_logout')]
|
||||
#[IsGranted('ROLE_USER')]
|
||||
public function logout(): void
|
||||
{}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\BusProNet\Model\Service;
|
||||
use App\BusProNet\Model\Travel;
|
||||
use App\Form\Model\BookingData;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
final class BookingType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add('participants', CollectionType::class, [
|
||||
'entry_type' => ParticipantType::class,
|
||||
'entry_options' => [
|
||||
'selectable_courses' => $options['travel']->getAdditionalServicesByGroup(Service::TOKEN_COURSES),
|
||||
'selectable_ski_passes' => $options['travel']->getAdditionalServicesByGroup(Service::TOKEN_SKI_PASS),
|
||||
'selectable_services' => $options['travel']->getAdditionalServicesByGroup(Service::TOKEN_ADDITIONAL),
|
||||
'selectable_board' => $options['travel']->getAdditionalServicesByGroup(Service::TOKEN_BOARD),
|
||||
'selectable_rentals' => $options['travel']->getAdditionalServicesByGroup(Service::TOKEN_RENTALS),
|
||||
'selectable_transportation_services_to' => $options['travel']
|
||||
->getTransportationServicesByDirection('HIN'),
|
||||
'selectable_transportation_services_fro' => $options['travel']
|
||||
->getTransportationServicesByDirection('RUECK'),
|
||||
],
|
||||
'allow_add' => false,
|
||||
'allow_delete' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver
|
||||
->setDefaults([
|
||||
'data_class' => BookingData::class,
|
||||
'mutable_fields' => [],
|
||||
])
|
||||
->setRequired(['travel'])
|
||||
->setAllowedTypes('travel', Travel::class)
|
||||
;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form\Extension;
|
||||
|
||||
use Symfony\Component\Form\AbstractTypeExtension;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class HtmxSubmitExtension extends AbstractTypeExtension
|
||||
{
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'hx_post' => null,
|
||||
'hx_target' => '#htmx-modal',
|
||||
'hx_swap' => 'outerHTML',
|
||||
'hx_indicator' => '#loading-indicator',
|
||||
'hx_trigger' => null,
|
||||
'hx_select' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function buildView(FormView $view, FormInterface $form, array $options): void
|
||||
{
|
||||
if (null !== $options['hx_post']) {
|
||||
$attr = [
|
||||
'hx-post' => $options['hx_post'],
|
||||
'hx-target' => $options['hx_target'],
|
||||
'hx-swap' => $options['hx_swap'],
|
||||
];
|
||||
if (null !== $options['hx_indicator']) {
|
||||
$attr['hx-indicator'] = $options['hx_indicator'];
|
||||
}
|
||||
if (null !== $options['hx_trigger']) {
|
||||
$attr['hx-trigger'] = $options['hx_trigger'];
|
||||
}
|
||||
if (null !== $options['hx_select']) {
|
||||
$attr['hx-select'] = $options['hx_select'];
|
||||
}
|
||||
$view->vars['attr'] = array_merge($view->vars['attr'], $attr);
|
||||
}
|
||||
}
|
||||
|
||||
public static function getExtendedTypes(): iterable
|
||||
{
|
||||
return [FormType::class];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form\Model;
|
||||
|
||||
use App\BusProNet\Model\Booking;
|
||||
use App\BusProNet\Model\PersonalData;
|
||||
use App\BusProNet\Model\Service;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
class BookingData
|
||||
{
|
||||
public ?Booking $booking = null;
|
||||
|
||||
#[Assert\Valid]
|
||||
public array $participants = [];
|
||||
|
||||
public static function fromBooking(Booking $booking): static
|
||||
{
|
||||
$instance = new static();
|
||||
|
||||
$instance->booking = $booking;
|
||||
|
||||
foreach ($booking->participants as $index => $participant) {
|
||||
/** @var PersonalData $participant */
|
||||
$participantData = ParticipantData::fromPersonalData($participant);
|
||||
$participantData->index = $index;
|
||||
$participantData->courses = $booking
|
||||
->getAdditionalServicesForParticipantByGroup($index, Service::TOKEN_COURSES);
|
||||
$participantData->skiPass = $booking
|
||||
->getAdditionalServicesForParticipantByGroup($index, Service::TOKEN_SKI_PASS);
|
||||
$participantData->additionalServices = $booking
|
||||
->getAdditionalServicesForParticipantByGroup($index, Service::TOKEN_ADDITIONAL);
|
||||
$participantData->board = $booking
|
||||
->getAdditionalServicesForParticipantByGroup($index, Service::TOKEN_BOARD);
|
||||
$participantData->rentals = $booking
|
||||
->getAdditionalServicesForParticipantByGroup($index, Service::TOKEN_RENTALS);
|
||||
// Different keys for direction used in booking data (H <=> HIN, R <=> RUECK)!
|
||||
$participantData->transportationServiceTo = $booking
|
||||
->getTransportationServiceForParticipantAndDirection($index, 'H');
|
||||
$participantData->transportationServiceFro = $booking
|
||||
->getTransportationServiceForParticipantAndDirection($index, 'R');
|
||||
$instance->participants[$index] = $participantData;
|
||||
}
|
||||
|
||||
return $instance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form\Model;
|
||||
|
||||
use App\BusProNet\Model\PersonalData;
|
||||
use App\BusProNet\Model\Service;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
|
||||
class ParticipantData
|
||||
{
|
||||
public ?int $index = null;
|
||||
public ?int $addressId = null;
|
||||
public ?int $personId = null;
|
||||
public ?string $status = null;
|
||||
|
||||
#[Assert\NotBlank(message: 'Bitte angeben')]
|
||||
public ?string $firstName = null;
|
||||
|
||||
#[Assert\NotBlank(message: 'Bitte angeben')]
|
||||
public ?string $lastName = null;
|
||||
public ?string $title = null;
|
||||
public ?string $gender = null;
|
||||
public ?string $nationality = null;
|
||||
|
||||
#[Assert\Range(notInRangeMessage: 'Bitte einen Wert im Bereich {{ min }}-{{ max }}cm angeben', min: 140, max: 210)]
|
||||
public ?string $height = null;
|
||||
|
||||
#[Assert\Range(notInRangeMessage: 'Bitte einen Wert im Bereich {{ min }}-{{ max }} angeben', min: 35, max: 49)]
|
||||
public ?string $shoeSize = null;
|
||||
|
||||
#[Assert\Range(notInRangeMessage: 'Bitte einen Wert im Bereich {{ min }}-{{ max }}kg angeben', min: 40, max: 130)]
|
||||
public ?string $weight = null;
|
||||
|
||||
#[Assert\NotNull(message: 'Bitte angeben')]
|
||||
public ?\DateTimeImmutable $dateOfBirth = null;
|
||||
|
||||
#[Assert\NotBlank(message: 'Bitte angeben')]
|
||||
public ?string $email = null;
|
||||
|
||||
#[Assert\NotBlank(message: 'Bitte angeben')]
|
||||
public ?string $mobile = null;
|
||||
public array $courses = [];
|
||||
public array $additionalServices = [];
|
||||
public array $skiPass = [];
|
||||
public array $board = [];
|
||||
public array $rentals = [];
|
||||
public ?Service $transportationServiceTo = null;
|
||||
public ?Service $transportationServiceFro = null;
|
||||
|
||||
#[Assert\Callback]
|
||||
public function assertBodyMeasurements(ExecutionContextInterface $context): void
|
||||
{
|
||||
if (0 === count($this->rentals)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (empty($this->height)) {
|
||||
$context->buildViolation('Bitte angeben wegen Leihmaterial')
|
||||
->atPath('height')
|
||||
->addViolation()
|
||||
;
|
||||
}
|
||||
|
||||
if (empty($this->shoeSize)) {
|
||||
$context->buildViolation('Bitte angeben Leihmaterial')
|
||||
->atPath('shoeSize')
|
||||
->addViolation()
|
||||
;
|
||||
}
|
||||
|
||||
if (empty($this->weight)) {
|
||||
$context->buildViolation('Bitte angeben Leihmaterial')
|
||||
->atPath('weight')
|
||||
->addViolation()
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
public static function fromPersonalData(PersonalData $personalData): static
|
||||
{
|
||||
$instance = new static();
|
||||
|
||||
$instance->addressId = $personalData->addressId;
|
||||
$instance->personId = $personalData->personId;
|
||||
$instance->firstName = $personalData->firstName;
|
||||
$instance->lastName = $personalData->name;
|
||||
$instance->gender = $personalData->gender;
|
||||
$instance->nationality = $personalData->nationality;
|
||||
$instance->email = $personalData->communication->email;
|
||||
$instance->mobile = $personalData->communication->mobile;
|
||||
$instance->dateOfBirth = $personalData->dateOfBirth;
|
||||
$instance->height = $personalData->height;
|
||||
$instance->weight = $personalData->weight;
|
||||
$instance->shoeSize = $personalData->shoeSize;
|
||||
|
||||
return $instance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\BusProNet\Form\CountryType;
|
||||
use App\BusProNet\Model\Service;
|
||||
use App\Form\Model\ParticipantData;
|
||||
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\IntegerType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class ParticipantType 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',
|
||||
'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' => 'Email',
|
||||
])
|
||||
->add('mobile', TextType::class, [
|
||||
'label' => 'Telefon (mobil)',
|
||||
])
|
||||
->add('height', IntegerType::class, [
|
||||
'label' => 'Körpergröße [cm]',
|
||||
'required' => false,
|
||||
])
|
||||
->add('shoeSize', IntegerType::class, [
|
||||
'label' => 'Schuhgröße',
|
||||
'required' => false,
|
||||
])
|
||||
->add('weight', IntegerType::class, [
|
||||
'label' => 'Gewicht [kg]',
|
||||
'required' => false,
|
||||
])
|
||||
->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($options) {
|
||||
/** @var ParticipantData $participant */
|
||||
$participant = $event->getData();
|
||||
$participantIndex = $participant->index;
|
||||
|
||||
$form = $event->getForm();
|
||||
|
||||
$commonChoiceFieldOptions = [
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
'choice_value' => 'id',
|
||||
'choice_label' => function (?Service $service) use ($participantIndex) {
|
||||
if (null === $service) {
|
||||
return null;
|
||||
}
|
||||
$price = $service->individualPrice[$participantIndex] ?? $service->price;
|
||||
if (null === $price || 0.0 === $price) {
|
||||
return $service->label;
|
||||
}
|
||||
|
||||
return sprintf('%s (%s€)',
|
||||
$service->label,
|
||||
number_format($price, 2, ',', '.')
|
||||
);
|
||||
},
|
||||
'choice_attr' => function (?Service $service) {
|
||||
if (true === $service->mandatory) {
|
||||
return [
|
||||
'checked' => true,
|
||||
'disabled' => true,
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
},
|
||||
];
|
||||
|
||||
$form
|
||||
->add('courses', ChoiceType::class, [
|
||||
...$commonChoiceFieldOptions,
|
||||
'label' => 'Kurse',
|
||||
'choices' => $options['selectable_courses'],
|
||||
])
|
||||
->add('additionalServices', ChoiceType::class, [
|
||||
...$commonChoiceFieldOptions,
|
||||
'label' => 'Zusatzleistungen',
|
||||
'choices' => $options['selectable_services'],
|
||||
])
|
||||
->add('skiPass', ChoiceType::class, [
|
||||
...$commonChoiceFieldOptions,
|
||||
'label' => 'Skipass',
|
||||
'choices' => $options['selectable_ski_passes'],
|
||||
])
|
||||
->add('board', ChoiceType::class, [
|
||||
...$commonChoiceFieldOptions,
|
||||
'label' => 'Verpflegung',
|
||||
'choices' => $options['selectable_board'],
|
||||
])
|
||||
->add('rentals', ChoiceType::class, [
|
||||
...$commonChoiceFieldOptions,
|
||||
'label' => 'Verleih',
|
||||
'choices' => $options['selectable_rentals'],
|
||||
])
|
||||
->add('transportationServiceTo', ChoiceType::class, [
|
||||
...$commonChoiceFieldOptions,
|
||||
'label' => 'Anreise',
|
||||
'multiple' => false,
|
||||
'choices' => $options['selectable_transportation_services_to'],
|
||||
])
|
||||
->add('transportationServiceFro', ChoiceType::class, [
|
||||
...$commonChoiceFieldOptions,
|
||||
'label' => 'Rückreise',
|
||||
'multiple' => false,
|
||||
'choices' => $options['selectable_transportation_services_fro'],
|
||||
])
|
||||
;
|
||||
})
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => ParticipantData::class,
|
||||
'selectable_courses' => [],
|
||||
'selectable_ski_passes' => [],
|
||||
'selectable_services' => [],
|
||||
'selectable_board' => [],
|
||||
'selectable_rentals' => [],
|
||||
'selectable_transportation_services_to' => [],
|
||||
'selectable_transportation_services_fro' => [],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\BusProNet\Form\CountryType;
|
||||
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;
|
||||
|
||||
class PersonalDataType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('gender', ChoiceType::class, [
|
||||
'label' => 'Gender',
|
||||
'choices' => [
|
||||
'M' => 'M',
|
||||
'W' => 'W',
|
||||
'D' => 'D',
|
||||
],
|
||||
])
|
||||
->add('firstName', TextType::class, [
|
||||
'label' => 'Name',
|
||||
])
|
||||
->add('name', TextType::class, [
|
||||
'label' => 'Nachname',
|
||||
])
|
||||
->add('dateOfBirth', BirthdayType::class, [
|
||||
'label' => 'Geburtsdatum',
|
||||
'html5' => true,
|
||||
'widget' => 'single_text',
|
||||
'input' => 'datetime_immutable',
|
||||
])
|
||||
->add('street', TextType::class, [
|
||||
'label' => 'Straße',
|
||||
'property_path' => 'address.street',
|
||||
])
|
||||
->add('postCode', TextType::class, [
|
||||
'label' => 'PLZ',
|
||||
'property_path' => 'address.postCode',
|
||||
])
|
||||
->add('city', TextType::class, [
|
||||
'label' => 'Stadt',
|
||||
'property_path' => 'address.city',
|
||||
])
|
||||
->add('country', CountryType::class, [
|
||||
'label' => 'Land',
|
||||
'property_path' => 'address.country',
|
||||
])
|
||||
->add('email', EmailType::class, [
|
||||
'label' => 'E-Mail',
|
||||
'property_path' => 'communication.email',
|
||||
])
|
||||
->add('phone', TextType::class, [
|
||||
'label' => 'Telefon',
|
||||
'property_path' => 'communication.phone',
|
||||
])
|
||||
->add('mobile', TextType::class, [
|
||||
'label' => 'Mobil',
|
||||
'property_path' => 'communication.mobile',
|
||||
])
|
||||
;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Htmx;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class HxRedirectResponse extends Response
|
||||
{
|
||||
public function __construct(string $url)
|
||||
{
|
||||
return parent::__construct(null, Response::HTTP_OK, ['HX-Redirect' => $url]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Htmx;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class HxTriggerResponse extends Response
|
||||
{
|
||||
public function __construct(string $content, string $trigger)
|
||||
{
|
||||
return parent::__construct($content, Response::HTTP_OK, ['HX-Trigger' => $trigger]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Security\Voter;
|
||||
|
||||
use App\BusProNet\Model\Booking;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
|
||||
class BookingVoter extends Voter
|
||||
{
|
||||
public const VIEW = 'VIEW';
|
||||
public const EDIT = 'EDIT';
|
||||
|
||||
public function __construct(private readonly RequestStack $requestStack)
|
||||
{
|
||||
}
|
||||
|
||||
protected function supports(string $attribute, mixed $subject): bool
|
||||
{
|
||||
if (false === in_array($attribute, [self::VIEW, self::EDIT])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $subject instanceof Booking;
|
||||
}
|
||||
|
||||
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
|
||||
{
|
||||
$bpnUser = $this->requestStack->getSession()->get('bpn_user');
|
||||
|
||||
if (null === $bpnUser) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var Booking $booking */
|
||||
$booking = $subject;
|
||||
|
||||
if ($booking->applicant->personId !== $bpnUser->getPersonId()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $booking->isEditable();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user