wip: insurance booking phases 1 and 2
This commit is contained in:
@@ -0,0 +1,402 @@
|
|||||||
|
# Insurance Booking System Implementation Plan
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document outlines the complete implementation plan for making travel insurances bookable per participant in the MyEP Next Booking system. The implementation includes sophisticated criteria matching, automatic re-selection when participant prices change, and comprehensive form integration.
|
||||||
|
|
||||||
|
## Current System Status
|
||||||
|
|
||||||
|
### ✅ Already Implemented
|
||||||
|
- **Insurance XML Parsing**: `InsuranceParser` and `InsuranceLoader` classes
|
||||||
|
- **Insurance Model**: Complete with price ranges, age constraints, dates, family flags
|
||||||
|
- **Family Status Logic**: `BookingCreateDto::isFamilyBooking()` method
|
||||||
|
- **Individual Pricing**: `BookingPriceCalculatorService::calculateIndividualParticipantPrice()` method
|
||||||
|
- **Field Handler Architecture**: Existing pattern for dynamic form fields
|
||||||
|
- **HTMX Integration**: Real-time form updates infrastructure
|
||||||
|
|
||||||
|
### ❌ Missing Implementation
|
||||||
|
- Insurance subtype parsing and type resolution system
|
||||||
|
- Participant insurance field and selection logic
|
||||||
|
- Insurance matching service with criteria validation
|
||||||
|
- Auto-reselection when participant price changes
|
||||||
|
- Form integration and field handlers
|
||||||
|
- Frontend templates and UX
|
||||||
|
|
||||||
|
## Key Technical Discoveries
|
||||||
|
|
||||||
|
### Insurance XML Structure Analysis
|
||||||
|
- **Individual Insurances**: Have `unterart` (subtype) attribute (RRV, PAK, OHN)
|
||||||
|
- **Insurance Packages**: No subtype but contain individual insurances
|
||||||
|
- **Family Detection**: Use `familienversicherung` boolean attribute (not label parsing)
|
||||||
|
- **Package Type Resolution**: Analyze contained insurances to determine dominant type
|
||||||
|
|
||||||
|
### Age Calculation Requirement
|
||||||
|
- **Critical**: Use travel start date for age calculation, not current date
|
||||||
|
- Insurance eligibility based on participant's age at time of travel
|
||||||
|
|
||||||
|
## Implementation Phases
|
||||||
|
|
||||||
|
### Phase 1: Insurance Type System
|
||||||
|
**Goal**: Add type resolution capability to distinguish insurance types
|
||||||
|
|
||||||
|
#### 1.1 Extend Insurance Model
|
||||||
|
- [ ] Add `subType` property to `Insurance` model (from XML `unterart`)
|
||||||
|
- [ ] Add computed `type` property (resolved via service)
|
||||||
|
- [ ] Update serialization groups if needed
|
||||||
|
|
||||||
|
#### 1.2 Update Insurance Parser
|
||||||
|
- [ ] Modify `InsuranceParser::parseInsuranceNode()` to parse `unterart` attribute
|
||||||
|
- [ ] Add subtype to individual insurance parsing
|
||||||
|
- [ ] Ensure package parsing maintains existing functionality
|
||||||
|
|
||||||
|
#### 1.3 Create Insurance Type Resolver
|
||||||
|
- [ ] Create `InsuranceTypeResolver` service
|
||||||
|
- [ ] Implement type resolution for individual insurances
|
||||||
|
- [ ] Implement package type resolution via contained insurance analysis
|
||||||
|
- [ ] Define type constants: `TRAVEL_CANCELLATION`, `TRAVEL_PROTECTION`, etc.
|
||||||
|
- [ ] Handle family variants using `familyInsurance` boolean
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Type Constants
|
||||||
|
const TRAVEL_CANCELLATION = 'TRAVEL_CANCELLATION';
|
||||||
|
const TRAVEL_CANCELLATION_FAMILY = 'TRAVEL_CANCELLATION_FAMILY';
|
||||||
|
const TRAVEL_PROTECTION = 'TRAVEL_PROTECTION';
|
||||||
|
const TRAVEL_PROTECTION_FAMILY = 'TRAVEL_PROTECTION_FAMILY';
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.4 Testing
|
||||||
|
- [ ] Create `InsuranceTypeResolverTest`
|
||||||
|
- [ ] Test individual insurance type resolution
|
||||||
|
- [ ] Test package type resolution
|
||||||
|
- [ ] Test family variant detection
|
||||||
|
- [ ] Verify existing insurance parsing still works
|
||||||
|
|
||||||
|
### Phase 2: Enhanced Age Calculation
|
||||||
|
**Goal**: Support age calculation at specific dates (travel start date)
|
||||||
|
|
||||||
|
#### 2.1 Update ParticipantDto
|
||||||
|
- [ ] Add `getAgeAtDate(\DateTimeImmutable $referenceDate): ?int` method
|
||||||
|
- [ ] Keep existing `getAge(): ?int` for backward compatibility
|
||||||
|
- [ ] Ensure proper null handling for missing birth dates
|
||||||
|
|
||||||
|
#### 2.2 Testing
|
||||||
|
- [ ] Add tests for `getAgeAtDate()` method
|
||||||
|
- [ ] Test edge cases (leap years, same day, etc.)
|
||||||
|
- [ ] Verify existing age calculation still works
|
||||||
|
|
||||||
|
### Phase 3: Insurance Matching Service
|
||||||
|
**Goal**: Implement comprehensive insurance matching with all criteria
|
||||||
|
|
||||||
|
#### 3.1 Create Insurance Matching Service
|
||||||
|
- [ ] Create `InsuranceMatchingService` class
|
||||||
|
- [ ] Implement `getMatchingInsurances()` method with all criteria:
|
||||||
|
- Age at travel date validation
|
||||||
|
- Price range validation
|
||||||
|
- Booking date validation
|
||||||
|
- Travel date validation
|
||||||
|
- Family insurance validation
|
||||||
|
- [ ] Implement `getMatchingInsurancesByType()` for auto-reselection
|
||||||
|
- [ ] Add "No Insurance" option handling
|
||||||
|
|
||||||
|
#### 3.2 Auto-Reselection Logic
|
||||||
|
- [ ] Implement `handlePriceChange()` method
|
||||||
|
- [ ] Find matching insurance of same type when price changes
|
||||||
|
- [ ] Maintain coverage when possible, fallback to null
|
||||||
|
- [ ] Log auto-reselection events for debugging
|
||||||
|
|
||||||
|
#### 3.3 Service Registration
|
||||||
|
- [ ] Register service in `services.yaml`
|
||||||
|
- [ ] Configure dependencies (InsuranceLoader, InsuranceTypeResolver)
|
||||||
|
|
||||||
|
#### 3.4 Testing
|
||||||
|
- [ ] Create comprehensive `InsuranceMatchingServiceTest`
|
||||||
|
- [ ] Test all matching criteria combinations
|
||||||
|
- [ ] Test auto-reselection scenarios
|
||||||
|
- [ ] Test edge cases and boundary conditions
|
||||||
|
|
||||||
|
### Phase 4: Participant DTO Enhancement
|
||||||
|
**Goal**: Add insurance property and integrate with pricing
|
||||||
|
|
||||||
|
#### 4.1 Extend ParticipantDto
|
||||||
|
- [ ] Add `?Insurance $insurance = null` property
|
||||||
|
- [ ] Add validation groups if needed
|
||||||
|
- [ ] Ensure proper serialization/deserialization
|
||||||
|
|
||||||
|
#### 4.2 Update Price Calculation
|
||||||
|
- [ ] Modify `BookingPriceCalculatorService::calculateParticipantServiceTotal()`
|
||||||
|
- [ ] Include insurance price in participant total
|
||||||
|
- [ ] Update `calculateIndividualParticipantPrice()` to include insurance
|
||||||
|
- [ ] Update `calculateAllParticipantIndividualPrices()` accordingly
|
||||||
|
|
||||||
|
#### 4.3 Testing
|
||||||
|
- [ ] Update `BookingPriceCalculatorServiceTest`
|
||||||
|
- [ ] Test price calculation with insurance
|
||||||
|
- [ ] Test pricing without insurance
|
||||||
|
- [ ] Verify individual participant pricing includes insurance
|
||||||
|
|
||||||
|
### Phase 5: Form Field Handler
|
||||||
|
**Goal**: Create form field handler for insurance selection
|
||||||
|
|
||||||
|
#### 5.1 Create Insurance Field Handler
|
||||||
|
- [ ] Create `ParticipantInsuranceFieldHandler` extending `AbstractParticipantFieldHandler`
|
||||||
|
- [ ] Implement field processing logic
|
||||||
|
- [ ] Handle insurance selection validation
|
||||||
|
- [ ] Implement auto-reselection on price changes
|
||||||
|
- [ ] Clear insurance if criteria no longer match
|
||||||
|
|
||||||
|
#### 5.2 Field Dependencies
|
||||||
|
- [ ] Define dependencies: `dateOfBirth` (for age calculation)
|
||||||
|
- [ ] Handle field visibility based on available insurances
|
||||||
|
- [ ] Implement proper error handling
|
||||||
|
|
||||||
|
#### 5.3 Service Registration
|
||||||
|
- [ ] Register handler in `services.yaml` with proper tags
|
||||||
|
- [ ] Set appropriate priority in relation to other handlers
|
||||||
|
|
||||||
|
#### 5.4 Testing
|
||||||
|
- [ ] Create `ParticipantInsuranceFieldHandlerTest`
|
||||||
|
- [ ] Test field processing
|
||||||
|
- [ ] Test auto-reselection scenarios
|
||||||
|
- [ ] Test validation logic
|
||||||
|
|
||||||
|
### Phase 6: Form Integration
|
||||||
|
**Goal**: Add insurance field to participant form
|
||||||
|
|
||||||
|
#### 6.1 Update Form Type
|
||||||
|
- [ ] Modify `BookingCreateParticipantType`
|
||||||
|
- [ ] Add insurance choice field
|
||||||
|
- [ ] Configure field type and options
|
||||||
|
- [ ] Add HTMX trigger attributes
|
||||||
|
|
||||||
|
#### 6.2 Field Options Provider
|
||||||
|
- [ ] Add insurance field to `ParticipantFieldOptionsProvider`
|
||||||
|
- [ ] Generate "No Insurance" option
|
||||||
|
- [ ] Generate valid insurance options per participant
|
||||||
|
- [ ] Use `InsuranceMatchingService` for filtering
|
||||||
|
- [ ] Format options with price and type information
|
||||||
|
|
||||||
|
#### 6.3 Field State Provider Integration
|
||||||
|
- [ ] Update `CreateFieldStateProvider` if needed
|
||||||
|
- [ ] Handle field visibility conditions
|
||||||
|
- [ ] Register field state conditions
|
||||||
|
|
||||||
|
#### 6.4 Testing
|
||||||
|
- [ ] Test form field rendering
|
||||||
|
- [ ] Test field options generation
|
||||||
|
- [ ] Test HTMX integration
|
||||||
|
|
||||||
|
### Phase 7: Controller Integration
|
||||||
|
**Goal**: Update controllers to handle insurance data
|
||||||
|
|
||||||
|
#### 7.1 Update Step 2 Controller
|
||||||
|
- [ ] Ensure insurance data is included in template variables
|
||||||
|
- [ ] Handle insurance-related HTMX updates
|
||||||
|
- [ ] Update participant price calculations to include insurance
|
||||||
|
|
||||||
|
#### 7.2 Error Handling
|
||||||
|
- [ ] Add proper error handling for insurance validation
|
||||||
|
- [ ] Handle insurance matching failures gracefully
|
||||||
|
- [ ] Provide user-friendly error messages
|
||||||
|
|
||||||
|
#### 7.3 Testing
|
||||||
|
- [ ] Test controller responses include insurance data
|
||||||
|
- [ ] Test HTMX updates work correctly
|
||||||
|
- [ ] Test error scenarios
|
||||||
|
|
||||||
|
### Phase 8: Frontend Templates
|
||||||
|
**Goal**: Add insurance field to templates and display pricing
|
||||||
|
|
||||||
|
#### 8.1 Template Updates
|
||||||
|
- [ ] Add insurance field to `create_step_2.html.twig`
|
||||||
|
- [ ] Position field appropriately in participant sections
|
||||||
|
- [ ] Add proper labeling and help text
|
||||||
|
- [ ] Configure HTMX attributes for real-time updates
|
||||||
|
|
||||||
|
#### 8.2 Pricing Display
|
||||||
|
- [ ] Update participant headers to show insurance pricing
|
||||||
|
- [ ] Include insurance in individual participant price display
|
||||||
|
- [ ] Update booking summary to include insurance totals
|
||||||
|
|
||||||
|
#### 8.3 UX Enhancements
|
||||||
|
- [ ] Show criteria-based availability information
|
||||||
|
- [ ] Display auto-reselection notifications
|
||||||
|
- [ ] Add loading states for HTMX updates
|
||||||
|
|
||||||
|
### Phase 9: Advanced Features
|
||||||
|
**Goal**: Implement applicant control and bulk operations
|
||||||
|
|
||||||
|
#### 9.1 Applicant Insurance Control
|
||||||
|
- [ ] Add special UI for first participant (applicant)
|
||||||
|
- [ ] Implement bulk insurance selection
|
||||||
|
- [ ] Respect individual criteria while allowing bulk operations
|
||||||
|
- [ ] Add override capabilities with proper validation
|
||||||
|
|
||||||
|
#### 9.2 Real-time Price Integration
|
||||||
|
- [ ] Monitor price changes via HTMX
|
||||||
|
- [ ] Trigger auto-reselection when participant price changes
|
||||||
|
- [ ] Provide visual feedback for automatic changes
|
||||||
|
- [ ] Update pricing summary dynamically
|
||||||
|
|
||||||
|
#### 9.3 Enhanced UX
|
||||||
|
- [ ] Add insurance type grouping in selection
|
||||||
|
- [ ] Implement insurance comparison features
|
||||||
|
- [ ] Add detailed insurance information display
|
||||||
|
- [ ] Improve mobile responsiveness
|
||||||
|
|
||||||
|
### Phase 10: Testing & Quality Assurance
|
||||||
|
**Goal**: Comprehensive testing and code quality
|
||||||
|
|
||||||
|
#### 10.1 Integration Testing
|
||||||
|
- [ ] Create end-to-end insurance booking tests
|
||||||
|
- [ ] Test complete booking flow with insurances
|
||||||
|
- [ ] Test auto-reselection scenarios
|
||||||
|
- [ ] Test applicant control features
|
||||||
|
|
||||||
|
#### 10.2 Performance Testing
|
||||||
|
- [ ] Test insurance matching performance with large datasets
|
||||||
|
- [ ] Optimize insurance filtering algorithms
|
||||||
|
- [ ] Test HTMX update performance
|
||||||
|
|
||||||
|
#### 10.3 Code Quality
|
||||||
|
- [ ] Run PHP CS Fixer on all new files
|
||||||
|
- [ ] Ensure PSR-12 compliance
|
||||||
|
- [ ] Add proper PHPDoc documentation
|
||||||
|
- [ ] Review and optimize service dependencies
|
||||||
|
|
||||||
|
#### 10.4 User Acceptance Testing
|
||||||
|
- [ ] Test with real insurance data
|
||||||
|
- [ ] Validate business logic with stakeholders
|
||||||
|
- [ ] Test edge cases and error scenarios
|
||||||
|
- [ ] Gather user feedback on UX
|
||||||
|
|
||||||
|
## Technical Architecture
|
||||||
|
|
||||||
|
### New Classes Overview
|
||||||
|
|
||||||
|
```
|
||||||
|
src/Service/
|
||||||
|
├── InsuranceTypeResolver.php # Type resolution for insurances and packages
|
||||||
|
├── InsuranceMatchingService.php # Criteria-based insurance matching
|
||||||
|
└── Insurance/
|
||||||
|
├── Criteria/
|
||||||
|
│ ├── AgeCriterion.php # Age-based matching
|
||||||
|
│ ├── PriceCriterion.php # Price range matching
|
||||||
|
│ ├── DateCriterion.php # Date validation
|
||||||
|
│ └── FamilyCriterion.php # Family insurance validation
|
||||||
|
└── Matcher/
|
||||||
|
└── InsuranceMatcher.php # Core matching logic
|
||||||
|
|
||||||
|
src/Form/Service/
|
||||||
|
├── ParticipantInsuranceFieldHandler.php # Form field processing
|
||||||
|
└── Insurance/
|
||||||
|
└── InsuranceFieldOptionsProvider.php # Field options generation
|
||||||
|
|
||||||
|
tests/Service/
|
||||||
|
├── InsuranceTypeResolverTest.php
|
||||||
|
├── InsuranceMatchingServiceTest.php
|
||||||
|
└── Insurance/
|
||||||
|
└── Criteria/
|
||||||
|
├── AgeCriterionTest.php
|
||||||
|
├── PriceCriterionTest.php
|
||||||
|
├── DateCriterionTest.php
|
||||||
|
└── FamilyCriterionTest.php
|
||||||
|
|
||||||
|
tests/Form/Service/
|
||||||
|
└── ParticipantInsuranceFieldHandlerTest.php
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Changes
|
||||||
|
- **None required** - All insurance data loaded from XML
|
||||||
|
|
||||||
|
### Configuration Changes
|
||||||
|
- Service registrations in `services.yaml`
|
||||||
|
- Field handler tags and priorities
|
||||||
|
- Form field configurations
|
||||||
|
|
||||||
|
## Implementation Guidelines
|
||||||
|
|
||||||
|
### Code Standards
|
||||||
|
- Follow PSR-12 coding standards
|
||||||
|
- Use `declare(strict_types=1)` on all files
|
||||||
|
- Apply Symfony coding standards via php-cs-fixer
|
||||||
|
- Use English for all variable and constant names
|
||||||
|
- Follow existing architectural patterns
|
||||||
|
|
||||||
|
### Testing Standards
|
||||||
|
- Minimum 90% code coverage for new classes
|
||||||
|
- Unit tests for all service methods
|
||||||
|
- Integration tests for form processing
|
||||||
|
- End-to-end tests for complete booking flow
|
||||||
|
- Performance tests for matching algorithms
|
||||||
|
|
||||||
|
### Documentation Standards
|
||||||
|
- PHPDoc for all public methods
|
||||||
|
- Business logic documentation in comments
|
||||||
|
- Update CLAUDE.md with new features
|
||||||
|
- Create user documentation for insurance features
|
||||||
|
|
||||||
|
## Risk Mitigation
|
||||||
|
|
||||||
|
### Technical Risks
|
||||||
|
1. **Performance**: Large insurance datasets could slow matching
|
||||||
|
- **Mitigation**: Implement caching and optimize algorithms
|
||||||
|
2. **Complexity**: Auto-reselection logic could introduce bugs
|
||||||
|
- **Mitigation**: Comprehensive testing and logging
|
||||||
|
3. **Data Integrity**: Price changes could cause inconsistent states
|
||||||
|
- **Mitigation**: Atomic operations and validation
|
||||||
|
|
||||||
|
### Business Risks
|
||||||
|
1. **Incorrect Matching**: Wrong insurance eligibility could cause issues
|
||||||
|
- **Mitigation**: Thorough testing with real data and stakeholder validation
|
||||||
|
2. **User Confusion**: Complex insurance options could confuse users
|
||||||
|
- **Mitigation**: Clear UX design and comprehensive help text
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
### Functional Requirements
|
||||||
|
- ✅ Participants can select appropriate insurances based on all criteria
|
||||||
|
- ✅ Auto-reselection works when participant price changes
|
||||||
|
- ✅ Applicant can manage insurances for all participants
|
||||||
|
- ✅ Real-time pricing updates include insurance costs
|
||||||
|
- ✅ Form validation prevents invalid insurance selections
|
||||||
|
|
||||||
|
### Performance Requirements
|
||||||
|
- ✅ Insurance matching completes within 100ms for typical datasets
|
||||||
|
- ✅ HTMX updates complete within 500ms
|
||||||
|
- ✅ Page load times remain under 2 seconds
|
||||||
|
|
||||||
|
### Quality Requirements
|
||||||
|
- ✅ 90%+ code coverage for new functionality
|
||||||
|
- ✅ Zero critical bugs in production
|
||||||
|
- ✅ PSR-12 compliance for all new code
|
||||||
|
- ✅ Comprehensive documentation
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
### Phase 11+ (Future)
|
||||||
|
- Insurance comparison tools
|
||||||
|
- Advanced filtering and search
|
||||||
|
- Insurance recommendation engine
|
||||||
|
- Historical insurance selection analytics
|
||||||
|
- Multi-language insurance descriptions
|
||||||
|
- PDF insurance documentation generation
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### External Dependencies
|
||||||
|
- Insurance XML data must be available and properly formatted
|
||||||
|
- BusProNet API integration for insurance booking
|
||||||
|
- Travel data must include proper date information
|
||||||
|
|
||||||
|
### Internal Dependencies
|
||||||
|
- Existing individual pricing calculation system
|
||||||
|
- Field handler architecture
|
||||||
|
- HTMX integration infrastructure
|
||||||
|
- Family status determination logic
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Document Version**: 1.0
|
||||||
|
**Last Updated**: 2025-09-26
|
||||||
|
**Author**: Claude Code Implementation Plan
|
||||||
|
**Status**: Ready for Implementation
|
||||||
@@ -25,6 +25,9 @@ class Insurance
|
|||||||
#[Groups(['api:single', 'api:list'])]
|
#[Groups(['api:single', 'api:list'])]
|
||||||
public ?string $label = null;
|
public ?string $label = null;
|
||||||
|
|
||||||
|
#[Groups(['api:single', 'api:list'])]
|
||||||
|
public ?string $subType = null;
|
||||||
|
|
||||||
#[Groups(['api:single', 'api:list'])]
|
#[Groups(['api:single', 'api:list'])]
|
||||||
public ?float $price = null;
|
public ?float $price = null;
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ class InsuranceParser extends AbstractParser
|
|||||||
$insurance->id = $isPackage ? $idValue : (int) $idValue;
|
$insurance->id = $isPackage ? $idValue : (int) $idValue;
|
||||||
$insurance->code = $node->attr('code');
|
$insurance->code = $node->attr('code');
|
||||||
$insurance->label = $node->attr('bezeichnung');
|
$insurance->label = $node->attr('bezeichnung');
|
||||||
|
$insurance->subType = $node->attr('unterart');
|
||||||
$insurance->price = $node->attr('preis') ?
|
$insurance->price = $node->attr('preis') ?
|
||||||
$this->stringToFloat($node->attr('preis')) : null;
|
$this->stringToFloat($node->attr('preis')) : null;
|
||||||
$insurance->familyInsurance = $this->stringToBool($node->attr('familienversicherung'));
|
$insurance->familyInsurance = $this->stringToBool($node->attr('familienversicherung'));
|
||||||
|
|||||||
@@ -108,21 +108,23 @@ class ParticipantDto
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates the participant's current age in complete years.
|
* Calculates the participant's age in complete years.
|
||||||
*
|
*
|
||||||
* Uses the same logic as existing age evaluators in the system
|
* Uses the same logic as existing age evaluators in the system
|
||||||
* for consistency across age-related calculations.
|
* for consistency across age-related calculations.
|
||||||
*
|
*
|
||||||
|
* @param \DateTimeImmutable|null $referenceDate The date to calculate age at (defaults to current date)
|
||||||
|
*
|
||||||
* @return int|null The calculated age in complete years, or null if no birth date
|
* @return int|null The calculated age in complete years, or null if no birth date
|
||||||
*/
|
*/
|
||||||
public function getAge(): ?int
|
public function getAge(?\DateTimeImmutable $referenceDate = null): ?int
|
||||||
{
|
{
|
||||||
if (null === $this->dateOfBirth) {
|
if (null === $this->dateOfBirth) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$today = new \DateTimeImmutable();
|
$referenceDate = $referenceDate ?? new \DateTimeImmutable();
|
||||||
|
|
||||||
return $this->dateOfBirth->diff($today)->y;
|
return $this->dateOfBirth->diff($referenceDate)->y;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
use App\BusProNet\Model\Insurance;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves insurance types for both individual insurances and packages.
|
||||||
|
*
|
||||||
|
* This service provides consistent type resolution by mapping insurance subtypes
|
||||||
|
* to standardized type constants. For packages, it analyzes contained insurances
|
||||||
|
* to determine the dominant (non-deductible) type and applies family variants
|
||||||
|
* based on the familyInsurance flag.
|
||||||
|
*/
|
||||||
|
class InsuranceTypeResolver
|
||||||
|
{
|
||||||
|
public const TRAVEL_CANCELLATION = 'TRAVEL_CANCELLATION';
|
||||||
|
public const TRAVEL_CANCELLATION_FAMILY = 'TRAVEL_CANCELLATION_FAMILY';
|
||||||
|
public const TRAVEL_PROTECTION = 'TRAVEL_PROTECTION';
|
||||||
|
public const TRAVEL_PROTECTION_FAMILY = 'TRAVEL_PROTECTION_FAMILY';
|
||||||
|
public const DEDUCTIBLE = 'DEDUCTIBLE';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the standardized type for an insurance or package.
|
||||||
|
*
|
||||||
|
* @param Insurance $insurance The insurance to resolve the type for
|
||||||
|
*
|
||||||
|
* @return string|null The resolved type constant or null if unresolvable
|
||||||
|
*/
|
||||||
|
public function resolveType(Insurance $insurance): ?string
|
||||||
|
{
|
||||||
|
if (!$insurance->package) {
|
||||||
|
return $this->mapSubtypeToType($insurance->subType, $insurance->familyInsurance);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->resolvePackageType($insurance);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the type for an insurance package by analyzing contained insurances.
|
||||||
|
*
|
||||||
|
* @param Insurance $package The package to analyze
|
||||||
|
*
|
||||||
|
* @return string|null The resolved type or null if no primary type found
|
||||||
|
*/
|
||||||
|
private function resolvePackageType(Insurance $package): ?string
|
||||||
|
{
|
||||||
|
// Get primary (non-deductible) insurance type from contained insurances
|
||||||
|
$primaryType = null;
|
||||||
|
foreach ($package->containedInsurances as $contained) {
|
||||||
|
$baseType = $this->mapSubtypeToBaseType($contained->subType);
|
||||||
|
if ($baseType && self::DEDUCTIBLE !== $baseType) {
|
||||||
|
$primaryType = $baseType;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply family suffix using package's familyInsurance flag
|
||||||
|
return $primaryType ? $primaryType.($package->familyInsurance ? '_FAMILY' : '') : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps a subtype to a full type including family variant.
|
||||||
|
*
|
||||||
|
* @param string|null $subtype The subtype to map
|
||||||
|
* @param bool $isFamily Whether this is a family insurance
|
||||||
|
*
|
||||||
|
* @return string|null The mapped type or null if unmappable
|
||||||
|
*/
|
||||||
|
private function mapSubtypeToType(?string $subtype, bool $isFamily): ?string
|
||||||
|
{
|
||||||
|
$baseType = $this->mapSubtypeToBaseType($subtype);
|
||||||
|
|
||||||
|
return $baseType ? $baseType.($isFamily ? '_FAMILY' : '') : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps a subtype to its base type constant.
|
||||||
|
*
|
||||||
|
* @param string|null $subtype The subtype to map
|
||||||
|
*
|
||||||
|
* @return string|null The base type constant or null if unmappable
|
||||||
|
*/
|
||||||
|
private function mapSubtypeToBaseType(?string $subtype): ?string
|
||||||
|
{
|
||||||
|
return match ($subtype) {
|
||||||
|
'RRV' => self::TRAVEL_CANCELLATION,
|
||||||
|
'PAK' => self::TRAVEL_PROTECTION,
|
||||||
|
'OHN' => self::DEDUCTIBLE,
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all available insurance type constants.
|
||||||
|
*
|
||||||
|
* @return array<string> Array of all type constants
|
||||||
|
*/
|
||||||
|
public function getAllTypes(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::TRAVEL_CANCELLATION,
|
||||||
|
self::TRAVEL_CANCELLATION_FAMILY,
|
||||||
|
self::TRAVEL_PROTECTION,
|
||||||
|
self::TRAVEL_PROTECTION_FAMILY,
|
||||||
|
self::DEDUCTIBLE,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a type is a family variant.
|
||||||
|
*
|
||||||
|
* @param string $type The type to check
|
||||||
|
*
|
||||||
|
* @return bool True if the type is a family variant
|
||||||
|
*/
|
||||||
|
public function isFamilyType(string $type): bool
|
||||||
|
{
|
||||||
|
return str_ends_with($type, '_FAMILY');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the base type from a family type.
|
||||||
|
*
|
||||||
|
* @param string $type The type to get the base for
|
||||||
|
*
|
||||||
|
* @return string The base type (without _FAMILY suffix)
|
||||||
|
*/
|
||||||
|
public function getBaseType(string $type): string
|
||||||
|
{
|
||||||
|
return str_replace('_FAMILY', '', $type);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Tests\BusProNet\XmlParser;
|
||||||
|
|
||||||
|
use App\BusProNet\XmlParser\InsuranceParser;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\DomCrawler\Crawler;
|
||||||
|
|
||||||
|
class InsuranceParserTest extends TestCase
|
||||||
|
{
|
||||||
|
private InsuranceParser $parser;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->parser = new InsuranceParser();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testParseIndividualInsuranceWithSubtype(): void
|
||||||
|
{
|
||||||
|
$xmlContent = '<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<versicherungsstammdaten>
|
||||||
|
<versicherungen>
|
||||||
|
<versicherung id="1" idbuspro="178917" code="903100" bezeichnung="Reise-Rücktritt" preis="5,00" preisartprozent="False" unterart="RRV" familienversicherung="False" paarversicherung="False" zusatzversicherung="False" reisedatumvon="01.09.2024" reisedatumbis="31.12.2030" buchungdatumvon="01.03.2025" buchungdatumbis="31.12.2030" altervon="0" alterbis="64" reisepreisvon="0,00" reisepreisbis="100,00" reisedauervon="0" reisedauerbis="255" />
|
||||||
|
<versicherung id="2" idbuspro="177236" code="913320" bezeichnung="Reiseschutz Platin Auto/Bahn/Bus (Europa)" preis="9,00" preisartprozent="False" unterart="PAK" familienversicherung="True" paarversicherung="False" zusatzversicherung="False" reisedatumvon="01.09.2024" reisedatumbis="31.12.2030" buchungdatumvon="01.03.2025" buchungdatumbis="31.12.2030" altervon="0" alterbis="64" reisepreisvon="0,00" reisepreisbis="100,00" reisedauervon="1" reisedauerbis="45" />
|
||||||
|
<versicherung id="3" idbuspro="177270" code="903551" bezeichnung="Selbstbehaltübernahme" preis="5,00" preisartprozent="False" unterart="OHN" familienversicherung="False" paarversicherung="False" zusatzversicherung="True" reisedatumvon="01.09.2024" reisedatumbis="31.12.2030" buchungdatumvon="01.03.2025" buchungdatumbis="31.12.2030" altervon="0" alterbis="64" reisepreisvon="0,00" reisepreisbis="600,00" reisedauervon="0" reisedauerbis="255" />
|
||||||
|
</versicherungen>
|
||||||
|
<versicherungspakete>
|
||||||
|
<versicherungspaket id="1" idbuspro="P1000320" bezeichnung="Reiseschutz Platin Auto/Bahn/Bus (Europa) + Selbstbehaltübernahme" preis="14,00" preisartprozent="False" reisedatumvon="01.09.2024" reisedatumbis="31.12.2030" altervon="0" alterbis="64" reisepreisvon="0" reisepreisbis="100,00" familienversicherung="False">
|
||||||
|
<enthalteneversicherungen>
|
||||||
|
<enthalteneversicherung idbuspro="177236" />
|
||||||
|
<enthalteneversicherung idbuspro="177270" />
|
||||||
|
</enthalteneversicherungen>
|
||||||
|
</versicherungspaket>
|
||||||
|
</versicherungspakete>
|
||||||
|
</versicherungsstammdaten>';
|
||||||
|
|
||||||
|
$crawler = new Crawler($xmlContent);
|
||||||
|
$insurances = $this->parser->parse($crawler);
|
||||||
|
|
||||||
|
// Test individual insurances + package (3 individual + 1 package = 4 total)
|
||||||
|
$this->assertCount(4, $insurances);
|
||||||
|
|
||||||
|
// Test travel cancellation insurance (RRV)
|
||||||
|
$cancellationInsurance = $insurances[178917];
|
||||||
|
$this->assertEquals(178917, $cancellationInsurance->id);
|
||||||
|
$this->assertEquals('903100', $cancellationInsurance->code);
|
||||||
|
$this->assertEquals('Reise-Rücktritt', $cancellationInsurance->label);
|
||||||
|
$this->assertEquals('RRV', $cancellationInsurance->subType);
|
||||||
|
$this->assertEquals(5.0, $cancellationInsurance->price);
|
||||||
|
$this->assertFalse($cancellationInsurance->familyInsurance);
|
||||||
|
$this->assertFalse($cancellationInsurance->package);
|
||||||
|
|
||||||
|
// Test travel protection insurance (PAK) with family flag
|
||||||
|
$protectionInsurance = $insurances[177236];
|
||||||
|
$this->assertEquals(177236, $protectionInsurance->id);
|
||||||
|
$this->assertEquals('913320', $protectionInsurance->code);
|
||||||
|
$this->assertEquals('Reiseschutz Platin Auto/Bahn/Bus (Europa)', $protectionInsurance->label);
|
||||||
|
$this->assertEquals('PAK', $protectionInsurance->subType);
|
||||||
|
$this->assertEquals(9.0, $protectionInsurance->price);
|
||||||
|
$this->assertTrue($protectionInsurance->familyInsurance);
|
||||||
|
$this->assertFalse($protectionInsurance->package);
|
||||||
|
|
||||||
|
// Test deductible insurance (OHN) - should be included because it's referenced by package
|
||||||
|
$deductibleInsurance = $insurances[177270];
|
||||||
|
$this->assertEquals(177270, $deductibleInsurance->id);
|
||||||
|
$this->assertEquals('903551', $deductibleInsurance->code);
|
||||||
|
$this->assertEquals('Selbstbehaltübernahme', $deductibleInsurance->label);
|
||||||
|
$this->assertEquals('OHN', $deductibleInsurance->subType);
|
||||||
|
$this->assertEquals(5.0, $deductibleInsurance->price);
|
||||||
|
$this->assertFalse($deductibleInsurance->familyInsurance);
|
||||||
|
$this->assertFalse($deductibleInsurance->package);
|
||||||
|
|
||||||
|
// Test package
|
||||||
|
$package = $insurances['P1000320'];
|
||||||
|
$this->assertEquals('P1000320', $package->id);
|
||||||
|
$this->assertEquals('Reiseschutz Platin Auto/Bahn/Bus (Europa) + Selbstbehaltübernahme', $package->label);
|
||||||
|
$this->assertNull($package->subType); // Packages don't have subtypes
|
||||||
|
$this->assertEquals(14.0, $package->price);
|
||||||
|
$this->assertFalse($package->familyInsurance);
|
||||||
|
$this->assertTrue($package->package);
|
||||||
|
$this->assertEquals([177236, 177270], $package->containedInsuranceIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testParsePackageWithoutSubtype(): void
|
||||||
|
{
|
||||||
|
$xmlContent = '<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<versicherungsstammdaten>
|
||||||
|
<versicherungen>
|
||||||
|
<versicherung id="1" idbuspro="177236" code="913320" bezeichnung="Test Insurance" preis="9,00" preisartprozent="False" unterart="PAK" familienversicherung="False" paarversicherung="False" zusatzversicherung="False" />
|
||||||
|
</versicherungen>
|
||||||
|
<versicherungspakete>
|
||||||
|
<versicherungspaket id="1" idbuspro="P1000320" bezeichnung="Test Package" preis="14,00" familienversicherung="True">
|
||||||
|
<enthalteneversicherungen>
|
||||||
|
<enthalteneversicherung idbuspro="177236" />
|
||||||
|
</enthalteneversicherungen>
|
||||||
|
</versicherungspaket>
|
||||||
|
</versicherungspakete>
|
||||||
|
</versicherungsstammdaten>';
|
||||||
|
|
||||||
|
$crawler = new Crawler($xmlContent);
|
||||||
|
$insurances = $this->parser->parse($crawler);
|
||||||
|
|
||||||
|
$this->assertCount(2, $insurances);
|
||||||
|
|
||||||
|
// Test package has no subType but has familyInsurance flag
|
||||||
|
$package = $insurances['P1000320'];
|
||||||
|
$this->assertEquals('P1000320', $package->id);
|
||||||
|
$this->assertNull($package->subType);
|
||||||
|
$this->assertTrue($package->familyInsurance);
|
||||||
|
$this->assertTrue($package->package);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testParseInsuranceWithNullSubtype(): void
|
||||||
|
{
|
||||||
|
$xmlContent = '<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<versicherungsstammdaten>
|
||||||
|
<versicherungen>
|
||||||
|
<versicherung id="1" idbuspro="178917" code="903100" bezeichnung="Test Insurance" preis="5,00" preisartprozent="False" familienversicherung="False" paarversicherung="False" zusatzversicherung="False" />
|
||||||
|
</versicherungen>
|
||||||
|
</versicherungsstammdaten>';
|
||||||
|
|
||||||
|
$crawler = new Crawler($xmlContent);
|
||||||
|
$insurances = $this->parser->parse($crawler);
|
||||||
|
|
||||||
|
$this->assertCount(1, $insurances);
|
||||||
|
|
||||||
|
$insurance = $insurances[178917];
|
||||||
|
$this->assertEquals(178917, $insurance->id);
|
||||||
|
$this->assertNull($insurance->subType); // Missing unterart attribute should result in null
|
||||||
|
$this->assertFalse($insurance->package);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testParseEmptyXml(): void
|
||||||
|
{
|
||||||
|
$xmlContent = '<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<versicherungsstammdaten>
|
||||||
|
<versicherungen>
|
||||||
|
</versicherungen>
|
||||||
|
<versicherungspakete>
|
||||||
|
</versicherungspakete>
|
||||||
|
</versicherungsstammdaten>';
|
||||||
|
|
||||||
|
$crawler = new Crawler($xmlContent);
|
||||||
|
$insurances = $this->parser->parse($crawler);
|
||||||
|
|
||||||
|
$this->assertEmpty($insurances);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testParseZusatzversicherungFiltering(): void
|
||||||
|
{
|
||||||
|
$xmlContent = '<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<versicherungsstammdaten>
|
||||||
|
<versicherungen>
|
||||||
|
<versicherung id="1" idbuspro="178917" code="903100" bezeichnung="Normal Insurance" preis="5,00" preisartprozent="False" unterart="RRV" familienversicherung="False" paarversicherung="False" zusatzversicherung="False" />
|
||||||
|
<versicherung id="2" idbuspro="177270" code="903551" bezeichnung="Zusatz Insurance Referenced" preis="5,00" preisartprozent="False" unterart="OHN" familienversicherung="False" paarversicherung="False" zusatzversicherung="True" />
|
||||||
|
<versicherung id="3" idbuspro="999999" code="999999" bezeichnung="Zusatz Insurance Not Referenced" preis="5,00" preisartprozent="False" unterart="OHN" familienversicherung="False" paarversicherung="False" zusatzversicherung="True" />
|
||||||
|
</versicherungen>
|
||||||
|
<versicherungspakete>
|
||||||
|
<versicherungspaket id="1" idbuspro="P1000320" bezeichnung="Test Package" preis="14,00" familienversicherung="False">
|
||||||
|
<enthalteneversicherungen>
|
||||||
|
<enthalteneversicherung idbuspro="177270" />
|
||||||
|
</enthalteneversicherungen>
|
||||||
|
</versicherungspaket>
|
||||||
|
</versicherungspakete>
|
||||||
|
</versicherungsstammdaten>';
|
||||||
|
|
||||||
|
$crawler = new Crawler($xmlContent);
|
||||||
|
$insurances = $this->parser->parse($crawler);
|
||||||
|
|
||||||
|
// Should include: normal insurance (178917), referenced zusatz (177270), and package (P1000320)
|
||||||
|
// Should exclude: unreferenced zusatz (999999)
|
||||||
|
$this->assertCount(3, $insurances);
|
||||||
|
$this->assertArrayHasKey(178917, $insurances);
|
||||||
|
$this->assertArrayHasKey(177270, $insurances);
|
||||||
|
$this->assertArrayHasKey('P1000320', $insurances);
|
||||||
|
$this->assertArrayNotHasKey(999999, $insurances);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Tests\Form\Model;
|
||||||
|
|
||||||
|
use App\Form\Model\ParticipantDto;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class ParticipantDtoTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testGetAgeReturnsNullWhenDateOfBirthIsNull(): void
|
||||||
|
{
|
||||||
|
$participant = new ParticipantDto();
|
||||||
|
$participant->dateOfBirth = null;
|
||||||
|
|
||||||
|
$this->assertNull($participant->getAge());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetAgeCalculatesCurrentAgeWhenNoReferenceDateProvided(): void
|
||||||
|
{
|
||||||
|
$participant = new ParticipantDto();
|
||||||
|
$participant->dateOfBirth = new \DateTimeImmutable('1990-06-15');
|
||||||
|
|
||||||
|
$expectedAge = (new \DateTimeImmutable())->diff($participant->dateOfBirth)->y;
|
||||||
|
$actualAge = $participant->getAge();
|
||||||
|
|
||||||
|
$this->assertEquals($expectedAge, $actualAge);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetAgeCalculatesAgeAtSpecificReferenceDate(): void
|
||||||
|
{
|
||||||
|
$participant = new ParticipantDto();
|
||||||
|
$participant->dateOfBirth = new \DateTimeImmutable('1990-06-15');
|
||||||
|
$referenceDate = new \DateTimeImmutable('2024-06-14'); // Day before 34th birthday
|
||||||
|
|
||||||
|
$result = $participant->getAge($referenceDate);
|
||||||
|
|
||||||
|
$this->assertEquals(33, $result); // Still 33 years old
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetAgeCalculatesAgeAtSpecificReferenceDateAfterBirthday(): void
|
||||||
|
{
|
||||||
|
$participant = new ParticipantDto();
|
||||||
|
$participant->dateOfBirth = new \DateTimeImmutable('1990-06-15');
|
||||||
|
$referenceDate = new \DateTimeImmutable('2024-06-15'); // Exactly 34th birthday
|
||||||
|
|
||||||
|
$result = $participant->getAge($referenceDate);
|
||||||
|
|
||||||
|
$this->assertEquals(34, $result); // Now 34 years old
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetAgeCalculatesAgeForInsuranceEligibilityScenario(): void
|
||||||
|
{
|
||||||
|
// Travel starts on 2024-08-01, participant born 1989-12-25
|
||||||
|
$participant = new ParticipantDto();
|
||||||
|
$participant->dateOfBirth = new \DateTimeImmutable('1989-12-25');
|
||||||
|
$travelStartDate = new \DateTimeImmutable('2024-08-01');
|
||||||
|
|
||||||
|
$result = $participant->getAge($travelStartDate);
|
||||||
|
|
||||||
|
// On 2024-08-01, they are still 34 (birthday not yet reached)
|
||||||
|
$this->assertEquals(34, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetAgeCalculatesAgeForInsuranceEligibilityScenarioAfterBirthday(): void
|
||||||
|
{
|
||||||
|
// Travel starts on 2024-12-26, participant born 1989-12-25
|
||||||
|
$participant = new ParticipantDto();
|
||||||
|
$participant->dateOfBirth = new \DateTimeImmutable('1989-12-25');
|
||||||
|
$travelStartDate = new \DateTimeImmutable('2024-12-26');
|
||||||
|
|
||||||
|
$result = $participant->getAge($travelStartDate);
|
||||||
|
|
||||||
|
// On 2024-12-26, they just turned 35
|
||||||
|
$this->assertEquals(35, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetAgeHandlesLeapYearScenarios(): void
|
||||||
|
{
|
||||||
|
// Born on Feb 29, 2000 (leap year)
|
||||||
|
$participant = new ParticipantDto();
|
||||||
|
$participant->dateOfBirth = new \DateTimeImmutable('2000-02-29');
|
||||||
|
$referenceDate = new \DateTimeImmutable('2023-02-28'); // Non-leap year
|
||||||
|
|
||||||
|
$result = $participant->getAge($referenceDate);
|
||||||
|
|
||||||
|
$this->assertEquals(22, $result); // Still 22, birthday is technically next day
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetAgeReturnsNullWithReferenceDateWhenDateOfBirthIsNull(): void
|
||||||
|
{
|
||||||
|
$participant = new ParticipantDto();
|
||||||
|
$participant->dateOfBirth = null;
|
||||||
|
$referenceDate = new \DateTimeImmutable('2024-08-01');
|
||||||
|
|
||||||
|
$result = $participant->getAge($referenceDate);
|
||||||
|
|
||||||
|
$this->assertNull($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetAgeWithYoungParticipant(): void
|
||||||
|
{
|
||||||
|
// Child born in 2020
|
||||||
|
$participant = new ParticipantDto();
|
||||||
|
$participant->dateOfBirth = new \DateTimeImmutable('2020-03-15');
|
||||||
|
$referenceDate = new \DateTimeImmutable('2024-08-01');
|
||||||
|
|
||||||
|
$result = $participant->getAge($referenceDate);
|
||||||
|
|
||||||
|
$this->assertEquals(4, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetAgeWithElderlyParticipant(): void
|
||||||
|
{
|
||||||
|
// Elderly participant born in 1940
|
||||||
|
$participant = new ParticipantDto();
|
||||||
|
$participant->dateOfBirth = new \DateTimeImmutable('1940-05-20');
|
||||||
|
$referenceDate = new \DateTimeImmutable('2024-08-01');
|
||||||
|
|
||||||
|
$result = $participant->getAge($referenceDate);
|
||||||
|
|
||||||
|
$this->assertEquals(84, $result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Tests\Service;
|
||||||
|
|
||||||
|
use App\BusProNet\Model\Insurance;
|
||||||
|
use App\Service\InsuranceTypeResolver;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class InsuranceTypeResolverTest extends TestCase
|
||||||
|
{
|
||||||
|
private InsuranceTypeResolver $resolver;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->resolver = new InsuranceTypeResolver();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testResolveTypeForIndividualTravelCancellationInsurance(): void
|
||||||
|
{
|
||||||
|
$insurance = new Insurance();
|
||||||
|
$insurance->package = false;
|
||||||
|
$insurance->subType = 'RRV';
|
||||||
|
$insurance->familyInsurance = false;
|
||||||
|
|
||||||
|
$result = $this->resolver->resolveType($insurance);
|
||||||
|
|
||||||
|
$this->assertEquals(InsuranceTypeResolver::TRAVEL_CANCELLATION, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testResolveTypeForIndividualFamilyTravelCancellationInsurance(): void
|
||||||
|
{
|
||||||
|
$insurance = new Insurance();
|
||||||
|
$insurance->package = false;
|
||||||
|
$insurance->subType = 'RRV';
|
||||||
|
$insurance->familyInsurance = true;
|
||||||
|
|
||||||
|
$result = $this->resolver->resolveType($insurance);
|
||||||
|
|
||||||
|
$this->assertEquals(InsuranceTypeResolver::TRAVEL_CANCELLATION_FAMILY, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testResolveTypeForIndividualTravelProtectionInsurance(): void
|
||||||
|
{
|
||||||
|
$insurance = new Insurance();
|
||||||
|
$insurance->package = false;
|
||||||
|
$insurance->subType = 'PAK';
|
||||||
|
$insurance->familyInsurance = false;
|
||||||
|
|
||||||
|
$result = $this->resolver->resolveType($insurance);
|
||||||
|
|
||||||
|
$this->assertEquals(InsuranceTypeResolver::TRAVEL_PROTECTION, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testResolveTypeForIndividualFamilyTravelProtectionInsurance(): void
|
||||||
|
{
|
||||||
|
$insurance = new Insurance();
|
||||||
|
$insurance->package = false;
|
||||||
|
$insurance->subType = 'PAK';
|
||||||
|
$insurance->familyInsurance = true;
|
||||||
|
|
||||||
|
$result = $this->resolver->resolveType($insurance);
|
||||||
|
|
||||||
|
$this->assertEquals(InsuranceTypeResolver::TRAVEL_PROTECTION_FAMILY, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testResolveTypeForIndividualDeductibleInsurance(): void
|
||||||
|
{
|
||||||
|
$insurance = new Insurance();
|
||||||
|
$insurance->package = false;
|
||||||
|
$insurance->subType = 'OHN';
|
||||||
|
$insurance->familyInsurance = false;
|
||||||
|
|
||||||
|
$result = $this->resolver->resolveType($insurance);
|
||||||
|
|
||||||
|
$this->assertEquals(InsuranceTypeResolver::DEDUCTIBLE, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testResolveTypeForIndividualUnknownSubtype(): void
|
||||||
|
{
|
||||||
|
$insurance = new Insurance();
|
||||||
|
$insurance->package = false;
|
||||||
|
$insurance->subType = 'UNKNOWN';
|
||||||
|
$insurance->familyInsurance = false;
|
||||||
|
|
||||||
|
$result = $this->resolver->resolveType($insurance);
|
||||||
|
|
||||||
|
$this->assertNull($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testResolveTypeForIndividualNullSubtype(): void
|
||||||
|
{
|
||||||
|
$insurance = new Insurance();
|
||||||
|
$insurance->package = false;
|
||||||
|
$insurance->subType = null;
|
||||||
|
$insurance->familyInsurance = false;
|
||||||
|
|
||||||
|
$result = $this->resolver->resolveType($insurance);
|
||||||
|
|
||||||
|
$this->assertNull($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testResolveTypeForPackageWithTravelProtectionContent(): void
|
||||||
|
{
|
||||||
|
// Create contained insurances
|
||||||
|
$protectionInsurance = new Insurance();
|
||||||
|
$protectionInsurance->subType = 'PAK';
|
||||||
|
|
||||||
|
$deductibleInsurance = new Insurance();
|
||||||
|
$deductibleInsurance->subType = 'OHN';
|
||||||
|
|
||||||
|
// Create package
|
||||||
|
$package = new Insurance();
|
||||||
|
$package->package = true;
|
||||||
|
$package->familyInsurance = false;
|
||||||
|
$package->containedInsurances = [$protectionInsurance, $deductibleInsurance];
|
||||||
|
|
||||||
|
$result = $this->resolver->resolveType($package);
|
||||||
|
|
||||||
|
$this->assertEquals(InsuranceTypeResolver::TRAVEL_PROTECTION, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testResolveTypeForFamilyPackageWithTravelProtectionContent(): void
|
||||||
|
{
|
||||||
|
// Create contained insurances
|
||||||
|
$protectionInsurance = new Insurance();
|
||||||
|
$protectionInsurance->subType = 'PAK';
|
||||||
|
|
||||||
|
$deductibleInsurance = new Insurance();
|
||||||
|
$deductibleInsurance->subType = 'OHN';
|
||||||
|
|
||||||
|
// Create family package
|
||||||
|
$package = new Insurance();
|
||||||
|
$package->package = true;
|
||||||
|
$package->familyInsurance = true;
|
||||||
|
$package->containedInsurances = [$protectionInsurance, $deductibleInsurance];
|
||||||
|
|
||||||
|
$result = $this->resolver->resolveType($package);
|
||||||
|
|
||||||
|
$this->assertEquals(InsuranceTypeResolver::TRAVEL_PROTECTION_FAMILY, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testResolveTypeForPackageWithTravelCancellationContent(): void
|
||||||
|
{
|
||||||
|
// Create contained insurances
|
||||||
|
$cancellationInsurance = new Insurance();
|
||||||
|
$cancellationInsurance->subType = 'RRV';
|
||||||
|
|
||||||
|
$deductibleInsurance = new Insurance();
|
||||||
|
$deductibleInsurance->subType = 'OHN';
|
||||||
|
|
||||||
|
// Create package
|
||||||
|
$package = new Insurance();
|
||||||
|
$package->package = true;
|
||||||
|
$package->familyInsurance = false;
|
||||||
|
$package->containedInsurances = [$cancellationInsurance, $deductibleInsurance];
|
||||||
|
|
||||||
|
$result = $this->resolver->resolveType($package);
|
||||||
|
|
||||||
|
$this->assertEquals(InsuranceTypeResolver::TRAVEL_CANCELLATION, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testResolveTypeForPackageWithOnlyDeductibleContent(): void
|
||||||
|
{
|
||||||
|
// Create contained insurance (only deductible)
|
||||||
|
$deductibleInsurance = new Insurance();
|
||||||
|
$deductibleInsurance->subType = 'OHN';
|
||||||
|
|
||||||
|
// Create package
|
||||||
|
$package = new Insurance();
|
||||||
|
$package->package = true;
|
||||||
|
$package->familyInsurance = false;
|
||||||
|
$package->containedInsurances = [$deductibleInsurance];
|
||||||
|
|
||||||
|
$result = $this->resolver->resolveType($package);
|
||||||
|
|
||||||
|
$this->assertNull($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testResolveTypeForPackageWithNoContainedInsurances(): void
|
||||||
|
{
|
||||||
|
$package = new Insurance();
|
||||||
|
$package->package = true;
|
||||||
|
$package->familyInsurance = false;
|
||||||
|
$package->containedInsurances = [];
|
||||||
|
|
||||||
|
$result = $this->resolver->resolveType($package);
|
||||||
|
|
||||||
|
$this->assertNull($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testResolveTypeForPackageWithMultiplePrimaryTypes(): void
|
||||||
|
{
|
||||||
|
// Create contained insurances with multiple primary types
|
||||||
|
$protectionInsurance = new Insurance();
|
||||||
|
$protectionInsurance->subType = 'PAK';
|
||||||
|
|
||||||
|
$cancellationInsurance = new Insurance();
|
||||||
|
$cancellationInsurance->subType = 'RRV';
|
||||||
|
|
||||||
|
// Create package - should return first non-deductible type found
|
||||||
|
$package = new Insurance();
|
||||||
|
$package->package = true;
|
||||||
|
$package->familyInsurance = false;
|
||||||
|
$package->containedInsurances = [$protectionInsurance, $cancellationInsurance];
|
||||||
|
|
||||||
|
$result = $this->resolver->resolveType($package);
|
||||||
|
|
||||||
|
$this->assertEquals(InsuranceTypeResolver::TRAVEL_PROTECTION, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetAllTypes(): void
|
||||||
|
{
|
||||||
|
$types = $this->resolver->getAllTypes();
|
||||||
|
|
||||||
|
$expectedTypes = [
|
||||||
|
InsuranceTypeResolver::TRAVEL_CANCELLATION,
|
||||||
|
InsuranceTypeResolver::TRAVEL_CANCELLATION_FAMILY,
|
||||||
|
InsuranceTypeResolver::TRAVEL_PROTECTION,
|
||||||
|
InsuranceTypeResolver::TRAVEL_PROTECTION_FAMILY,
|
||||||
|
InsuranceTypeResolver::DEDUCTIBLE,
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->assertEquals($expectedTypes, $types);
|
||||||
|
$this->assertCount(5, $types);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsFamilyType(): void
|
||||||
|
{
|
||||||
|
$this->assertTrue($this->resolver->isFamilyType(InsuranceTypeResolver::TRAVEL_CANCELLATION_FAMILY));
|
||||||
|
$this->assertTrue($this->resolver->isFamilyType(InsuranceTypeResolver::TRAVEL_PROTECTION_FAMILY));
|
||||||
|
$this->assertFalse($this->resolver->isFamilyType(InsuranceTypeResolver::TRAVEL_CANCELLATION));
|
||||||
|
$this->assertFalse($this->resolver->isFamilyType(InsuranceTypeResolver::TRAVEL_PROTECTION));
|
||||||
|
$this->assertFalse($this->resolver->isFamilyType(InsuranceTypeResolver::DEDUCTIBLE));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetBaseType(): void
|
||||||
|
{
|
||||||
|
$this->assertEquals(
|
||||||
|
InsuranceTypeResolver::TRAVEL_CANCELLATION,
|
||||||
|
$this->resolver->getBaseType(InsuranceTypeResolver::TRAVEL_CANCELLATION_FAMILY)
|
||||||
|
);
|
||||||
|
$this->assertEquals(
|
||||||
|
InsuranceTypeResolver::TRAVEL_PROTECTION,
|
||||||
|
$this->resolver->getBaseType(InsuranceTypeResolver::TRAVEL_PROTECTION_FAMILY)
|
||||||
|
);
|
||||||
|
$this->assertEquals(
|
||||||
|
InsuranceTypeResolver::TRAVEL_CANCELLATION,
|
||||||
|
$this->resolver->getBaseType(InsuranceTypeResolver::TRAVEL_CANCELLATION)
|
||||||
|
);
|
||||||
|
$this->assertEquals(
|
||||||
|
InsuranceTypeResolver::DEDUCTIBLE,
|
||||||
|
$this->resolver->getBaseType(InsuranceTypeResolver::DEDUCTIBLE)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user