diff --git a/src/BusProNet/Model/AgeConstraintResult.php b/src/BusProNet/Model/AgeConstraintResult.php new file mode 100644 index 0000000..0a308fb --- /dev/null +++ b/src/BusProNet/Model/AgeConstraintResult.php @@ -0,0 +1,40 @@ +ageFrom || null !== $this->ageTo; + } + + public function hasBirthYearConstraints(): bool + { + return null !== $this->birthYearFrom || null !== $this->birthYearTo; + } + + public function isEmpty(): bool + { + return false === $this->hasAgeConstraints() && false === $this->hasBirthYearConstraints(); + } +} \ No newline at end of file diff --git a/src/BusProNet/Model/Service.php b/src/BusProNet/Model/Service.php index 045a6c2..077c661 100644 --- a/src/BusProNet/Model/Service.php +++ b/src/BusProNet/Model/Service.php @@ -72,4 +72,25 @@ class Service #[Groups(['api:single', 'api:list'])] public ?string $direction = null; + + #[Groups(['api:single', 'api:list'])] + public ?int $ageFrom = null; + + #[Groups(['api:single', 'api:list'])] + public ?int $ageTo = null; + + #[Groups(['api:single', 'api:list'])] + public ?int $birthYearFrom = null; + + #[Groups(['api:single', 'api:list'])] + public ?int $birthYearTo = null; + + #[Groups(['api:single', 'api:list'])] + public ?string $ageConstraintType = null; + + #[Groups(['api:single'])] + public ?array $ageConstraintMetadata = null; + + #[Groups(['api:single'])] + public ?string $rawAgeConstraintData = null; } diff --git a/src/BusProNet/XmlParser/AgeConstraintParserInterface.php b/src/BusProNet/XmlParser/AgeConstraintParserInterface.php new file mode 100644 index 0000000..93764ef --- /dev/null +++ b/src/BusProNet/XmlParser/AgeConstraintParserInterface.php @@ -0,0 +1,45 @@ +addParser(new BirthYearConstraintParser()); + } + + public function addParser(AgeConstraintParserInterface $parser): void + { + $this->parsers[] = $parser; + } + + public function parseConstraints(string $constraintData): AgeConstraintResult + { + // Handle multiple constraint types (semicolon-separated, e.g., 'JG:2007-2009;GL:5-8') + $constraints = array_map('trim', explode(';', $constraintData)); + $results = []; + + foreach ($constraints as $constraint) { + if (empty($constraint)) { + continue; + } + + foreach ($this->parsers as $parser) { + if (true === $parser->canParse($constraint)) { + $results[] = $parser->parse($constraint); + break; // First matching parser wins + } + } + } + + // Merge results if multiple constraints found + return $this->mergeConstraintResults($results, $constraintData); + } + + private function mergeConstraintResults(array $results, string $rawData): AgeConstraintResult + { + if (empty($results)) { + return new AgeConstraintResult(type: 'unknown', rawData: $rawData); + } + + if (1 === count($results)) { + return $results[0]; + } + + // Merge multiple constraint results + $type = 'mixed'; + $ageFrom = null; + $ageTo = null; + $birthYearFrom = null; + $birthYearTo = null; + $metadata = ['merged_from' => []]; + + foreach ($results as $result) { + $ageFrom = $this->mergeMinValue($ageFrom, $result->ageFrom); + $ageTo = $this->mergeMaxValue($ageTo, $result->ageTo); + $birthYearFrom = $this->mergeMinValue($birthYearFrom, $result->birthYearFrom); + $birthYearTo = $this->mergeMaxValue($birthYearTo, $result->birthYearTo); + $metadata['merged_from'][] = $result->type; + } + + return new AgeConstraintResult( + type: $type, + ageFrom: $ageFrom, + ageTo: $ageTo, + birthYearFrom: $birthYearFrom, + birthYearTo: $birthYearTo, + metadata: $metadata, + rawData: $rawData + ); + } + + private function mergeMinValue(?int $current, ?int $new): ?int + { + if (null === $current) { + return $new; + } + if (null === $new) { + return $current; + } + + return max($current, $new); // Most restrictive minimum + } + + private function mergeMaxValue(?int $current, ?int $new): ?int + { + if (null === $current) { + return $new; + } + if (null === $new) { + return $current; + } + + return min($current, $new); // Most restrictive maximum + } +} \ No newline at end of file diff --git a/src/BusProNet/XmlParser/BirthYearConstraintParser.php b/src/BusProNet/XmlParser/BirthYearConstraintParser.php new file mode 100644 index 0000000..dfd8e9f --- /dev/null +++ b/src/BusProNet/XmlParser/BirthYearConstraintParser.php @@ -0,0 +1,67 @@ +canParse($constraintData)) { + throw new \InvalidArgumentException('Cannot parse constraint data: ' . $constraintData); + } + + $yearData = substr($constraintData, strlen(self::BIRTH_YEAR_PREFIX)); + + // Parse range format "2007-2009" + if (str_contains($yearData, '-')) { + [$fromYear, $toYear] = explode('-', $yearData, 2); + + return new AgeConstraintResult( + type: 'birth_year', + birthYearFrom: (int) trim($fromYear), + birthYearTo: (int) trim($toYear), + metadata: [ + 'range_type' => 'birth_year_range', + 'original_format' => $yearData, + ], + rawData: $constraintData + ); + } + + // Parse single year format "2007" + $year = (int) trim($yearData); + + return new AgeConstraintResult( + type: 'birth_year', + birthYearFrom: $year, + birthYearTo: $year, + metadata: [ + 'range_type' => 'birth_year_single', + 'original_format' => $yearData, + ], + rawData: $constraintData + ); + } + + public function getConstraintType(): string + { + return 'birth_year'; + } +} \ No newline at end of file diff --git a/src/BusProNet/XmlParser/TravelParser.php b/src/BusProNet/XmlParser/TravelParser.php index 3dc6ddc..9794e5c 100644 --- a/src/BusProNet/XmlParser/TravelParser.php +++ b/src/BusProNet/XmlParser/TravelParser.php @@ -22,6 +22,12 @@ use Symfony\Component\DomCrawler\Crawler; */ class TravelParser extends AbstractParser { + private AgeConstraintParserRegistry $ageConstraintRegistry; + + public function __construct() + { + $this->ageConstraintRegistry = new AgeConstraintParserRegistry(); + } /** * Parse XML node into a Travel object. * @@ -129,6 +135,9 @@ class TravelParser extends AbstractParser ->stringToFloat($this->getStringOrNullValue($serviceNode->filterXPath('//preis'))); $service->status = $this->getStringOrNullValue($serviceNode->filterXPath('//status')); + // Parse age constraints + $this->parseServiceAgeConstraints($serviceNode, $service); + $additionalServices[$serviceId] = $service; }); @@ -302,4 +311,59 @@ class TravelParser extends AbstractParser return $rooms; } + + /** + * Parse age constraints from service XML node and apply them to the service. + * + * Handles both absolute age constraints (altervon/alterbis) and extensible + * constraint data (hinweis_stamm) that may contain birth year ranges or + * future constraint types. + * + * @param Crawler $serviceNode The XML node containing service data + * @param Service $service The service object to populate with constraints + */ + private function parseServiceAgeConstraints(Crawler $serviceNode, Service $service): void + { + // Parse absolute age constraints (altervon/alterbis) + $ageFrom = $this->getIntOrNullValue($serviceNode->filterXPath('.//altervon')); + $ageTo = $this->getIntOrNullValue($serviceNode->filterXPath('.//alterbis')); + + // Parse extensible constraint data (hinweis_stamm) + $constraintData = $this->getStringOrNullValue($serviceNode->filterXPath('.//hinweis_stamm')); + + $constraintResult = null; + if (null !== $constraintData && '' !== trim($constraintData)) { + $constraintResult = $this->ageConstraintRegistry->parseConstraints($constraintData); + } + + // Apply absolute age constraints + if (null !== $ageFrom || null !== $ageTo) { + $service->ageFrom = $ageFrom; + $service->ageTo = $ageTo; + + if (null !== $constraintResult && false === $constraintResult->isEmpty()) { + // Mixed constraints scenario + $service->ageConstraintType = 'mixed'; + $service->birthYearFrom = $constraintResult->birthYearFrom; + $service->birthYearTo = $constraintResult->birthYearTo; + $service->ageConstraintMetadata = array_merge( + $constraintResult->metadata, + ['has_absolute_age' => true, 'has_birth_year' => true] + ); + } else { + $service->ageConstraintType = 'absolute_age'; + } + } elseif (null !== $constraintResult && false === $constraintResult->isEmpty()) { + // Only constraint data (birth year, etc.) + $service->ageConstraintType = $constraintResult->type; + $service->birthYearFrom = $constraintResult->birthYearFrom; + $service->birthYearTo = $constraintResult->birthYearTo; + $service->ageConstraintMetadata = $constraintResult->metadata; + } + + // Always store raw data for debugging/future parsing + if (null !== $constraintData) { + $service->rawAgeConstraintData = $constraintData; + } + } } diff --git a/src/Form/BookingCreateParticipantType.php b/src/Form/BookingCreateParticipantType.php index 86b26e2..81a656c 100644 --- a/src/Form/BookingCreateParticipantType.php +++ b/src/Form/BookingCreateParticipantType.php @@ -105,7 +105,7 @@ class BookingCreateParticipantType extends AbstractType $submittedData = $event->getData(); $form = $event->getForm(); - if (!is_array($submittedData)) { + if (false === is_array($submittedData)) { return; } @@ -118,7 +118,7 @@ class BookingCreateParticipantType extends AbstractType // Get participant index from form data $participantData = $form->getData(); - if (null === $participantData || !property_exists($participantData, 'index')) { + if (null === $participantData || false === property_exists($participantData, 'index')) { return; } @@ -153,7 +153,7 @@ class BookingCreateParticipantType extends AbstractType continue; } - if ($form->has($fieldName)) { + if (true === $form->has($fieldName)) { $field = $form->get($fieldName); $currentOptions = $field->getConfig()->getOptions(); @@ -168,7 +168,7 @@ class BookingCreateParticipantType extends AbstractType } // Apply body dimension field states to the nested bodyDimensions form - if (!empty($bodyDimensionStates) && $form->has('bodyDimensions')) { + if (false === empty($bodyDimensionStates) && true === $form->has('bodyDimensions')) { $this->applyBodyDimensionStates($form, $bodyDimensionStates); } } @@ -212,17 +212,51 @@ class BookingCreateParticipantType extends AbstractType // Get base field options $fieldOptions = $this->fieldOptionsProvider->getFieldOptions($fieldName, $bookingDto, $participantIndex); - // Apply dynamic field state if conditions exist - if ($this->fieldStateProvider->hasStateConditions($fieldName)) { - $fieldState = $this->fieldStateProvider->getFieldState($fieldName, $bookingDto, $participantIndex); - $fieldOptions = $this->mergeFieldState($fieldOptions, $fieldState); - } + // Only add the field if there are actually choices/options available + if (true === $this->hasValidFieldOptions($fieldOptions)) { + // Apply dynamic field state if conditions exist + if (true === $this->fieldStateProvider->hasStateConditions($fieldName)) { + $fieldState = $this->fieldStateProvider->getFieldState($fieldName, $bookingDto, $participantIndex); + $fieldOptions = $this->mergeFieldState($fieldOptions, $fieldState); + } - $form->add($fieldName, ChoiceType::class, $fieldOptions); + $form->add($fieldName, ChoiceType::class, $fieldOptions); + } } } } + /** + * Checks if field options contain valid choices for rendering. + * + * This method validates that the field options contain either choices array, + * choice_loader, or other valid choice sources. Empty choice arrays or + * null choice loaders indicate the field should not be rendered. + * + * @param array $fieldOptions The field options to validate + * + * @return bool True if the field has valid options for rendering, false otherwise + */ + private function hasValidFieldOptions(array $fieldOptions): bool + { + // Check if choices array exists and is not empty + if (false === empty($fieldOptions['choices'])) { + return true; + } + + // Check if choice_loader exists and is not null + if (isset($fieldOptions['choice_loader'])) { + return true; + } + + // Check for other valid choice sources + if (isset($fieldOptions['choice_list'])) { + return true; + } + + return false; + } + /** * Merges field state modifications into existing field options. * diff --git a/src/Form/Service/Condition/DateOfBirthProvidedCondition.php b/src/Form/Service/Condition/DateOfBirthProvidedCondition.php new file mode 100644 index 0000000..7c747a6 --- /dev/null +++ b/src/Form/Service/Condition/DateOfBirthProvidedCondition.php @@ -0,0 +1,69 @@ + $formData Current form data (unused for this condition) + * + * @return bool True if the participant has provided their date of birth, false otherwise + */ + public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool + { + $participant = $bookingDto->getParticipant($participantIndex); + + return null !== $participant && null !== $participant->dateOfBirth; + } + + /** + * Returns field names that this condition depends on. + * + * The date of birth condition depends on the participant's dateOfBirth field. + * When this field changes, any conditions based on birth date availability + * should be re-evaluated. + * + * @return string[] Array containing 'dateOfBirth' field name + */ + public function getDependentFields(): array + { + return ['dateOfBirth']; + } + + /** + * Returns a human-readable description of this condition. + * + * Provides a clear description of what this condition checks for, + * useful for debugging and understanding field state logic. + * + * @return string Description of the date of birth requirement + */ + public function getDescription(): string + { + return 'Participant must provide their date of birth'; + } +} \ No newline at end of file diff --git a/src/Form/Service/CreateFieldStateProvider.php b/src/Form/Service/CreateFieldStateProvider.php index 42efd5f..035abd1 100644 --- a/src/Form/Service/CreateFieldStateProvider.php +++ b/src/Form/Service/CreateFieldStateProvider.php @@ -5,6 +5,8 @@ declare(strict_types=1); namespace App\Form\Service; use App\Form\Service\Abstract\AbstractFieldStateProvider; +use App\Form\Service\Condition\CompositeCondition; +use App\Form\Service\Condition\DateOfBirthProvidedCondition; use App\Form\Service\Condition\RentalSelectionCondition; /** @@ -14,8 +16,9 @@ use App\Form\Service\Condition\RentalSelectionCondition; * for participant fields in the create flow. It extends the common field * state functionality provided by AbstractFieldStateProvider. * - * Currently, no specific field state conditions are implemented for the - * create workflow, but the infrastructure is ready for future additions. + * Current field state conditions: + * - Body dimension fields become required when rental services are selected + * - Age-dependent service fields are hidden until birth date is provided */ class CreateFieldStateProvider extends AbstractFieldStateProvider { @@ -60,6 +63,26 @@ class CreateFieldStateProvider extends AbstractFieldStateProvider 'required' => $rentalCondition, ]; + // Hide age-dependent fields when no date of birth is provided + $dateOfBirthProvidedCondition = new DateOfBirthProvidedCondition(); + + // Age-dependent service fields are hidden until birth date is provided + $this->fieldStateConditions['courses'] = [ + 'hidden' => CompositeCondition::not($dateOfBirthProvidedCondition), + ]; + + $this->fieldStateConditions['additionalServices'] = [ + 'hidden' => CompositeCondition::not($dateOfBirthProvidedCondition), + ]; + + $this->fieldStateConditions['rentals'] = [ + 'hidden' => CompositeCondition::not($dateOfBirthProvidedCondition), + ]; + + $this->fieldStateConditions['board'] = [ + 'hidden' => CompositeCondition::not($dateOfBirthProvidedCondition), + ]; + // Example field state conditions would be registered here // For demonstration purposes, here are some example patterns: diff --git a/src/Form/Service/ParticipantFieldOptionsProvider.php b/src/Form/Service/ParticipantFieldOptionsProvider.php index df9febd..e6c7334 100644 --- a/src/Form/Service/ParticipantFieldOptionsProvider.php +++ b/src/Form/Service/ParticipantFieldOptionsProvider.php @@ -10,6 +10,7 @@ use App\Form\Model\BookingCreateDto; use App\Form\Model\BookingDtoInterface; use App\Form\Service\Abstract\AbstractFieldOptionsProvider; use App\Form\Service\Factory\ParticipantRoomChoiceLoaderFactory; +use App\Form\Service\ServiceAgeEvaluator; /** * Provides dynamic field options for participant form fields. @@ -40,9 +41,8 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider * * @param ParticipantRoomChoiceLoaderFactory $roomChoiceLoaderFactory Factory for creating room choice loaders */ - public function __construct( - private readonly ParticipantRoomChoiceLoaderFactory $roomChoiceLoaderFactory, - ) { + public function __construct(private readonly ParticipantRoomChoiceLoaderFactory $roomChoiceLoaderFactory) + { parent::__construct(); } @@ -86,23 +86,31 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider : null, ]; - // Courses field provider - provides available courses from travel data - $this->fieldOptionProviders['courses'] = fn (BookingDtoInterface $bookingDto) => [ + // Courses field provider - provides age-appropriate courses from travel data + $this->fieldOptionProviders['courses'] = fn (BookingDtoInterface $bookingDto, int $participantIndex) => [ 'label' => 'Kurse', 'multiple' => true, 'expanded' => true, 'required' => false, - 'choices' => $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_COURSES), + 'choices' => $this->filterServicesByAgeConstraints( + $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_COURSES), + $bookingDto, + $participantIndex + ), 'choice_label' => 'label', ]; - // Additional services field provider - provides additional services with mandatory pre-selection - $this->fieldOptionProviders['additionalServices'] = fn (BookingDtoInterface $bookingDto) => [ + // Additional services field provider - provides age-appropriate additional services with mandatory pre-selection + $this->fieldOptionProviders['additionalServices'] = fn (BookingDtoInterface $bookingDto, int $participantIndex) => [ 'label' => 'Zusatzleistungen', 'multiple' => true, 'expanded' => true, 'required' => false, - 'choices' => $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_ADDITIONAL), + 'choices' => $this->filterServicesByAgeConstraints( + $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_ADDITIONAL), + $bookingDto, + $participantIndex + ), 'choice_value' => 'id', 'choice_label' => 'label', 'choice_attr' => function (?Service $service) { @@ -124,23 +132,31 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider }, ]; - // Board field provider - provides available board options from travel data - $this->fieldOptionProviders['board'] = fn (BookingDtoInterface $bookingDto) => [ + // Board field provider - provides age-appropriate board options from travel data + $this->fieldOptionProviders['board'] = fn (BookingDtoInterface $bookingDto, int $participantIndex) => [ 'label' => 'Verpflegung', 'multiple' => true, 'expanded' => true, 'required' => false, - 'choices' => $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_BOARD), + 'choices' => $this->filterServicesByAgeConstraints( + $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_BOARD), + $bookingDto, + $participantIndex + ), 'choice_label' => 'label', ]; - // Rentals field provider - provides available rental options from travel data filtered by date range - $this->fieldOptionProviders['rentals'] = fn (BookingDtoInterface $bookingDto) => [ + // Rentals field provider - provides age-appropriate rental options from travel data filtered by date range + $this->fieldOptionProviders['rentals'] = fn (BookingDtoInterface $bookingDto, int $participantIndex) => [ 'label' => 'Leihmaterial', 'multiple' => true, 'expanded' => true, 'required' => false, - 'choices' => $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_RENTALS, true, true), + 'choices' => $this->filterServicesByAgeConstraints( + $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_RENTALS, true, true), + $bookingDto, + $participantIndex + ), 'choice_label' => 'label', ]; @@ -155,4 +171,38 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider // ], // ]; } + + /** + * Filters services based on participant's age constraints. + * + * Removes services that have age restrictions the participant doesn't meet. + * If no age evaluator is configured or participant has no birth date, + * returns empty array to be handled by field visibility conditions. + * + * @param array $services Services to filter + * @param BookingDtoInterface $bookingDto Booking data containing participant info + * @param int $participantIndex Index of participant to evaluate + * + * @return array Filtered services array + */ + private function filterServicesByAgeConstraints(array $services, BookingDtoInterface $bookingDto, int $participantIndex): array + { + $ageEvaluator = new ServiceAgeEvaluator(); + + $participant = $bookingDto->getParticipant($participantIndex); + + // If no birth date provided, return empty array (handled by field visibility conditions) + if (null === $participant || null === $participant->dateOfBirth) { + return []; + } + + return array_filter($services, function (Service $service) use ($bookingDto, $participantIndex, $ageEvaluator) { + // No age constraints = available to all + if (false === $ageEvaluator->canEvaluate($service)) { + return true; + } + + return $ageEvaluator->isServiceAvailableForParticipant($service, $bookingDto, $participantIndex); + }); + } } diff --git a/src/Form/Service/ServiceAgeEvaluator.php b/src/Form/Service/ServiceAgeEvaluator.php new file mode 100644 index 0000000..d6d6987 --- /dev/null +++ b/src/Form/Service/ServiceAgeEvaluator.php @@ -0,0 +1,196 @@ +ageConstraintType; + } + + /** + * Evaluates if a service is available for a specific participant. + * + * Checks the participant's age or birth year against the service's + * constraint requirements. Returns false if the participant doesn't + * meet the age requirements or if no birth date is provided. + * + * @param Service $service The service to evaluate + * @param BookingDtoInterface $bookingDto The booking containing participant data + * @param int $participantIndex The index of the participant to evaluate + * + * @return bool True if the service is available for the participant + */ + public function isServiceAvailableForParticipant(Service $service, BookingDtoInterface $bookingDto, int $participantIndex): bool + { + $participant = $bookingDto->getParticipant($participantIndex); + + if (null === $participant || null === $participant->dateOfBirth) { + return false; // Cannot evaluate without birth date + } + + return match($service->ageConstraintType) { + 'absolute_age' => $this->evaluateAbsoluteAge($service, $participant->dateOfBirth), + 'birth_year' => $this->evaluateBirthYear($service, $participant->dateOfBirth), + 'mixed' => $this->evaluateAbsoluteAge($service, $participant->dateOfBirth) + && $this->evaluateBirthYear($service, $participant->dateOfBirth), + default => true // No constraints or unknown type + }; + } + + /** + * Evaluates absolute age constraints against participant's current age. + * + * @param Service $service The service with age constraints + * @param \DateTimeImmutable $dateOfBirth The participant's date of birth + * + * @return bool True if the participant meets the absolute age requirements + */ + private function evaluateAbsoluteAge(Service $service, \DateTimeImmutable $dateOfBirth): bool + { + $age = $this->calculateAge($dateOfBirth); + + if (null !== $service->ageFrom && $age < $service->ageFrom) { + return false; + } + + if (null !== $service->ageTo && $age > $service->ageTo) { + return false; + } + + return true; + } + + /** + * Evaluates birth year constraints against participant's birth year. + * + * @param Service $service The service with birth year constraints + * @param \DateTimeImmutable $dateOfBirth The participant's date of birth + * + * @return bool True if the participant meets the birth year requirements + */ + private function evaluateBirthYear(Service $service, \DateTimeImmutable $dateOfBirth): bool + { + $birthYear = (int) $dateOfBirth->format('Y'); + + if (null !== $service->birthYearFrom && $birthYear < $service->birthYearFrom) { + return false; + } + + if (null !== $service->birthYearTo && $birthYear > $service->birthYearTo) { + return false; + } + + return true; + } + + /** + * Calculates age in years from a date of birth. + * + * Uses DateTimeImmutable to ensure accurate age calculations accounting + * for leap years and exact birth date anniversaries. + * + * @param \DateTimeImmutable $dateOfBirth The participant's date of birth + * + * @return int The calculated age in complete years + */ + private function calculateAge(\DateTimeImmutable $dateOfBirth): int + { + $today = new \DateTimeImmutable(); + + return (int) $dateOfBirth->diff($today)->y; + } + + /** + * Gets a human-readable description of the service's age constraints. + * + * Generates descriptive text explaining the age requirements, + * useful for user interfaces and debugging. + * + * @param Service $service The service to describe + * + * @return string Description of the age constraints + */ + public function getConstraintDescription(Service $service): string + { + return match($service->ageConstraintType) { + 'absolute_age' => $this->getAbsoluteAgeDescription($service), + 'birth_year' => $this->getBirthYearDescription($service), + 'mixed' => sprintf('%s and %s', + $this->getAbsoluteAgeDescription($service), + $this->getBirthYearDescription($service)), + default => 'No age restrictions' + }; + } + + /** + * Gets description for absolute age constraints. + * + * @param Service $service The service with age constraints + * + * @return string Description of absolute age requirements + */ + private function getAbsoluteAgeDescription(Service $service): string + { + if (null !== $service->ageFrom && null !== $service->ageTo) { + return sprintf('Ages %d-%d', $service->ageFrom, $service->ageTo); + } + + if (null !== $service->ageFrom) { + return sprintf('Age %d+', $service->ageFrom); + } + + if (null !== $service->ageTo) { + return sprintf('Age up to %d', $service->ageTo); + } + + return ''; + } + + /** + * Gets description for birth year constraints. + * + * @param Service $service The service with birth year constraints + * + * @return string Description of birth year requirements + */ + private function getBirthYearDescription(Service $service): string + { + if (null !== $service->birthYearFrom && null !== $service->birthYearTo) { + if ($service->birthYearFrom === $service->birthYearTo) { + return sprintf('Born in %d', $service->birthYearFrom); + } + return sprintf('Born %d-%d', $service->birthYearFrom, $service->birthYearTo); + } + + if (null !== $service->birthYearFrom) { + return sprintf('Born %d or later', $service->birthYearFrom); + } + + if (null !== $service->birthYearTo) { + return sprintf('Born up to %d', $service->birthYearTo); + } + + return ''; + } +} \ No newline at end of file