feat: pre-flight check of agency bookings

addresses #869bqxr4q
This commit is contained in:
Björn Fromme
2026-07-10 14:33:09 +02:00
parent f9142e3080
commit b41b19d4c6
26 changed files with 1818 additions and 304 deletions
+59 -13
View File
@@ -12,6 +12,7 @@ use App\BusProNet\Model\Service;
use App\Validator\Constraints as AppAssert;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use function Symfony\Component\String\u;
#[AppAssert\Participant(groups: ['booking_edit', 'booking_create'])]
class ParticipantDto
@@ -374,6 +375,37 @@ class ParticipantDto
$this->shoeSize = $this->normalizeBodyDimensionValue($this->shoeSize);
}
/**
* Normalizes participant data loaded from API/session sources.
*
* This is deliberately limited to mechanical cleanup:
* - trim whitespace
* - convert empty strings to null
* - normalize legacy body dimension values
*
* It does not fill business-required values.
*/
public function normalizeLoadedData(): void
{
$this->firstName = $this->normalizeNullableString($this->firstName);
$this->lastName = $this->normalizeNullableString($this->lastName);
$this->title = $this->normalizeNullableString($this->title);
$this->gender = $this->normalizeNullableString($this->gender);
$this->nationality = $this->normalizeNullableString($this->nationality);
$this->email = $this->normalizeNullableString($this->email);
$this->mobile = $this->normalizeNullableString($this->mobile);
$this->remarksRoom = $this->normalizeNullableString($this->remarksRoom);
$this->licensePlate = $this->normalizeNullableString($this->licensePlate);
$this->purchaseVoucherCode = $this->normalizeNullableString($this->purchaseVoucherCode);
$this->promoVoucherCode = $this->normalizeNullableString($this->promoVoucherCode);
if (null !== $this->address) {
$this->address->normalize();
}
$this->normalizeBodyDimensions();
}
/**
* Restores the DTO from session/serialization data.
*
@@ -393,8 +425,7 @@ class ParticipantDto
$this->address = new Address();
}
// TODO: Remove after all legacy drafts/session snapshots with body-dimension strings have expired.
$this->normalizeBodyDimensions();
$this->normalizeLoadedData();
}
private function normalizeBodyDimensionValue(mixed $value): ?string
@@ -407,7 +438,7 @@ class ParticipantDto
return null;
}
$value = trim($value);
$value = u($value)->trim()->toString();
if ('' === $value || false === ctype_digit($value)) {
return null;
}
@@ -415,22 +446,37 @@ class ParticipantDto
return (string) (int) $value;
}
private function normalizeNullableString(?string $value): ?string
{
if (null === $value) {
return null;
}
$trimmed = u($value)->trim()->toString();
return '' === $trimmed ? null : $trimmed;
}
/**
* Validates that the applicant (index 0) has a complete address.
* Validates that the participant has a complete address when required.
*
* This callback only applies to the applicant participant. Address validation includes:
* - Address object must exist
* - All required address fields must be filled (street, postCode, city, country)
* In regular create/edit submission flows this only applies to participant 0.
* The edit overview preflight checks address fields separately for the applicant only.
*/
#[Assert\Callback(groups: ['strict_required'])]
public function validateApplicantAddress(ExecutionContextInterface $context): void
{
// Only validate applicant's address
// Only validate applicant's address in regular create/edit submission flows.
if (0 !== $this->index) {
return;
}
// Address object is required for applicant
$this->validateAddressFields($context);
}
private function validateAddressFields(ExecutionContextInterface $context): void
{
// Address object is required for the validated participant
if (null === $this->address) {
$context->buildViolation('Bitte angeben')
->atPath('address')
@@ -440,25 +486,25 @@ class ParticipantDto
}
// Validate address subfields
if (null === $this->address->street || '' === trim($this->address->street)) {
if (null === $this->address->street || u($this->address->street)->trim()->isEmpty()) {
$context->buildViolation('Bitte angeben')
->atPath('address.street')
->addViolation();
}
if (null === $this->address->postCode || '' === trim($this->address->postCode)) {
if (null === $this->address->postCode || u($this->address->postCode)->trim()->isEmpty()) {
$context->buildViolation('Bitte angeben')
->atPath('address.postCode')
->addViolation();
}
if (null === $this->address->city || '' === trim($this->address->city)) {
if (null === $this->address->city || u($this->address->city)->trim()->isEmpty()) {
$context->buildViolation('Bitte angeben')
->atPath('address.city')
->addViolation();
}
if (null === $this->address->country || '' === trim($this->address->country)) {
if (null === $this->address->country || u($this->address->country)->trim()->isEmpty()) {
$context->buildViolation('Bitte angeben')
->atPath('address.country')
->addViolation();