feat: correct determination of inquiry status and conditional voucher field display

This commit is contained in:
Björn Fromme
2026-03-16 11:59:11 +01:00
parent 85998534fe
commit 1f2f2751e4
19 changed files with 468 additions and 193 deletions
+2 -3
View File
@@ -14,6 +14,7 @@ use App\BusProNet\Model\Notification;
use App\BusProNet\Model\PersonalData;
use App\BusProNet\Model\PromoVoucher;
use App\BusProNet\Model\PurchaseVoucher;
use App\BusProNet\Model\ServiceAvailabilityResponse;
use App\BusProNet\Model\Travel;
use App\BusProNet\Traits\ApiClientTrait;
use App\BusProNet\XmlParser\ApiResponseParser;
@@ -226,8 +227,6 @@ class ApiClient
*/
public function createBookingInquiry(BookingDto $bookingDto, bool $debug = false): Notification|BookingResponse
{
// Forcibly override booking status to generate inquiry payload
$bookingDto->bookingStatus = 'A';
$payload = $this->bookingDataProcessor->createBookingRequestPayload($bookingDto, Constants::BOOKING_TYPE_INQUIRY);
$data = [
@@ -285,7 +284,7 @@ class ApiClient
/**
* @throws ApiClientException
*/
public function getAvailabilities(int $dateId): Notification|BaseData
public function getAvailabilities(int $dateId): Notification|ServiceAvailabilityResponse
{
$data = [
'user' => $this->config['bpn_username'],
+5
View File
@@ -52,6 +52,11 @@ final class Constants
public const BOOKING_TYPE_INQUIRY = 'Anfrage';
public const BOOKING_TYPE_BOOKING = 'Buchung';
// Booking status codes from buchungstatusmoeglich attribute
public const BOOKING_STATUS_FREE = 'F';
public const BOOKING_STATUS_INQUIRY = 'A';
public const BOOKING_STATUS_OPEN = 'O'; // Optionsbuchung - not relevant for current implementation
// Payment methods
public const PAYMENT_METHOD_TRANSFER = 'transfer';
public const PAYMENT_METHOD_DEBIT = 'debit';
@@ -893,10 +893,12 @@ class BookingDataProcessor
}
}
// Add promotional voucher as aktionscode (allowed for inquiry bookings)
$promotionalCode = $this->getPromoVoucherCodeForParticipant($participant);
if (null !== $promotionalCode) {
$participantData['aktionscode'] = $promotionalCode;
// Add promotional voucher as aktionscode (excluded from inquiry bookings)
if (false === $isInquiryBooking) {
$promotionalCode = $this->getPromoVoucherCodeForParticipant($participant);
if (null !== $promotionalCode) {
$participantData['aktionscode'] = $promotionalCode;
}
}
// Add goodwill voucher as participant-level einloesecode
@@ -926,15 +928,16 @@ class BookingDataProcessor
$this->addServicesFromMap($payload, 'versicherungen', 'versicherung', '@idversicherung', $insuranceMap);
// Add purchase vouchers (regular purchase vouchers, excluding goodwill vouchers)
// Purchase vouchers are allowed for both inquiry and final bookings
// Goodwill vouchers are handled separately and excluded from inquiry bookings
$purchaseVouchers = $this->collectPurchaseVouchers($bookingDto);
if (false === empty($purchaseVouchers)) {
$payload['gutscheine']['gutschein'] = [];
foreach ($purchaseVouchers as $code) {
$payload['gutscheine']['gutschein'][] = [
'@einloesecode' => $code,
];
// All vouchers (purchase, promotional, goodwill) are excluded from inquiry bookings
if (false === $isInquiryBooking) {
$purchaseVouchers = $this->collectPurchaseVouchers($bookingDto);
if (false === empty($purchaseVouchers)) {
$payload['gutscheine']['gutschein'] = [];
foreach ($purchaseVouchers as $code) {
$payload['gutscheine']['gutschein'][] = [
'@einloesecode' => $code,
];
}
}
}
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace App\BusProNet\Model;
/**
* Response model for service availability API calls.
*
* Contains both service-level availability data and travel-level
* booking status information from the VERFUEGBARKEIT endpoint.
*/
class ServiceAvailabilityResponse
{
/**
* @param array<int, Availability> $services Service availability data indexed by service ID
* @param array<string> $allowedBookingStatus Allowed booking status codes (e.g., ['F', 'O', 'A'])
* @param string|null $travelStatus Travel status from API (e.g., 'frei', 'anfrage')
*/
public function __construct(
private readonly array $services,
public readonly array $allowedBookingStatus = [],
public readonly ?string $travelStatus = null,
) {
}
/**
* @return array<int, Availability>
*/
public function getServices(): array
{
return $this->services;
}
}
+70 -6
View File
@@ -56,6 +56,15 @@ class Travel
#[Groups(['api:single'])]
public ?string $status = null;
/**
* Allowed booking status codes from buchungstatusmoeglich attribute.
* Array of status codes (F, A, O) indicating which booking types are permitted.
*
* @var array<string>
*/
#[Groups(['api:single'])]
public array $allowedBookingStatus = [];
#[Groups(['api:list', 'api:single'])]
public ?float $priceFrom = null;
@@ -223,19 +232,18 @@ class Travel
}
/**
* Retrieves all available rooms for booking.
* Retrieves all bookable rooms (both regular and inquiry).
*
* Filters the rooms collection to return only rooms that have availability
* greater than zero and have an available status. This ensures only
* bookable rooms are returned for selection.
* Returns rooms that have availability greater than zero and a bookable status
* (either 'Frei' for regular booking or 'Anfrage' for inquiry booking).
*
* @return array<int, Room> The filtered array of available rooms, indexed by room ID
* @return array<int, Room> The filtered array of bookable rooms, indexed by room ID
*/
public function getAvailableRooms(): array
{
$result = [];
foreach ($this->rooms as $room) {
if ($room->available > 0 && Constants::STATUS_AVAILABLE === $room->status) {
if ($room->available > 0 && $this->isBookableRoomStatus($room->status)) {
$result[$room->id] = $room;
}
}
@@ -243,6 +251,50 @@ class Travel
return $result;
}
/**
* Checks if the travel has any bookable rooms.
*
* @return bool True if at least one room is bookable
*/
public function hasBookableRooms(): bool
{
return [] !== $this->getAvailableRooms();
}
/**
* Checks if all bookable rooms require inquiry booking.
*
* Returns true if there are bookable rooms but none with 'Frei' status.
* This indicates the booking must be submitted as an inquiry.
*
* @return bool True if only inquiry rooms are available
*/
public function requiresInquiryBooking(): bool
{
$bookableRooms = $this->getAvailableRooms();
if ([] === $bookableRooms) {
return false;
}
foreach ($bookableRooms as $room) {
if (Constants::STATUS_AVAILABLE === $room->status) {
return false;
}
}
return true;
}
/**
* Checks if a room status allows booking.
*/
private function isBookableRoomStatus(?string $status): bool
{
return Constants::STATUS_AVAILABLE === $status
|| Constants::STATUS_ON_REQUEST === $status;
}
/**
* Retrieves a room by its ID.
*
@@ -260,4 +312,16 @@ class Travel
return false === empty($rooms) ? reset($rooms) : null;
}
/**
* Checks if a specific booking status code is allowed for this travel.
*
* @param string $statusCode The booking status code to check (F, A, or O)
*
* @return bool True if the status is allowed, false otherwise
*/
public function isBookingStatusAllowed(string $statusCode): bool
{
return in_array($statusCode, $this->allowedBookingStatus, true);
}
}
+12 -5
View File
@@ -4,6 +4,7 @@ namespace App\BusProNet\XmlLoader;
use App\BusProNet\Model\BaseData;
use App\BusProNet\Model\MutableData;
use App\BusProNet\Model\ServiceAvailabilityResponse;
use App\BusProNet\Model\Travel;
use App\BusProNet\Utility\DateCodeUtility;
use App\BusProNet\XmlParser\TravelParser;
@@ -278,20 +279,26 @@ class TravelLoader extends AbstractLoader
* Apply availability data to travel services.
*
* Updates the availability status of additional and transportation
* services based on the provided availability data.
* services, and the allowed booking status, based on the provided
* availability data.
*
* @param Travel $travel The travel object to update
* @param BaseData $availabilities The availability data for services
* @param Travel $travel The travel object to update
* @param ServiceAvailabilityResponse $availabilities The availability data for services
*/
public function patchAvailabilities(Travel $travel, BaseData $availabilities): void
public function patchAvailabilities(Travel $travel, ServiceAvailabilityResponse $availabilities): void
{
$serviceAvailabilities = $availabilities->getItems();
$serviceAvailabilities = $availabilities->getServices();
foreach ([...$travel->additionalServices, ...$travel->transportationServices] as $service) {
if (array_key_exists($service->id, $serviceAvailabilities)) {
$service->available = $serviceAvailabilities[$service->id]->available;
}
}
// Patch travel-level booking status from availability response
if ([] !== $availabilities->allowedBookingStatus) {
$travel->allowedBookingStatus = $availabilities->allowedBookingStatus;
}
}
/**
@@ -5,11 +5,12 @@ namespace App\BusProNet\XmlParser;
use App\BusProNet\Model\Availability;
use App\BusProNet\Model\BaseData;
use App\BusProNet\Model\Room;
use App\BusProNet\Model\ServiceAvailabilityResponse;
use Symfony\Component\DomCrawler\Crawler;
class AvailabilitiesParser extends AbstractParser
{
public function parseServices(Crawler $result): BaseData
public function parseServices(Crawler $result): ServiceAvailabilityResponse
{
$availabilities = [];
@@ -26,7 +27,21 @@ class AvailabilitiesParser extends AbstractParser
})
;
return new BaseData($availabilities);
// Parse travel-level booking status from <reise> node
$allowedBookingStatus = [];
$travelStatus = null;
$reiseNode = $result->filterXPath('//reise');
if ($reiseNode->count() > 0) {
$travelStatus = $reiseNode->attr('status');
$bookingStatusPossible = $reiseNode->attr('buchungstatusmoeglich') ?? '';
if ('' !== $bookingStatusPossible) {
$allowedBookingStatus = str_split($bookingStatusPossible);
}
}
return new ServiceAvailabilityResponse($availabilities, $allowedBookingStatus, $travelStatus);
}
public function parseRooms(Crawler $result): BaseData
-1
View File
@@ -62,7 +62,6 @@ class TravelParser extends AbstractParser
}
$travel->type = $node->attr('reiseart');
$travel->status = $this->getStringOrNullValue($node->filterXPath('//status_hin'));
$travel->priceFrom = $this->stringToFloat($this->getStringOrNullValue($node->filterXPath('//abpreis')));
$travel->selectionGroups = $this->getSelectionGroups($node->filterXPath('//selektiongruppe'));
$travel->additionalServices = $this