Files
myep/src/Form/Service/CreateFieldStateProvider.php
T

353 lines
15 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
),
];
$this->fieldStateConditions['veg'] = [
'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
),
];
// Pickup field - shown only when outbound transportation is BUS (pickup is for outbound journey)
$this->fieldStateConditions['pickup'] = [
'hidden' => CompositeCondition::not(
ServiceSubTypeCondition::equals('transportationOutbound', DirectionMapper::SUBTYPE_BUS_API)
),
];
// Show differentDropOff checkbox only when BOTH outbound and inbound are BUS
$this->fieldStateConditions['differentDropOff'] = [
'hidden' => CompositeCondition::not(
CompositeCondition::and(
ServiceSubTypeCondition::equals('transportationOutbound', DirectionMapper::SUBTYPE_BUS_API),
ServiceSubTypeCondition::equals('transportationInbound', DirectionMapper::SUBTYPE_BUS_API)
)
),
];
// Show drop-off when inbound is BUS (options provider further gates on checkbox state in BUS+BUS)
$this->fieldStateConditions['dropOff'] = [
'hidden' => CompositeCondition::not(
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')
// ),
// ];
}
}