feat: correct determination of inquiry status and conditional voucher field display
This commit is contained in:
@@ -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'],
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user