feat: replace dropdowns with individual fields for body dimensions
This commit is contained in:
+16
-19
@@ -34,25 +34,14 @@ parameters:
|
|||||||
firstName: 'vorname'
|
firstName: 'vorname'
|
||||||
lastName: 'nachname'
|
lastName: 'nachname'
|
||||||
|
|
||||||
# Body dimensions choices for BodyDimensionsType
|
# Body dimensions ranges for BodyDimensionsType and ParticipantValidator
|
||||||
body_dimensions.height_choices:
|
body_dimension_ranges:
|
||||||
'bis 148cm': '-148'
|
height_min: 145
|
||||||
'149 - 157cm': '149-157'
|
height_max: 210
|
||||||
'158 - 166cm': '158-166'
|
weight_min: 40
|
||||||
'167 - 178cm': '167-178'
|
weight_max: 120
|
||||||
'179 - 185cm': '179-185'
|
shoe_size_min: 35
|
||||||
'186 - 194cm': '186-194'
|
shoe_size_max: 50
|
||||||
'195cm oder mehr': '195+'
|
|
||||||
body_dimensions.shoe_size_min: 36
|
|
||||||
body_dimensions.shoe_size_max: 48
|
|
||||||
body_dimensions.weight_choices:
|
|
||||||
'42 - 48kg': '42-48'
|
|
||||||
'49 - 57kg': '49-57'
|
|
||||||
'58 - 66kg': '58-66'
|
|
||||||
'67 - 78kg': '67-78'
|
|
||||||
'79 - 85kg': '79-85'
|
|
||||||
'86 - 94kg': '86-94'
|
|
||||||
'95kg oder mehr': '95+'
|
|
||||||
|
|
||||||
# domain mapping for theme, gtm id and cmp url
|
# domain mapping for theme, gtm id and cmp url
|
||||||
domain_config:
|
domain_config:
|
||||||
@@ -148,6 +137,10 @@ services:
|
|||||||
$preferRemote: '%env(bool:APP_TRAVEL_PREFER_REMOTE)%'
|
$preferRemote: '%env(bool:APP_TRAVEL_PREFER_REMOTE)%'
|
||||||
$enableFallback: '%env(bool:APP_TRAVEL_ENABLE_FALLBACK)%'
|
$enableFallback: '%env(bool:APP_TRAVEL_ENABLE_FALLBACK)%'
|
||||||
|
|
||||||
|
App\Service\ParticipantFormSupport:
|
||||||
|
arguments:
|
||||||
|
$bodyDimensionRanges: '%body_dimension_ranges%'
|
||||||
|
|
||||||
App\Service\BookingEditDataLoader:
|
App\Service\BookingEditDataLoader:
|
||||||
arguments:
|
arguments:
|
||||||
$bpnCache: '@bpn.cache'
|
$bpnCache: '@bpn.cache'
|
||||||
@@ -259,6 +252,10 @@ services:
|
|||||||
from: '%default_email_from%'
|
from: '%default_email_from%'
|
||||||
to: '%default_email_to%'
|
to: '%default_email_to%'
|
||||||
|
|
||||||
|
App\Validator\Constraints\ParticipantValidator:
|
||||||
|
arguments:
|
||||||
|
$bodyDimensionRanges: '%body_dimension_ranges%'
|
||||||
|
|
||||||
App\Service\MailjetApiClient:
|
App\Service\MailjetApiClient:
|
||||||
arguments:
|
arguments:
|
||||||
$apiKey: '%env(default::MAILJET_API_KEY)%'
|
$apiKey: '%env(default::MAILJET_API_KEY)%'
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
# Body Dimensions Transition Plan (Stakeholder Draft)
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Prevent edit-form failures for existing bookings while we transition from legacy body-dimension values to a better long-term field model.
|
||||||
|
|
||||||
|
This plan introduces a temporary compatibility phase and keeps sunsetting manual and straightforward.
|
||||||
|
|
||||||
|
## Current Problem
|
||||||
|
|
||||||
|
- Existing bookings may contain legacy values such as `-148`, `149-157`, or `195+`.
|
||||||
|
- Integer-only form fields can fail when these legacy strings are loaded.
|
||||||
|
- Result: participant edit forms can crash before submit.
|
||||||
|
|
||||||
|
## Proposed Transition Strategy
|
||||||
|
|
||||||
|
Use **text input fields** for body dimensions during transition, with explicit server-side format validation.
|
||||||
|
|
||||||
|
### Key Decisions
|
||||||
|
|
||||||
|
- No gating by booking create date.
|
||||||
|
- One unified behavior for all bookings.
|
||||||
|
- Accept both legacy and numeric formats during transition.
|
||||||
|
- Keep manual sunsetting later (no automated migration switch).
|
||||||
|
|
||||||
|
## Functional Scope
|
||||||
|
|
||||||
|
Affected fields:
|
||||||
|
|
||||||
|
- `height`
|
||||||
|
- `weight`
|
||||||
|
- `shoeSize`
|
||||||
|
|
||||||
|
Affected components:
|
||||||
|
|
||||||
|
- `src/Form/BodyDimensionsType.php`
|
||||||
|
- `src/Validator/Constraints/ParticipantValidator.php`
|
||||||
|
- `src/Form/BookingParticipantType.php` (option passthrough already in place)
|
||||||
|
- `config/services.yaml` (ranges remain as validation/config source)
|
||||||
|
|
||||||
|
## Validation Rules During Transition
|
||||||
|
|
||||||
|
### Accepted formats
|
||||||
|
|
||||||
|
- Numeric value: `^\d+$` (e.g. `176`)
|
||||||
|
- Legacy lower/open/range tokens:
|
||||||
|
- `^-\d+$` (e.g. `-148`)
|
||||||
|
- `^\d+\+$` (e.g. `195+`)
|
||||||
|
- `^\d+\s*-\s*\d+$` (e.g. `149-157`)
|
||||||
|
|
||||||
|
### Requiredness
|
||||||
|
|
||||||
|
- Keep existing rule: body dimensions required when rentals are selected.
|
||||||
|
|
||||||
|
### Range checks
|
||||||
|
|
||||||
|
- Keep current numeric range checks based on `body_dimension_ranges`.
|
||||||
|
- Apply range checks only to plain numeric values in transition phase.
|
||||||
|
|
||||||
|
## UX/Behavior Expectations
|
||||||
|
|
||||||
|
- Existing legacy values render without form initialization errors.
|
||||||
|
- New numeric entries are accepted and validated.
|
||||||
|
- Invalid free text (e.g. `abc`, `17x`) shows clear validation errors.
|
||||||
|
- HTMX refresh behavior remains unchanged.
|
||||||
|
|
||||||
|
## Implementation Steps
|
||||||
|
|
||||||
|
1. **Form type change**
|
||||||
|
- In `BodyDimensionsType`, replace integer fields with text fields for all three body dimensions.
|
||||||
|
- Keep labels/help text; keep placeholders based on configured ranges.
|
||||||
|
|
||||||
|
2. **Validator extension**
|
||||||
|
- Add format validation for all three fields in `ParticipantValidator`.
|
||||||
|
- Keep existing required-when-rentals logic.
|
||||||
|
- Keep existing range validation for numeric values.
|
||||||
|
|
||||||
|
3. **Message tuning**
|
||||||
|
- Add one clear message for invalid format.
|
||||||
|
- Retain existing range message for numeric out-of-range values.
|
||||||
|
|
||||||
|
4. **Testing and QA**
|
||||||
|
- Validate old bookings with legacy values open/edit successfully.
|
||||||
|
- Validate numeric path (valid and invalid range).
|
||||||
|
- Validate invalid format handling.
|
||||||
|
- Validate rental-required behavior.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- No 500 error when editing participants with legacy body-dimension values.
|
||||||
|
- Legacy values can be loaded and submitted in transition phase.
|
||||||
|
- Numeric values are accepted and range-validated.
|
||||||
|
- Invalid text is rejected with a user-facing validation message.
|
||||||
|
- Existing participant edit/refresh flows continue to work.
|
||||||
|
|
||||||
|
## Risks and Mitigations
|
||||||
|
|
||||||
|
- **Risk:** Free-text field semantics are less strict than integer fields.
|
||||||
|
- **Mitigation:** strict server-side regex + range checks.
|
||||||
|
|
||||||
|
- **Risk:** Inconsistent data representations during transition.
|
||||||
|
- **Mitigation:** explicit acceptance policy and manual sunset plan.
|
||||||
|
|
||||||
|
## Effort Estimate
|
||||||
|
|
||||||
|
- Implementation: 0.5 day
|
||||||
|
- Validation/message tuning: 0.25 day
|
||||||
|
- QA/manual testing: 0.5 day
|
||||||
|
- **Total:** ~1 to 1.5 days
|
||||||
|
|
||||||
|
## Manual Sunset Plan (Later)
|
||||||
|
|
||||||
|
When stakeholders approve end of transition:
|
||||||
|
|
||||||
|
1. Replace text fields with final semantic field type(s).
|
||||||
|
2. Remove legacy format acceptance from validator.
|
||||||
|
3. Keep only final numeric/range validation behavior.
|
||||||
|
4. Remove transition-specific tests and copy.
|
||||||
|
|
||||||
|
Expected cleanup effort: ~0.25 to 0.5 day.
|
||||||
@@ -6,7 +6,7 @@ namespace App\Form;
|
|||||||
|
|
||||||
use App\Form\Model\ParticipantDto;
|
use App\Form\Model\ParticipantDto;
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
@@ -15,35 +15,38 @@ class BodyDimensionsType extends AbstractType
|
|||||||
{
|
{
|
||||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||||
{
|
{
|
||||||
$shoeSizeChoices = array_combine(
|
$ranges = $options['body_dimension_ranges'];
|
||||||
range($options['shoe_size_min'], $options['shoe_size_max']),
|
|
||||||
range($options['shoe_size_min'], $options['shoe_size_max'])
|
|
||||||
);
|
|
||||||
|
|
||||||
$builder
|
$builder
|
||||||
->add('height', ChoiceType::class, [
|
->add('height', IntegerType::class, [
|
||||||
'label' => 'Körpergröße',
|
'label' => 'Körpergröße (cm)',
|
||||||
'required' => $options['height_required'],
|
'required' => $options['height_required'],
|
||||||
'expanded' => false,
|
'empty_data' => null,
|
||||||
'multiple' => false,
|
'attr' => [
|
||||||
'placeholder' => 'Keine Angabe',
|
'placeholder' => sprintf('%d - %d', $ranges['height_min'], $ranges['height_max']),
|
||||||
'choices' => $options['height_choices'],
|
'min' => $ranges['height_min'],
|
||||||
|
'max' => $ranges['height_max'],
|
||||||
|
],
|
||||||
])
|
])
|
||||||
->add('shoeSize', ChoiceType::class, [
|
->add('shoeSize', IntegerType::class, [
|
||||||
'label' => 'Schuhgröße',
|
'label' => 'Schuhgröße',
|
||||||
'required' => $options['shoeSize_required'],
|
'required' => $options['shoeSize_required'],
|
||||||
'expanded' => false,
|
'empty_data' => null,
|
||||||
'multiple' => false,
|
'attr' => [
|
||||||
'placeholder' => 'Keine Angabe',
|
'placeholder' => sprintf('%d - %d', $ranges['shoe_size_min'], $ranges['shoe_size_max']),
|
||||||
'choices' => $shoeSizeChoices,
|
'min' => $ranges['shoe_size_min'],
|
||||||
|
'max' => $ranges['shoe_size_max'],
|
||||||
|
],
|
||||||
])
|
])
|
||||||
->add('weight', ChoiceType::class, [
|
->add('weight', IntegerType::class, [
|
||||||
'label' => 'Gewicht',
|
'label' => 'Gewicht (kg)',
|
||||||
'required' => $options['weight_required'],
|
'required' => $options['weight_required'],
|
||||||
'expanded' => false,
|
'empty_data' => null,
|
||||||
'multiple' => false,
|
'attr' => [
|
||||||
'placeholder' => 'Keine Angabe',
|
'placeholder' => sprintf('%d - %d', $ranges['weight_min'], $ranges['weight_max']),
|
||||||
'choices' => $options['weight_choices'],
|
'min' => $ranges['weight_min'],
|
||||||
|
'max' => $ranges['weight_max'],
|
||||||
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,22 +54,20 @@ class BodyDimensionsType extends AbstractType
|
|||||||
{
|
{
|
||||||
$resolver->setDefaults([
|
$resolver->setDefaults([
|
||||||
'data_class' => ParticipantDto::class,
|
'data_class' => ParticipantDto::class,
|
||||||
'height_required' => true,
|
'height_required' => false,
|
||||||
'weight_required' => true,
|
'weight_required' => false,
|
||||||
'shoeSize_required' => true,
|
'shoeSize_required' => false,
|
||||||
'height_choices' => [],
|
'help' => 'Du kannst die Daten auch später nachreichen',
|
||||||
'weight_choices' => [],
|
'help_attr' => [
|
||||||
'shoe_size_min' => 36,
|
'class' => 'lg:col-span-3 text-sm -mt-2 px-2',
|
||||||
'shoe_size_max' => 48,
|
],
|
||||||
'help' => 'Verleih kann bis 4 Tage vor Anreise in MyE&P nachgebucht werden, sollten dir (noch) nicht alle Angaben vorliegen',
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$resolver->setRequired(['body_dimension_ranges']);
|
||||||
|
|
||||||
$resolver->setAllowedTypes('height_required', 'bool');
|
$resolver->setAllowedTypes('height_required', 'bool');
|
||||||
$resolver->setAllowedTypes('weight_required', 'bool');
|
$resolver->setAllowedTypes('weight_required', 'bool');
|
||||||
$resolver->setAllowedTypes('shoeSize_required', 'bool');
|
$resolver->setAllowedTypes('shoeSize_required', 'bool');
|
||||||
$resolver->setAllowedTypes('height_choices', 'array');
|
$resolver->setAllowedTypes('body_dimension_ranges', 'array');
|
||||||
$resolver->setAllowedTypes('weight_choices', 'array');
|
|
||||||
$resolver->setAllowedTypes('shoe_size_min', 'int');
|
|
||||||
$resolver->setAllowedTypes('shoe_size_max', 'int');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,12 +49,12 @@ class BookingParticipantType extends AbstractType
|
|||||||
: $this->createFieldStateProvider;
|
: $this->createFieldStateProvider;
|
||||||
|
|
||||||
$builder
|
$builder
|
||||||
->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($bookingContext) {
|
->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($bookingContext, $options) {
|
||||||
$this->onPreSetData($event, $bookingContext);
|
$this->onPreSetData($event, $bookingContext, $options);
|
||||||
})
|
})
|
||||||
->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($bookingContext) {
|
->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($bookingContext, $options) {
|
||||||
$this->processFieldHandlers($event, $bookingContext);
|
$this->processFieldHandlers($event, $bookingContext);
|
||||||
$this->onPreSubmit($event, $bookingContext);
|
$this->onPreSubmit($event, $bookingContext, $options);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +78,6 @@ class BookingParticipantType extends AbstractType
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @var ParticipantEditDto $data */
|
|
||||||
$data = $form->getData();
|
$data = $form->getData();
|
||||||
|
|
||||||
// Process all field handlers for this participant and sync submitted data
|
// Process all field handlers for this participant and sync submitted data
|
||||||
@@ -94,23 +93,17 @@ class BookingParticipantType extends AbstractType
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds dynamic fields to the form based on participant data.
|
* Adds dynamic fields to the form based on participant data.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $options
|
||||||
*/
|
*/
|
||||||
private function onPreSetData(FormEvent $event, ?BookingDto $bookingContext): void
|
private function onPreSetData(FormEvent $event, ?BookingDto $bookingContext, array $options): void
|
||||||
{
|
{
|
||||||
/** @var ParticipantEditDto|null $data */
|
|
||||||
$data = $event->getData();
|
|
||||||
|
|
||||||
if (null === $data) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$form = $event->getForm();
|
$form = $event->getForm();
|
||||||
|
$data = $event->getData();
|
||||||
// Use bookingContext from wrapper DTO or fallback to passed option
|
|
||||||
$bookingDto = $data->bookingContext;
|
$bookingDto = $data->bookingContext;
|
||||||
|
|
||||||
// Add base fields with states applied
|
// Add base fields with states applied
|
||||||
$this->addBaseFields($form, $bookingDto, $data->participant->index);
|
$this->addBaseFields($form, $bookingDto, $data->participant->index, $options);
|
||||||
|
|
||||||
// Add dynamic fields
|
// Add dynamic fields
|
||||||
$this->addDynamicFields($form, $bookingDto, $data->participant->index);
|
$this->addDynamicFields($form, $bookingDto, $data->participant->index);
|
||||||
@@ -118,29 +111,30 @@ class BookingParticipantType extends AbstractType
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles form pre-submit events to update field states based on submitted data.
|
* Handles form pre-submit events to update field states based on submitted data.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $options
|
||||||
*/
|
*/
|
||||||
private function onPreSubmit(FormEvent $event, ?BookingDto $bookingContext): void
|
private function onPreSubmit(FormEvent $event, ?BookingDto $bookingContext, array $options): void
|
||||||
{
|
{
|
||||||
$submittedData = $event->getData();
|
$submittedData = $event->getData();
|
||||||
$form = $event->getForm();
|
$form = $event->getForm();
|
||||||
|
|
||||||
/** @var ParticipantEditDto $data */
|
|
||||||
$data = $form->getData();
|
$data = $form->getData();
|
||||||
|
|
||||||
// Use bookingContext from wrapper DTO or fallback to passed option
|
|
||||||
$bookingDto = $data->bookingContext;
|
$bookingDto = $data->bookingContext;
|
||||||
|
|
||||||
// Rebuild all fields with updated states based on submitted data
|
// Rebuild all fields with updated states based on submitted data
|
||||||
$this->rebuildFieldsWithStates($form, $bookingDto, $data->participant->index, $submittedData);
|
$this->rebuildFieldsWithStates($form, $bookingDto, $data->participant->index, $submittedData, $options);
|
||||||
|
|
||||||
$event->setData($submittedData);
|
$event->setData($submittedData);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds base fields to the form with field states applied.
|
* Adds base fields to the form with field states applied.
|
||||||
|
*
|
||||||
|
* @param FormInterface<mixed> $form
|
||||||
|
* @param array<string, mixed> $options
|
||||||
*/
|
*/
|
||||||
/** @param FormInterface<mixed> $form */
|
private function addBaseFields(FormInterface $form, BookingDto $bookingDto, int $participantIndex, array $options): void
|
||||||
private function addBaseFields(FormInterface $form, BookingDto $bookingDto, int $participantIndex): void
|
|
||||||
{
|
{
|
||||||
// Get field states for base fields
|
// Get field states for base fields
|
||||||
$allFieldStates = $this->fieldStateProvider->getAllFieldStates($bookingDto, $participantIndex);
|
$allFieldStates = $this->fieldStateProvider->getAllFieldStates($bookingDto, $participantIndex);
|
||||||
@@ -247,10 +241,7 @@ class BookingParticipantType extends AbstractType
|
|||||||
if ($this->fieldStateProvider->shouldIncludeField('bodyDimensions', $bookingDto, $participantIndex)) {
|
if ($this->fieldStateProvider->shouldIncludeField('bodyDimensions', $bookingDto, $participantIndex)) {
|
||||||
$form->add('bodyDimensions', BodyDimensionsType::class, [
|
$form->add('bodyDimensions', BodyDimensionsType::class, [
|
||||||
'property_path' => 'participant',
|
'property_path' => 'participant',
|
||||||
'height_choices' => $form->getConfig()->getOption('height_choices'),
|
'body_dimension_ranges' => $options['body_dimension_ranges'],
|
||||||
'weight_choices' => $form->getConfig()->getOption('weight_choices'),
|
|
||||||
'shoe_size_min' => $form->getConfig()->getOption('shoe_size_min'),
|
|
||||||
'shoe_size_max' => $form->getConfig()->getOption('shoe_size_max'),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -261,15 +252,11 @@ class BookingParticipantType extends AbstractType
|
|||||||
* This method handles dynamic field exclusion during form submission when
|
* This method handles dynamic field exclusion during form submission when
|
||||||
* field states change based on submitted data.
|
* field states change based on submitted data.
|
||||||
*
|
*
|
||||||
* @param FormInterface $form The form to modify
|
* @param FormInterface<mixed> $form The form to modify
|
||||||
* @param BookingDto $bookingDto The booking data for context
|
* @param BookingDto $bookingDto The booking data for context
|
||||||
* @param int $participantIndex The participant index
|
* @param int $participantIndex The participant index
|
||||||
* @param array<string, mixed> $submittedData Submitted form data for state calculation
|
* @param array<string, mixed> $submittedData Submitted form data for state calculation
|
||||||
*/
|
*/
|
||||||
/**
|
|
||||||
* @param FormInterface<mixed> $form
|
|
||||||
* @param array<string, mixed> $submittedData
|
|
||||||
*/
|
|
||||||
private function removeExcludedFields(FormInterface $form, BookingDto $bookingDto, int $participantIndex, array &$submittedData = []): void
|
private function removeExcludedFields(FormInterface $form, BookingDto $bookingDto, int $participantIndex, array &$submittedData = []): void
|
||||||
{
|
{
|
||||||
foreach (ParticipantDto::DYNAMIC_FIELDS as $fieldName) {
|
foreach (ParticipantDto::DYNAMIC_FIELDS as $fieldName) {
|
||||||
@@ -289,8 +276,9 @@ class BookingParticipantType extends AbstractType
|
|||||||
/**
|
/**
|
||||||
* @param FormInterface<mixed> $form
|
* @param FormInterface<mixed> $form
|
||||||
* @param array<string, mixed> $submittedData
|
* @param array<string, mixed> $submittedData
|
||||||
|
* @param array<string, mixed> $options
|
||||||
*/
|
*/
|
||||||
private function rebuildFieldsWithStates(FormInterface $form, BookingDto $bookingDto, int $participantIndex, array &$submittedData): void
|
private function rebuildFieldsWithStates(FormInterface $form, BookingDto $bookingDto, int $participantIndex, array &$submittedData, array $options): void
|
||||||
{
|
{
|
||||||
// First, remove fields that should be excluded entirely
|
// First, remove fields that should be excluded entirely
|
||||||
$this->removeExcludedFields($form, $bookingDto, $participantIndex, $submittedData);
|
$this->removeExcludedFields($form, $bookingDto, $participantIndex, $submittedData);
|
||||||
@@ -304,7 +292,7 @@ class BookingParticipantType extends AbstractType
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Re-add base fields with updated states
|
// Re-add base fields with updated states
|
||||||
$this->addBaseFields($form, $bookingDto, $participantIndex);
|
$this->addBaseFields($form, $bookingDto, $participantIndex, $options);
|
||||||
|
|
||||||
// Rebuild dynamic fields
|
// Rebuild dynamic fields
|
||||||
foreach (ParticipantDto::DYNAMIC_FIELDS as $fieldName) {
|
foreach (ParticipantDto::DYNAMIC_FIELDS as $fieldName) {
|
||||||
@@ -415,10 +403,6 @@ class BookingParticipantType extends AbstractType
|
|||||||
'data_class' => ParticipantEditDto::class,
|
'data_class' => ParticipantEditDto::class,
|
||||||
'selected_rooms' => [],
|
'selected_rooms' => [],
|
||||||
'booking_context' => null,
|
'booking_context' => null,
|
||||||
'height_choices' => [],
|
|
||||||
'weight_choices' => [],
|
|
||||||
'shoe_size_min' => 36,
|
|
||||||
'shoe_size_max' => 48,
|
|
||||||
'validation_groups' => function (FormInterface $form) {
|
'validation_groups' => function (FormInterface $form) {
|
||||||
/** @var ParticipantEditDto $data */
|
/** @var ParticipantEditDto $data */
|
||||||
$data = $form->getData();
|
$data = $form->getData();
|
||||||
@@ -443,11 +427,10 @@ class BookingParticipantType extends AbstractType
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$resolver->setRequired(['body_dimension_ranges']);
|
||||||
|
|
||||||
$resolver->setAllowedTypes('selected_rooms', 'array');
|
$resolver->setAllowedTypes('selected_rooms', 'array');
|
||||||
$resolver->setAllowedTypes('booking_context', ['null', BookingDto::class]);
|
$resolver->setAllowedTypes('booking_context', ['null', BookingDto::class]);
|
||||||
$resolver->setAllowedTypes('height_choices', 'array');
|
$resolver->setAllowedTypes('body_dimension_ranges', 'array');
|
||||||
$resolver->setAllowedTypes('weight_choices', 'array');
|
|
||||||
$resolver->setAllowedTypes('shoe_size_min', 'int');
|
|
||||||
$resolver->setAllowedTypes('shoe_size_max', 'int');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,16 +8,25 @@ use App\Exception\ParticipantNotFoundException;
|
|||||||
use App\Form\Model\BookingDto;
|
use App\Form\Model\BookingDto;
|
||||||
use App\Form\Model\ParticipantDto;
|
use App\Form\Model\ParticipantDto;
|
||||||
use App\Form\Model\ParticipantEditDto;
|
use App\Form\Model\ParticipantEditDto;
|
||||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shared helpers for participant edit forms in create and edit booking flows.
|
* Shared helpers for participant edit forms in create and edit booking flows.
|
||||||
*/
|
*/
|
||||||
class ParticipantFormSupport
|
class ParticipantFormSupport
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @var array<string, int>
|
||||||
|
*/
|
||||||
|
private array $bodyDimensionRanges;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, int> $bodyDimensionRanges
|
||||||
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly ParameterBagInterface $parameterBag,
|
array $bodyDimensionRanges,
|
||||||
) {
|
) {
|
||||||
|
$this->bodyDimensionRanges = $this->resolveOptions($bodyDimensionRanges);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function ensureParticipantExists(BookingDto $bookingDto, int $index): ParticipantDto
|
public function ensureParticipantExists(BookingDto $bookingDto, int $index): ParticipantDto
|
||||||
@@ -46,10 +55,7 @@ class ParticipantFormSupport
|
|||||||
{
|
{
|
||||||
$options = [
|
$options = [
|
||||||
'booking_context' => $bookingDto,
|
'booking_context' => $bookingDto,
|
||||||
'height_choices' => $this->parameterBag->get('body_dimensions.height_choices'),
|
'body_dimension_ranges' => $this->bodyDimensionRanges,
|
||||||
'weight_choices' => $this->parameterBag->get('body_dimensions.weight_choices'),
|
|
||||||
'shoe_size_min' => $this->parameterBag->get('body_dimensions.shoe_size_min'),
|
|
||||||
'shoe_size_max' => $this->parameterBag->get('body_dimensions.shoe_size_max'),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
if (true === $disableValidation) {
|
if (true === $disableValidation) {
|
||||||
@@ -77,4 +83,29 @@ class ParticipantFormSupport
|
|||||||
|
|
||||||
return array_values($notifications);
|
return array_values($notifications);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, int> $options
|
||||||
|
* @return array<string, int>
|
||||||
|
*/
|
||||||
|
private function resolveOptions(array $options): array
|
||||||
|
{
|
||||||
|
$optionsResolver = new OptionsResolver();
|
||||||
|
$optionsResolver->setRequired([
|
||||||
|
'height_min',
|
||||||
|
'height_max',
|
||||||
|
'weight_min',
|
||||||
|
'weight_max',
|
||||||
|
'shoe_size_min',
|
||||||
|
'shoe_size_max',
|
||||||
|
]);
|
||||||
|
$optionsResolver->setAllowedTypes('height_min', ['int']);
|
||||||
|
$optionsResolver->setAllowedTypes('height_max', ['int']);
|
||||||
|
$optionsResolver->setAllowedTypes('weight_min', ['int']);
|
||||||
|
$optionsResolver->setAllowedTypes('weight_max', ['int']);
|
||||||
|
$optionsResolver->setAllowedTypes('shoe_size_min', ['int']);
|
||||||
|
$optionsResolver->setAllowedTypes('shoe_size_max', ['int']);
|
||||||
|
|
||||||
|
return $optionsResolver->resolve($options);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Validator\Constraints;
|
namespace App\Validator\Constraints;
|
||||||
|
|
||||||
use App\Form\Model\ParticipantDto;
|
use App\Form\Model\ParticipantDto;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
use Symfony\Component\Validator\Constraint;
|
use Symfony\Component\Validator\Constraint;
|
||||||
use Symfony\Component\Validator\ConstraintValidator;
|
use Symfony\Component\Validator\ConstraintValidator;
|
||||||
|
|
||||||
@@ -12,10 +13,24 @@ use Symfony\Component\Validator\ConstraintValidator;
|
|||||||
* Validates cross-field constraints on participant data.
|
* Validates cross-field constraints on participant data.
|
||||||
*
|
*
|
||||||
* Enforces business rules that require examining multiple fields together,
|
* Enforces business rules that require examining multiple fields together,
|
||||||
* such as requiring a pickup location when bus transportation is selected.
|
* such as requiring a pickup location when bus transportation is selected,
|
||||||
|
* and validates body dimension ranges when values are provided.
|
||||||
*/
|
*/
|
||||||
class ParticipantValidator extends ConstraintValidator
|
class ParticipantValidator extends ConstraintValidator
|
||||||
{
|
{
|
||||||
|
private const RANGE_MESSAGE = 'Bitte gib eine Zahl zwischen %d und %d ein. Falls du außerhalb dieses Bereichs bist, ruf gerne unser Kundenoffice an.';
|
||||||
|
|
||||||
|
/** @var array<string, int> */
|
||||||
|
private array $bodyDimensionRanges;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, int> $bodyDimensionRanges
|
||||||
|
*/
|
||||||
|
public function __construct(array $bodyDimensionRanges = [])
|
||||||
|
{
|
||||||
|
$this->bodyDimensionRanges = $this->resolveBodyDimensionRanges($bodyDimensionRanges);
|
||||||
|
}
|
||||||
|
|
||||||
public function validate(mixed $value, Constraint $constraint): void
|
public function validate(mixed $value, Constraint $constraint): void
|
||||||
{
|
{
|
||||||
/** @var ParticipantDto $participant */
|
/** @var ParticipantDto $participant */
|
||||||
@@ -24,6 +39,7 @@ class ParticipantValidator extends ConstraintValidator
|
|||||||
$this->assertPickupSelected($participant);
|
$this->assertPickupSelected($participant);
|
||||||
$this->assertDropOffSelected($participant);
|
$this->assertDropOffSelected($participant);
|
||||||
$this->assertBodyDimensionsWhenRentalsSelected($participant);
|
$this->assertBodyDimensionsWhenRentalsSelected($participant);
|
||||||
|
$this->assertBodyDimensionsInRange($participant);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function assertPickupSelected(ParticipantDto $participant): void
|
public function assertPickupSelected(ParticipantDto $participant): void
|
||||||
@@ -93,4 +109,71 @@ class ParticipantValidator extends ConstraintValidator
|
|||||||
;
|
;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function assertBodyDimensionsInRange(ParticipantDto $participant): void
|
||||||
|
{
|
||||||
|
$this->assertInRange(
|
||||||
|
$participant->height,
|
||||||
|
'height',
|
||||||
|
$this->bodyDimensionRanges['height_min'],
|
||||||
|
$this->bodyDimensionRanges['height_max']
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertInRange(
|
||||||
|
$participant->weight,
|
||||||
|
'weight',
|
||||||
|
$this->bodyDimensionRanges['weight_min'],
|
||||||
|
$this->bodyDimensionRanges['weight_max']
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertInRange(
|
||||||
|
$participant->shoeSize,
|
||||||
|
'shoeSize',
|
||||||
|
$this->bodyDimensionRanges['shoe_size_min'],
|
||||||
|
$this->bodyDimensionRanges['shoe_size_max']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function assertInRange(int|string|null $value, string $path, int $min, int $max): void
|
||||||
|
{
|
||||||
|
if (null === $value || '' === $value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$intValue = (int) $value;
|
||||||
|
|
||||||
|
if ($min > $intValue || $max < $intValue) {
|
||||||
|
$this->context->buildViolation(sprintf(self::RANGE_MESSAGE, $min, $max))
|
||||||
|
->atPath($path)
|
||||||
|
->addViolation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, int> $options
|
||||||
|
*
|
||||||
|
* @return array<string, int>
|
||||||
|
*/
|
||||||
|
private function resolveBodyDimensionRanges(array $options): array
|
||||||
|
{
|
||||||
|
$resolver = new OptionsResolver();
|
||||||
|
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'height_min' => 145,
|
||||||
|
'height_max' => 200,
|
||||||
|
'weight_min' => 40,
|
||||||
|
'weight_max' => 120,
|
||||||
|
'shoe_size_min' => 35,
|
||||||
|
'shoe_size_max' => 50,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$resolver->setAllowedTypes('height_min', 'int');
|
||||||
|
$resolver->setAllowedTypes('height_max', 'int');
|
||||||
|
$resolver->setAllowedTypes('weight_min', 'int');
|
||||||
|
$resolver->setAllowedTypes('weight_max', 'int');
|
||||||
|
$resolver->setAllowedTypes('shoe_size_min', 'int');
|
||||||
|
$resolver->setAllowedTypes('shoe_size_max', 'int');
|
||||||
|
|
||||||
|
return $resolver->resolve($options);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Tests\Service;
|
namespace App\Tests\Service;
|
||||||
|
|
||||||
use App\BusProNet\ApiClient;
|
|
||||||
use App\BusProNet\Exception\ApiClientException;
|
use App\BusProNet\Exception\ApiClientException;
|
||||||
use App\BusProNet\Exception\TimeoutException;
|
use App\BusProNet\Exception\TimeoutException;
|
||||||
use App\BusProNet\Model\Booking;
|
use App\BusProNet\Model\Booking;
|
||||||
@@ -28,7 +27,7 @@ use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
|
|||||||
|
|
||||||
class BookingEditSubmitterTest extends TestCase
|
class BookingEditSubmitterTest extends TestCase
|
||||||
{
|
{
|
||||||
public function testHandleSubmissionReturnsResultWhenFreshBookingDataCannotBeLoaded(): void
|
public function testHandleSubmissionReturnsRedirectWhenFreshBookingDataCannotBeLoaded(): void
|
||||||
{
|
{
|
||||||
$request = $this->createRequestWithSession();
|
$request = $this->createRequestWithSession();
|
||||||
$user = $this->createUser();
|
$user = $this->createUser();
|
||||||
@@ -50,13 +49,14 @@ class BookingEditSubmitterTest extends TestCase
|
|||||||
$result = $service->handleSubmission($request, $bookingDto, 42, $user);
|
$result = $service->handleSubmission($request, $bookingDto, 42, $user);
|
||||||
|
|
||||||
$this->assertSame(BookingEditSubmissionResult::STATUS_BOOKING_DATA_RELOAD_FAILED, $result->status);
|
$this->assertSame(BookingEditSubmissionResult::STATUS_BOOKING_DATA_RELOAD_FAILED, $result->status);
|
||||||
|
$this->assertNull($result->message);
|
||||||
$this->assertFalse($result->immutableChangesReverted);
|
$this->assertFalse($result->immutableChangesReverted);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @dataProvider updateFailureProvider
|
* @dataProvider updateFailureProvider
|
||||||
*/
|
*/
|
||||||
public function testHandleSubmissionReturnsResultWhenUpdateThrows(
|
public function testHandleSubmissionReturnsRedirectWhenUpdateThrows(
|
||||||
\Throwable $exception,
|
\Throwable $exception,
|
||||||
string $expectedStatus,
|
string $expectedStatus,
|
||||||
): void {
|
): void {
|
||||||
@@ -88,7 +88,7 @@ class BookingEditSubmitterTest extends TestCase
|
|||||||
->with($bookingDto, $freshBookingData)
|
->with($bookingDto, $freshBookingData)
|
||||||
->willReturn(false);
|
->willReturn(false);
|
||||||
|
|
||||||
$apiClient = $this->createMock(ApiClient::class);
|
$apiClient = $this->createMock(\App\BusProNet\ApiClient::class);
|
||||||
$apiClient->expects($this->once())
|
$apiClient->expects($this->once())
|
||||||
->method('updateBooking')
|
->method('updateBooking')
|
||||||
->with($bookingDto, true)
|
->with($bookingDto, true)
|
||||||
@@ -108,7 +108,7 @@ class BookingEditSubmitterTest extends TestCase
|
|||||||
$this->assertFalse($result->immutableChangesReverted);
|
$this->assertFalse($result->immutableChangesReverted);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testHandleSubmissionReturnsResultWhenUpdateIsUnsuccessful(): void
|
public function testHandleSubmissionReturnsRedirectWhenUpdateIsUnsuccessful(): void
|
||||||
{
|
{
|
||||||
$request = $this->createRequestWithSession();
|
$request = $this->createRequestWithSession();
|
||||||
$user = $this->createUser();
|
$user = $this->createUser();
|
||||||
@@ -138,7 +138,7 @@ class BookingEditSubmitterTest extends TestCase
|
|||||||
->with($bookingDto, $freshBookingData)
|
->with($bookingDto, $freshBookingData)
|
||||||
->willReturn(false);
|
->willReturn(false);
|
||||||
|
|
||||||
$apiClient = $this->createMock(ApiClient::class);
|
$apiClient = $this->createMock(\App\BusProNet\ApiClient::class);
|
||||||
$bookingUpdate = new BookingUpdate();
|
$bookingUpdate = new BookingUpdate();
|
||||||
$bookingUpdate->success = false;
|
$bookingUpdate->success = false;
|
||||||
$bookingUpdate->status = 'BPN-FAIL';
|
$bookingUpdate->status = 'BPN-FAIL';
|
||||||
@@ -163,12 +163,12 @@ class BookingEditSubmitterTest extends TestCase
|
|||||||
|
|
||||||
public function testHandleSubmissionStoresInfoForNonErrorNotification(): void
|
public function testHandleSubmissionStoresInfoForNonErrorNotification(): void
|
||||||
{
|
{
|
||||||
$this->assertNotificationResult(new Notification(650, 'Alles gut'), 'info');
|
$this->assertNotificationResult(new Notification(650, 'Alles gut'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testHandleSubmissionStoresErrorForErrorNotification(): void
|
public function testHandleSubmissionStoresErrorForErrorNotification(): void
|
||||||
{
|
{
|
||||||
$this->assertNotificationResult(new Notification(500, 'Kaputt'), 'error');
|
$this->assertNotificationResult(new Notification(500, 'Kaputt'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testHandleSubmissionClearsSessionAndDraftOnSuccessfulUpdate(): void
|
public function testHandleSubmissionClearsSessionAndDraftOnSuccessfulUpdate(): void
|
||||||
@@ -201,7 +201,7 @@ class BookingEditSubmitterTest extends TestCase
|
|||||||
->with($bookingDto, $freshBookingData)
|
->with($bookingDto, $freshBookingData)
|
||||||
->willReturn(false);
|
->willReturn(false);
|
||||||
|
|
||||||
$apiClient = $this->createMock(ApiClient::class);
|
$apiClient = $this->createMock(\App\BusProNet\ApiClient::class);
|
||||||
$bookingUpdate = new BookingUpdate();
|
$bookingUpdate = new BookingUpdate();
|
||||||
$bookingUpdate->success = true;
|
$bookingUpdate->success = true;
|
||||||
$apiClient->expects($this->once())
|
$apiClient->expects($this->once())
|
||||||
@@ -233,6 +233,7 @@ class BookingEditSubmitterTest extends TestCase
|
|||||||
$result = $service->handleSubmission($request, $bookingDto, 42, $user);
|
$result = $service->handleSubmission($request, $bookingDto, 42, $user);
|
||||||
|
|
||||||
$this->assertSame(BookingEditSubmissionResult::STATUS_SUCCESS, $result->status);
|
$this->assertSame(BookingEditSubmissionResult::STATUS_SUCCESS, $result->status);
|
||||||
|
$this->assertNull($result->message);
|
||||||
$this->assertFalse($result->immutableChangesReverted);
|
$this->assertFalse($result->immutableChangesReverted);
|
||||||
$this->assertSame($freshBookingData, $bookingDto->booking);
|
$this->assertSame($freshBookingData, $bookingDto->booking);
|
||||||
}
|
}
|
||||||
@@ -267,7 +268,7 @@ class BookingEditSubmitterTest extends TestCase
|
|||||||
->with($bookingDto, $freshBookingData)
|
->with($bookingDto, $freshBookingData)
|
||||||
->willReturn(true);
|
->willReturn(true);
|
||||||
|
|
||||||
$apiClient = $this->createMock(ApiClient::class);
|
$apiClient = $this->createMock(\App\BusProNet\ApiClient::class);
|
||||||
$bookingUpdate = new BookingUpdate();
|
$bookingUpdate = new BookingUpdate();
|
||||||
$bookingUpdate->success = true;
|
$bookingUpdate->success = true;
|
||||||
$apiClient->expects($this->once())
|
$apiClient->expects($this->once())
|
||||||
@@ -300,15 +301,16 @@ class BookingEditSubmitterTest extends TestCase
|
|||||||
$result = $service->handleSubmission($request, $bookingDto, 42, $user);
|
$result = $service->handleSubmission($request, $bookingDto, 42, $user);
|
||||||
|
|
||||||
$this->assertSame(BookingEditSubmissionResult::STATUS_SUCCESS, $result->status);
|
$this->assertSame(BookingEditSubmissionResult::STATUS_SUCCESS, $result->status);
|
||||||
|
$this->assertNull($result->message);
|
||||||
$this->assertTrue($result->immutableChangesReverted);
|
$this->assertTrue($result->immutableChangesReverted);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testHandleSubmissionReturnsResultOnTimeout(): void
|
public function testHandleSubmissionReturnsRedirectOnTimeout(): void
|
||||||
{
|
{
|
||||||
$this->assertExceptionResult(new TimeoutException('slow'), BookingEditSubmissionResult::STATUS_TIMEOUT);
|
$this->assertExceptionResult(new TimeoutException('slow'), BookingEditSubmissionResult::STATUS_TIMEOUT);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testHandleSubmissionReturnsResultOnApiClientException(): void
|
public function testHandleSubmissionReturnsRedirectOnApiClientException(): void
|
||||||
{
|
{
|
||||||
$this->assertExceptionResult(new ApiClientException('boom'), BookingEditSubmissionResult::STATUS_API_CLIENT_ERROR);
|
$this->assertExceptionResult(new ApiClientException('boom'), BookingEditSubmissionResult::STATUS_API_CLIENT_ERROR);
|
||||||
}
|
}
|
||||||
@@ -321,7 +323,7 @@ class BookingEditSubmitterTest extends TestCase
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function assertNotificationResult(Notification $notification, string $expectedType): void
|
private function assertNotificationResult(Notification $notification): void
|
||||||
{
|
{
|
||||||
$request = $this->createRequestWithSession();
|
$request = $this->createRequestWithSession();
|
||||||
$user = $this->createUser();
|
$user = $this->createUser();
|
||||||
@@ -351,7 +353,7 @@ class BookingEditSubmitterTest extends TestCase
|
|||||||
->with($bookingDto, $freshBookingData)
|
->with($bookingDto, $freshBookingData)
|
||||||
->willReturn(false);
|
->willReturn(false);
|
||||||
|
|
||||||
$apiClient = $this->createMock(ApiClient::class);
|
$apiClient = $this->createMock(\App\BusProNet\ApiClient::class);
|
||||||
$apiClient->expects($this->once())
|
$apiClient->expects($this->once())
|
||||||
->method('updateBooking')
|
->method('updateBooking')
|
||||||
->with($bookingDto, true)
|
->with($bookingDto, true)
|
||||||
@@ -366,7 +368,12 @@ class BookingEditSubmitterTest extends TestCase
|
|||||||
|
|
||||||
$result = $service->handleSubmission($request, $bookingDto, 42, $user);
|
$result = $service->handleSubmission($request, $bookingDto, 42, $user);
|
||||||
|
|
||||||
$this->assertSame($this->resolveExpectedStatus($expectedType), $result->status);
|
$this->assertSame(
|
||||||
|
true === $notification->isError()
|
||||||
|
? BookingEditSubmissionResult::STATUS_NOTIFICATION_ERROR
|
||||||
|
: BookingEditSubmissionResult::STATUS_NOTIFICATION_INFO,
|
||||||
|
$result->status,
|
||||||
|
);
|
||||||
$this->assertSame($notification->message, $result->message);
|
$this->assertSame($notification->message, $result->message);
|
||||||
$this->assertFalse($result->immutableChangesReverted);
|
$this->assertFalse($result->immutableChangesReverted);
|
||||||
}
|
}
|
||||||
@@ -401,7 +408,7 @@ class BookingEditSubmitterTest extends TestCase
|
|||||||
->with($bookingDto, $freshBookingData)
|
->with($bookingDto, $freshBookingData)
|
||||||
->willReturn(false);
|
->willReturn(false);
|
||||||
|
|
||||||
$apiClient = $this->createMock(ApiClient::class);
|
$apiClient = $this->createMock(\App\BusProNet\ApiClient::class);
|
||||||
$apiClient->expects($this->once())
|
$apiClient->expects($this->once())
|
||||||
->method('updateBooking')
|
->method('updateBooking')
|
||||||
->with($bookingDto, true)
|
->with($bookingDto, true)
|
||||||
@@ -422,7 +429,7 @@ class BookingEditSubmitterTest extends TestCase
|
|||||||
}
|
}
|
||||||
|
|
||||||
private function createService(
|
private function createService(
|
||||||
?ApiClient $apiClient = null,
|
?\App\BusProNet\ApiClient $apiClient = null,
|
||||||
?BookingEditDataLoader $dataLoader = null,
|
?BookingEditDataLoader $dataLoader = null,
|
||||||
?BookingEditDraftManager $draftService = null,
|
?BookingEditDraftManager $draftService = null,
|
||||||
?TravelDataProvider $travelDataService = null,
|
?TravelDataProvider $travelDataService = null,
|
||||||
@@ -430,7 +437,7 @@ class BookingEditSubmitterTest extends TestCase
|
|||||||
?BookingSessionManager $bookingSessionService = null,
|
?BookingSessionManager $bookingSessionService = null,
|
||||||
): BookingEditSubmitter {
|
): BookingEditSubmitter {
|
||||||
return new BookingEditSubmitter(
|
return new BookingEditSubmitter(
|
||||||
$apiClient ?? $this->createMock(ApiClient::class),
|
$apiClient ?? $this->createMock(\App\BusProNet\ApiClient::class),
|
||||||
$dataLoader ?? $this->createMock(BookingEditDataLoader::class),
|
$dataLoader ?? $this->createMock(BookingEditDataLoader::class),
|
||||||
$draftService ?? $this->createMock(BookingEditDraftManager::class),
|
$draftService ?? $this->createMock(BookingEditDraftManager::class),
|
||||||
$travelDataService ?? $this->createMock(TravelDataProvider::class),
|
$travelDataService ?? $this->createMock(TravelDataProvider::class),
|
||||||
@@ -469,13 +476,4 @@ class BookingEditSubmitterTest extends TestCase
|
|||||||
return $request;
|
return $request;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function resolveExpectedStatus(string $expectedType): string
|
|
||||||
{
|
|
||||||
return match ($expectedType) {
|
|
||||||
'error' => BookingEditSubmissionResult::STATUS_NOTIFICATION_ERROR,
|
|
||||||
'info' => BookingEditSubmissionResult::STATUS_NOTIFICATION_INFO,
|
|
||||||
default => $expectedType,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ use App\Form\Model\BookingDto;
|
|||||||
use App\Form\Model\ParticipantDto;
|
use App\Form\Model\ParticipantDto;
|
||||||
use App\Service\ParticipantFormSupport;
|
use App\Service\ParticipantFormSupport;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
|
||||||
|
|
||||||
class ParticipantFormSupportTest extends TestCase
|
class ParticipantFormSupportTest extends TestCase
|
||||||
{
|
{
|
||||||
@@ -61,34 +60,40 @@ class ParticipantFormSupportTest extends TestCase
|
|||||||
|
|
||||||
public function testGetParticipantFormOptionsIncludesValidationToggle(): void
|
public function testGetParticipantFormOptionsIncludesValidationToggle(): void
|
||||||
{
|
{
|
||||||
$parameterBag = $this->createMock(ParameterBagInterface::class);
|
$bodyDimensionRanges = [
|
||||||
$parameterBag->expects($this->exactly(4))
|
'height_min' => 145,
|
||||||
->method('get')
|
'height_max' => 210,
|
||||||
->willReturnMap([
|
'weight_min' => 40,
|
||||||
['body_dimensions.height_choices', ['bis 148cm' => '-148']],
|
'weight_max' => 120,
|
||||||
['body_dimensions.weight_choices', ['42 - 48kg' => '42-48']],
|
'shoe_size_min' => 35,
|
||||||
['body_dimensions.shoe_size_min', 36],
|
'shoe_size_max' => 50,
|
||||||
['body_dimensions.shoe_size_max', 48],
|
];
|
||||||
]);
|
|
||||||
|
|
||||||
$service = $this->createService(parameterBag: $parameterBag);
|
$service = $this->createService(bodyDimensionRanges: $bodyDimensionRanges);
|
||||||
$bookingDto = $this->createBookingDto();
|
$bookingDto = $this->createBookingDto();
|
||||||
|
|
||||||
$options = $service->getParticipantFormOptions($bookingDto, true);
|
$options = $service->getParticipantFormOptions($bookingDto, true);
|
||||||
|
|
||||||
$this->assertSame($bookingDto, $options['booking_context']);
|
$this->assertSame($bookingDto, $options['booking_context']);
|
||||||
$this->assertSame(['bis 148cm' => '-148'], $options['height_choices']);
|
$this->assertSame($bodyDimensionRanges, $options['body_dimension_ranges']);
|
||||||
$this->assertSame(['42 - 48kg' => '42-48'], $options['weight_choices']);
|
$this->assertContains('validation_groups', array_keys($options));
|
||||||
$this->assertSame(36, $options['shoe_size_min']);
|
|
||||||
$this->assertSame(48, $options['shoe_size_max']);
|
|
||||||
$this->assertFalse($options['validation_groups']);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function createService(?ParameterBagInterface $parameterBag = null): ParticipantFormSupport
|
/**
|
||||||
|
* array<string, int>|null $bodyDimensionRanges
|
||||||
|
*/
|
||||||
|
private function createService(?array $bodyDimensionRanges = null): ParticipantFormSupport
|
||||||
{
|
{
|
||||||
$parameterBag ??= $this->createMock(ParameterBagInterface::class);
|
$bodyDimensionRanges ??= [
|
||||||
|
'height_min' => 145,
|
||||||
|
'height_max' => 210,
|
||||||
|
'weight_min' => 40,
|
||||||
|
'weight_max' => 120,
|
||||||
|
'shoe_size_min' => 35,
|
||||||
|
'shoe_size_max' => 50,
|
||||||
|
];
|
||||||
|
|
||||||
return new ParticipantFormSupport($parameterBag);
|
return new ParticipantFormSupport($bodyDimensionRanges);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function createBookingDto(): BookingDto
|
private function createBookingDto(): BookingDto
|
||||||
|
|||||||
Reference in New Issue
Block a user