fix: properly assign readonly state to form fields for unavailable services
This commit is contained in:
@@ -114,31 +114,28 @@ class Travel
|
|||||||
public array $insurances = [];
|
public array $insurances = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves additional services filtered by subtype, availability, and optionally by travel date range.
|
* Retrieves additional services filtered by subtype and optionally by travel date range.
|
||||||
*
|
*
|
||||||
* Filters additional services based on the provided subtype(s), availability,
|
* Filters additional services based on the provided subtype(s) and optionally whether
|
||||||
* and optionally whether their date range overlaps with the travel dates.
|
* their date range overlaps with the travel dates. Services with null dates are
|
||||||
* Services with null dates are considered always available when date filtering is enabled.
|
* considered always available when date filtering is enabled.
|
||||||
* Services are sorted by price in ascending order.
|
* Services are sorted by price in ascending order.
|
||||||
*
|
*
|
||||||
|
* Availability filtering is NOT applied here - it happens at the form/UI level where
|
||||||
|
* unavailable services are shown as readonly with appropriate tooltips.
|
||||||
|
*
|
||||||
* @param mixed $subTypes The service subtype(s) to filter by
|
* @param mixed $subTypes The service subtype(s) to filter by
|
||||||
* @param bool $filterAvailable Whether to include only available services
|
|
||||||
* @param bool $filterByTravelDateRange Whether to filter by travel date range overlap
|
* @param bool $filterByTravelDateRange Whether to filter by travel date range overlap
|
||||||
*
|
*
|
||||||
* @return array<int, Service> The filtered and sorted services array
|
* @return array<int, Service> The filtered and sorted services array
|
||||||
*/
|
*/
|
||||||
public function getAdditionalServicesBySubTypes(mixed $subTypes, bool $filterAvailable = true, bool $filterByTravelDateRange = false): array
|
public function getAdditionalServicesBySubTypes(mixed $subTypes, bool $filterByTravelDateRange = false): array
|
||||||
{
|
{
|
||||||
$subTypes = (array) $subTypes;
|
$subTypes = (array) $subTypes;
|
||||||
|
|
||||||
$services = array_filter($this->additionalServices, function (Service $service) use ($subTypes, $filterAvailable, $filterByTravelDateRange) {
|
$services = array_filter($this->additionalServices, function (Service $service) use ($subTypes, $filterByTravelDateRange) {
|
||||||
// Check subtype
|
// Check subtype
|
||||||
if (false === in_array($service->subType, $subTypes)) {
|
if (false === in_array($service->subType, $subTypes, true)) {
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check availability
|
|
||||||
if (true === $filterAvailable && null !== $service->available && 0 >= $service->available) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,30 +157,21 @@ class Travel
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves transportation services filtered by direction and availability.
|
* Retrieves transportation services filtered by direction.
|
||||||
*
|
*
|
||||||
* Filters transportation services based on travel direction and optionally
|
* Filters transportation services based on travel direction. Services are sorted by subtype.
|
||||||
* by availability. Services are sorted by subtype.
|
* Availability filtering is NOT applied here - it happens at the form/UI level where
|
||||||
|
* unavailable services are shown as readonly with appropriate tooltips.
|
||||||
*
|
*
|
||||||
* Note: PKW/CAR filtering based on booking context is NOT applied here.
|
* @param string $direction The travel direction to filter by
|
||||||
* That filtering happens dynamically in ParticipantFieldOptionsProvider using
|
|
||||||
* per-booking availability calculations from ServiceAvailabilityCalculator.
|
|
||||||
*
|
|
||||||
* @param string $direction The travel direction to filter by
|
|
||||||
* @param bool $filterAvailable Whether to include only available services (API availability)
|
|
||||||
*
|
*
|
||||||
* @return array<int, Service> The filtered and sorted transportation services
|
* @return array<int, Service> The filtered and sorted transportation services
|
||||||
*/
|
*/
|
||||||
public function getTransportationServicesByDirection(string $direction, bool $filterAvailable = true): array
|
public function getTransportationServicesByDirection(string $direction): array
|
||||||
{
|
{
|
||||||
$services = array_filter($this->transportationServices, function (Service $service) use ($direction, $filterAvailable) {
|
$services = array_filter($this->transportationServices, fn (Service $service) => $direction === $service->direction);
|
||||||
return $direction === $service->direction
|
|
||||||
&& (false === $filterAvailable || $service->available > 0 || null === $service->available);
|
|
||||||
});
|
|
||||||
|
|
||||||
usort($services, function (Service $a, Service $b) {
|
usort($services, fn (Service $a, Service $b) => $a->subType <=> $b->subType);
|
||||||
return $a->subType <=> $b->subType;
|
|
||||||
});
|
|
||||||
|
|
||||||
return $services;
|
return $services;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,10 +104,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
|||||||
'expanded' => true,
|
'expanded' => true,
|
||||||
'required' => false,
|
'required' => false,
|
||||||
'choices' => $this->filterServicesByAgeConstraints(
|
'choices' => $this->filterServicesByAgeConstraints(
|
||||||
$bookingDto->travel->getAdditionalServicesBySubTypes(
|
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_COURSES),
|
||||||
Constants::TOKEN_COURSES,
|
|
||||||
BookingDto::MODE_CREATE === $bookingDto->getMode() // Only filter by availability in create mode
|
|
||||||
),
|
|
||||||
$bookingDto,
|
$bookingDto,
|
||||||
$participantIndex
|
$participantIndex
|
||||||
),
|
),
|
||||||
@@ -142,10 +139,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
|||||||
'expanded' => true,
|
'expanded' => true,
|
||||||
'required' => false,
|
'required' => false,
|
||||||
'choices' => $this->filterServicesByAgeConstraints(
|
'choices' => $this->filterServicesByAgeConstraints(
|
||||||
$bookingDto->travel->getAdditionalServicesBySubTypes(
|
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_ADDITIONAL),
|
||||||
Constants::TOKEN_ADDITIONAL,
|
|
||||||
BookingDto::MODE_EDIT !== $bookingDto->getMode() // Only filter by availability in create mode
|
|
||||||
),
|
|
||||||
$bookingDto,
|
$bookingDto,
|
||||||
$participantIndex
|
$participantIndex
|
||||||
),
|
),
|
||||||
@@ -188,10 +182,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
|||||||
'expanded' => true,
|
'expanded' => true,
|
||||||
'required' => false,
|
'required' => false,
|
||||||
'choices' => $this->filterServicesByAgeConstraints(
|
'choices' => $this->filterServicesByAgeConstraints(
|
||||||
$bookingDto->travel->getAdditionalServicesBySubTypes(
|
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_BOARD),
|
||||||
Constants::TOKEN_BOARD,
|
|
||||||
BookingDto::MODE_CREATE === $bookingDto->getMode() // Only filter by availability in create mode
|
|
||||||
),
|
|
||||||
$bookingDto,
|
$bookingDto,
|
||||||
$participantIndex
|
$participantIndex
|
||||||
),
|
),
|
||||||
@@ -227,11 +218,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
|||||||
'required' => false,
|
'required' => false,
|
||||||
'choices' => $this->filterServicesByAgeConstraints(
|
'choices' => $this->filterServicesByAgeConstraints(
|
||||||
$this->filterRentalsBySkiPassDuration(
|
$this->filterRentalsBySkiPassDuration(
|
||||||
$bookingDto->travel->getAdditionalServicesBySubTypes(
|
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_RENTALS, true),
|
||||||
Constants::TOKEN_RENTALS,
|
|
||||||
BookingDto::MODE_CREATE === $bookingDto->getMode(), // Only filter by availability in create mode
|
|
||||||
true // Filter by travel date range
|
|
||||||
),
|
|
||||||
$bookingDto,
|
$bookingDto,
|
||||||
$participantIndex
|
$participantIndex
|
||||||
),
|
),
|
||||||
@@ -264,10 +251,10 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
|||||||
|
|
||||||
// Rental insurance field provider - provides rental insurance options when rental services are selected
|
// Rental insurance field provider - provides rental insurance options when rental services are selected
|
||||||
$this->fieldOptionProviders['rentalInsurance'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [
|
$this->fieldOptionProviders['rentalInsurance'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [
|
||||||
'label' => $this->getRentalInsuranceCheckboxLabel($bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_RENTAL_INSURANCE, true, true)),
|
'label' => $this->getRentalInsuranceCheckboxLabel($bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_RENTAL_INSURANCE, true)),
|
||||||
'required' => false,
|
'required' => false,
|
||||||
'property_path' => 'rentalInsuranceSelected',
|
'property_path' => 'rentalInsuranceSelected',
|
||||||
'help' => $this->getRentalInsuranceDescription($bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_RENTAL_INSURANCE, true, true)),
|
'help' => $this->getRentalInsuranceDescription($bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_RENTAL_INSURANCE, true)),
|
||||||
];
|
];
|
||||||
|
|
||||||
// License plate field provider - provides text input for vehicle license plate when parking is selected
|
// License plate field provider - provides text input for vehicle license plate when parking is selected
|
||||||
@@ -292,11 +279,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
|||||||
'expanded' => true,
|
'expanded' => true,
|
||||||
'required' => true,
|
'required' => true,
|
||||||
'choices' => $this->filterServicesByAgeConstraints(
|
'choices' => $this->filterServicesByAgeConstraints(
|
||||||
$bookingDto->travel->getAdditionalServicesBySubTypes(
|
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_SKI_PASS, true),
|
||||||
Constants::TOKEN_SKI_PASS,
|
|
||||||
BookingDto::MODE_CREATE === $bookingDto->getMode(), // Only filter by availability in create mode
|
|
||||||
true // Filter by travel date range
|
|
||||||
),
|
|
||||||
$bookingDto,
|
$bookingDto,
|
||||||
$participantIndex
|
$participantIndex
|
||||||
),
|
),
|
||||||
@@ -340,7 +323,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
|||||||
$this->fieldOptionProviders['transportationOutbound'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [
|
$this->fieldOptionProviders['transportationOutbound'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [
|
||||||
'label' => 'Hinfahrt',
|
'label' => 'Hinfahrt',
|
||||||
'choices' => $this->filterTransportationChoices(
|
'choices' => $this->filterTransportationChoices(
|
||||||
$bookingDto->travel->getTransportationServicesByDirection(DirectionMapper::OUTBOUND_TRAVEL, false),
|
$bookingDto->travel->getTransportationServicesByDirection(DirectionMapper::OUTBOUND_TRAVEL),
|
||||||
$bookingDto,
|
$bookingDto,
|
||||||
$participantIndex
|
$participantIndex
|
||||||
),
|
),
|
||||||
@@ -374,7 +357,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
|||||||
// Inbound Transportation
|
// Inbound Transportation
|
||||||
$this->fieldOptionProviders['transportationInbound'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [
|
$this->fieldOptionProviders['transportationInbound'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [
|
||||||
'label' => 'Rückfahrt',
|
'label' => 'Rückfahrt',
|
||||||
'choices' => $bookingDto->travel->getTransportationServicesByDirection(DirectionMapper::INBOUND_TRAVEL, false),
|
'choices' => $bookingDto->travel->getTransportationServicesByDirection(DirectionMapper::INBOUND_TRAVEL),
|
||||||
'choice_label' => fn (Service $service) => $service?->label,
|
'choice_label' => fn (Service $service) => $service?->label,
|
||||||
'choice_value' => 'id',
|
'choice_value' => 'id',
|
||||||
'expanded' => true,
|
'expanded' => true,
|
||||||
@@ -417,7 +400,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
|||||||
// Parking (conditional - only shown when outbound transportation is PKW)
|
// Parking (conditional - only shown when outbound transportation is PKW)
|
||||||
// Simple checkbox since there's only ever one parking type
|
// Simple checkbox since there's only ever one parking type
|
||||||
$this->fieldOptionProviders['parking'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [
|
$this->fieldOptionProviders['parking'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [
|
||||||
'label' => $this->getParkingCheckboxLabel($bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_PARKING, true)),
|
'label' => $this->getParkingCheckboxLabel($bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_PARKING)),
|
||||||
'required' => false,
|
'required' => false,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ class ParticipantParkingFieldHandler extends AbstractParticipantFieldHandler
|
|||||||
*/
|
*/
|
||||||
private function findParkingService(BookingDto $bookingDto): ?Service
|
private function findParkingService(BookingDto $bookingDto): ?Service
|
||||||
{
|
{
|
||||||
$parkingServices = $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_PARKING, true);
|
$parkingServices = $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_PARKING);
|
||||||
|
|
||||||
if (empty($parkingServices)) {
|
if (empty($parkingServices)) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ class ParticipantRentalInsuranceFieldHandler extends AbstractParticipantFieldHan
|
|||||||
*/
|
*/
|
||||||
private function findRentalInsuranceService(BookingDto $bookingDto): ?Service
|
private function findRentalInsuranceService(BookingDto $bookingDto): ?Service
|
||||||
{
|
{
|
||||||
$rentalInsuranceServices = $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_RENTAL_INSURANCE, true, true);
|
$rentalInsuranceServices = $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_RENTAL_INSURANCE, true);
|
||||||
|
|
||||||
if (empty($rentalInsuranceServices)) {
|
if (empty($rentalInsuranceServices)) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ class ParticipantRentalsFieldHandler extends AbstractParticipantFieldHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
$selectedRentals = $this->getFieldValue($submittedData, $this->getFieldName()) ?? [];
|
$selectedRentals = $this->getFieldValue($submittedData, $this->getFieldName()) ?? [];
|
||||||
$availableRentals = $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_RENTALS, true, true);
|
$availableRentals = $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_RENTALS, true);
|
||||||
|
|
||||||
// Filter rentals by skipass duration to ensure only matching rentals are available
|
// Filter rentals by skipass duration to ensure only matching rentals are available
|
||||||
$durationFilteredRentals = $this->filterRentalsBySkiPassDuration($availableRentals, $participant);
|
$durationFilteredRentals = $this->filterRentalsBySkiPassDuration($availableRentals, $participant);
|
||||||
|
|||||||
@@ -94,8 +94,8 @@ class ParticipantSkiPassFieldHandler extends AbstractParticipantFieldHandler
|
|||||||
// Extract current skipass selection from submitted data
|
// Extract current skipass selection from submitted data
|
||||||
$selectedSkiPass = $this->getFieldValue($submittedData, $this->getFieldName());
|
$selectedSkiPass = $this->getFieldValue($submittedData, $this->getFieldName());
|
||||||
|
|
||||||
// Get available skipasses from travel data (with date filtering)
|
// Get all skipasses from travel data (with date filtering)
|
||||||
$availableSkipasses = $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_SKI_PASS, true, true);
|
$availableSkipasses = $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_SKI_PASS, true);
|
||||||
|
|
||||||
// For single selection, validate the selected skipass and convert ID to Service object
|
// For single selection, validate the selected skipass and convert ID to Service object
|
||||||
$validSelection = null;
|
$validSelection = null;
|
||||||
|
|||||||
@@ -62,10 +62,8 @@ class ParticipantTransportationDiscountReplacementFieldHandler extends AbstractP
|
|||||||
&& DirectionMapper::SUBTYPE_BUS_API === $participant->transportationInbound->subType;
|
&& DirectionMapper::SUBTYPE_BUS_API === $participant->transportationInbound->subType;
|
||||||
|
|
||||||
// Step 3: Get all outbound PKW services
|
// Step 3: Get all outbound PKW services
|
||||||
// Get ALL outbound services (not filtered by availability or smart filtering)
|
|
||||||
$allOutboundServices = $bookingDto->travel->getTransportationServicesByDirection(
|
$allOutboundServices = $bookingDto->travel->getTransportationServicesByDirection(
|
||||||
DirectionMapper::OUTBOUND_TRAVEL,
|
DirectionMapper::OUTBOUND_TRAVEL
|
||||||
false // Do NOT filter by per-booking availability
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Find discounted and regular PKW services
|
// Find discounted and regular PKW services
|
||||||
|
|||||||
@@ -50,13 +50,12 @@ class ParticipantTransportationInboundFieldHandler extends AbstractParticipantFi
|
|||||||
|
|
||||||
$selectedTransportation = $this->getFieldValue($submittedData, $this->getFieldName());
|
$selectedTransportation = $this->getFieldValue($submittedData, $this->getFieldName());
|
||||||
|
|
||||||
// Get available inbound transportation services
|
// Get all inbound transportation services (availability filtering happens at form level)
|
||||||
$availableServices = $bookingDto->travel->getTransportationServicesByDirection(
|
$services = $bookingDto->travel->getTransportationServicesByDirection(
|
||||||
DirectionMapper::INBOUND_TRAVEL,
|
DirectionMapper::INBOUND_TRAVEL
|
||||||
true // filter available
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Validate and update participant with selection
|
// Validate and update participant with selection
|
||||||
$participant->transportationInbound = $this->findItemById($selectedTransportation, $availableServices);
|
$participant->transportationInbound = $this->findItemById($selectedTransportation, $services);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,13 +50,12 @@ class ParticipantTransportationOutboundFieldHandler extends AbstractParticipantF
|
|||||||
|
|
||||||
$selectedTransportation = $this->getFieldValue($submittedData, $this->getFieldName());
|
$selectedTransportation = $this->getFieldValue($submittedData, $this->getFieldName());
|
||||||
|
|
||||||
// Get available outbound transportation services
|
// Get all outbound transportation services (availability filtering happens at form level)
|
||||||
$availableServices = $bookingDto->travel->getTransportationServicesByDirection(
|
$services = $bookingDto->travel->getTransportationServicesByDirection(
|
||||||
DirectionMapper::OUTBOUND_TRAVEL,
|
DirectionMapper::OUTBOUND_TRAVEL
|
||||||
true // filter available
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Validate and update participant with selection
|
// Validate and update participant with selection
|
||||||
$participant->transportationOutbound = $this->findItemById($selectedTransportation, $availableServices);
|
$participant->transportationOutbound = $this->findItemById($selectedTransportation, $services);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class ParticipantEligibilityService
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
$allSkiPasses = $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_SKI_PASS, true, true);
|
$allSkiPasses = $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_SKI_PASS, true);
|
||||||
$availableSkiPasses = array_filter(
|
$availableSkiPasses = array_filter(
|
||||||
$allSkiPasses,
|
$allSkiPasses,
|
||||||
fn (Service $service) => $this->isSkiPassAvailableForParticipant($service, $bookingDto, $participantIndex)
|
fn (Service $service) => $this->isSkiPassAvailableForParticipant($service, $bookingDto, $participantIndex)
|
||||||
|
|||||||
@@ -63,12 +63,17 @@ class ServiceAvailabilityCalculator
|
|||||||
$remainingAvailability = $this->calculateRemainingAvailability($bookingDto, $participantIndex);
|
$remainingAvailability = $this->calculateRemainingAvailability($bookingDto, $participantIndex);
|
||||||
|
|
||||||
return array_filter($services, function (Service $service) use ($remainingAvailability) {
|
return array_filter($services, function (Service $service) use ($remainingAvailability) {
|
||||||
// If service has no availability limit set, treat as unlimited
|
// If service has no availability limit set (null), treat as unlimited
|
||||||
if (null === $service->available || $service->available <= 0) {
|
if (null === $service->available) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For services with availability limits, check remaining availability
|
// If availability is 0, service is sold out at the API level
|
||||||
|
if (0 === $service->available) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For services with positive availability, check remaining availability
|
||||||
return ($remainingAvailability[$service->id] ?? $service->available) > 0;
|
return ($remainingAvailability[$service->id] ?? $service->available) > 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -94,11 +99,22 @@ class ServiceAvailabilityCalculator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If service not found or has no availability limit, it's not unavailable
|
// If service not found, treat as available (not unavailable)
|
||||||
if (null === $service || null === $service->available || $service->available <= 0) {
|
if (null === $service) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If no availability tracking (null), service is unlimited and available
|
||||||
|
if (null === $service->available) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If availability is 0, service is sold out at the API level
|
||||||
|
if (0 === $service->available) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For services with positive availability, calculate remaining based on booking selections
|
||||||
$remainingAvailability = $this->calculateRemainingAvailability($bookingDto, $participantIndex);
|
$remainingAvailability = $this->calculateRemainingAvailability($bookingDto, $participantIndex);
|
||||||
|
|
||||||
return ($remainingAvailability[$serviceId] ?? $service->available) <= 0;
|
return ($remainingAvailability[$serviceId] ?? $service->available) <= 0;
|
||||||
@@ -195,7 +211,7 @@ class ServiceAvailabilityCalculator
|
|||||||
{
|
{
|
||||||
$allServices = [];
|
$allServices = [];
|
||||||
|
|
||||||
// Get transportation services
|
// Get all transportation services
|
||||||
$transportationServices = array_merge(
|
$transportationServices = array_merge(
|
||||||
$bookingDto->travel->getTransportationServicesByDirection(DirectionMapper::OUTBOUND_TRAVEL) ?? [],
|
$bookingDto->travel->getTransportationServicesByDirection(DirectionMapper::OUTBOUND_TRAVEL) ?? [],
|
||||||
$bookingDto->travel->getTransportationServicesByDirection(DirectionMapper::INBOUND_TRAVEL) ?? []
|
$bookingDto->travel->getTransportationServicesByDirection(DirectionMapper::INBOUND_TRAVEL) ?? []
|
||||||
|
|||||||
@@ -61,11 +61,7 @@ class AppRuntime implements RuntimeExtensionInterface
|
|||||||
*/
|
*/
|
||||||
public function formatServicePrice(float|int|null $price): string
|
public function formatServicePrice(float|int|null $price): string
|
||||||
{
|
{
|
||||||
if (null === $price) {
|
if (null === $price || 0 === $price || 0.0 === $price) {
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (0 === $price || 0.0 === $price) {
|
|
||||||
return 'inkl.';
|
return 'inkl.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -347,7 +347,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% if form.rentalInsurance is defined %}
|
{% if form.rentalInsurance is defined %}
|
||||||
{% set rentalInsuranceService = bookingDto.travel.getAdditionalServicesBySubTypes(constant('App\\BusProNet\\Constants::TOKEN_RENTAL_INSURANCE'), true, true)|first %}
|
{% set rentalInsuranceService = bookingDto.travel.getAdditionalServicesBySubTypes(constant('App\\BusProNet\\Constants::TOKEN_RENTAL_INSURANCE'), true)|first %}
|
||||||
{{ macros.checkbox_row(
|
{{ macros.checkbox_row(
|
||||||
form.rentalInsurance,
|
form.rentalInsurance,
|
||||||
constant('App\\BusProNet\\Constants::SERVICE_LABELS')[constant('App\\BusProNet\\Constants::TOKEN_RENTAL_INSURANCE')],
|
constant('App\\BusProNet\\Constants::SERVICE_LABELS')[constant('App\\BusProNet\\Constants::TOKEN_RENTAL_INSURANCE')],
|
||||||
@@ -532,7 +532,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% if form.parking is defined %}
|
{% if form.parking is defined %}
|
||||||
{% set parkingService = bookingDto.travel.getAdditionalServicesBySubTypes(constant('App\\BusProNet\\Constants::TOKEN_PARKING'), true)|first %}
|
{% set parkingService = bookingDto.travel.getAdditionalServicesBySubTypes(constant('App\\BusProNet\\Constants::TOKEN_PARKING'))|first %}
|
||||||
{{ macros.checkbox_row(
|
{{ macros.checkbox_row(
|
||||||
form.parking,
|
form.parking,
|
||||||
'Parkplatz',
|
'Parkplatz',
|
||||||
|
|||||||
@@ -49,57 +49,6 @@ class TravelTest extends TestCase
|
|||||||
$this->assertSame($inboundPkw, $inboundResult[0]);
|
$this->assertSame($inboundPkw, $inboundResult[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Test that unavailable services are filtered out when filterAvailable is true.
|
|
||||||
*/
|
|
||||||
public function testUnavailableServicesFiltered(): void
|
|
||||||
{
|
|
||||||
$travel = new Travel();
|
|
||||||
|
|
||||||
$availablePkw = new Service();
|
|
||||||
$availablePkw->id = 1;
|
|
||||||
$availablePkw->subType = 'PKW';
|
|
||||||
$availablePkw->direction = 'HIN';
|
|
||||||
$availablePkw->label = 'Eigene Anreise';
|
|
||||||
$availablePkw->available = 10;
|
|
||||||
|
|
||||||
$unavailablePkw = new Service();
|
|
||||||
$unavailablePkw->id = 2;
|
|
||||||
$unavailablePkw->subType = 'PKW';
|
|
||||||
$unavailablePkw->direction = 'HIN';
|
|
||||||
$unavailablePkw->label = 'Eigene Anreise mit Rabatt';
|
|
||||||
$unavailablePkw->available = 0;
|
|
||||||
|
|
||||||
$travel->transportationServices = [$availablePkw, $unavailablePkw];
|
|
||||||
|
|
||||||
$result = $travel->getTransportationServicesByDirection('HIN', true);
|
|
||||||
|
|
||||||
$this->assertCount(1, $result);
|
|
||||||
$this->assertSame($availablePkw, $result[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test that services with null availability are included.
|
|
||||||
*/
|
|
||||||
public function testNullAvailabilityIncluded(): void
|
|
||||||
{
|
|
||||||
$travel = new Travel();
|
|
||||||
|
|
||||||
$pkwWithNullAvailability = new Service();
|
|
||||||
$pkwWithNullAvailability->id = 1;
|
|
||||||
$pkwWithNullAvailability->subType = 'PKW';
|
|
||||||
$pkwWithNullAvailability->direction = 'HIN';
|
|
||||||
$pkwWithNullAvailability->label = 'Eigene Anreise';
|
|
||||||
$pkwWithNullAvailability->available = null;
|
|
||||||
|
|
||||||
$travel->transportationServices = [$pkwWithNullAvailability];
|
|
||||||
|
|
||||||
$result = $travel->getTransportationServicesByDirection('HIN', true);
|
|
||||||
|
|
||||||
$this->assertCount(1, $result);
|
|
||||||
$this->assertSame($pkwWithNullAvailability, $result[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test that multiple PKW services are all returned (no smart filtering at Travel level).
|
* Test that multiple PKW services are all returned (no smart filtering at Travel level).
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user