diff --git a/src/Service/InsuranceMatchingService.php b/src/Service/InsuranceMatchingService.php new file mode 100644 index 0000000..cbbd0f2 --- /dev/null +++ b/src/Service/InsuranceMatchingService.php @@ -0,0 +1,225 @@ + $insurances Available insurances to filter + * @param ParticipantDto $participant The participant to match insurances for + * @param BookingCreateDto $booking The booking context for additional criteria + * + * @return array Filtered array of eligible insurances + */ + public function getEligibleInsurances(array $insurances, ParticipantDto $participant, BookingCreateDto $booking): array + { + $travelStartDate = $booking->travel->dateFrom; + $travelEndDate = $booking->travel->dateTo; + + // Cannot match insurances without travel dates + if (null === $travelStartDate || null === $travelEndDate) { + return []; + } + + $bookingDate = Carbon::now()->toDateTimeImmutable(); + $travelPrice = $this->calculateTravelPrice($booking); + $travelDurationDays = $this->calculateTravelDurationDays($travelStartDate, $travelEndDate); + + return array_filter($insurances, function (Insurance $insurance) use ($participant, $travelStartDate, $travelEndDate, $bookingDate, $travelPrice, $travelDurationDays) { + return $this->isInsuranceEligible($insurance, $participant, $travelStartDate, $travelEndDate, $bookingDate, $travelPrice, $travelDurationDays); + }); + } + + /** + * Checks if a specific insurance is eligible for a participant. + * + * @param Insurance $insurance The insurance to check + * @param ParticipantDto $participant The participant + * @param \DateTimeImmutable $travelStartDate Travel start date + * @param \DateTimeImmutable $travelEndDate Travel end date + * @param \DateTimeImmutable $bookingDate Booking date + * @param float $travelPrice Total travel price + * @param int $travelDurationDays Travel duration in days + * + * @return bool True if insurance is eligible + */ + private function isInsuranceEligible( + Insurance $insurance, + ParticipantDto $participant, + \DateTimeImmutable $travelStartDate, + \DateTimeImmutable $travelEndDate, + \DateTimeImmutable $bookingDate, + float $travelPrice, + int $travelDurationDays, + ): bool { + // Age constraints + if (!$this->checkAgeConstraints($insurance, $participant, $travelStartDate)) { + return false; + } + + // Travel date constraints + if (!$this->checkTravelDateConstraints($insurance, $travelStartDate, $travelEndDate)) { + return false; + } + + // Booking date constraints + if (!$this->checkBookingDateConstraints($insurance, $bookingDate)) { + return false; + } + + // Travel price constraints + if (!$this->checkTravelPriceConstraints($insurance, $travelPrice)) { + return false; + } + + // Travel duration constraints + if (!$this->checkTravelDurationConstraints($insurance, $travelDurationDays)) { + return false; + } + + return true; + } + + /** + * Checks if participant age meets insurance age constraints. + */ + private function checkAgeConstraints(Insurance $insurance, ParticipantDto $participant, \DateTimeImmutable $travelStartDate): bool + { + $participantAge = $participant->getAge($travelStartDate); + + if (null === $participantAge) { + return false; // Cannot determine age eligibility without birth date + } + + // Check minimum age + if (null !== $insurance->ageFrom && $participantAge < $insurance->ageFrom) { + return false; + } + + // Check maximum age + if (null !== $insurance->ageTo && $participantAge > $insurance->ageTo) { + return false; + } + + return true; + } + + /** + * Checks if travel dates fall within insurance validity period. + */ + private function checkTravelDateConstraints(Insurance $insurance, \DateTimeImmutable $travelStartDate, \DateTimeImmutable $travelEndDate): bool + { + // Check travel start date + if (null !== $insurance->travelDateFrom && $travelStartDate < $insurance->travelDateFrom) { + return false; + } + + if (null !== $insurance->travelDateTo && $travelStartDate > $insurance->travelDateTo) { + return false; + } + + // Check travel end date + if (null !== $insurance->travelDateTo && $travelEndDate > $insurance->travelDateTo) { + return false; + } + + return true; + } + + /** + * Checks if booking date falls within insurance booking window. + */ + private function checkBookingDateConstraints(Insurance $insurance, \DateTimeImmutable $bookingDate): bool + { + // Check booking window start + if (null !== $insurance->bookingDateFrom && $bookingDate < $insurance->bookingDateFrom) { + return false; + } + + // Check booking window end + if (null !== $insurance->bookingDateTo && $bookingDate > $insurance->bookingDateTo) { + return false; + } + + return true; + } + + /** + * Checks if travel price falls within insurance price range. + */ + private function checkTravelPriceConstraints(Insurance $insurance, float $travelPrice): bool + { + // Check minimum price + if (null !== $insurance->travelPriceFrom && $travelPrice < $insurance->travelPriceFrom) { + return false; + } + + // Check maximum price + if (null !== $insurance->travelPriceTo && $travelPrice > $insurance->travelPriceTo) { + return false; + } + + return true; + } + + /** + * Checks if travel duration falls within insurance duration limits. + */ + private function checkTravelDurationConstraints(Insurance $insurance, int $travelDurationDays): bool + { + // Check minimum duration + if (null !== $insurance->travelDurationFrom && $travelDurationDays < $insurance->travelDurationFrom) { + return false; + } + + // Check maximum duration + if (null !== $insurance->travelDurationTo && $travelDurationDays > $insurance->travelDurationTo) { + return false; + } + + return true; + } + + /** + * Calculates the total travel price from booking data. + * + * @param BookingCreateDto $booking The booking to calculate price for + * + * @return float The total travel price + */ + private function calculateTravelPrice(BookingCreateDto $booking): float + { + // For now, return 0.0 as placeholder - this will be enhanced + // when we integrate with the existing pricing calculation system + return 0.0; + } + + /** + * Calculates travel duration in days. + * + * @param \DateTimeImmutable $startDate Travel start date + * @param \DateTimeImmutable $endDate Travel end date + * + * @return int Duration in days + */ + private function calculateTravelDurationDays(\DateTimeImmutable $startDate, \DateTimeImmutable $endDate): int + { + return $startDate->diff($endDate)->days; + } +} diff --git a/tests/Service/InsuranceMatchingServiceTest.php b/tests/Service/InsuranceMatchingServiceTest.php new file mode 100644 index 0000000..4a132fd --- /dev/null +++ b/tests/Service/InsuranceMatchingServiceTest.php @@ -0,0 +1,270 @@ +service = new InsuranceMatchingService(); + + // Set a fixed test date for consistent test results + Carbon::setTestNow('2024-06-01 12:00:00'); + } + + protected function tearDown(): void + { + // Reset Carbon's test now after each test + Carbon::setTestNow(); + } + + public function testGetEligibleInsurancesWithMatchingCriteria(): void + { + $participant = $this->createParticipant('1990-06-15'); + $booking = $this->createBooking('2024-08-01', '2024-08-08'); + + $insurance = $this->createInsurance([ + 'ageFrom' => 20, + 'ageTo' => 50, + 'travelDateFrom' => new \DateTimeImmutable('2024-01-01'), + 'travelDateTo' => new \DateTimeImmutable('2024-12-31'), + 'bookingDateFrom' => new \DateTimeImmutable('2024-01-01'), + 'bookingDateTo' => new \DateTimeImmutable('2024-12-31'), + 'travelPriceFrom' => 0.0, + 'travelPriceTo' => 1000.0, + 'travelDurationFrom' => 1, + 'travelDurationTo' => 14, + ]); + + $result = $this->service->getEligibleInsurances([$insurance], $participant, $booking); + + $this->assertCount(1, $result); + $this->assertContains($insurance, $result); + } + + public function testGetEligibleInsurancesExcludesInsuranceWithAgeTooYoung(): void + { + $participant = $this->createParticipant('2010-06-15'); // 14 years old in 2024 + $booking = $this->createBooking('2024-08-01', '2024-08-08'); + + $insurance = $this->createInsurance(['ageFrom' => 18, 'ageTo' => 65]); + + $result = $this->service->getEligibleInsurances([$insurance], $participant, $booking); + + $this->assertEmpty($result); + } + + public function testGetEligibleInsurancesExcludesInsuranceWithAgeTooOld(): void + { + $participant = $this->createParticipant('1950-06-15'); // 74 years old in 2024 + $booking = $this->createBooking('2024-08-01', '2024-08-08'); + + $insurance = $this->createInsurance(['ageFrom' => 18, 'ageTo' => 65]); + + $result = $this->service->getEligibleInsurances([$insurance], $participant, $booking); + + $this->assertEmpty($result); + } + + public function testGetEligibleInsurancesExcludesInsuranceWithTravelDateTooEarly(): void + { + $participant = $this->createParticipant('1990-06-15'); + $booking = $this->createBooking('2024-08-01', '2024-08-08'); + + $insurance = $this->createInsurance([ + 'travelDateFrom' => new \DateTimeImmutable('2024-09-01'), + 'travelDateTo' => new \DateTimeImmutable('2024-12-31'), + ]); + + $result = $this->service->getEligibleInsurances([$insurance], $participant, $booking); + + $this->assertEmpty($result); + } + + public function testGetEligibleInsurancesExcludesInsuranceWithTravelDateTooLate(): void + { + $participant = $this->createParticipant('1990-06-15'); + $booking = $this->createBooking('2024-08-01', '2024-08-08'); + + $insurance = $this->createInsurance([ + 'travelDateFrom' => new \DateTimeImmutable('2024-01-01'), + 'travelDateTo' => new \DateTimeImmutable('2024-07-31'), + ]); + + $result = $this->service->getEligibleInsurances([$insurance], $participant, $booking); + + $this->assertEmpty($result); + } + + public function testGetEligibleInsurancesExcludesInsuranceWithBookingDateTooEarly(): void + { + $participant = $this->createParticipant('1990-06-15'); + $booking = $this->createBooking('2024-08-01', '2024-08-08'); + + $insurance = $this->createInsurance([ + 'bookingDateFrom' => new \DateTimeImmutable('2025-01-01'), + 'bookingDateTo' => new \DateTimeImmutable('2025-12-31'), + ]); + + $result = $this->service->getEligibleInsurances([$insurance], $participant, $booking); + + $this->assertEmpty($result); + } + + public function testGetEligibleInsurancesExcludesInsuranceWithBookingDateTooLate(): void + { + $participant = $this->createParticipant('1990-06-15'); + $booking = $this->createBooking('2024-08-01', '2024-08-08'); + + $insurance = $this->createInsurance([ + 'bookingDateFrom' => new \DateTimeImmutable('2023-01-01'), + 'bookingDateTo' => new \DateTimeImmutable('2023-12-31'), + ]); + + $result = $this->service->getEligibleInsurances([$insurance], $participant, $booking); + + $this->assertEmpty($result); + } + + public function testGetEligibleInsurancesExcludesInsuranceWithTravelDurationTooShort(): void + { + $participant = $this->createParticipant('1990-06-15'); + $booking = $this->createBooking('2024-08-01', '2024-08-03'); // 2 days + + $insurance = $this->createInsurance(['travelDurationFrom' => 7, 'travelDurationTo' => 30]); + + $result = $this->service->getEligibleInsurances([$insurance], $participant, $booking); + + $this->assertEmpty($result); + } + + public function testGetEligibleInsurancesExcludesInsuranceWithTravelDurationTooLong(): void + { + $participant = $this->createParticipant('1990-06-15'); + $booking = $this->createBooking('2024-08-01', '2024-08-31'); // 30 days + + $insurance = $this->createInsurance(['travelDurationFrom' => 1, 'travelDurationTo' => 14]); + + $result = $this->service->getEligibleInsurances([$insurance], $participant, $booking); + + $this->assertEmpty($result); + } + + public function testGetEligibleInsurancesHandlesParticipantWithoutBirthDate(): void + { + $participant = new ParticipantDto(); + $participant->dateOfBirth = null; + $booking = $this->createBooking('2024-08-01', '2024-08-08'); + + $insurance = $this->createInsurance(['ageFrom' => 18, 'ageTo' => 65]); + + $result = $this->service->getEligibleInsurances([$insurance], $participant, $booking); + + $this->assertEmpty($result); + } + + public function testGetEligibleInsurancesWithNullConstraints(): void + { + $participant = $this->createParticipant('1990-06-15'); + $booking = $this->createBooking('2024-08-01', '2024-08-08'); + + // Insurance with no constraints should match any criteria + $insurance = $this->createInsurance([]); + + $result = $this->service->getEligibleInsurances([$insurance], $participant, $booking); + + $this->assertCount(1, $result); + $this->assertContains($insurance, $result); + } + + public function testGetEligibleInsurancesWithMultipleInsurances(): void + { + $participant = $this->createParticipant('1990-06-15'); + $booking = $this->createBooking('2024-08-01', '2024-08-08'); + + $eligibleInsurance1 = $this->createInsurance(['ageFrom' => 20, 'ageTo' => 50]); + $eligibleInsurance2 = $this->createInsurance(['ageFrom' => 30, 'ageTo' => 40]); + $ineligibleInsurance = $this->createInsurance(['ageFrom' => 60, 'ageTo' => 80]); + + $insurances = [$eligibleInsurance1, $eligibleInsurance2, $ineligibleInsurance]; + $result = $this->service->getEligibleInsurances($insurances, $participant, $booking); + + $this->assertCount(2, $result); + $this->assertContains($eligibleInsurance1, $result); + $this->assertContains($eligibleInsurance2, $result); + $this->assertNotContains($ineligibleInsurance, $result); + } + + public function testGetEligibleInsurancesWithAgeExactlyAtBoundary(): void + { + $participant = $this->createParticipant('1990-08-01'); // Exactly 34 on travel start + $booking = $this->createBooking('2024-08-01', '2024-08-08'); + + $insurance = $this->createInsurance(['ageFrom' => 34, 'ageTo' => 34]); + + $result = $this->service->getEligibleInsurances([$insurance], $participant, $booking); + + $this->assertCount(1, $result); + $this->assertContains($insurance, $result); + } + + public function testGetEligibleInsurancesWithTravelDurationExactlyAtBoundary(): void + { + $participant = $this->createParticipant('1990-06-15'); + $booking = $this->createBooking('2024-08-01', '2024-08-08'); // Exactly 7 days + + $insurance = $this->createInsurance(['travelDurationFrom' => 7, 'travelDurationTo' => 7]); + + $result = $this->service->getEligibleInsurances([$insurance], $participant, $booking); + + $this->assertCount(1, $result); + $this->assertContains($insurance, $result); + } + + private function createParticipant(string $dateOfBirth): ParticipantDto + { + $participant = new ParticipantDto(); + $participant->dateOfBirth = new \DateTimeImmutable($dateOfBirth); + + return $participant; + } + + private function createBooking(string $travelDateFrom, string $travelDateTo): BookingCreateDto + { + $travel = $this->createTravel($travelDateFrom, $travelDateTo); + $booking = new BookingCreateDto($travel, 1); + + return $booking; + } + + private function createTravel(string $dateFrom, string $dateTo): \App\BusProNet\Model\Travel + { + $travel = new \App\BusProNet\Model\Travel(); + $travel->dateFrom = new \DateTimeImmutable($dateFrom); + $travel->dateTo = new \DateTimeImmutable($dateTo); + + return $travel; + } + + private function createInsurance(array $constraints = []): Insurance + { + $insurance = new Insurance(); + + foreach ($constraints as $property => $value) { + $insurance->$property = $value; + } + + return $insurance; + } +}