feat: proper determination of agency initiated bookings

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.
This commit is contained in:
Björn Fromme committed 2026-03-16 12:02:27 +01:00
1 parent f55ed3dc4c
commit e222f448bb
20 files changed
+201 -137

No files matched your search

@@ -1,87 +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 determines if personal data fields should be shown as static text.
*
* This condition protects the logged-in user's personal data from being modified
* 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
*
* The condition compares the participant's personId with the booking applicant's
* personId to determine if the participant IS the logged-in user:
* - Customer-initiated bookings: Participant 0's personId matches applicant's personId
* - Agency-initiated bookings: Participant 0's personId differs from applicant's personId
*
* In CREATE mode: Checks if participant has a personId (prepopulated from BPN account)
* In EDIT mode: Compares participant's personId with applicant's personId
*/
class AuthenticatedUserPersonalDataCondition implements FieldConditionInterface
{
/**
* Evaluates if personal data fields should be shown as static text.
*
* In CREATE mode: Returns true when participant has a personId (prepopulated).
* In EDIT mode: Returns true when participant's personId matches applicant's personId,
* indicating the participant IS the logged-in user.
*
* @param BookingDto $bookingDto The 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 fields should be shown as static text
*/
public function evaluate(BookingDto $bookingDto, int $participantIndex, array $formData): bool
{
$participant = $bookingDto->getParticipant($participantIndex);
if (null === $participant) {
return false;
}
// In edit mode, compare participant's personId with applicant's personId
// 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;
}
/**
* Returns field names that this condition depends on.
*
* This condition is based on personId which is set during prepopulation
* and doesn't change during form interaction, so no field dependencies.
*
* @return string[] Empty array - no field dependencies
*/
public function getDependentFields(): array
{
return [];
}
/**
* Returns a human-readable description of this condition.
*
* @return string Description of the authenticated user personal data protection logic
*/
public function getDescription(): string
{
return 'Personal data is shown as static text for the logged-in user (edit via personal data form)';
}
}
@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace App\Form\Service\Condition;
use App\Form\Model\BookingDto;
use App\Form\Service\Contract\FieldConditionInterface;
/**
* Condition that determines if the first participant's personal data should be read-only.
*
* This condition evaluates read-only state based on agency type and authentication state,
* replacing the previous personId-based detection with agency code-based detection.
*
* Business Rules:
* - CREATE mode (anonymous user): First participant data is editable (user must fill it in)
* - CREATE mode (logged-in, non-internal agency): First participant = applicant, data is read-only
* - CREATE mode (logged-in, internal agency): First participant ≠ applicant, data is editable
* - EDIT mode (internal agency): First participant ≠ applicant, data is editable (mutability applies separately)
* - EDIT mode (non-internal agency): First participant = applicant, always read-only
*
* Non-first participants are never affected by this condition - they return false here
* but may still be read-only via PersonalDataMutabilityCondition in edit mode.
*/
class FirstParticipantReadOnlyCondition implements FieldConditionInterface
{
/**
* Evaluates if personal data fields should be shown as static text for the first participant.
*
* @param BookingDto $bookingDto The 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 fields should be shown as static text (read-only)
*/
public function evaluate(BookingDto $bookingDto, int $participantIndex, array $formData): bool
{
// This condition only applies to first participant (agency-based read-only)
// Non-first participants: return false here, but may still be read-only via PersonalDataMutabilityCondition
if (0 !== $participantIndex) {
return false;
}
$participant = $bookingDto->getParticipant($participantIndex);
// CREATE mode logic
if (BookingDto::MODE_CREATE === $bookingDto->getMode()) {
// Anonymous user (no personId) = must fill in data = editable
if (null === $participant?->personId) {
return false;
}
// Internal agency = first participant ≠ applicant = editable
if ($bookingDto->isInternalAgencyBooking()) {
return false;
}
// Logged-in user with non-internal agency = data prepopulated = read-only
return true;
}
// EDIT mode logic
// Internal agency = first participant ≠ applicant = editable (mutability check applies separately)
if ($bookingDto->isInternalAgencyBooking()) {
return false;
}
// Non-internal agency = first participant = applicant = always read-only
return true;
}
/**
* Returns field names that this condition depends on.
*
* This condition is based on agency code and personId which are set during initialization
* and don't change during form interaction, so no field dependencies.
*
* @return string[] Empty array - no field dependencies
*/
public function getDependentFields(): array
{
return [];
}
/**
* Returns a human-readable description of this condition.
*
* @return string Description of the first participant read-only logic
*/
public function getDescription(): string
{
return 'First participant personal data is read-only for non-internal agency bookings with logged-in users';
}
}
@@ -10,12 +10,13 @@ use App\Form\Service\Contract\FieldConditionInterface;
/**
* Condition that determines if personal data fields should be hidden based on BPN mutability flag.
*
* This condition is used in EDIT MODE ONLY to respect the BPN API's per-participant
* mutability flag (`aenderungmoeglich`). When a participant's data is not mutable
* (mutable=false), personal data fields should be hidden and displayed as static text.
* This condition respects the BPN API's per-participant mutability flag (`aenderungmoeglich`).
* When a participant's data is not mutable (mutable=false), personal data fields should be
* hidden and displayed as static text.
*
* This differs from AuthenticatedUserPersonalDataCondition which is used in CREATE MODE
* to protect authenticated applicant data.
* This condition is combined with FirstParticipantReadOnlyCondition via OR in edit mode.
* FirstParticipantReadOnlyCondition handles agency-based read-only logic for first participant,
* while this condition applies BPN mutability rules to all participants.
*
* In edit mode, BPN determines mutability based on business rules (e.g., payment status,
* booking state, etc.). We must respect this flag to prevent users from attempting to