For booking that are agency initiated (groups) different rules apply concerning mutability and requirement of personal data and whether the first participant is the applicant or not. This commit adds logic to evaluate the agency id assigned to the booking data to decide.
332 lines
14 KiB
PHP
332 lines
14 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Form\Service;
|
|
|
|
use App\BusProNet\Utility\DirectionMapper;
|
|
use App\Form\Service\Abstract\AbstractFieldStateProvider;
|
|
use App\Form\Service\Condition\AgeRangeCondition;
|
|
use App\Form\Service\Condition\BabyAgeCondition;
|
|
use App\Form\Service\Condition\BookingEligibilityCondition;
|
|
use App\Form\Service\Condition\BulkInsuranceBookingCondition;
|
|
use App\Form\Service\Condition\CompositeCondition;
|
|
use App\Form\Service\Condition\DateOfBirthProvidedCondition;
|
|
use App\Form\Service\Condition\FieldValueCondition;
|
|
use App\Form\Service\Condition\FinalBookingOnlyCondition;
|
|
use App\Form\Service\Condition\FirstParticipantCondition;
|
|
use App\Form\Service\Condition\FirstParticipantReadOnlyCondition;
|
|
use App\Form\Service\Condition\MultipleParticipantsCondition;
|
|
use App\Form\Service\Condition\RentalInsuranceAvailableCondition;
|
|
use App\Form\Service\Condition\RentalSelectionCondition;
|
|
use App\Form\Service\Condition\RoomSelectionCondition;
|
|
use App\Form\Service\Condition\ServiceSubTypeCondition;
|
|
use App\Form\Service\Condition\SingleRoomTypeCondition;
|
|
use App\Form\Service\Condition\SkiPassSelectionCondition;
|
|
use App\Service\ParticipantEligibilityService;
|
|
|
|
/**
|
|
* Field state provider for the booking create workflow.
|
|
*
|
|
* This service calculates dynamic field states (readonly, disabled, static_text, etc.)
|
|
* for participant fields in the create flow. It extends the common field
|
|
* state functionality provided by AbstractFieldStateProvider.
|
|
*
|
|
* Current field state conditions:
|
|
* - Personal data fields render as static text for authenticated users (prevents duplicate BPN records)
|
|
* - Body dimension fields are hidden unless rental services are selected
|
|
* - Age-dependent service fields are hidden until birth date is provided
|
|
* - Transportation pickup fields are hidden by default, shown only when transportation type is BUS
|
|
* - Parking field is hidden by default, shown only when outbound transportation is PKW
|
|
* - All service fields are hidden for ineligible participants (no skipasses available for their age)
|
|
*/
|
|
class CreateFieldStateProvider extends AbstractFieldStateProvider
|
|
{
|
|
public function __construct(
|
|
private readonly ParticipantEligibilityService $participantEligibilityService,
|
|
) {
|
|
parent::__construct();
|
|
}
|
|
|
|
/**
|
|
* Registers field state conditions for the create workflow.
|
|
*
|
|
* This method defines the conditional logic for field states in the
|
|
* booking creation process. Each field can have multiple state conditions
|
|
* (readonly, disabled, hidden, required) that are evaluated independently.
|
|
*
|
|
* Adding new field state conditions:
|
|
* To add conditional state logic for a field, register conditions here:
|
|
*
|
|
* $this->fieldStateConditions['fieldName'] = [
|
|
* 'readonly' => new SomeCondition(),
|
|
* 'disabled' => CompositeCondition::and(
|
|
* new AgeRangeCondition(null, 17),
|
|
* FieldValueCondition::equals('someField', 'someValue')
|
|
* ),
|
|
* ];
|
|
*
|
|
* Condition Types:
|
|
* - 'readonly': Field is visible but not editable
|
|
* - 'disabled': Field interaction is disabled
|
|
* - 'required': Field becomes mandatory
|
|
* - 'hidden': Field is not displayed
|
|
*/
|
|
protected function registerFieldStateConditions(): void
|
|
{
|
|
// Booking eligibility condition - hide all service fields when no skipasses available for participant's age
|
|
$bookingEligibilityCondition = new BookingEligibilityCondition($this->participantEligibilityService);
|
|
|
|
$rentalCondition = new RentalSelectionCondition();
|
|
$skiPassCondition = new SkiPassSelectionCondition();
|
|
|
|
// First participant read-only condition
|
|
// Render personal data fields as static text for first participant in non-internal agency bookings
|
|
// when user is logged in (data is prepopulated from BPN account)
|
|
$firstParticipantReadOnlyCondition = new FirstParticipantReadOnlyCondition();
|
|
|
|
$this->fieldStateConditions['firstName'] = [
|
|
'static_text' => $firstParticipantReadOnlyCondition,
|
|
];
|
|
|
|
$this->fieldStateConditions['lastName'] = [
|
|
'static_text' => $firstParticipantReadOnlyCondition,
|
|
];
|
|
|
|
$this->fieldStateConditions['gender'] = [
|
|
'static_text' => $firstParticipantReadOnlyCondition,
|
|
];
|
|
|
|
$this->fieldStateConditions['nationality'] = [
|
|
'static_text' => $firstParticipantReadOnlyCondition,
|
|
];
|
|
|
|
$this->fieldStateConditions['dateOfBirth'] = [
|
|
'static_text' => $firstParticipantReadOnlyCondition,
|
|
];
|
|
|
|
$this->fieldStateConditions['email'] = [
|
|
'static_text' => $firstParticipantReadOnlyCondition,
|
|
];
|
|
|
|
// Mobile field: static text for first participant (when read-only), required for first participant (guest bookings)
|
|
$this->fieldStateConditions['mobile'] = [
|
|
'static_text' => $firstParticipantReadOnlyCondition,
|
|
'required' => new FirstParticipantCondition(),
|
|
];
|
|
|
|
// Address subfields - must render as static text to prevent creating duplicate BPN records
|
|
$this->fieldStateConditions['address.street'] = [
|
|
'static_text' => $firstParticipantReadOnlyCondition,
|
|
];
|
|
|
|
$this->fieldStateConditions['address.postCode'] = [
|
|
'static_text' => $firstParticipantReadOnlyCondition,
|
|
];
|
|
|
|
$this->fieldStateConditions['address.city'] = [
|
|
'static_text' => $firstParticipantReadOnlyCondition,
|
|
];
|
|
|
|
$this->fieldStateConditions['address.country'] = [
|
|
'static_text' => $firstParticipantReadOnlyCondition,
|
|
];
|
|
|
|
$this->fieldStateConditions['address.district'] = [
|
|
'static_text' => $firstParticipantReadOnlyCondition,
|
|
];
|
|
|
|
// Note: Body dimensions (height, weight, shoeSize) are NOT hidden for authenticated users
|
|
// These are preferences/measurements that can be updated without creating duplicate records
|
|
|
|
// Hide body dimensions section unless rental services are selected
|
|
$this->fieldStateConditions['bodyDimensions'] = [
|
|
'hidden' => CompositeCondition::not($rentalCondition),
|
|
];
|
|
|
|
// Hide age-dependent fields when no date of birth is provided
|
|
$dateOfBirthProvidedCondition = new DateOfBirthProvidedCondition();
|
|
|
|
// Baby age condition - hide ski pass for babies (0-2 years)
|
|
$babyAgeCondition = new BabyAgeCondition();
|
|
|
|
// Age-dependent service fields are hidden until birth date is provided OR when participant is ineligible
|
|
$this->fieldStateConditions['courses'] = [
|
|
'hidden' => CompositeCondition::or(
|
|
CompositeCondition::not($dateOfBirthProvidedCondition),
|
|
$bookingEligibilityCondition
|
|
),
|
|
];
|
|
|
|
$this->fieldStateConditions['additionalServices'] = [
|
|
'hidden' => CompositeCondition::or(
|
|
CompositeCondition::not($dateOfBirthProvidedCondition),
|
|
$bookingEligibilityCondition
|
|
),
|
|
];
|
|
|
|
// Skipass field - hidden when:
|
|
// - No date of birth provided
|
|
// - Participant is ineligible (no skipasses for their age)
|
|
// - Participant is baby age (0-2 years) - babies don't need ski passes
|
|
$this->fieldStateConditions['skiPass'] = [
|
|
'hidden' => CompositeCondition::or(
|
|
CompositeCondition::not($dateOfBirthProvidedCondition),
|
|
$bookingEligibilityCondition,
|
|
$babyAgeCondition
|
|
),
|
|
];
|
|
|
|
// Show rentals only when both date of birth is provided AND skipass is selected AND participant is eligible
|
|
$this->fieldStateConditions['rentals'] = [
|
|
'hidden' => CompositeCondition::or(
|
|
CompositeCondition::not($dateOfBirthProvidedCondition),
|
|
CompositeCondition::not($skiPassCondition),
|
|
$bookingEligibilityCondition
|
|
),
|
|
];
|
|
|
|
$this->fieldStateConditions['board'] = [
|
|
'hidden' => CompositeCondition::or(
|
|
CompositeCondition::not($dateOfBirthProvidedCondition),
|
|
$bookingEligibilityCondition
|
|
),
|
|
];
|
|
|
|
// Bulk insurance booking conditions
|
|
$bulkInsuranceBookingCondition = new BulkInsuranceBookingCondition();
|
|
|
|
// Show bulk insurance booking checkbox ONLY for first participant (index 0) when:
|
|
// - Date of birth is provided
|
|
// - There are multiple participants (bulk assignment is meaningless for single participant)
|
|
$this->fieldStateConditions['bulkInsuranceBooking'] = [
|
|
'hidden' => CompositeCondition::or(
|
|
CompositeCondition::not($dateOfBirthProvidedCondition), // Hide until date of birth provided
|
|
CompositeCondition::not(new FirstParticipantCondition()), // Hide for non-first participants
|
|
CompositeCondition::not(new MultipleParticipantsCondition()) // Hide when only one participant
|
|
),
|
|
];
|
|
|
|
// Hide insurance field until date of birth is provided OR when bulk insurance booking is active OR when participant is ineligible
|
|
$this->fieldStateConditions['insurance'] = [
|
|
'hidden' => CompositeCondition::or(
|
|
CompositeCondition::not($dateOfBirthProvidedCondition),
|
|
$bulkInsuranceBookingCondition,
|
|
$bookingEligibilityCondition
|
|
),
|
|
];
|
|
|
|
// Room-specific field conditions
|
|
$sharedRoomCondition = new RoomSelectionCondition(['mbz']);
|
|
|
|
// Show remarks room field only when room with code 'mbz' is selected
|
|
$this->fieldStateConditions['remarksRoom'] = [
|
|
'hidden' => CompositeCondition::not($sharedRoomCondition),
|
|
];
|
|
|
|
// Transportation-related field conditions
|
|
// All transportation fields require date of birth and participant eligibility
|
|
|
|
// Outbound transportation - hidden until date of birth provided AND participant is eligible
|
|
$this->fieldStateConditions['transportationOutbound'] = [
|
|
'hidden' => CompositeCondition::or(
|
|
CompositeCondition::not($dateOfBirthProvidedCondition),
|
|
$bookingEligibilityCondition
|
|
),
|
|
];
|
|
|
|
// Inbound transportation - hidden until date of birth provided AND participant is eligible
|
|
$this->fieldStateConditions['transportationInbound'] = [
|
|
'hidden' => CompositeCondition::or(
|
|
CompositeCondition::not($dateOfBirthProvidedCondition),
|
|
$bookingEligibilityCondition
|
|
),
|
|
];
|
|
|
|
// Show pickup only when either outbound or inbound transportation is BUS (hidden by default)
|
|
$this->fieldStateConditions['pickup'] = [
|
|
'hidden' => CompositeCondition::not(
|
|
CompositeCondition::or(
|
|
ServiceSubTypeCondition::equals('transportationOutbound', DirectionMapper::SUBTYPE_BUS_API),
|
|
ServiceSubTypeCondition::equals('transportationInbound', DirectionMapper::SUBTYPE_BUS_API)
|
|
)
|
|
),
|
|
];
|
|
|
|
// Show parking only when outbound transportation is PKW AND participant is 18+ (hidden by default)
|
|
// Parking is offered at holiday destination for those arriving by car
|
|
// Participants under 18 cannot drive in Germany, so parking is not relevant for them
|
|
$minimumDrivingAgeCondition = new AgeRangeCondition(18);
|
|
$this->fieldStateConditions['parking'] = [
|
|
'hidden' => CompositeCondition::or(
|
|
CompositeCondition::not(
|
|
ServiceSubTypeCondition::equals('transportationOutbound', DirectionMapper::SUBTYPE_CAR_API)
|
|
),
|
|
CompositeCondition::not($minimumDrivingAgeCondition)
|
|
),
|
|
];
|
|
|
|
// Show rental insurance only when rental services are selected AND LVS services exist (hidden by default)
|
|
$rentalInsuranceAvailableCondition = new RentalInsuranceAvailableCondition();
|
|
$this->fieldStateConditions['rentalInsurance'] = [
|
|
'hidden' => CompositeCondition::or(
|
|
CompositeCondition::not($rentalCondition),
|
|
CompositeCondition::not($rentalInsuranceAvailableCondition)
|
|
),
|
|
];
|
|
|
|
// Show license plate only when parking is selected (hidden by default)
|
|
$this->fieldStateConditions['licensePlate'] = [
|
|
'hidden' => FieldValueCondition::equals('parking', false),
|
|
];
|
|
|
|
// Make address field required for first participant (index 0)
|
|
// Also render as static text for first participant when read-only
|
|
$this->fieldStateConditions['address'] = [
|
|
'static_text' => $firstParticipantReadOnlyCondition,
|
|
'required' => new FirstParticipantCondition(),
|
|
];
|
|
|
|
// Room assignment field:
|
|
// - Hidden until date of birth is provided (age determines room eligibility, e.g., Baby rooms)
|
|
// - Rendered as static text when only one room type is selected (auto-assigned by RoomAssignmentService)
|
|
$this->fieldStateConditions['assignedRoomId'] = [
|
|
'hidden' => CompositeCondition::not($dateOfBirthProvidedCondition),
|
|
'static_text' => new SingleRoomTypeCondition(),
|
|
];
|
|
|
|
// Voucher fields - hidden for inquiry bookings (status='A')
|
|
// Vouchers cannot be redeemed or submitted for inquiry bookings
|
|
$finalBookingOnlyCondition = new FinalBookingOnlyCondition();
|
|
|
|
$this->fieldStateConditions['purchaseVoucherCode'] = [
|
|
'hidden' => CompositeCondition::not($finalBookingOnlyCondition),
|
|
];
|
|
|
|
$this->fieldStateConditions['promoVoucherCode'] = [
|
|
'hidden' => CompositeCondition::not($finalBookingOnlyCondition),
|
|
];
|
|
|
|
// Example field state conditions would be registered here
|
|
// For demonstration purposes, here are some example patterns:
|
|
|
|
// Example 1: Make assignedRoomId readonly for participants under 18
|
|
// $this->fieldStateConditions['assignedRoomId'] = [
|
|
// 'readonly' => new AgeRangeCondition(null, 17),
|
|
// ];
|
|
|
|
// Example 2: Disable service selection if no room is assigned
|
|
// $this->fieldStateConditions['serviceSelection'] = [
|
|
// 'disabled' => FieldValueCondition::isEmpty('assignedRoomId'),
|
|
// ];
|
|
|
|
// Example 3: Complex condition with multiple criteria
|
|
// $this->fieldStateConditions['advancedOptions'] = [
|
|
// 'hidden' => CompositeCondition::or(
|
|
// new AgeRangeCondition(null, 15),
|
|
// FieldValueCondition::equals('userType', 'basic')
|
|
// ),
|
|
// ];
|
|
}
|
|
}
|