refactor: split buspro ApiClient into domain clients

This commit is contained in:
fromme committed 2026-10-02 13:55:30 +02:00
1 parent 0a55544ca8
commit b755a6f43e
52 files changed
+805 -695

No files matched your search

-399
View File
@@ -1,399 +0,0 @@
<?php
namespace App\BusProNet;
use App\BusProNet\DataProcessor\BookingDataProcessor;
use App\BusProNet\DataProcessor\VoucherSalePayloadBuilder;
use App\BusProNet\Exception\ApiClientException;
use App\BusProNet\Model\Agency;
use App\BusProNet\Model\BaseData;
use App\BusProNet\Model\Booking;
use App\BusProNet\Model\BookingResponse;
use App\BusProNet\Model\BookingUpdate;
use App\BusProNet\Model\CrmAttributes;
use App\BusProNet\Model\ExtendedServiceAvailabilityResponse;
use App\BusProNet\Model\Notification;
use App\BusProNet\Model\PersonalData;
use App\BusProNet\Model\PersonalDataUpdateResponse;
use App\BusProNet\Model\PromoVoucher;
use App\BusProNet\Model\PurchaseVoucher;
use App\BusProNet\Model\RegistrationResponse;
use App\BusProNet\Model\ServiceAvailabilityResponse;
use App\BusProNet\Model\Travel;
use App\BusProNet\Model\VoucherPaymentMethod;
use App\BusProNet\Model\VoucherPreview;
use App\BusProNet\Model\VoucherSaleResponse;
use App\BusProNet\Model\VoucherTemplates;
use App\BusProNet\Transport\XmlChannel;
use App\Form\Model\BookingDto;
use App\Form\Model\RegistrationDto;
use App\Form\Model\VoucherPurchaseDto;
class ApiClient
{
public const TYPE_NOTIFICATION = 'HINWEIS';
public const TYPE_CUSTOMER_DATA = 'KUNDENKONTO';
public const TYPE_BASE_DATA_COUNTRIES = 'STAMMLAENDER';
public const TYPE_MUTABLE_DATA = 'MOEGLICHEAENDERUNGEN';
public const TYPE_AVAILABILITY = 'VERFUEGBARKEIT';
public const TYPE_AVAILABILITY_EXTENDED = 'VERFUEGBARKEIT2';
public const TYPE_AVAILABILITY_HOTEL = 'VERFUEGBARKEITHOTEL';
public const TYPE_BOOKING_UPDATE = 'BUCHUNGAENDERUNG';
public const TYPE_BOOKING = 'BUCHUNG';
public const TYPE_PRODUCTS = 'PRODUKTE';
public const TYPE_PRODUCT_DATA = 'PRODUKTDATEN';
public const TYPE_AGENCIES = 'AGENTUREN';
public const TYPE_PURCHASE_VOUCHER = 'GUTSCHEINPRUEFUNGEINLOESUNG';
public const TYPE_PROMO_VOUCHER = 'AKTIONSGUTSCHEIN';
// Selling Kaufgutscheine. Not to be confused with TYPE_PURCHASE_VOUCHER, which redeems one in a booking.
public const TYPE_VOUCHER_TEMPLATES = 'GUTSCHEINVORLAGEN';
public const TYPE_VOUCHER_PAYMENT_METHODS = 'GUTSCHEINZAHLUNGSARTEN';
public const TYPE_VOUCHER_PREVIEW = 'GUTSCHEINVORSCHAU';
public const TYPE_VOUCHER_SALE = 'GUTSCHEINBUCHUNG';
public function __construct(
private readonly XmlChannel $channel,
private readonly BookingDataProcessor $bookingDataProcessor,
private readonly VoucherSalePayloadBuilder $voucherSalePayloadBuilder,
) {
}
/**
* @throws ApiClientException
*/
public function getPersonalData(string $email, string $password): Notification|PersonalData
{
return $this->customerRequest('Adressdaten', ['email' => $email, 'passwort' => $password]);
}
/**
* @throws ApiClientException
*/
public function register(RegistrationDto $registrationData): Notification|RegistrationResponse
{
return $this->customerRequest('Adresse_Neu', ['adressdaten' => $registrationData->toPayload()]);
}
/**
* @throws ApiClientException
*/
public function resetPassword(string $email): Notification
{
return $this->customerRequest('Passwort_Anfrage', ['email' => $email]);
}
/**
* @throws ApiClientException
*/
public function updatePersonalData(
string $email,
string $password,
PersonalData $personalData,
bool $debug = false,
): Notification|PersonalDataUpdateResponse {
return $this->customerRequest('Adressdaten_Ändern', [
'email' => $email,
'passwort' => $password,
'idadresse' => $personalData->addressId,
'adressdaten' => $personalData->toPayload(),
], $debug);
}
/**
* @throws ApiClientException
*/
public function createAddress(
PersonalData $personalData,
bool $debug = false,
): RegistrationResponse|Notification {
return $this->customerRequest('Adresse_Neu', [
'adressdaten' => $personalData->toPayload(false),
'ohnemailversand' => 'True',
], $debug);
}
/**
* @throws ApiClientException
*/
public function updateNewsletterRegistration(string $email, string $password, PersonalData $personalData): Notification|PersonalData
{
return $this->customerRequest('Newsletter', [
'email' => $email,
'passwort' => $password,
'idadresse' => $personalData->addressId,
'newsletter' => [
'email' => $email,
'anmeldung' => $personalData->communication->newsletter ? 'True' : 'False',
],
]);
}
/**
* @throws ApiClientException
*/
public function getBookings(string $email, string $password): Notification|BaseData
{
return $this->customerRequest('Vorgänge', ['email' => $email, 'passwort' => $password]);
}
/**
* @throws ApiClientException
*/
public function getBooking(string $email, string $password, int $id): Notification|Booking
{
return $this->customerRequest('Vorgang_Details', ['email' => $email, 'passwort' => $password, 'idbuchung' => $id]);
}
/**
* @throws ApiClientException
*/
public function updateBooking(BookingDto $formData, bool $debug = false): Notification|BookingUpdate
{
return $this->channel->request(static::TYPE_BOOKING_UPDATE, [
'buchungsart' => Constants::BOOKING_TYPE_BOOKING,
...$this->bookingDataProcessor->createUpdateRequestPayload($formData),
], [], $debug);
}
/**
* First phase of the two-phase booking process: validates the booking and returns the
* prices without creating it.
*
* @throws ApiClientException
*/
public function createBookingInquiry(BookingDto $bookingDto, bool $debug = false): Notification|BookingResponse
{
return $this->sendBooking($bookingDto, Constants::BOOKING_TYPE_INQUIRY, $debug);
}
/**
* Second phase of the two-phase booking process: creates the booking after a successful
* inquiry.
*
* @throws ApiClientException
*/
public function createBooking(BookingDto $bookingDto, bool $debug = false): Notification|BookingResponse
{
return $this->sendBooking($bookingDto, Constants::BOOKING_TYPE_BOOKING, $debug);
}
/**
* @throws ApiClientException
*/
public function getMutableData(int $dateId): Notification|BaseData
{
return $this->channel->request(static::TYPE_MUTABLE_DATA, ['idreise' => $dateId]);
}
/**
* @throws ApiClientException
*/
public function getAvailabilities(int $dateId): Notification|ServiceAvailabilityResponse
{
return $this->channel->request(static::TYPE_AVAILABILITY, ['idreise' => $dateId]);
}
/**
* @throws ApiClientException
*/
public function getAvailabilitiesExtended(int $dateId): Notification|ExtendedServiceAvailabilityResponse
{
return $this->channel->request(static::TYPE_AVAILABILITY_EXTENDED, ['idreise' => $dateId]);
}
/**
* @throws ApiClientException
*/
public function getHotelAvailability(int $dateId, int $hotelId, \DateTimeInterface $dateTo): Notification|BaseData
{
return $this->channel->request(static::TYPE_AVAILABILITY_HOTEL, [
'idreise' => $dateId,
'idpartner' => $hotelId,
'terminbis' => $dateTo->format('d.m.Y'),
]);
}
/**
* @throws ApiClientException
*/
public function getCrmAttributes(string $email, string $password): Notification|CrmAttributes
{
return $this->customerRequest('SelektionCRM', ['email' => $email, 'passwort' => $password]);
}
/**
* @throws ApiClientException
*/
public function getBaseData(string $type): Notification|BaseData
{
return $this->channel->request($type);
}
/**
* @throws ApiClientException
*/
public function getDocuments(string $email, string $password, int $bookingId, string $type): mixed
{
return $this->customerRequest($type, ['email' => $email, 'passwort' => $password, 'idbuchung' => $bookingId]);
}
/**
* @throws ApiClientException
*/
public function getTravelData(int $travelId, ?int $hotelId = null): Notification|Travel
{
return $this->channel->request(static::TYPE_PRODUCT_DATA, ['idprodukt' => $travelId], ['hotelId' => $hotelId]);
}
/**
* Returns the unparsed response, which has the structure of BusPro's XML export and is
* stored in the export directory as is.
*
* @throws ApiClientException
*/
public function getTravelDataXml(int $travelId): string
{
return $this->channel->requestRaw(static::TYPE_PRODUCT_DATA, ['idprodukt' => $travelId]);
}
/**
* @throws ApiClientException
*/
public function getProducts(): Notification|BaseData
{
return $this->channel->request(static::TYPE_PRODUCTS);
}
/**
* Changes rarely, so callers cache it for long periods.
*
* @return Agency[]|Notification
*
* @throws ApiClientException
*/
public function getAgencies(): array|Notification
{
return $this->channel->request(static::TYPE_AGENCIES);
}
/**
* Looks up a purchase voucher (Gutschein) by its redemption code. Purchase vouchers, regular
* or goodwill (Kulanz), carry a balance that each redemption reduces.
*
* @throws ApiClientException
*/
public function validatePurchaseVoucher(string $redemptionCode): Notification|PurchaseVoucher
{
return $this->channel->request(static::TYPE_PURCHASE_VOUCHER, ['einloesecode' => $redemptionCode]);
}
/**
* Validates a promo voucher (Aktionsgutschein) for a travel and participant price. Promo
* vouchers are absolute discounts, per person ("P") or per booking ("B") as pro_buchung_person
* says.
*
* @throws ApiClientException
*/
public function validatePromoVoucher(string $promoCode, int $travelId, float $participantPrice): Notification|PromoVoucher
{
return $this->channel->request(static::TYPE_PROMO_VOUCHER, [
'aktionsgutschein' => $promoCode,
'idreise' => $travelId,
'preis' => number_format($participantPrice, 2, ',', ''),
]);
}
/**
* Fetches the voucher designs on sale and the amount bounds.
*
* @throws ApiClientException
*/
public function getVoucherTemplates(): Notification|VoucherTemplates
{
return $this->channel->request(static::TYPE_VOUCHER_TEMPLATES);
}
/**
* Fetches the payment methods offered for voucher purchases.
*
* @return list<VoucherPaymentMethod>|Notification
*
* @throws ApiClientException
*/
public function getVoucherPaymentMethods(): array|Notification
{
return $this->channel->request(static::TYPE_VOUCHER_PAYMENT_METHODS);
}
/**
* Renders the PDF preview of a voucher design with the entered data.
*
* @throws ApiClientException
*/
public function getVoucherPreview(VoucherPurchaseDto $voucherPurchaseDto): Notification|VoucherPreview
{
return $this->channel->request(static::TYPE_VOUCHER_PREVIEW, $this->voucherSalePayloadBuilder->buildPreviewPayload($voucherPurchaseDto));
}
/**
* Dry run of a voucher purchase: full validation, nothing is issued.
*
* Every HINWEIS is a rejection on this path, including 650 (missing address data), so the
* result must not be judged by Notification::isError().
*
* @throws ApiClientException
*/
public function createVoucherSaleInquiry(VoucherPurchaseDto $voucherPurchaseDto): Notification|VoucherSaleResponse
{
return $this->sendVoucherSale($voucherPurchaseDto, Constants::BOOKING_TYPE_INQUIRY);
}
/**
* Buys the voucher. BusPro mails it to the buyer once the payment has arrived.
*
* Every HINWEIS is a rejection on this path, including 650.
*
* @throws ApiClientException
*/
public function createVoucherSale(VoucherPurchaseDto $voucherPurchaseDto): Notification|VoucherSaleResponse
{
return $this->sendVoucherSale($voucherPurchaseDto, Constants::BOOKING_TYPE_BOOKING);
}
/**
* Replays a request as is, except for the key, which is regenerated for the current date.
* Returns the unparsed response.
*
* @throws ApiClientException
*/
public function sendRawXml(string $xml, bool $debug = false): string
{
return $this->channel->replay($xml, $debug);
}
/**
* @throws ApiClientException
*/
private function sendBooking(BookingDto $bookingDto, string $bookingType, bool $debug): Notification|BookingResponse
{
return $this->channel->request(static::TYPE_BOOKING, $this->bookingDataProcessor->createBookingRequestPayload($bookingDto, $bookingType), [], $debug);
}
/**
* @throws ApiClientException
*/
private function sendVoucherSale(VoucherPurchaseDto $voucherPurchaseDto, string $saleType): Notification|VoucherSaleResponse
{
return $this->channel->request(static::TYPE_VOUCHER_SALE, $this->voucherSalePayloadBuilder->buildSalePayload($voucherPurchaseDto, $saleType));
}
/**
* @param array<string, mixed> $fields
*
* @throws ApiClientException
*/
private function customerRequest(string $art, array $fields, bool $debug = false): mixed
{
return $this->channel->request(static::TYPE_CUSTOMER_DATA, ['art' => $art, ...$fields], [], $debug);
}
}
+119
View File
@@ -0,0 +1,119 @@
<?php
namespace App\BusProNet\Client;
use App\BusProNet\Exception\ApiClientException;
use App\BusProNet\MessageType;
use App\BusProNet\Model\CrmAttributes;
use App\BusProNet\Model\Notification;
use App\BusProNet\Model\PersonalData;
use App\BusProNet\Model\PersonalDataUpdateResponse;
use App\BusProNet\Model\RegistrationResponse;
use App\BusProNet\Transport\XmlChannel;
use App\Form\Model\RegistrationDto;
/**
* A customer's BusPro account: login, address data, newsletter, CRM selections.
*/
class AccountClient
{
public function __construct(
private readonly XmlChannel $channel,
) {
}
/**
* @throws ApiClientException
*/
public function getPersonalData(string $email, string $password): Notification|PersonalData
{
return $this->channel->request(MessageType::CustomerData, [
'art' => 'Adressdaten',
'email' => $email,
'passwort' => $password,
]);
}
/**
* @throws ApiClientException
*/
public function register(RegistrationDto $registrationData): Notification|RegistrationResponse
{
return $this->channel->request(MessageType::CustomerData, [
'art' => 'Adresse_Neu',
'adressdaten' => $registrationData->toPayload(),
]);
}
/**
* @throws ApiClientException
*/
public function resetPassword(string $email): Notification
{
return $this->channel->request(MessageType::CustomerData, [
'art' => 'Passwort_Anfrage',
'email' => $email,
]);
}
/**
* @throws ApiClientException
*/
public function updatePersonalData(
string $email,
string $password,
PersonalData $personalData,
bool $debug = false,
): Notification|PersonalDataUpdateResponse {
return $this->channel->request(MessageType::CustomerData, [
'art' => 'Adressdaten_Ändern',
'email' => $email,
'passwort' => $password,
'idadresse' => $personalData->addressId,
'adressdaten' => $personalData->toPayload(),
], [], $debug);
}
/**
* @throws ApiClientException
*/
public function createAddress(
PersonalData $personalData,
bool $debug = false,
): RegistrationResponse|Notification {
return $this->channel->request(MessageType::CustomerData, [
'art' => 'Adresse_Neu',
'adressdaten' => $personalData->toPayload(false),
'ohnemailversand' => 'True',
], [], $debug);
}
/**
* @throws ApiClientException
*/
public function updateNewsletterRegistration(string $email, string $password, PersonalData $personalData): Notification|PersonalData
{
return $this->channel->request(MessageType::CustomerData, [
'art' => 'Newsletter',
'email' => $email,
'passwort' => $password,
'idadresse' => $personalData->addressId,
'newsletter' => [
'email' => $email,
'anmeldung' => $personalData->communication->newsletter ? 'True' : 'False',
],
]);
}
/**
* @throws ApiClientException
*/
public function getCrmAttributes(string $email, string $password): Notification|CrmAttributes
{
return $this->channel->request(MessageType::CustomerData, [
'art' => 'SelektionCRM',
'email' => $email,
'passwort' => $password,
]);
}
}
+106
View File
@@ -0,0 +1,106 @@
<?php
namespace App\BusProNet\Client;
use App\BusProNet\Constants;
use App\BusProNet\DataProcessor\BookingDataProcessor;
use App\BusProNet\Exception\ApiClientException;
use App\BusProNet\MessageType;
use App\BusProNet\Model\BaseData;
use App\BusProNet\Model\Booking;
use App\BusProNet\Model\BookingResponse;
use App\BusProNet\Model\BookingUpdate;
use App\BusProNet\Model\Notification;
use App\BusProNet\Transport\XmlChannel;
use App\Form\Model\BookingDto;
/**
* A customer's bookings: listing, details and documents, creating and changing them.
*/
class BookingClient
{
public function __construct(
private readonly XmlChannel $channel,
private readonly BookingDataProcessor $bookingDataProcessor,
) {
}
/**
* @throws ApiClientException
*/
public function getBookings(string $email, string $password): Notification|BaseData
{
return $this->channel->request(MessageType::CustomerData, [
'art' => 'Vorgänge',
'email' => $email,
'passwort' => $password,
]);
}
/**
* @throws ApiClientException
*/
public function getBooking(string $email, string $password, int $id): Notification|Booking
{
return $this->channel->request(MessageType::CustomerData, [
'art' => 'Vorgang_Details',
'email' => $email,
'passwort' => $password,
'idbuchung' => $id,
]);
}
/**
* @throws ApiClientException
*/
public function getDocuments(string $email, string $password, int $bookingId, string $type): mixed
{
return $this->channel->request(MessageType::CustomerData, [
'art' => $type,
'email' => $email,
'passwort' => $password,
'idbuchung' => $bookingId,
]);
}
/**
* @throws ApiClientException
*/
public function updateBooking(BookingDto $formData, bool $debug = false): Notification|BookingUpdate
{
return $this->channel->request(MessageType::BookingUpdate, [
'buchungsart' => Constants::BOOKING_TYPE_BOOKING,
...$this->bookingDataProcessor->createUpdateRequestPayload($formData),
], [], $debug);
}
/**
* First phase of the two-phase booking process: validates the booking and returns the
* prices without creating it.
*
* @throws ApiClientException
*/
public function createBookingInquiry(BookingDto $bookingDto, bool $debug = false): Notification|BookingResponse
{
return $this->sendBooking($bookingDto, Constants::BOOKING_TYPE_INQUIRY, $debug);
}
/**
* Second phase of the two-phase booking process: creates the booking after a successful
* inquiry.
*
* @throws ApiClientException
*/
public function createBooking(BookingDto $bookingDto, bool $debug = false): Notification|BookingResponse
{
return $this->sendBooking($bookingDto, Constants::BOOKING_TYPE_BOOKING, $debug);
}
/**
* @throws ApiClientException
*/
private function sendBooking(BookingDto $bookingDto, string $bookingType, bool $debug): Notification|BookingResponse
{
return $this->channel->request(MessageType::Booking, $this->bookingDataProcessor->createBookingRequestPayload($bookingDto, $bookingType), [], $debug);
}
}
@@ -0,0 +1,39 @@
<?php
namespace App\BusProNet\Client;
use App\BusProNet\Exception\ApiClientException;
use App\BusProNet\MessageType;
use App\BusProNet\Model\Agency;
use App\BusProNet\Model\BaseData;
use App\BusProNet\Model\Notification;
use App\BusProNet\Transport\XmlChannel;
/**
* Lists that change rarely and that callers cache for long periods.
*/
class ReferenceDataClient
{
public function __construct(
private readonly XmlChannel $channel,
) {
}
/**
* @return Agency[]|Notification
*
* @throws ApiClientException
*/
public function getAgencies(): array|Notification
{
return $this->channel->request(MessageType::Agencies);
}
/**
* @throws ApiClientException
*/
public function getBaseData(MessageType $type): Notification|BaseData
{
return $this->channel->request($type);
}
}
+87
View File
@@ -0,0 +1,87 @@
<?php
namespace App\BusProNet\Client;
use App\BusProNet\Exception\ApiClientException;
use App\BusProNet\MessageType;
use App\BusProNet\Model\BaseData;
use App\BusProNet\Model\ExtendedServiceAvailabilityResponse;
use App\BusProNet\Model\Notification;
use App\BusProNet\Model\ServiceAvailabilityResponse;
use App\BusProNet\Model\Travel;
use App\BusProNet\Transport\XmlChannel;
/**
* Travels from the live API. Most reads belong in TravelDataProvider, which arbitrates
* between this, the XML export and the snapshots.
*/
class TravelClient
{
public function __construct(
private readonly XmlChannel $channel,
) {
}
/**
* @throws ApiClientException
*/
public function getTravelData(int $travelId, ?int $hotelId = null): Notification|Travel
{
return $this->channel->request(MessageType::ProductData, ['idprodukt' => $travelId], ['hotelId' => $hotelId]);
}
/**
* Returns the unparsed response, which has the structure of BusPro's XML export and is
* stored in the export directory as is.
*
* @throws ApiClientException
*/
public function getTravelDataXml(int $travelId): string
{
return $this->channel->requestRaw(MessageType::ProductData, ['idprodukt' => $travelId]);
}
/**
* @throws ApiClientException
*/
public function getProducts(): Notification|BaseData
{
return $this->channel->request(MessageType::Products);
}
/**
* @throws ApiClientException
*/
public function getMutableData(int $dateId): Notification|BaseData
{
return $this->channel->request(MessageType::MutableData, ['idreise' => $dateId]);
}
/**
* @throws ApiClientException
*/
public function getAvailabilities(int $dateId): Notification|ServiceAvailabilityResponse
{
return $this->channel->request(MessageType::Availability, ['idreise' => $dateId]);
}
/**
* @throws ApiClientException
*/
public function getAvailabilitiesExtended(int $dateId): Notification|ExtendedServiceAvailabilityResponse
{
return $this->channel->request(MessageType::AvailabilityExtended, ['idreise' => $dateId]);
}
/**
* @throws ApiClientException
*/
public function getHotelAvailability(int $dateId, int $hotelId, \DateTimeInterface $dateTo): Notification|BaseData
{
return $this->channel->request(MessageType::AvailabilityHotel, [
'idreise' => $dateId,
'idpartner' => $hotelId,
'terminbis' => $dateTo->format('d.m.Y'),
]);
}
}
+121
View File
@@ -0,0 +1,121 @@
<?php
namespace App\BusProNet\Client;
use App\BusProNet\Constants;
use App\BusProNet\DataProcessor\VoucherSalePayloadBuilder;
use App\BusProNet\Exception\ApiClientException;
use App\BusProNet\MessageType;
use App\BusProNet\Model\Notification;
use App\BusProNet\Model\PromoVoucher;
use App\BusProNet\Model\PurchaseVoucher;
use App\BusProNet\Model\VoucherPaymentMethod;
use App\BusProNet\Model\VoucherPreview;
use App\BusProNet\Model\VoucherSaleResponse;
use App\BusProNet\Model\VoucherTemplates;
use App\BusProNet\Transport\XmlChannel;
use App\Form\Model\VoucherPurchaseDto;
/**
* Redeeming vouchers in a booking, and selling purchase vouchers (Kaufgutscheine).
*/
class VoucherClient
{
public function __construct(
private readonly XmlChannel $channel,
private readonly VoucherSalePayloadBuilder $voucherSalePayloadBuilder,
) {
}
/**
* Looks up a purchase voucher (Gutschein) by its redemption code. Purchase vouchers, regular
* or goodwill (Kulanz), carry a balance that each redemption reduces.
*
* @throws ApiClientException
*/
public function validatePurchaseVoucher(string $redemptionCode): Notification|PurchaseVoucher
{
return $this->channel->request(MessageType::PurchaseVoucher, ['einloesecode' => $redemptionCode]);
}
/**
* Validates a promo voucher (Aktionsgutschein) for a travel and participant price. Promo
* vouchers are absolute discounts, per person ("P") or per booking ("B") as pro_buchung_person
* says.
*
* @throws ApiClientException
*/
public function validatePromoVoucher(string $promoCode, int $travelId, float $participantPrice): Notification|PromoVoucher
{
return $this->channel->request(MessageType::PromoVoucher, [
'aktionsgutschein' => $promoCode,
'idreise' => $travelId,
'preis' => number_format($participantPrice, 2, ',', ''),
]);
}
/**
* Fetches the voucher designs on sale and the amount bounds.
*
* @throws ApiClientException
*/
public function getVoucherTemplates(): Notification|VoucherTemplates
{
return $this->channel->request(MessageType::VoucherTemplates);
}
/**
* Fetches the payment methods offered for voucher purchases.
*
* @return list<VoucherPaymentMethod>|Notification
*
* @throws ApiClientException
*/
public function getVoucherPaymentMethods(): array|Notification
{
return $this->channel->request(MessageType::VoucherPaymentMethods);
}
/**
* Renders the PDF preview of a voucher design with the entered data.
*
* @throws ApiClientException
*/
public function getVoucherPreview(VoucherPurchaseDto $voucherPurchaseDto): Notification|VoucherPreview
{
return $this->channel->request(MessageType::VoucherPreview, $this->voucherSalePayloadBuilder->buildPreviewPayload($voucherPurchaseDto));
}
/**
* Dry run of a voucher purchase: full validation, nothing is issued.
*
* Every HINWEIS is a rejection on this path, including 650 (missing address data), so the
* result must not be judged by Notification::isError().
*
* @throws ApiClientException
*/
public function createVoucherSaleInquiry(VoucherPurchaseDto $voucherPurchaseDto): Notification|VoucherSaleResponse
{
return $this->sendVoucherSale($voucherPurchaseDto, Constants::BOOKING_TYPE_INQUIRY);
}
/**
* Buys the voucher. BusPro mails it to the buyer once the payment has arrived.
*
* Every HINWEIS is a rejection on this path, including 650.
*
* @throws ApiClientException
*/
public function createVoucherSale(VoucherPurchaseDto $voucherPurchaseDto): Notification|VoucherSaleResponse
{
return $this->sendVoucherSale($voucherPurchaseDto, Constants::BOOKING_TYPE_BOOKING);
}
/**
* @throws ApiClientException
*/
private function sendVoucherSale(VoucherPurchaseDto $voucherPurchaseDto, string $saleType): Notification|VoucherSaleResponse
{
return $this->channel->request(MessageType::VoucherSale, $this->voucherSalePayloadBuilder->buildSalePayload($voucherPurchaseDto, $saleType));
}
}
@@ -2,7 +2,8 @@
namespace App\BusProNet\DataProvider;
use App\BusProNet\ApiClient;
use App\BusProNet\Client\ReferenceDataClient;
use App\BusProNet\MessageType;
use App\BusProNet\Model\Country;
use Psr\Cache\InvalidArgumentException;
use Symfony\Contracts\Cache\CacheInterface;
@@ -10,7 +11,7 @@ use Symfony\Contracts\Cache\ItemInterface;
class CountryDataProvider
{
public function __construct(private readonly ApiClient $apiClient, private readonly CacheInterface $cache)
public function __construct(private readonly ReferenceDataClient $referenceDataClient, private readonly CacheInterface $cache)
{
}
@@ -22,8 +23,8 @@ class CountryDataProvider
$item->expiresAfter(6 * 60 * 60);
return $this
->apiClient
->getBaseData(ApiClient::TYPE_BASE_DATA_COUNTRIES)
->referenceDataClient
->getBaseData(MessageType::Countries)
->getItems()
;
});
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace App\BusProNet;
/**
* The `typ` of BusPro's `<satz>`: the command on a request, the kind of answer on a response.
*/
enum MessageType: string
{
// Only ever a response: BusPro's refusal or remark, whatever was asked.
case Notification = 'HINWEIS';
case CustomerData = 'KUNDENKONTO';
case Countries = 'STAMMLAENDER';
case MutableData = 'MOEGLICHEAENDERUNGEN';
case Availability = 'VERFUEGBARKEIT';
case AvailabilityExtended = 'VERFUEGBARKEIT2';
case AvailabilityHotel = 'VERFUEGBARKEITHOTEL';
case BookingUpdate = 'BUCHUNGAENDERUNG';
case Booking = 'BUCHUNG';
case Products = 'PRODUKTE';
case ProductData = 'PRODUKTDATEN';
case Agencies = 'AGENTUREN';
case PurchaseVoucher = 'GUTSCHEINPRUEFUNGEINLOESUNG';
case PromoVoucher = 'AKTIONSGUTSCHEIN';
// Selling Kaufgutscheine. Not to be confused with PurchaseVoucher, which redeems one in a booking.
case VoucherTemplates = 'GUTSCHEINVORLAGEN';
case VoucherPaymentMethods = 'GUTSCHEINZAHLUNGSARTEN';
case VoucherPreview = 'GUTSCHEINVORSCHAU';
case VoucherSale = 'GUTSCHEINBUCHUNG';
}
+11 -10
View File
@@ -5,6 +5,7 @@ namespace App\BusProNet\Transport;
use App\BusProNet\Exception\ApiClientException;
use App\BusProNet\Exception\ImmediateConnectionCloseException;
use App\BusProNet\Exception\ResponseParserException;
use App\BusProNet\MessageType;
use App\BusProNet\XmlParser\ApiResponseParser;
use App\Service\XmlDumpWriter;
use Psr\Log\LoggerInterface;
@@ -42,7 +43,7 @@ class XmlChannel
*
* @throws ApiClientException
*/
public function request(string $type, array $fields = [], array $parserArgs = [], bool $debug = false): mixed
public function request(MessageType $type, array $fields = [], array $parserArgs = [], bool $debug = false): mixed
{
$data = $this->envelope($type, $fields);
@@ -50,10 +51,10 @@ class XmlChannel
$requestId = $this->requestIds->next();
$dump = $debug || true === $this->config['debug'];
$xml = $this->exchange($requestId, $this->serializeXml($data), $type, 'Sending request to BPN API', $dump);
$xml = $this->exchange($requestId, $this->serializeXml($data), $type->value, 'Sending request to BPN API', $dump);
return $this->parseResponse($type, $xml, $parserArgs, $requestId);
}, $type);
}, $type->value);
}
/**
@@ -63,7 +64,7 @@ class XmlChannel
*
* @throws ApiClientException
*/
public function requestRaw(string $type, array $fields = []): string
public function requestRaw(MessageType $type, array $fields = []): string
{
$data = $this->envelope($type, $fields);
@@ -71,7 +72,7 @@ class XmlChannel
$requestId = $this->requestIds->next();
$body = $this->serializeXml($data);
return $this->exchange($requestId, $body, $type, 'Sending raw request to BPN API', true === $this->config['debug']);
return $this->exchange($requestId, $body, $type->value, 'Sending raw request to BPN API', true === $this->config['debug']);
});
}
@@ -93,12 +94,12 @@ class XmlChannel
*
* @return array<string, mixed>
*/
private function envelope(string $type, array $fields): array
private function envelope(MessageType $type, array $fields): array
{
return [
'user' => $this->config['bpn_username'],
'key' => $this->createKey($type),
'satz' => ['@typ' => $type],
'key' => $this->createKey($type->value),
'satz' => ['@typ' => $type->value],
...$fields,
];
}
@@ -205,7 +206,7 @@ class XmlChannel
*
* @throws ApiClientException
*/
private function parseResponse(string $type, string $xml, array $parserArgs, string $requestId, bool $dumpOnFailure = true): mixed
private function parseResponse(MessageType $type, string $xml, array $parserArgs, string $requestId, bool $dumpOnFailure = true): mixed
{
try {
return $this->responseParser->parseXmlString($type, $xml, $parserArgs);
@@ -218,7 +219,7 @@ class XmlChannel
$this->logger->error('Unable to parse response received from API', [
'request_id' => $requestId,
'type' => $type,
'type' => $type->value,
'response_length' => strlen($xml),
'error_message' => $e->getMessage(),
]);
+3 -3
View File
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\BusProNet\XmlLoader;
use App\BusProNet\ApiClient;
use App\BusProNet\Client\ReferenceDataClient;
use App\BusProNet\Model\Agency;
use App\BusProNet\Model\Notification;
use Psr\Cache\InvalidArgumentException;
@@ -19,7 +19,7 @@ class AgencyLoader
public function __construct(
private readonly CacheInterface $cache,
private readonly ApiClient $apiClient,
private readonly ReferenceDataClient $referenceDataClient,
private readonly LoggerInterface $logger,
) {
}
@@ -34,7 +34,7 @@ class AgencyLoader
// Cache for 24 hours (agencies change infrequently)
$item->expiresAfter(24 * 60 * 60);
$result = $this->apiClient->getAgencies();
$result = $this->referenceDataClient->getAgencies();
if ($result instanceof Notification) {
$this->logger->error('Failed to fetch agencies from BPN API', [
+28 -26
View File
@@ -2,8 +2,8 @@
namespace App\BusProNet\XmlParser;
use App\BusProNet\ApiClient;
use App\BusProNet\Exception\ResponseParserException;
use App\BusProNet\MessageType;
use App\BusProNet\XmlCrawlerFactory;
class ApiResponseParser extends AbstractParser
@@ -14,10 +14,11 @@ class ApiResponseParser extends AbstractParser
}
/**
* @param array<string, mixed> $additionalArgs
*
* @throws ResponseParserException
*/
/** @param array<string, mixed> $additionalArgs */
public function parseXmlString(string $type, string $xml, array $additionalArgs = []): mixed
public function parseXmlString(MessageType $type, string $xml, array $additionalArgs = []): mixed
{
$crawler = XmlCrawlerFactory::create($xml);
@@ -25,17 +26,20 @@ class ApiResponseParser extends AbstractParser
throw new ResponseParserException('Unable to parse XML response: '.XmlCrawlerFactory::diagnose($xml));
}
// Override type unless present in XML to catch error responses
$responseType = $this
->getStringOrNullValue($crawler->filterXPath('//ergebnis/satz/@typ')) ?? $type;
// The type in the XML wins, to catch error responses. BusPro may send one we don't know.
$typeAttribute = $this->getStringOrNullValue($crawler->filterXPath('//ergebnis/satz/@typ'));
$responseType = null === $typeAttribute ? $type : MessageType::tryFrom($typeAttribute);
if (null === $responseType) {
throw new ResponseParserException(sprintf('Unrecognised BusProNet response type "%s"', $typeAttribute));
}
$resultNode = $crawler->filterXPath('//ergebnis');
try {
switch ($responseType) {
case ApiClient::TYPE_NOTIFICATION:
case MessageType::Notification:
return (new NotificationParser())->parse($crawler);
case ApiClient::TYPE_CUSTOMER_DATA:
case MessageType::CustomerData:
$subType = $this->getStringOrNullValue($resultNode->filterXPath('//art'));
if (null === $subType || '' === $subType) {
throw new ResponseParserException('Unable to determine customer data subtype from XML response');
@@ -63,23 +67,23 @@ class ApiResponseParser extends AbstractParser
}
throw new ResponseParserException(sprintf('Unrecognised customer data subtype "%s"', $subType));
case ApiClient::TYPE_BASE_DATA_COUNTRIES:
case MessageType::Countries:
return (new CountriesParser())->parse($resultNode);
case ApiClient::TYPE_MUTABLE_DATA:
case MessageType::MutableData:
return (new MutableDataParser())->parse($resultNode);
case ApiClient::TYPE_AVAILABILITY:
case MessageType::Availability:
return (new AvailabilitiesParser())->parseServices($resultNode);
case ApiClient::TYPE_AVAILABILITY_EXTENDED:
case MessageType::AvailabilityExtended:
return (new ExtendedAvailabilitiesParser())->parseServices($resultNode);
case ApiClient::TYPE_AVAILABILITY_HOTEL:
case MessageType::AvailabilityHotel:
return (new AvailabilitiesParser())->parseRooms($resultNode);
case ApiClient::TYPE_BOOKING_UPDATE:
case MessageType::BookingUpdate:
return (new BookingUpdateParser())->parse($resultNode);
case ApiClient::TYPE_BOOKING:
case MessageType::Booking:
return (new BookingResponseParser())->parse($resultNode);
case ApiClient::TYPE_PRODUCTS:
case MessageType::Products:
return (new ProductsParser())->parse($resultNode);
case ApiClient::TYPE_PRODUCT_DATA:
case MessageType::ProductData:
$travelNode = $crawler->filterXPath('//reise/termin');
if (0 === $travelNode->count()) {
@@ -87,25 +91,23 @@ class ApiResponseParser extends AbstractParser
}
return $this->travelParser->parse($travelNode->first(), ...$additionalArgs);
case ApiClient::TYPE_AGENCIES:
case MessageType::Agencies:
return (new AgencyParser())->parse($resultNode);
case ApiClient::TYPE_PURCHASE_VOUCHER:
case MessageType::PurchaseVoucher:
return (new PurchaseVoucherParser())->parse($resultNode);
case ApiClient::TYPE_PROMO_VOUCHER:
case MessageType::PromoVoucher:
return (new PromoVoucherParser())->parse($resultNode);
case ApiClient::TYPE_VOUCHER_TEMPLATES:
case MessageType::VoucherTemplates:
return (new VoucherTemplatesParser())->parse($resultNode);
case ApiClient::TYPE_VOUCHER_PAYMENT_METHODS:
case MessageType::VoucherPaymentMethods:
return (new VoucherPaymentMethodsParser())->parse($resultNode);
case ApiClient::TYPE_VOUCHER_PREVIEW:
case MessageType::VoucherPreview:
return (new VoucherPreviewParser())->parse($resultNode);
case ApiClient::TYPE_VOUCHER_SALE:
case MessageType::VoucherSale:
return (new VoucherSaleResponseParser())->parse($resultNode);
}
} catch (\InvalidArgumentException $e) {
throw new ResponseParserException('Invalid XML response structure: '.$e->getMessage(), 0, $e);
}
throw new ResponseParserException(sprintf('Unrecognised BusProNet response type "%s"', $responseType));
}
}
+6 -4
View File
@@ -4,7 +4,8 @@ declare(strict_types=1);
namespace App\Command;
use App\BusProNet\ApiClient;
use App\BusProNet\Client\BookingClient;
use App\BusProNet\Client\TravelClient;
use App\BusProNet\Constants;
use App\BusProNet\DataProcessor\BookingDataProcessor;
use App\BusProNet\Model\Booking;
@@ -78,7 +79,8 @@ class BookingRepairDraftCommand extends Command
public function __construct(
private readonly BookingEditDraftRepository $draftRepository,
private readonly EntityManagerInterface $entityManager,
private readonly ApiClient $apiClient,
private readonly BookingClient $bookingClient,
private readonly TravelClient $travelClient,
private readonly Crypt $crypt,
private readonly TravelDataProvider $travelDataProvider,
private readonly BookingDataProcessor $bookingDataProcessor,
@@ -214,7 +216,7 @@ class BookingRepairDraftCommand extends Command
private function fetchLiveBooking(SymfonyStyle $io, User $user, int $bookingId): ?Booking
{
$result = $this->apiClient->getBooking(
$result = $this->bookingClient->getBooking(
(string) $user->getEmail(),
$this->crypt->decrypt((string) $user->getPassword()),
$bookingId,
@@ -300,7 +302,7 @@ class BookingRepairDraftCommand extends Command
}
}
$live = $this->apiClient->getAvailabilitiesExtended((int) $travel->id);
$live = $this->travelClient->getAvailabilitiesExtended((int) $travel->id);
if ($live instanceof Notification) {
$io->warning(sprintf(
'VERFUEGBARKEIT2 failed (%d %s); falling back to travel data, which may be stale.',
+3 -3
View File
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Command;
use App\BusProNet\ApiClient;
use App\BusProNet\Client\TravelClient;
use App\BusProNet\Exception\ApiClientException;
use League\Flysystem\FilesystemException;
use League\Flysystem\FilesystemOperator;
@@ -29,7 +29,7 @@ class BpnFetchTravelCommand extends Command
];
public function __construct(
private readonly ApiClient $apiClient,
private readonly TravelClient $travelClient,
private readonly FilesystemOperator $xmlExport,
private readonly CacheInterface $cache,
private readonly LoggerInterface $logger,
@@ -56,7 +56,7 @@ class BpnFetchTravelCommand extends Command
$io->text(sprintf('Fetching travel data for product ID %d...', $travelId));
try {
$xml = $this->apiClient->getTravelDataXml($travelId);
$xml = $this->travelClient->getTravelDataXml($travelId);
} catch (ApiClientException $e) {
$io->error(sprintf('API request failed: %s', $e->getMessage()));
$this->logger->error('Failed to fetch travel data from API', [
+3 -3
View File
@@ -4,8 +4,8 @@ declare(strict_types=1);
namespace App\Command;
use App\BusProNet\ApiClient;
use App\BusProNet\Exception\ApiClientException;
use App\BusProNet\Transport\XmlChannel;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
@@ -21,7 +21,7 @@ use Symfony\Component\Console\Style\SymfonyStyle;
class BpnReplayCommand extends Command
{
public function __construct(
private readonly ApiClient $apiClient,
private readonly XmlChannel $channel,
) {
parent::__construct();
}
@@ -77,7 +77,7 @@ class BpnReplayCommand extends Command
$io->info('Sending request to BPN API...');
try {
$response = $this->apiClient->sendRawXml($xml);
$response = $this->channel->replay($xml);
} catch (ApiClientException $e) {
$io->error(sprintf('API request failed: %s', $e->getMessage()));
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Controller\Account;
use App\BusProNet\ApiClient;
use App\BusProNet\Client\AccountClient;
use App\BusProNet\Exception\ApiClientException;
use App\BusProNet\Model\Notification;
use App\BusProNet\Model\PersonalData;
@@ -39,7 +39,7 @@ class PersonalDataController extends AbstractController
public const SESSION_REDIRECT_KEY = '_profile_completion_redirect';
/**
* @param ApiClient $apiClient BusProNet API client for data operations
* @param AccountClient $accountClient BusProNet account client
* @param Crypt $crypt Encryption service for password handling
* @param BookingEditDataLoader $dataLoader Data loader for cache invalidation
* @param ProfileCompletenessChecker $completenessChecker Profile validation service
@@ -48,7 +48,7 @@ class PersonalDataController extends AbstractController
* @param LoggerInterface $logger Logger for audit trails and debugging
*/
public function __construct(
private readonly ApiClient $apiClient,
private readonly AccountClient $accountClient,
private readonly Crypt $crypt,
private readonly BookingEditDataLoader $dataLoader,
private readonly ProfileCompletenessChecker $completenessChecker,
@@ -100,7 +100,7 @@ class PersonalDataController extends AbstractController
if ($personalDataForm->isSubmitted() && $personalDataForm->isValid()) {
try {
$updateResult = $this->apiClient->updatePersonalData($email, $password, $personalData);
$updateResult = $this->accountClient->updatePersonalData($email, $password, $personalData);
if ($updateResult instanceof Notification) {
$this->logger->error('Personal data update rejected by BusProNet', [
@@ -223,7 +223,7 @@ class PersonalDataController extends AbstractController
try {
$personalData = $this
->apiClient
->accountClient
->getPersonalData($email, $password);
} catch (ApiClientException $e) {
$this->addFlash('error', 'Deine persönlichen Daten konnten nicht abgerufen werden');
+3 -3
View File
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Controller\Api;
use App\BusProNet\ApiClient;
use App\BusProNet\Client\AccountClient;
use App\BusProNet\Exception\ApiClientException;
use App\BusProNet\Model\ContactFormSubmission;
use App\BusProNet\Model\Notification;
@@ -21,7 +21,7 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
#[IsGranted('ROLE_OAUTH2_API')]
class ContactFormController extends AbstractController
{
public function __construct(private readonly ApiClient $apiClient, private readonly LoggerInterface $logger)
public function __construct(private readonly AccountClient $accountClient, private readonly LoggerInterface $logger)
{
}
@@ -31,7 +31,7 @@ class ContactFormController extends AbstractController
$personalData = PersonalData::fromContactFormSubmission($dto);
try {
$result = $this->apiClient->createAddress($personalData);
$result = $this->accountClient->createAddress($personalData);
if ($result instanceof Notification) {
$this->logger->info('Unable to submit contact form', [
@@ -2,7 +2,7 @@
namespace App\Controller\Api;
use App\BusProNet\ApiClient;
use App\BusProNet\Client\AccountClient;
use App\BusProNet\Exception\ApiClientException;
use App\BusProNet\Model\Notification;
use App\Entity\User;
@@ -17,7 +17,7 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
#[IsGranted('ROLE_OAUTH2_PROFILE')]
class CrmAttributeController extends AbstractController
{
public function __construct(private readonly ApiClient $apiClient, private readonly Crypt $crypt)
public function __construct(private readonly AccountClient $accountClient, private readonly Crypt $crypt)
{
}
@@ -30,7 +30,7 @@ class CrmAttributeController extends AbstractController
$password = $this->crypt->decrypt($user->getPassword());
try {
$data = $this->apiClient->getCrmAttributes($email, $password);
$data = $this->accountClient->getCrmAttributes($email, $password);
if ($data instanceof Notification) {
return new JsonResponse(['message' => $data->message, 'code' => $data->code], Response::HTTP_BAD_REQUEST);
+3 -3
View File
@@ -2,7 +2,7 @@
namespace App\Controller\Api;
use App\BusProNet\ApiClient;
use App\BusProNet\Client\TravelClient;
use App\BusProNet\Exception\ApiClientException;
use Psr\Cache\InvalidArgumentException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@@ -17,7 +17,7 @@ use Symfony\Contracts\Cache\ItemInterface;
class ProductController extends AbstractController
{
public function __construct(
private readonly ApiClient $apiClient,
private readonly TravelClient $travelClient,
private readonly CacheInterface $cache,
) {
}
@@ -29,7 +29,7 @@ class ProductController extends AbstractController
$products = $this->cache->get('api_products', function (ItemInterface $item) {
$item->expiresAfter(3600); // 1 hour cache
return $this->apiClient->getProducts();
return $this->travelClient->getProducts();
});
return $this->json($products);
+3 -3
View File
@@ -2,7 +2,7 @@
namespace App\Controller\Api;
use App\BusProNet\ApiClient;
use App\BusProNet\Client\TravelClient;
use App\BusProNet\Exception\ApiClientException;
use App\BusProNet\Model\Travel;
use App\BusProNet\Utility\DateCodeUtility;
@@ -20,7 +20,7 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
class TravelController extends AbstractController
{
public function __construct(
private readonly ApiClient $apiClient,
private readonly TravelClient $travelClient,
private readonly TravelDataProvider $travelDataService,
) {
}
@@ -89,7 +89,7 @@ class TravelController extends AbstractController
#[MapDateTime(format: 'Y-m-d')] \DateTimeImmutable $dateTo,
): JsonResponse {
try {
$result = $this->apiClient->getHotelAvailability($dateId, $hotelId, $dateTo);
$result = $this->travelClient->getHotelAvailability($dateId, $hotelId, $dateTo);
return $this->json($result);
} catch (ApiClientException $e) {
+3 -3
View File
@@ -2,7 +2,7 @@
namespace App\Controller\Api;
use App\BusProNet\ApiClient;
use App\BusProNet\Client\AccountClient;
use App\BusProNet\Exception\ApiClientException;
use App\BusProNet\Model\Notification;
use App\BusProNet\Model\PersonalData;
@@ -19,7 +19,7 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
#[IsGranted('ROLE_OAUTH2_EMAIL')]
class UserinfoController extends AbstractController
{
public function __construct(private readonly ApiClient $apiClient, private readonly Crypt $crypt)
public function __construct(private readonly AccountClient $accountClient, private readonly Crypt $crypt)
{
}
@@ -48,7 +48,7 @@ class UserinfoController extends AbstractController
$password = $this->crypt->decrypt($user->getPassword());
try {
$data = $this->apiClient->getPersonalData($email, $password);
$data = $this->accountClient->getPersonalData($email, $password);
if ($data instanceof Notification) {
return new JsonResponse(['message' => $data->message, 'code' => $data->code], Response::HTTP_BAD_REQUEST);
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Controller\Booking\Create;
use App\BusProNet\ApiClient;
use App\BusProNet\Client\BookingClient;
use App\BusProNet\Exception\TimeoutException;
use App\BusProNet\Model\BookingResponse;
use App\BusProNet\Model\Notification;
@@ -40,7 +40,7 @@ class Step3Controller extends AbstractBookingCreateController
private readonly BookingCreateContextFactory $createContextFactory,
private readonly BookingPriceCalculator $priceCalculator,
private readonly BookingPriceMismatchAnalyzer $priceMismatchDiagnostics,
private readonly ApiClient $apiClient,
private readonly BookingClient $bookingClient,
private readonly ParticipantFormSupport $participantFormSupportService,
private readonly LoggerInterface $logger,
) {
@@ -76,7 +76,7 @@ class Step3Controller extends AbstractBookingCreateController
$this->bookingService->applyCreateBookingStatusRules($bookingCreateDto);
// Validate booking data with API by submitting an inquiry booking
$inquiryResponse = $this->apiClient->createBookingInquiry($bookingCreateDto);
$inquiryResponse = $this->bookingClient->createBookingInquiry($bookingCreateDto);
// Surface any insurance corrections made during payload building (e.g. a
// stale price tier reassigned/cleared right before submission)
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Controller\Booking\Create;
use App\BusProNet\ApiClient;
use App\BusProNet\Client\BookingClient;
use App\BusProNet\Exception\TimeoutException;
use App\BusProNet\Model\Notification;
use App\Entity\User;
@@ -37,7 +37,7 @@ class Step4Controller extends AbstractBookingCreateController
private readonly BookingConfigurator $bookingService,
private readonly BookingSessionManager $bookingSessionService,
private readonly BookingCreateContextFactory $createContextFactory,
private readonly ApiClient $apiClient,
private readonly BookingClient $bookingClient,
private readonly CacheInterface $cache,
private readonly BookingNewsletterOptIn $bookingNewsletterOptIn,
private readonly ParticipantFormSupport $participantFormSupportService,
@@ -80,7 +80,7 @@ class Step4Controller extends AbstractBookingCreateController
$this->bookingService->applyCreateBookingStatusRules($bookingCreateDto);
// Submit final booking (already validated in Step 3)
$bookingResponse = $this->apiClient->createBooking($bookingCreateDto);
$bookingResponse = $this->bookingClient->createBooking($bookingCreateDto);
// Surface any insurance corrections made during payload building (e.g. a
// stale price tier reassigned/cleared right before submission)
@@ -2,7 +2,7 @@
namespace App\Controller\Booking;
use App\BusProNet\ApiClient;
use App\BusProNet\Client\BookingClient;
use App\BusProNet\Exception\ApiClientException;
use App\BusProNet\Model\Notification;
use App\Entity\User;
@@ -20,7 +20,7 @@ use function Symfony\Component\String\u;
class DownloadController extends AbstractController
{
public function __construct(
private readonly ApiClient $apiClient,
private readonly BookingClient $bookingClient,
private readonly Crypt $crypt,
private readonly LoggerInterface $logger,
) {
@@ -60,7 +60,7 @@ class DownloadController extends AbstractController
try {
$file = $this
->apiClient
->bookingClient
->getDocuments($email, $password, $id, $type);
} catch (ApiClientException $e) {
$file = null;
+3 -3
View File
@@ -2,7 +2,7 @@
namespace App\Controller\Booking;
use App\BusProNet\ApiClient;
use App\BusProNet\Client\BookingClient;
use App\BusProNet\Exception\ApiClientException;
use App\BusProNet\Model\BaseData;
use App\BusProNet\Model\Notification;
@@ -19,7 +19,7 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
class IndexController extends AbstractController
{
public function __construct(
private readonly ApiClient $apiClient,
private readonly BookingClient $bookingClient,
private readonly TravelLoader $travelDataLoader,
private readonly Crypt $crypt,
private readonly LoggerInterface $logger,
@@ -36,7 +36,7 @@ class IndexController extends AbstractController
$password = $this->crypt->decrypt($user->getPassword());
try {
$bookings = $this->apiClient->getBookings($email, $password);
$bookings = $this->bookingClient->getBookings($email, $password);
} catch (ApiClientException $e) {
$this->addFlash('error', 'Buchungen nicht abrufbar');
$bookings = new BaseData([]);
+3 -3
View File
@@ -2,7 +2,7 @@
namespace App\Controller;
use App\BusProNet\ApiClient;
use App\BusProNet\Client\AccountClient;
use App\BusProNet\Exception\ApiClientException;
use App\BusProNet\Model\Notification;
use App\Form\Model\RegistrationDto;
@@ -17,7 +17,7 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
class RegistrationController extends AbstractController
{
public function __construct(
private readonly ApiClient $apiClient,
private readonly AccountClient $accountClient,
private readonly LoggerInterface $logger,
) {
}
@@ -34,7 +34,7 @@ class RegistrationController extends AbstractController
if ($form->isSubmitted() && $form->isValid()) {
try {
$response = $this->apiClient->register($registrationData);
$response = $this->accountClient->register($registrationData);
if ($response instanceof Notification && true === $response->isError()) {
$this->addFlash('error', 'Möglicherweise bist du bereits registriert. Bitte setze dein Passwort zurück.');
} else {
+3 -3
View File
@@ -2,7 +2,7 @@
namespace App\Controller;
use App\BusProNet\ApiClient;
use App\BusProNet\Client\AccountClient;
use App\BusProNet\Exception\ApiClientException;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@@ -16,7 +16,7 @@ use Symfony\Component\Validator\Constraints\NotBlank;
class ResetPasswordController extends AbstractController
{
public function __construct(private readonly ApiClient $apiClient, private readonly LoggerInterface $logger)
public function __construct(private readonly AccountClient $accountClient, private readonly LoggerInterface $logger)
{
}
@@ -46,7 +46,7 @@ class ResetPasswordController extends AbstractController
if ($form->isSubmitted() && $form->isValid()) {
try {
$this->apiClient->resetPassword($form->get('email')->getData());
$this->accountClient->resetPassword($form->get('email')->getData());
$this->addFlash('info', 'Du erhältst in Kürze eine E-Mail mit einem Link zum Zurücksetzen deines Passworts – vorausgesetzt, bei deiner Buchung wurde eine E-Mail-Adresse angegeben.');
$this->logger->info('Initiated password reset', [
'email' => $form->get('email')->getData(),
+3 -3
View File
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Controller\Voucher;
use App\BusProNet\ApiClient;
use App\BusProNet\Client\VoucherClient;
use App\BusProNet\Exception\ApiClientException;
use App\BusProNet\Model\Notification;
use App\Exception\VoucherSessionNotFoundException;
@@ -31,7 +31,7 @@ class Step1Controller extends AbstractVoucherPurchaseController
VoucherPurchaseSessionManager $sessionManager,
VoucherThumbnailResolver $thumbnailResolver,
private readonly VoucherTemplateProvider $templateProvider,
private readonly ApiClient $apiClient,
private readonly VoucherClient $voucherClient,
private readonly ValidatorInterface $validator,
private readonly LoggerInterface $logger,
) {
@@ -136,7 +136,7 @@ class Step1Controller extends AbstractVoucherPurchaseController
$request->getSession()->save();
try {
$preview = $this->apiClient->getVoucherPreview($dto);
$preview = $this->voucherClient->getVoucherPreview($dto);
} catch (ApiClientException $e) {
$this->logger->error('Voucher preview failed', ['error' => $e->getMessage()]);
+4 -4
View File
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Controller\Voucher;
use App\BusProNet\ApiClient;
use App\BusProNet\Client\VoucherClient;
use App\BusProNet\Exception\TimeoutException;
use App\BusProNet\Model\Notification;
use App\Form\Model\VoucherPurchaseDto;
@@ -32,7 +32,7 @@ class Step3Controller extends AbstractVoucherPurchaseController
public function __construct(
VoucherPurchaseSessionManager $sessionManager,
VoucherThumbnailResolver $thumbnailResolver,
private readonly ApiClient $apiClient,
private readonly VoucherClient $voucherClient,
private readonly VoucherSaleRejectionMapper $rejectionMapper,
private readonly LoggerInterface $logger,
) {
@@ -78,7 +78,7 @@ class Step3Controller extends AbstractVoucherPurchaseController
private function runDryRun(Request $request, VoucherPurchaseDto $dto, FormInterface $form): Response
{
try {
$response = $this->apiClient->createVoucherSaleInquiry($dto);
$response = $this->voucherClient->createVoucherSaleInquiry($dto);
} catch (\Exception $e) {
$this->logger->error('Voucher sale inquiry failed', ['exception' => $e->getMessage()]);
$this->addFlash('error', 'Der Gutscheinkauf konnte gerade nicht geprüft werden. Bitte versuche es später noch einmal.');
@@ -106,7 +106,7 @@ class Step3Controller extends AbstractVoucherPurchaseController
private function purchase(Request $request, VoucherPurchaseDto $dto, FormInterface $form): Response
{
try {
$response = $this->apiClient->createVoucherSale($dto);
$response = $this->voucherClient->createVoucherSale($dto);
} catch (TimeoutException $e) {
// The purchase may have gone through on BusPro's side, so a blind retry could buy twice
$this->logger->error('Voucher sale timeout', ['exception' => $e->getMessage()]);
+4 -4
View File
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Security;
use App\BusProNet\ApiClient;
use App\BusProNet\Client\AccountClient;
use App\BusProNet\Exception\ApiClientException;
use App\BusProNet\Exception\ImmediateConnectionCloseException;
use App\BusProNet\Exception\TimeoutException;
@@ -50,7 +50,7 @@ class BpnAuthenticator extends AbstractLoginFormAuthenticator implements Authent
public function __construct(
private readonly UrlGeneratorInterface $urlGenerator,
private readonly ApiClient $apiClient,
private readonly AccountClient $accountClient,
private readonly EntityManagerInterface $entityManager,
private readonly Crypt $crypt,
private readonly ProfileCompletenessChecker $completenessChecker,
@@ -77,7 +77,7 @@ class BpnAuthenticator extends AbstractLoginFormAuthenticator implements Authent
$password = md5($passwordPlain);
try {
$response = $this->apiClient->getPersonalData($email, $password);
$response = $this->accountClient->getPersonalData($email, $password);
} catch (ApiClientException $e) {
$message = $this->isConnectionError($e) ?
'Der Server ist momentan nicht erreichbar. Bitte versuche es später erneut.' : $e->getMessage();
@@ -103,7 +103,7 @@ class BpnAuthenticator extends AbstractLoginFormAuthenticator implements Authent
private function createOrUpdateLocalUser(string $email, string $password, PersonalData $personalData): User
{
try {
$crmAttributes = $this->apiClient->getCrmAttributes($email, $password);
$crmAttributes = $this->accountClient->getCrmAttributes($email, $password);
} catch (ApiClientException $e) {
$message = $this->isConnectionError($e) ?
'Der Server ist momentan nicht erreichbar. Bitte versuche es später erneut. :(' : $e->getMessage();
+3 -3
View File
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Service;
use App\BusProNet\ApiClient;
use App\BusProNet\Client\BookingClient;
use App\BusProNet\DataProcessor\BookingDataProcessor;
use App\BusProNet\Model\Booking;
use App\BusProNet\Model\Notification;
@@ -33,7 +33,7 @@ class BookingEditDataLoader
private array $droppedDraftServiceLabels = [];
public function __construct(
private readonly ApiClient $apiClient,
private readonly BookingClient $bookingClient,
private readonly BookingDataProcessor $bookingDataProcessor,
private readonly BookingSessionManager $bookingSessionService,
private readonly BookingChangeTracker $fingerprintService,
@@ -226,7 +226,7 @@ class BookingEditDataLoader
$item->expiresAfter(300);
$item->tag([$userTag]);
return $this->apiClient->getBooking($email, $password, $bookingId);
return $this->bookingClient->getBooking($email, $password, $bookingId);
});
} catch (InvalidArgumentException) {
return null;
+3 -3
View File
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Service;
use App\BusProNet\ApiClient;
use App\BusProNet\Client\BookingClient;
use App\BusProNet\Exception\ApiClientException;
use App\BusProNet\Exception\TimeoutException;
use App\BusProNet\Model\Notification;
@@ -20,7 +20,7 @@ use Symfony\Component\HttpFoundation\Request;
class BookingEditSubmitter
{
public function __construct(
private readonly ApiClient $apiClient,
private readonly BookingClient $bookingClient,
private readonly BookingEditDataLoader $dataLoader,
private readonly BookingEditDraftManager $draftService,
private readonly TravelDataProvider $travelDataService,
@@ -90,7 +90,7 @@ class BookingEditSubmitter
}
try {
$response = $this->apiClient->updateBooking($bookingDto, true);
$response = $this->bookingClient->updateBooking($bookingDto, true);
if ($response instanceof Notification) {
$this->logger->error('Booking update not successful', [
'email' => $email,
+3 -3
View File
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Service;
use App\BusProNet\ApiClient;
use App\BusProNet\Client\AccountClient;
use App\BusProNet\Model\Address;
use App\BusProNet\Model\Notification;
use App\BusProNet\Model\PersonalData;
@@ -26,7 +26,7 @@ class ParticipantDataPrefiller
public const TOKEN = '#KUN#';
public function __construct(
private readonly ApiClient $apiClient,
private readonly AccountClient $accountClient,
private readonly Crypt $crypt,
private readonly LoggerInterface $logger,
) {
@@ -90,7 +90,7 @@ class ParticipantDataPrefiller
$password = $this->crypt->decrypt($user->getPassword());
$email = $user->getEmail();
$result = $this->apiClient->getPersonalData($email, $password);
$result = $this->accountClient->getPersonalData($email, $password);
if ($result instanceof Notification) {
$this->logger->warning('Failed to fetch personal data for prepopulation', [
+5 -5
View File
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Service;
use App\BusProNet\ApiClient;
use App\BusProNet\Client\TravelClient;
use App\BusProNet\Exception\ApiClientException;
use App\BusProNet\Model\BaseData;
use App\BusProNet\Model\Notification;
@@ -38,7 +38,7 @@ class TravelDataProvider
public function __construct(
private readonly TravelLoader $travelLoader,
private readonly ApiClient $apiClient,
private readonly TravelClient $travelClient,
private readonly CacheInterface $cache,
private readonly LoggerInterface $logger,
private readonly TravelSnapshotManager $travelSnapshotService,
@@ -183,7 +183,7 @@ class TravelDataProvider
return null;
}
$result = $this->apiClient->getTravelData($productId, $hotelId);
$result = $this->travelClient->getTravelData($productId, $hotelId);
if (!$result instanceof Travel) {
$this->logger->debug('API returned non-travel result', [
@@ -522,7 +522,7 @@ class TravelDataProvider
private function fetchMutabilityData(int $dateId): ?BaseData
{
try {
$mutableData = $this->apiClient->getMutableData($dateId);
$mutableData = $this->travelClient->getMutableData($dateId);
if ($mutableData instanceof Notification) {
$this->logger->warning('API returned notification for mutability data', [
@@ -552,7 +552,7 @@ class TravelDataProvider
private function fetchAvailabilityData(int $dateId): ?ServiceAvailabilityResponse
{
try {
$availabilities = $this->apiClient->getAvailabilities($dateId);
$availabilities = $this->travelClient->getAvailabilities($dateId);
if ($availabilities instanceof Notification) {
$this->logger->warning('API returned notification for availability data', [
+3 -3
View File
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Service;
use App\BusProNet\ApiClient;
use App\BusProNet\Client\TravelClient;
use App\BusProNet\Exception\ApiClientException;
use App\BusProNet\Model\ExtendedAvailability;
use App\BusProNet\Model\ExtendedServiceAvailabilityResponse;
@@ -32,7 +32,7 @@ class TravelSnapshotManager
public function __construct(
private readonly TravelSnapshotRepository $snapshotRepository,
private readonly EntityManagerInterface $entityManager,
private readonly ApiClient $apiClient,
private readonly TravelClient $travelClient,
private readonly LoggerInterface $logger,
private readonly SerializerInterface $serializer,
private readonly int $retentionBufferDays = 14,
@@ -210,7 +210,7 @@ class TravelSnapshotManager
if (false === array_key_exists($dateId, $extendedResponseByDateId)) {
$response = null;
try {
$response = $this->apiClient->getAvailabilitiesExtended($dateId);
$response = $this->travelClient->getAvailabilitiesExtended($dateId);
} catch (ApiClientException $e) {
$extendedResponseByDateId[$dateId] = null;
$this->logger->warning('Failed to fetch extended availability', [
+4 -4
View File
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Service;
use App\BusProNet\ApiClient;
use App\BusProNet\Client\VoucherClient;
use App\BusProNet\Exception\ApiClientException;
use App\BusProNet\Model\Notification;
use App\BusProNet\Model\VoucherPaymentMethod;
@@ -27,7 +27,7 @@ class VoucherTemplateProvider
private const string PAYMENT_METHOD_CACHE_KEY = 'voucher_shop.payment_method';
public function __construct(
private readonly ApiClient $apiClient,
private readonly VoucherClient $voucherClient,
private readonly TagAwareCacheInterface $cache,
private readonly LoggerInterface $logger,
) {
@@ -42,7 +42,7 @@ class VoucherTemplateProvider
$item->expiresAfter(self::CACHE_TTL);
$item->tag([self::CACHE_TAG]);
$result = $this->request(fn () => $this->apiClient->getVoucherTemplates(), 'templates');
$result = $this->request(fn () => $this->voucherClient->getVoucherTemplates(), 'templates');
if ([] === $result->templates) {
$this->logger->error('BusPro offers no voucher templates for sale');
@@ -65,7 +65,7 @@ class VoucherTemplateProvider
$item->tag([self::CACHE_TAG]);
/** @var list<VoucherPaymentMethod> $paymentMethods */
$paymentMethods = $this->request(fn () => $this->apiClient->getVoucherPaymentMethods(), 'payment methods');
$paymentMethods = $this->request(fn () => $this->voucherClient->getVoucherPaymentMethods(), 'payment methods');
foreach ($paymentMethods as $paymentMethod) {
if ($paymentMethod->isTransfer()) {
return $paymentMethod;
+4 -4
View File
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Service;
use App\BusProNet\ApiClient;
use App\BusProNet\Client\VoucherClient;
use App\BusProNet\Exception\ApiClientException;
use App\BusProNet\Model\Notification;
use App\BusProNet\Model\PromoVoucherValidationResult;
@@ -18,7 +18,7 @@ class VoucherValidator
private const CACHE_TTL = 900; // 15 minutes
public function __construct(
private readonly ApiClient $apiClient,
private readonly VoucherClient $voucherClient,
private readonly CacheInterface $cache,
private readonly LoggerInterface $logger,
) {
@@ -34,7 +34,7 @@ class VoucherValidator
$item->expiresAfter(self::CACHE_TTL);
try {
$result = $this->apiClient->validatePurchaseVoucher($redemptionCode);
$result = $this->voucherClient->validatePurchaseVoucher($redemptionCode);
if ($result instanceof Notification) {
return new PurchaseVoucherValidationResult(
@@ -89,7 +89,7 @@ class VoucherValidator
$item->expiresAfter(self::CACHE_TTL);
try {
$result = $this->apiClient->validatePromoVoucher($promoCode, $travelId, $participantPrice);
$result = $this->voucherClient->validatePromoVoucher($promoCode, $travelId, $participantPrice);
if ($result instanceof Notification) {
return new PromoVoucherValidationResult(