wip: update docs
This commit is contained in:
@@ -0,0 +1,641 @@
|
||||
# Age Constraints Model Extension Plan
|
||||
|
||||
## Current Situation Analysis
|
||||
|
||||
**XML Data Contains Two Age Constraint Formats:**
|
||||
1. **Absolute Age**: `<altervon>6</altervon><alterbis>14</alterbis>` (current age 6-14)
|
||||
2. **Birth Year Ranges**: `<hinweis_stamm>JG:2007-2009</hinweis_stamm>` (birth years 2007-2009)
|
||||
|
||||
**Future Considerations:**
|
||||
- `hinweis_stamm` may contain additional constraint types beyond `JG:` (birth year)
|
||||
- Need extensible parsing system for future constraint formats
|
||||
- Maintain English naming conventions throughout
|
||||
|
||||
**Current State:**
|
||||
- `Service` model has `ageFrom`/`ageTo` properties but they're not populated
|
||||
- `TravelParser` doesn't parse age-related XML nodes
|
||||
- Form processing doesn't consider age constraints
|
||||
|
||||
## Required Changes
|
||||
|
||||
### 1. Extend Service Model with Extensible Age Constraints
|
||||
|
||||
**Add New Properties for Flexible Age Constraints:**
|
||||
```php
|
||||
// Add to Service class (src/BusProNet/Model/Service.php)
|
||||
|
||||
#[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; // 'absolute_age', 'birth_year', 'mixed'
|
||||
|
||||
#[Groups(['api:single'])]
|
||||
public ?array $ageConstraintMetadata = null; // Extensible metadata for future constraint types
|
||||
|
||||
#[Groups(['api:single'])]
|
||||
public ?string $rawAgeConstraintData = null; // Store original XML data for debugging/future parsing
|
||||
```
|
||||
|
||||
### 2. Create Extensible Age Constraint Parser System
|
||||
|
||||
**A. Create Age Constraint Parser Interface:**
|
||||
```php
|
||||
// src/BusProNet/XmlParser/Contract/AgeConstraintParserInterface.php
|
||||
interface AgeConstraintParserInterface
|
||||
{
|
||||
public function canParse(string $constraintData): bool;
|
||||
public function parse(string $constraintData): AgeConstraintResult;
|
||||
public function getConstraintType(): string;
|
||||
}
|
||||
```
|
||||
|
||||
**B. Create Age Constraint Result DTO:**
|
||||
```php
|
||||
// src/BusProNet/XmlParser/Model/AgeConstraintResult.php
|
||||
class AgeConstraintResult
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $type,
|
||||
public readonly ?int $ageFrom = null,
|
||||
public readonly ?int $ageTo = null,
|
||||
public readonly ?int $birthYearFrom = null,
|
||||
public readonly ?int $birthYearTo = null,
|
||||
public readonly array $metadata = [],
|
||||
public readonly ?string $rawData = null
|
||||
) {}
|
||||
|
||||
public function hasAgeConstraints(): bool
|
||||
{
|
||||
return null !== $this->ageFrom || null !== $this->ageTo;
|
||||
}
|
||||
|
||||
public function hasBirthYearConstraints(): bool
|
||||
{
|
||||
return null !== $this->birthYearFrom || null !== $this->birthYearTo;
|
||||
}
|
||||
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return !$this->hasAgeConstraints() && !$this->hasBirthYearConstraints();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**C. Create Birth Year Constraint Parser:**
|
||||
```php
|
||||
// src/BusProNet/XmlParser/AgeConstraint/BirthYearConstraintParser.php
|
||||
class BirthYearConstraintParser implements AgeConstraintParserInterface
|
||||
{
|
||||
private const BIRTH_YEAR_PREFIX = 'JG:';
|
||||
|
||||
public function canParse(string $constraintData): bool
|
||||
{
|
||||
return str_starts_with($constraintData, self::BIRTH_YEAR_PREFIX);
|
||||
}
|
||||
|
||||
public function parse(string $constraintData): AgeConstraintResult
|
||||
{
|
||||
if (!$this->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';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**D. Create Age Constraint Parser Registry:**
|
||||
```php
|
||||
// src/BusProNet/XmlParser/AgeConstraint/AgeConstraintParserRegistry.php
|
||||
class AgeConstraintParserRegistry
|
||||
{
|
||||
/** @var AgeConstraintParserInterface[] */
|
||||
private array $parsers = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// Register built-in parsers
|
||||
$this->addParser(new BirthYearConstraintParser());
|
||||
}
|
||||
|
||||
public function addParser(AgeConstraintParserInterface $parser): void
|
||||
{
|
||||
$this->parsers[] = $parser;
|
||||
}
|
||||
|
||||
public function parseConstraints(string $constraintData): AgeConstraintResult
|
||||
{
|
||||
// Try 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 ($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 (count($results) === 1) {
|
||||
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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Enhance TravelParser with Extensible Constraint Parsing
|
||||
|
||||
**Add Age Constraint Parsing to Service Methods:**
|
||||
```php
|
||||
// Add to TravelParser class
|
||||
private AgeConstraintParserRegistry $ageConstraintRegistry;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->ageConstraintRegistry = new AgeConstraintParserRegistry();
|
||||
// Future: inject via DI for custom parsers
|
||||
}
|
||||
|
||||
// Update getAdditionalServices() method:
|
||||
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 -> ageConstraintData)
|
||||
$constraintData = $this->getStringOrNullValue($serviceNode->filterXPath('.//hinweis_stamm'));
|
||||
|
||||
$constraintResult = null;
|
||||
if (null !== $constraintData && !empty(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 && !$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 && !$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;
|
||||
}
|
||||
}
|
||||
|
||||
// Update getAdditionalServices() method:
|
||||
public function getAdditionalServices(Crawler $node): array
|
||||
{
|
||||
$additionalServices = [];
|
||||
|
||||
$node->each(function (Crawler $serviceNode) use (&$additionalServices) {
|
||||
$serviceId = (int) $serviceNode->attr('idbuspro');
|
||||
|
||||
$service = new Service();
|
||||
$service->source = Constants::SOURCE_TRAVEL;
|
||||
$service->category = Constants::CATEGORY_ADDITIONAL;
|
||||
$service->id = $serviceId;
|
||||
$service->subType = $serviceNode->attr('unterart');
|
||||
$service->mandatory = $this->stringToBool($serviceNode->attr('pflicht'));
|
||||
$service->dateFrom = $this->stringToDate($serviceNode->attr('termin'));
|
||||
$service->dateTo = $this->stringToDate($serviceNode->attr('bis'));
|
||||
$service->label = $this->getStringOrNullValue($serviceNode->filterXPath('.//text'));
|
||||
$service->price = $this->stringToFloat($this->getStringOrNullValue($serviceNode->filterXPath('.//preis')));
|
||||
$service->status = $this->getStringOrNullValue($serviceNode->filterXPath('.//status'));
|
||||
|
||||
// Parse age constraints
|
||||
$this->parseServiceAgeConstraints($serviceNode, $service);
|
||||
|
||||
$additionalServices[$serviceId] = $service;
|
||||
});
|
||||
|
||||
return $additionalServices;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Create Extensible Age Evaluation System
|
||||
|
||||
**A. Enhanced Age Evaluation Interface:**
|
||||
```php
|
||||
// src/Form/Service/Contract/AgeEvaluatorInterface.php
|
||||
interface AgeEvaluatorInterface
|
||||
{
|
||||
public function canEvaluate(Service $service): bool;
|
||||
public function isServiceAvailableForParticipant(Service $service, BookingDtoInterface $bookingDto, int $participantIndex): bool;
|
||||
public function getConstraintDescription(Service $service): string;
|
||||
}
|
||||
```
|
||||
|
||||
**B. Create Service Age Evaluator:**
|
||||
```php
|
||||
// src/Form/Service/AgeEvaluator/ServiceAgeEvaluator.php
|
||||
class ServiceAgeEvaluator implements AgeEvaluatorInterface
|
||||
{
|
||||
public function canEvaluate(Service $service): bool
|
||||
{
|
||||
return null !== $service->ageConstraintType;
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private function calculateAge(\DateTimeImmutable $dateOfBirth): int
|
||||
{
|
||||
$today = new \DateTimeImmutable();
|
||||
return (int) $dateOfBirth->diff($today)->y;
|
||||
}
|
||||
|
||||
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'
|
||||
};
|
||||
}
|
||||
|
||||
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 '';
|
||||
}
|
||||
|
||||
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 '';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Enhanced Form Field Options Provider
|
||||
|
||||
**Update with Age-Aware Service Filtering:**
|
||||
```php
|
||||
// Add to ParticipantFieldOptionsProvider (simplified approach)
|
||||
// ServiceAgeEvaluator is instantiated directly when needed
|
||||
|
||||
protected function registerFieldOptionProviders(): void
|
||||
{
|
||||
// Enhanced field providers with age-aware filtering
|
||||
$this->fieldOptionProviders['courses'] = fn (BookingDtoInterface $bookingDto, int $participantIndex) => [
|
||||
'label' => 'Kurse',
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
'required' => false,
|
||||
'choices' => $this->filterServicesByAgeConstraints(
|
||||
$bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_COURSES),
|
||||
$bookingDto,
|
||||
$participantIndex
|
||||
),
|
||||
'choice_label' => 'label',
|
||||
];
|
||||
|
||||
// Similar updates for additionalServices, rentals, board, etc.
|
||||
}
|
||||
|
||||
private function filterServicesByAgeConstraints(array $services, BookingDtoInterface $bookingDto, int $participantIndex): array
|
||||
{
|
||||
$participant = $bookingDto->getParticipant($participantIndex);
|
||||
|
||||
// If no birth date provided, return empty array (handled by DateOfBirthProvidedCondition)
|
||||
if (null === $participant || null === $participant->dateOfBirth) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_filter($services, function (Service $service) use ($bookingDto, $participantIndex) {
|
||||
// No age constraints = available to all
|
||||
$ageEvaluator = new ServiceAgeEvaluator();
|
||||
if (!$ageEvaluator->canEvaluate($service)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $ageEvaluator->isServiceAvailableForParticipant($service, $bookingDto, $participantIndex);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Future Extension Examples
|
||||
|
||||
**A. Example: Adding Grade Level Constraints (GL:5-8)**
|
||||
```php
|
||||
class GradeLevelConstraintParser implements AgeConstraintParserInterface
|
||||
{
|
||||
private const GRADE_PREFIX = 'GL:';
|
||||
|
||||
public function canParse(string $constraintData): bool
|
||||
{
|
||||
return str_starts_with($constraintData, self::GRADE_PREFIX);
|
||||
}
|
||||
|
||||
public function parse(string $constraintData): AgeConstraintResult
|
||||
{
|
||||
$gradeData = substr($constraintData, strlen(self::GRADE_PREFIX));
|
||||
|
||||
if (str_contains($gradeData, '-')) {
|
||||
[$fromGrade, $toGrade] = explode('-', $gradeData, 2);
|
||||
|
||||
return new AgeConstraintResult(
|
||||
type: 'grade_level',
|
||||
metadata: [
|
||||
'grade_from' => (int) trim($fromGrade),
|
||||
'grade_to' => (int) trim($toGrade),
|
||||
'constraint_type' => 'grade_range'
|
||||
],
|
||||
rawData: $constraintData
|
||||
);
|
||||
}
|
||||
|
||||
// Single grade
|
||||
return new AgeConstraintResult(
|
||||
type: 'grade_level',
|
||||
metadata: [
|
||||
'grade' => (int) trim($gradeData),
|
||||
'constraint_type' => 'grade_single'
|
||||
],
|
||||
rawData: $constraintData
|
||||
);
|
||||
}
|
||||
|
||||
public function getConstraintType(): string
|
||||
{
|
||||
return 'grade_level';
|
||||
}
|
||||
}
|
||||
|
||||
// Register in registry constructor:
|
||||
$this->addParser(new GradeLevelConstraintParser());
|
||||
```
|
||||
|
||||
**B. Example: Complex Mixed Constraints (JG:2007-2009;GL:5-8)**
|
||||
- Registry automatically handles semicolon-separated constraints
|
||||
- Merges results into mixed constraint type
|
||||
- Evaluator can handle multiple constraint types
|
||||
|
||||
### 7. Add Helper Methods
|
||||
|
||||
**Add to AbstractParser:**
|
||||
```php
|
||||
protected function getIntOrNullValue(Crawler $node): ?int
|
||||
{
|
||||
$value = $this->getStringOrNullValue($node);
|
||||
|
||||
if (null === $value || '' === trim($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int) $value;
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. **Create extensible constraint parser system** (interfaces, registry, birth year parser)
|
||||
2. **Extend Service model** with new age constraint properties
|
||||
3. **Update TravelParser** with extensible constraint parsing
|
||||
4. **Create age evaluator system** for service filtering
|
||||
5. **Update field options provider** with age-aware filtering
|
||||
6. **Add comprehensive tests** for parsing and evaluation
|
||||
7. **Add helper methods** to parser base class
|
||||
8. **Update documentation** with extensible patterns
|
||||
|
||||
## Key Benefits
|
||||
|
||||
### Technical Benefits
|
||||
- **Fully extensible** - easy to add new constraint types (grade level, membership status, etc.)
|
||||
- **Backward compatible** - existing absolute age constraints continue working
|
||||
- **English naming** - all properties and methods use clear English names
|
||||
- **Robust parsing** - handles malformed data gracefully
|
||||
- **Debuggable** - stores raw constraint data for troubleshooting
|
||||
- **Testable** - clear separation of parsing, evaluation, and filtering concerns
|
||||
|
||||
### Future Extensibility
|
||||
- **Plugin architecture** - new constraint parsers can be added via DI
|
||||
- **Mixed constraints** - supports multiple constraint types per service
|
||||
- **Metadata storage** - extensible metadata for complex constraint types
|
||||
- **Version resilient** - unknown constraint types don't break existing functionality
|
||||
|
||||
### Business Benefits
|
||||
- **Accurate service filtering** - services only shown to eligible participants
|
||||
- **Clear constraint communication** - descriptive messages for age restrictions
|
||||
- **Flexible business rules** - supports complex eligibility scenarios
|
||||
|
||||
## Edge Cases Handled
|
||||
|
||||
- **Invalid constraint formats** - graceful handling with fallback to 'unknown' type
|
||||
- **Mixed constraint scenarios** - services with both absolute age and birth year requirements
|
||||
- **Empty/null constraint data** - treated as no constraints (available to all)
|
||||
- **Future constraint types** - unknown parsers don't break existing functionality
|
||||
- **Malformed date ranges** - validation and error handling in parsers
|
||||
- **Single vs range values** - supports both `JG:2007` and `JG:2007-2009` formats
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
- **Constraint parsing** for all supported formats and edge cases
|
||||
- **Age evaluation** for different constraint types and participant scenarios
|
||||
- **Service filtering** with mixed constraint types
|
||||
- **Registry behavior** with multiple parsers and constraint merging
|
||||
|
||||
### Integration Tests
|
||||
- **XML parsing** with real BPN export data containing age constraints
|
||||
- **Form field generation** with age-restricted services
|
||||
- **HTMX updates** when birth date changes affect service availability
|
||||
- **End-to-end booking flow** with age-restricted services
|
||||
|
||||
## Related Completed Improvements
|
||||
|
||||
**✅ Form Processing System Enhancements** (complementary to age constraints):
|
||||
- **Service Field HTMX Integration**: Fixed HTMX triggers for service fields (board, skipass, courses, etc.) to enable real-time updates for age-based field filtering
|
||||
- **Field Handler Data Storage**: Updated all service field handlers to store complete Service objects instead of IDs, enabling access to age constraint data
|
||||
- **Service Label Formatting**: Implemented smart service label formatting with pricing integration and quantity display
|
||||
- **Pricing Integration**: Service selections now properly integrate with pricing calculations, supporting age-restricted service pricing
|
||||
|
||||
These improvements provide the foundation for implementing age constraint filtering once the XML parsing and model extensions described in this plan are completed.
|
||||
|
||||
This plan provides a robust, extensible foundation for handling current age constraints while being prepared for future constraint types that may emerge from the XML data.
|
||||
Reference in New Issue
Block a user