fix: correctly distinguish between first participant and applicant
This commit is contained in:
@@ -1,45 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Form\Service\Condition;
|
|
||||||
|
|
||||||
use App\Form\Model\BookingDto;
|
|
||||||
use App\Form\Service\Contract\FieldConditionInterface;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Condition that checks if the participant is the applicant.
|
|
||||||
*
|
|
||||||
* This is used to apply field state logic (e.g., enable/disable fields)
|
|
||||||
* specifically for the applicant participant in the booking.
|
|
||||||
*
|
|
||||||
* The default logic assumes the applicant is the first participant (index 0),
|
|
||||||
* but this can be adjusted if your domain uses a different rule or property.
|
|
||||||
*/
|
|
||||||
class ApplicantCondition implements FieldConditionInterface
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Evaluates if the participant is the applicant.
|
|
||||||
*
|
|
||||||
* @param BookingDto $bookingDto The current booking data (create or edit)
|
|
||||||
* @param int $participantIndex The index of the participant being evaluated
|
|
||||||
* @param array<string, mixed> $formData Current form data (unused)
|
|
||||||
*
|
|
||||||
* @return bool True if the participant is the applicant, false otherwise
|
|
||||||
*/
|
|
||||||
public function evaluate(BookingDto $bookingDto, int $participantIndex, array $formData): bool
|
|
||||||
{
|
|
||||||
// Default: applicant is the first participant (index 0)
|
|
||||||
return 0 === $participantIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getDependentFields(): array
|
|
||||||
{
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getDescription(): string
|
|
||||||
{
|
|
||||||
return 'Checks if the participant is the applicant (index 0)';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,46 +8,37 @@ use App\Form\Model\BookingDto;
|
|||||||
use App\Form\Service\Contract\FieldConditionInterface;
|
use App\Form\Service\Contract\FieldConditionInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Condition that determines if personal data fields should be hidden for authenticated users.
|
* Condition that determines if personal data fields should be shown as static text.
|
||||||
*
|
*
|
||||||
* ⚠️ IMPORTANT: This condition is used in CREATE MODE ONLY to protect authenticated
|
* This condition protects the logged-in user's personal data from being modified
|
||||||
* applicant data during booking creation. It is NOT used in edit mode.
|
* within the booking form. Instead, users should edit their data via the personal
|
||||||
|
* data form to prevent:
|
||||||
|
* - Creating duplicate customer records in BPN
|
||||||
|
* - Disconnecting bookings from the user's account
|
||||||
|
* - Causing data inconsistencies between booking and account data
|
||||||
*
|
*
|
||||||
* In CREATE mode:
|
* The condition compares the participant's personId with the booking applicant's
|
||||||
* - When a logged-in user creates a booking, their personal data is prepopulated from
|
* personId to determine if the participant IS the logged-in user:
|
||||||
* their BPN account (including personId).
|
* - Customer-initiated bookings: Participant 0's personId matches applicant's personId
|
||||||
* - Their personal data fields should be hidden and displayed as static text to prevent
|
* - Agency-initiated bookings: Participant 0's personId differs from applicant's personId
|
||||||
* modifications that could:
|
|
||||||
* - Create duplicate customer records in BPN
|
|
||||||
* - Disconnect bookings from the user's account
|
|
||||||
* - Cause data inconsistencies between booking and account data
|
|
||||||
*
|
*
|
||||||
* In EDIT mode:
|
* In CREATE mode: Checks if participant has a personId (prepopulated from BPN account)
|
||||||
* - DO NOT use this condition. Edit mode uses PersonalDataMutabilityCondition instead,
|
* In EDIT mode: Compares participant's personId with applicant's personId
|
||||||
* which respects the BPN API's per-participant `mutable` flag (aenderungmoeglich).
|
|
||||||
* - The mutability flag determines editability for ALL participants uniformly in edit mode.
|
|
||||||
*
|
|
||||||
* When this condition is satisfied (returns true), the template should:
|
|
||||||
* - Hide the form fields for personal data
|
|
||||||
* - Display the values as static, read-only text via the field_or_static macro
|
|
||||||
*/
|
*/
|
||||||
class AuthenticatedUserPersonalDataCondition implements FieldConditionInterface
|
class AuthenticatedUserPersonalDataCondition implements FieldConditionInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Evaluates if personal data fields should be hidden (not editable) in CREATE mode.
|
* Evaluates if personal data fields should be shown as static text.
|
||||||
*
|
*
|
||||||
* Returns true when the participant has a personId set, indicating they are
|
* In CREATE mode: Returns true when participant has a personId (prepopulated).
|
||||||
* a logged-in user whose data was prepopulated from their BPN account.
|
* In EDIT mode: Returns true when participant's personId matches applicant's personId,
|
||||||
* When true, personal data fields should be hidden and displayed as static text.
|
* indicating the participant IS the logged-in user.
|
||||||
*
|
*
|
||||||
* This condition is only used during booking creation, not in edit mode.
|
* @param BookingDto $bookingDto The booking data
|
||||||
* Edit mode uses PersonalDataMutabilityCondition to respect BPN's mutability flag.
|
|
||||||
*
|
|
||||||
* @param BookingDto $bookingDto The current booking data (create flow)
|
|
||||||
* @param int $participantIndex The index of the participant being evaluated
|
* @param int $participantIndex The index of the participant being evaluated
|
||||||
* @param array<string, mixed> $formData Current form data (unused)
|
* @param array<string, mixed> $formData Current form data (unused)
|
||||||
*
|
*
|
||||||
* @return bool True if fields should be hidden (authenticated user in create mode)
|
* @return bool True if fields should be shown as static text
|
||||||
*/
|
*/
|
||||||
public function evaluate(BookingDto $bookingDto, int $participantIndex, array $formData): bool
|
public function evaluate(BookingDto $bookingDto, int $participantIndex, array $formData): bool
|
||||||
{
|
{
|
||||||
@@ -56,8 +47,18 @@ class AuthenticatedUserPersonalDataCondition implements FieldConditionInterface
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide personal data fields if participant has BPN person ID (authenticated user in create mode)
|
// In edit mode, compare participant's personId with applicant's personId
|
||||||
// The personId is set during prepopulation when a logged-in user starts creating a booking
|
// This distinguishes customer-initiated (match) from agency-initiated (no match) bookings
|
||||||
|
if (BookingDto::MODE_EDIT === $bookingDto->getMode()) {
|
||||||
|
$applicantPersonId = $bookingDto->booking?->applicant?->personId;
|
||||||
|
if (null === $applicantPersonId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $participant->personId === $applicantPersonId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In create mode, check if participant has personId (prepopulated from BPN account)
|
||||||
return null !== $participant->personId;
|
return null !== $participant->personId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,6 +82,6 @@ class AuthenticatedUserPersonalDataCondition implements FieldConditionInterface
|
|||||||
*/
|
*/
|
||||||
public function getDescription(): string
|
public function getDescription(): string
|
||||||
{
|
{
|
||||||
return 'Personal data is not editable for authenticated users in create mode (prevents duplicate records)';
|
return 'Personal data is shown as static text for the logged-in user (edit via personal data form)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Form\Service\Condition;
|
||||||
|
|
||||||
|
use App\Form\Model\BookingDto;
|
||||||
|
use App\Form\Service\Contract\FieldConditionInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Condition that checks if the participant is the first participant (index 0).
|
||||||
|
*
|
||||||
|
* This is used to apply field state logic specifically for the first participant
|
||||||
|
* in the booking, such as requiring address fields or showing DOB-dependent fields.
|
||||||
|
*
|
||||||
|
* Note: This checks if the participant is at index 0, NOT if they are the "applicant"
|
||||||
|
* in the BPN sense (the logged-in user). For agency-initiated bookings, the first
|
||||||
|
* participant may be different from the applicant/logged-in user.
|
||||||
|
*/
|
||||||
|
class FirstParticipantCondition implements FieldConditionInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Evaluates if the participant is the first participant.
|
||||||
|
*
|
||||||
|
* @param BookingDto $bookingDto The current booking data
|
||||||
|
* @param int $participantIndex The index of the participant being evaluated
|
||||||
|
* @param array<string, mixed> $formData Current form data (unused)
|
||||||
|
*
|
||||||
|
* @return bool True if the participant is the first participant (index 0)
|
||||||
|
*/
|
||||||
|
public function evaluate(BookingDto $bookingDto, int $participantIndex, array $formData): bool
|
||||||
|
{
|
||||||
|
return 0 === $participantIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDependentFields(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Checks if the participant is the first participant (index 0)';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,6 @@ namespace App\Form\Service;
|
|||||||
use App\BusProNet\Utility\DirectionMapper;
|
use App\BusProNet\Utility\DirectionMapper;
|
||||||
use App\Form\Service\Abstract\AbstractFieldStateProvider;
|
use App\Form\Service\Abstract\AbstractFieldStateProvider;
|
||||||
use App\Form\Service\Condition\AgeRangeCondition;
|
use App\Form\Service\Condition\AgeRangeCondition;
|
||||||
use App\Form\Service\Condition\ApplicantCondition;
|
|
||||||
use App\Form\Service\Condition\AuthenticatedUserPersonalDataCondition;
|
use App\Form\Service\Condition\AuthenticatedUserPersonalDataCondition;
|
||||||
use App\Form\Service\Condition\BabyAgeCondition;
|
use App\Form\Service\Condition\BabyAgeCondition;
|
||||||
use App\Form\Service\Condition\BookingEligibilityCondition;
|
use App\Form\Service\Condition\BookingEligibilityCondition;
|
||||||
@@ -16,6 +15,7 @@ use App\Form\Service\Condition\CompositeCondition;
|
|||||||
use App\Form\Service\Condition\DateOfBirthProvidedCondition;
|
use App\Form\Service\Condition\DateOfBirthProvidedCondition;
|
||||||
use App\Form\Service\Condition\FieldValueCondition;
|
use App\Form\Service\Condition\FieldValueCondition;
|
||||||
use App\Form\Service\Condition\FinalBookingOnlyCondition;
|
use App\Form\Service\Condition\FinalBookingOnlyCondition;
|
||||||
|
use App\Form\Service\Condition\FirstParticipantCondition;
|
||||||
use App\Form\Service\Condition\MultipleParticipantsCondition;
|
use App\Form\Service\Condition\MultipleParticipantsCondition;
|
||||||
use App\Form\Service\Condition\RentalSelectionCondition;
|
use App\Form\Service\Condition\RentalSelectionCondition;
|
||||||
use App\Form\Service\Condition\RoomSelectionCondition;
|
use App\Form\Service\Condition\RoomSelectionCondition;
|
||||||
@@ -107,10 +107,10 @@ class CreateFieldStateProvider extends AbstractFieldStateProvider
|
|||||||
'static_text' => $authenticatedUserCondition,
|
'static_text' => $authenticatedUserCondition,
|
||||||
];
|
];
|
||||||
|
|
||||||
// Mobile field: static text for authenticated users, required for guest applicants
|
// Mobile field: static text for authenticated users, required for first participant (guest bookings)
|
||||||
$this->fieldStateConditions['mobile'] = [
|
$this->fieldStateConditions['mobile'] = [
|
||||||
'static_text' => $authenticatedUserCondition,
|
'static_text' => $authenticatedUserCondition,
|
||||||
'required' => new ApplicantCondition(),
|
'required' => new FirstParticipantCondition(),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Address subfields - must render as static text to prevent creating duplicate BPN records
|
// Address subfields - must render as static text to prevent creating duplicate BPN records
|
||||||
@@ -194,13 +194,13 @@ class CreateFieldStateProvider extends AbstractFieldStateProvider
|
|||||||
// Bulk insurance booking conditions
|
// Bulk insurance booking conditions
|
||||||
$bulkInsuranceBookingCondition = new BulkInsuranceBookingCondition();
|
$bulkInsuranceBookingCondition = new BulkInsuranceBookingCondition();
|
||||||
|
|
||||||
// Show bulk insurance booking checkbox ONLY for applicant (index 0) when:
|
// Show bulk insurance booking checkbox ONLY for first participant (index 0) when:
|
||||||
// - Date of birth is provided
|
// - Date of birth is provided
|
||||||
// - There are multiple participants (bulk assignment is meaningless for single participant)
|
// - There are multiple participants (bulk assignment is meaningless for single participant)
|
||||||
$this->fieldStateConditions['bulkInsuranceBooking'] = [
|
$this->fieldStateConditions['bulkInsuranceBooking'] = [
|
||||||
'hidden' => CompositeCondition::or(
|
'hidden' => CompositeCondition::or(
|
||||||
CompositeCondition::not($dateOfBirthProvidedCondition), // Hide until date of birth provided
|
CompositeCondition::not($dateOfBirthProvidedCondition), // Hide until date of birth provided
|
||||||
CompositeCondition::not(new ApplicantCondition()), // Hide for non-applicants
|
CompositeCondition::not(new FirstParticipantCondition()), // Hide for non-first participants
|
||||||
CompositeCondition::not(new MultipleParticipantsCondition()) // Hide when only one participant
|
CompositeCondition::not(new MultipleParticipantsCondition()) // Hide when only one participant
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
@@ -274,11 +274,11 @@ class CreateFieldStateProvider extends AbstractFieldStateProvider
|
|||||||
'hidden' => FieldValueCondition::equals('parking', false),
|
'hidden' => FieldValueCondition::equals('parking', false),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Make address field required for applicant (participant index 0)
|
// Make address field required for first participant (index 0)
|
||||||
// Also render as static text for authenticated users to prevent duplicate BPN records
|
// Also render as static text for authenticated users to prevent duplicate BPN records
|
||||||
$this->fieldStateConditions['address'] = [
|
$this->fieldStateConditions['address'] = [
|
||||||
'static_text' => $authenticatedUserCondition,
|
'static_text' => $authenticatedUserCondition,
|
||||||
'required' => new ApplicantCondition(),
|
'required' => new FirstParticipantCondition(),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Room assignment field:
|
// Room assignment field:
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ use App\Form\Model\BookingDto;
|
|||||||
use App\Form\Service\Abstract\AbstractFieldStateProvider;
|
use App\Form\Service\Abstract\AbstractFieldStateProvider;
|
||||||
use App\Form\Service\Condition\AdditionalServicesMutabilityCondition;
|
use App\Form\Service\Condition\AdditionalServicesMutabilityCondition;
|
||||||
use App\Form\Service\Condition\AgeRangeCondition;
|
use App\Form\Service\Condition\AgeRangeCondition;
|
||||||
use App\Form\Service\Condition\ApplicantCondition;
|
|
||||||
use App\Form\Service\Condition\AuthenticatedUserPersonalDataCondition;
|
use App\Form\Service\Condition\AuthenticatedUserPersonalDataCondition;
|
||||||
use App\Form\Service\Condition\BookingModeCondition;
|
use App\Form\Service\Condition\BookingModeCondition;
|
||||||
use App\Form\Service\Condition\CompositeCondition;
|
use App\Form\Service\Condition\CompositeCondition;
|
||||||
use App\Form\Service\Condition\DateOfBirthProvidedCondition;
|
use App\Form\Service\Condition\DateOfBirthProvidedCondition;
|
||||||
use App\Form\Service\Condition\FieldValueCondition;
|
use App\Form\Service\Condition\FieldValueCondition;
|
||||||
|
use App\Form\Service\Condition\FirstParticipantCondition;
|
||||||
use App\Form\Service\Condition\PersonalDataMutabilityCondition;
|
use App\Form\Service\Condition\PersonalDataMutabilityCondition;
|
||||||
use App\Form\Service\Condition\PickupsMutabilityCondition;
|
use App\Form\Service\Condition\PickupsMutabilityCondition;
|
||||||
use App\Form\Service\Condition\RentalSelectionCondition;
|
use App\Form\Service\Condition\RentalSelectionCondition;
|
||||||
@@ -48,16 +48,14 @@ class EditFieldStateProvider extends AbstractFieldStateProvider
|
|||||||
$transportationServicesMutabilityCondition = new TransportationServicesMutabilityCondition();
|
$transportationServicesMutabilityCondition = new TransportationServicesMutabilityCondition();
|
||||||
$pickupsMutabilityCondition = new PickupsMutabilityCondition();
|
$pickupsMutabilityCondition = new PickupsMutabilityCondition();
|
||||||
|
|
||||||
// Personal data protection in edit mode has TWO rules:
|
// Personal data protection in edit mode:
|
||||||
// 1. Applicant with personId (authenticated user) - prevents editing master personal data
|
// 1. Participant with personId (linked to BPN account) - edit via personal data form instead
|
||||||
// Only applies to applicant (index 0), not other participants with personId
|
// 2. BPN mutability flag (mutable=false) - respects BPN business rules
|
||||||
// 2. BPN mutability flag (mutable=false) - respects BPN business rules for any participant
|
// Show fields as static text if EITHER condition is true
|
||||||
// Hide fields if EITHER condition is true
|
|
||||||
$applicantCondition = new ApplicantCondition();
|
|
||||||
$authenticatedUserCondition = new AuthenticatedUserPersonalDataCondition();
|
$authenticatedUserCondition = new AuthenticatedUserPersonalDataCondition();
|
||||||
$personalDataMutabilityCondition = new PersonalDataMutabilityCondition();
|
$personalDataMutabilityCondition = new PersonalDataMutabilityCondition();
|
||||||
$personalDataHiddenCondition = CompositeCondition::or(
|
$personalDataHiddenCondition = CompositeCondition::or(
|
||||||
CompositeCondition::and($applicantCondition, $authenticatedUserCondition),
|
$authenticatedUserCondition,
|
||||||
$personalDataMutabilityCondition
|
$personalDataMutabilityCondition
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -122,12 +120,12 @@ class EditFieldStateProvider extends AbstractFieldStateProvider
|
|||||||
'hidden' => CompositeCondition::not($rentalCondition),
|
'hidden' => CompositeCondition::not($rentalCondition),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Age-dependent service fields - hidden until birth date provided (except for applicant who always has DOB)
|
// Age-dependent service fields - hidden until birth date provided (except for first participant who always has DOB)
|
||||||
// Also readonly if services not mutable
|
// Also readonly if services not mutable
|
||||||
// For applicant in edit mode: DOB is always available (patched from booking), so never hide
|
// For first participant in edit mode: DOB is always available (patched from booking), so never hide
|
||||||
$hideUntilDobCondition = CompositeCondition::and(
|
$hideUntilDobCondition = CompositeCondition::and(
|
||||||
CompositeCondition::not($dateOfBirthProvidedCondition),
|
CompositeCondition::not($dateOfBirthProvidedCondition),
|
||||||
CompositeCondition::not(new ApplicantCondition()) // Don't hide for applicant
|
CompositeCondition::not(new FirstParticipantCondition()) // Don't hide for first participant
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->fieldStateConditions['courses'] = [
|
$this->fieldStateConditions['courses'] = [
|
||||||
@@ -145,14 +143,14 @@ class EditFieldStateProvider extends AbstractFieldStateProvider
|
|||||||
'readonly' => $additionalServicesMutabilityCondition,
|
'readonly' => $additionalServicesMutabilityCondition,
|
||||||
];
|
];
|
||||||
|
|
||||||
// Skipass - hidden until birth date (except applicant), readonly if services not mutable
|
// Skipass - hidden until birth date (except first participant), readonly if services not mutable
|
||||||
$this->fieldStateConditions['skiPass'] = [
|
$this->fieldStateConditions['skiPass'] = [
|
||||||
'hidden' => $hideUntilDobCondition,
|
'hidden' => $hideUntilDobCondition,
|
||||||
'readonly' => $additionalServicesMutabilityCondition,
|
'readonly' => $additionalServicesMutabilityCondition,
|
||||||
];
|
];
|
||||||
|
|
||||||
// Rentals - shown only when skipass selected, readonly if services not mutable
|
// Rentals - shown only when skipass selected, readonly if services not mutable
|
||||||
// For applicant: only check skipass, not DOB
|
// For first participant: only check skipass, not DOB
|
||||||
$this->fieldStateConditions['rentals'] = [
|
$this->fieldStateConditions['rentals'] = [
|
||||||
'hidden' => CompositeCondition::or(
|
'hidden' => CompositeCondition::or(
|
||||||
$hideUntilDobCondition,
|
$hideUntilDobCondition,
|
||||||
@@ -167,7 +165,7 @@ class EditFieldStateProvider extends AbstractFieldStateProvider
|
|||||||
'readonly' => $additionalServicesMutabilityCondition,
|
'readonly' => $additionalServicesMutabilityCondition,
|
||||||
];
|
];
|
||||||
|
|
||||||
// Transportation fields - hidden until birth date (except applicant), readonly if transportation not mutable
|
// Transportation fields - hidden until birth date (except first participant), readonly if transportation not mutable
|
||||||
$this->fieldStateConditions['transportationOutbound'] = [
|
$this->fieldStateConditions['transportationOutbound'] = [
|
||||||
'hidden' => $hideUntilDobCondition,
|
'hidden' => $hideUntilDobCondition,
|
||||||
'readonly' => $transportationServicesMutabilityCondition,
|
'readonly' => $transportationServicesMutabilityCondition,
|
||||||
|
|||||||
Reference in New Issue
Block a user