641 lines
23 KiB
Markdown
641 lines
23 KiB
Markdown
# 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. |