package org.acme.flighcrewscheduling.solver; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import jakarta.inject.Inject; import ai.timefold.solver.test.api.score.stream.ConstraintVerifier; import org.acme.flighcrewscheduling.domain.Airport; import org.acme.flighcrewscheduling.domain.Employee; import org.acme.flighcrewscheduling.domain.Flight; import org.acme.flighcrewscheduling.domain.FlightAssignment; import org.acme.flighcrewscheduling.domain.FlightCrewSchedule; import org.junit.jupiter.api.Test; import io.quarkus.test.junit.QuarkusTest; @QuarkusTest class FlightCrewSchedulingConstraintProviderTest { private final ConstraintVerifier constraintVerifier; @Inject public FlightCrewSchedulingConstraintProviderTest( ConstraintVerifier constraintVerifier) { this.constraintVerifier = constraintVerifier; } @Test void requiredSkill() { FlightAssignment assignment = new FlightAssignment("1", null, 0, "1"); Employee employee = new Employee("1"); employee.setSkills(List.of("2")); assignment.setEmployee(employee); constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::requiredSkill) .given(assignment) .penalizesBy(1); // missing requiredSkill } @Test void flightConflict() { Employee employee = new Employee("1"); Flight flight = new Flight("1", null, LocalDateTime.now(), null, LocalDateTime.now().plusMinutes(10)); FlightAssignment assignment = new FlightAssignment("1", flight); assignment.setEmployee(employee); Flight overlappingFlight = new Flight("1", null, LocalDateTime.now().plusMinutes(1), null, LocalDateTime.now().plusMinutes(11)); FlightAssignment overlappingAssignment = new FlightAssignment("2", overlappingFlight); overlappingAssignment.setEmployee(employee); constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::flightConflict) .given(assignment, overlappingAssignment) .penalizesBy(1); // one overlapping thirdFlight } @Test void transferBetweenTwoFlights() { Employee employee = new Employee("1"); Airport firstAirport = new Airport("1"); Airport secondAirport = new Airport("2"); Flight firstFlight = new Flight("1", firstAirport, LocalDateTime.now(), secondAirport, LocalDateTime.now().plusMinutes(10)); FlightAssignment firstAssignment = new FlightAssignment("1", firstFlight); firstAssignment.setEmployee(employee); Flight firstInvalidFlight = new Flight("2", firstAirport, LocalDateTime.now().plusMinutes(11), secondAirport, LocalDateTime.now().plusMinutes(12)); FlightAssignment firstInvalidAssignment = new FlightAssignment("2", firstInvalidFlight); firstInvalidAssignment.setEmployee(employee); Flight secondInvalidFlight = new Flight("3", firstAirport, LocalDateTime.now().plusMinutes(13), secondAirport, LocalDateTime.now().plusMinutes(14)); FlightAssignment secondInvalidAssignment = new FlightAssignment("3", secondInvalidFlight); secondInvalidAssignment.setEmployee(employee); constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::transferBetweenTwoFlights) .given(firstAssignment, firstInvalidAssignment, secondInvalidAssignment) .penalizesBy(2); // two invalid connections } @Test void employeeUnavailability() { var date = LocalDate.now(); var employee = new Employee("1"); employee.setUnavailableDays(List.of(date)); var flight = new Flight("1", null, date.atStartOfDay(), null, date.atStartOfDay().plusMinutes(10)); var assignment = new FlightAssignment("1", flight); assignment.setEmployee(employee); constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::employeeUnavailability) .given(assignment) .penalizesBy(1); // unavailable at departure flight.setDepartureUTCDateTime(date.minusDays(1).atStartOfDay()); constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::employeeUnavailability) .given(assignment) .penalizesBy(1); // unavailable during flight flight.setDepartureUTCDateTime(date.plusDays(1).atStartOfDay()); flight.setArrivalUTCDateTime(date.plusDays(1).atStartOfDay().plusMinutes(10)); constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::employeeUnavailability) .given(assignment) .penalizesBy(0); // employee available } @Test void firstAssignmentNotDepartingFromHome() { Employee employee = new Employee("1"); employee.setHomeAirport(new Airport("1")); employee.setUnavailableDays(List.of(LocalDate.now())); Flight flight = new Flight("1", new Airport("2"), LocalDateTime.now(), new Airport("3"), LocalDateTime.now().plusMinutes(10)); FlightAssignment assignment = new FlightAssignment("1", flight); assignment.setEmployee(employee); Flight secondFlight = new Flight("2", new Airport("2"), LocalDateTime.now().plusMinutes(1), new Airport("3"), LocalDateTime.now().plusMinutes(10)); FlightAssignment secondAssignment = new FlightAssignment("2", secondFlight); secondAssignment.setEmployee(employee); Flight thirdFlight = new Flight("3", new Airport("2"), LocalDateTime.now().plusMinutes(1), new Airport("3"), LocalDateTime.now().plusMinutes(10)); FlightAssignment thirdAssignment = new FlightAssignment("3", thirdFlight); thirdAssignment.setEmployee(employee); Employee secondEmployee = new Employee("2"); secondEmployee.setHomeAirport(new Airport("3")); secondEmployee.setUnavailableDays(List.of(LocalDate.now())); Flight fourthFlight = new Flight("4", new Airport("3"), LocalDateTime.now(), new Airport("4"), LocalDateTime.now().plusMinutes(10)); FlightAssignment fourthAssignment = new FlightAssignment("4", fourthFlight); fourthAssignment.setEmployee(secondEmployee); constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::firstAssignmentNotDepartingFromHome) .given(employee, secondEmployee, assignment, secondAssignment, thirdAssignment, fourthAssignment) .penalizesBy(1); // invalid first airport } @Test void lastAssignmentNotArrivingAtHome() { Employee employee = new Employee("1"); employee.setHomeAirport(new Airport("1")); employee.setUnavailableDays(List.of(LocalDate.now())); Flight firstFlight = new Flight("1", new Airport("2"), LocalDateTime.now(), new Airport("3"), LocalDateTime.now().plusMinutes(10)); FlightAssignment firstAssignment = new FlightAssignment("1", firstFlight); firstAssignment.setEmployee(employee); Flight secondFlight = new Flight("2", new Airport("3"), LocalDateTime.now().plusMinutes(11), new Airport("4"), LocalDateTime.now().plusMinutes(12)); FlightAssignment secondAssignment = new FlightAssignment("2", secondFlight); secondAssignment.setEmployee(employee); Employee secondEmployee = new Employee("2"); secondEmployee.setHomeAirport(new Airport("2")); secondEmployee.setUnavailableDays(List.of(LocalDate.now())); Flight thirdFlight = new Flight("3", new Airport("2"), LocalDateTime.now(), new Airport("3"), LocalDateTime.now().plusMinutes(10)); FlightAssignment thirdFlightAssignment = new FlightAssignment("3", thirdFlight); thirdFlightAssignment.setEmployee(secondEmployee); Flight fourthFlight = new Flight("4", new Airport("3"), LocalDateTime.now().plusMinutes(11), new Airport("2"), LocalDateTime.now().plusMinutes(12)); FlightAssignment fourthFlightAssignment = new FlightAssignment("4", fourthFlight); fourthFlightAssignment.setEmployee(secondEmployee); constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::lastAssignmentNotArrivingAtHome) .given(employee, secondEmployee, firstAssignment, secondAssignment, thirdFlightAssignment, fourthFlightAssignment) .penalizesBy(1); // invalid last airport } @Test void minimumRestAtHomeBase_satisfied() { Employee employee = new Employee("1"); Airport homeAirport = new Airport("LHR"); employee.setHomeAirport(homeAirport); LocalDateTime now = LocalDateTime.now(); // First flight: 8 hours duration, FDP = 8h + 65min = ~8.08h // Required rest at home base = max(8.08, 12) = 12 hours Flight firstFlight = new Flight("1", new Airport("LHR"), now, new Airport("JFK"), now.plusHours(8)); FlightAssignment firstAssignment = new FlightAssignment("1", firstFlight); firstAssignment.setEmployee(employee); // Second flight departs from home base 13 hours after first flight duty end // Duty end of first = arrival + 20 min = now + 8h + 20min // Second flight duty start = departure - 45 min // Gap = 13 hours (satisfies 12 hour minimum) LocalDateTime secondDeparture = now.plusHours(8).plusMinutes(20).plusHours(13).plusMinutes(45); Flight secondFlight = new Flight("2", homeAirport, secondDeparture, new Airport("BRU"), secondDeparture.plusHours(1)); FlightAssignment secondAssignment = new FlightAssignment("2", secondFlight); secondAssignment.setEmployee(employee); constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::minimumRestAtHomeBase) .given(firstAssignment, secondAssignment) .penalizesBy(0); // 13 hours rest satisfies 12 hour minimum } @Test void minimumRestAtHomeBase_violated() { Employee employee = new Employee("1"); Airport homeAirport = new Airport("LHR"); employee.setHomeAirport(homeAirport); LocalDateTime now = LocalDateTime.now(); // First flight: 8 hours duration Flight firstFlight = new Flight("1", new Airport("LHR"), now, new Airport("JFK"), now.plusHours(8)); FlightAssignment firstAssignment = new FlightAssignment("1", firstFlight); firstAssignment.setEmployee(employee); // Second flight departs from home base only 10 hours after first flight duty end // Violates 12 hour minimum by 2 hours = 120 minutes LocalDateTime secondDeparture = now.plusHours(8).plusMinutes(20).plusHours(10).plusMinutes(45); Flight secondFlight = new Flight("2", homeAirport, secondDeparture, new Airport("BRU"), secondDeparture.plusHours(1)); FlightAssignment secondAssignment = new FlightAssignment("2", secondFlight); secondAssignment.setEmployee(employee); constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::minimumRestAtHomeBase) .given(firstAssignment, secondAssignment) .penalizesBy(120); // 2 hours = 120 minutes violation } @Test void minimumRestAtHomeBase_notApplicableAwayFromHome() { Employee employee = new Employee("1"); Airport homeAirport = new Airport("LHR"); employee.setHomeAirport(homeAirport); LocalDateTime now = LocalDateTime.now(); Flight firstFlight = new Flight("1", new Airport("LHR"), now, new Airport("JFK"), now.plusHours(8)); FlightAssignment firstAssignment = new FlightAssignment("1", firstFlight); firstAssignment.setEmployee(employee); // Second flight does NOT depart from home base - constraint should not apply LocalDateTime secondDeparture = now.plusHours(8).plusMinutes(20).plusHours(5).plusMinutes(45); Flight secondFlight = new Flight("2", new Airport("JFK"), secondDeparture, new Airport("BRU"), secondDeparture.plusHours(1)); FlightAssignment secondAssignment = new FlightAssignment("2", secondFlight); secondAssignment.setEmployee(employee); constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::minimumRestAtHomeBase) .given(firstAssignment, secondAssignment) .penalizesBy(0); // constraint doesn't apply when not at home base } @Test void minimumRestAwayFromHomeBase_satisfied() { Employee employee = new Employee("1"); Airport homeAirport = new Airport("LHR"); employee.setHomeAirport(homeAirport); Airport jfk = new Airport("JFK"); Airport atl = new Airport("ATL"); LocalDateTime now = LocalDateTime.now(); // First flight ends at JFK Flight firstFlight = new Flight("1", new Airport("LHR"), now, jfk, now.plusHours(8)); FlightAssignment firstAssignment = new FlightAssignment("1", firstFlight); firstAssignment.setEmployee(employee); // Second flight departs from ATL (away from home) // Required rest = max(8.08, 10) = 10 hours (no taxi time adjustment for test simplicity) // Provide 11 hours rest LocalDateTime secondDeparture = now.plusHours(8).plusMinutes(20).plusHours(11).plusMinutes(45); Flight secondFlight = new Flight("2", atl, secondDeparture, new Airport("BRU"), secondDeparture.plusHours(2)); FlightAssignment secondAssignment = new FlightAssignment("2", secondFlight); secondAssignment.setEmployee(employee); constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::minimumRestAwayFromHomeBase) .given(firstAssignment, secondAssignment) .penalizesBy(0); // 11 hours satisfies 10 hour minimum } @Test void minimumRestAwayFromHomeBase_violated() { Employee employee = new Employee("1"); Airport homeAirport = new Airport("LHR"); employee.setHomeAirport(homeAirport); Airport jfk = new Airport("JFK"); Airport atl = new Airport("ATL"); LocalDateTime now = LocalDateTime.now(); Flight firstFlight = new Flight("1", new Airport("LHR"), now, jfk, now.plusHours(8)); FlightAssignment firstAssignment = new FlightAssignment("1", firstFlight); firstAssignment.setEmployee(employee); // Second flight away from home with only 8 hours rest // Violates 10 hour minimum by 2 hours = 120 minutes LocalDateTime secondDeparture = now.plusHours(8).plusMinutes(20).plusHours(8).plusMinutes(45); Flight secondFlight = new Flight("2", atl, secondDeparture, new Airport("BRU"), secondDeparture.plusHours(2)); FlightAssignment secondAssignment = new FlightAssignment("2", secondFlight); secondAssignment.setEmployee(employee); constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::minimumRestAwayFromHomeBase) .given(firstAssignment, secondAssignment) .penalizesBy(120); // 2 hours = 120 minutes violation } @Test void minimumRestAwayFromHomeBase_withTravelTimeAdjustment() { Employee employee = new Employee("1"); Airport homeAirport = new Airport("LHR"); employee.setHomeAirport(homeAirport); Airport jfk = new Airport("JFK"); jfk.setTaxiTimeInMinutes(java.util.Map.of("ATL", 180L)); // 3 hours taxi time Airport atl = new Airport("ATL"); LocalDateTime now = LocalDateTime.now(); Flight firstFlight = new Flight("1", new Airport("LHR"), now, jfk, now.plusHours(8)); FlightAssignment firstAssignment = new FlightAssignment("1", firstFlight); firstAssignment.setEmployee(employee); // Taxi time = 180 minutes, excess = 180 - 30 = 150 minutes // Travel adjustment = 150 * 2 / 60 = 5 hours // Required rest = max(8.08, 10) + 5 = 15 hours // Provide only 12 hours rest - violates by 3 hours = 180 minutes LocalDateTime secondDeparture = now.plusHours(8).plusMinutes(20).plusHours(12).plusMinutes(45); Flight secondFlight = new Flight("2", atl, secondDeparture, new Airport("BRU"), secondDeparture.plusHours(2)); FlightAssignment secondAssignment = new FlightAssignment("2", secondFlight); secondAssignment.setEmployee(employee); constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::minimumRestAwayFromHomeBase) .given(firstAssignment, secondAssignment) .penalizesBy(180); // 3 hours violation due to travel time adjustment } @Test void extendedRecoveryRestPeriod_satisfied_frequentLongRests() { Employee employee = new Employee("1"); Airport homeAirport = new Airport("LHR"); employee.setHomeAirport(homeAirport); LocalDateTime start = LocalDateTime.of(2025, 1, 1, 8, 0); // Pattern: Work 3 days, 40h rest, repeat (well within compliance) List given = new ArrayList<>(); given.add(employee); LocalDateTime currentTime = start; for (int cycle = 0; cycle < 3; cycle++) { // 3 assignments with short rests for (int i = 0; i < 3; i++) { Flight flight = new Flight("C" + cycle + "F" + i, homeAirport, currentTime, new Airport("JFK"), currentTime.plusHours(8)); FlightAssignment assignment = new FlightAssignment("C" + cycle + "A" + i, flight); assignment.setEmployee(employee); given.add(assignment); // 15 hour rest between assignments currentTime = currentTime.plusHours(8).plusMinutes(20).plusHours(15).plusMinutes(45); } // Long rest (40 hours) after every 3 assignments currentTime = currentTime.minusMinutes(45).plusHours(40).plusMinutes(45); } constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::extendedRecoveryRestPeriod) .given(given.toArray()) .penalizesBy(0); } @Test void extendedRecoveryRestPeriod_violated_continuousShortRests() { Employee employee = new Employee("1"); Airport homeAirport = new Airport("LHR"); employee.setHomeAirport(homeAirport); LocalDateTime start = LocalDateTime.of(2025, 1, 1, 8, 0); // Create 10 assignments over 10 days with only 15-hour rests (< 36h) // This spans ~230 hours without a qualifying rest - should violate List given = new ArrayList<>(); given.add(employee); LocalDateTime currentTime = start; for (int i = 0; i < 10; i++) { Flight flight = new Flight("F" + i, homeAirport, currentTime, new Airport("JFK"), currentTime.plusHours(8)); FlightAssignment assignment = new FlightAssignment("A" + i, flight); assignment.setEmployee(employee); given.add(assignment); // 15 hour rest (duty end to next duty start) // Duty end = arrival + 20min = currentTime + 8h 20min // Next duty start = duty end + 15h // Next departure = duty start + 45min currentTime = currentTime.plusHours(23).plusMinutes(20); } // Expected violations: multiple windows will exceed 168h without 36h rest // With 10 assignments over ~230 hours with no 36h rest, there are 2 violations constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::extendedRecoveryRestPeriod) .given(given.toArray()) .penalizesBy(2); } @Test void extendedRecoveryRestPeriod_satisfied_sparseSchedule() { Employee employee = new Employee("1"); Airport homeAirport = new Airport("LHR"); employee.setHomeAirport(homeAirport); LocalDateTime start = LocalDateTime.of(2025, 1, 1, 8, 0); // Two assignments 10 days apart with long rest Flight flight1 = new Flight("F1", homeAirport, start, new Airport("JFK"), start.plusHours(8)); FlightAssignment a1 = new FlightAssignment("A1", flight1); a1.setEmployee(employee); // 230 hour (9.5 day) rest period - well over 36h LocalDateTime secondDeparture = start.plusHours(8).plusMinutes(20) .plusHours(230).plusMinutes(45); Flight flight2 = new Flight("F2", homeAirport, secondDeparture, new Airport("JFK"), secondDeparture.plusHours(8)); FlightAssignment a2 = new FlightAssignment("A2", flight2); a2.setEmployee(employee); constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::extendedRecoveryRestPeriod) .given(employee, a1, a2) .penalizesBy(0); } @Test void extendedRecoveryRestPeriod_edgeCase_singleAssignment() { Employee employee = new Employee("1"); Airport homeAirport = new Airport("LHR"); employee.setHomeAirport(homeAirport); LocalDateTime start = LocalDateTime.of(2025, 1, 1, 8, 0); Flight flight = new Flight("F1", homeAirport, start, new Airport("JFK"), start.plusHours(8)); FlightAssignment assignment = new FlightAssignment("A1", flight); assignment.setEmployee(employee); // Single assignment - no violation possible constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::extendedRecoveryRestPeriod) .given(employee, assignment) .penalizesBy(0); } @Test void extendedRecoveryRestPeriod_satisfied_weeklyPattern() { Employee employee = new Employee("1"); Airport homeAirport = new Airport("LHR"); employee.setHomeAirport(homeAirport); LocalDateTime start = LocalDateTime.of(2025, 1, 1, 8, 0); // Realistic pattern: Work Mon-Fri with 15h rests, then 48h weekend rest List given = new ArrayList<>(); given.add(employee); LocalDateTime currentTime = start; // Week 1: Mon-Fri for (int i = 0; i < 5; i++) { Flight flight = new Flight("W1F" + i, homeAirport, currentTime, new Airport("JFK"), currentTime.plusHours(8)); FlightAssignment assignment = new FlightAssignment("W1A" + i, flight); assignment.setEmployee(employee); given.add(assignment); currentTime = currentTime.plusHours(23).plusMinutes(20); // 15h rest } // Weekend rest: 48 hours currentTime = currentTime.minusMinutes(45).plusHours(48).plusMinutes(45); // Week 2: Mon-Fri for (int i = 0; i < 5; i++) { Flight flight = new Flight("W2F" + i, homeAirport, currentTime, new Airport("JFK"), currentTime.plusHours(8)); FlightAssignment assignment = new FlightAssignment("W2A" + i, flight); assignment.setEmployee(employee); given.add(assignment); currentTime = currentTime.plusHours(23).plusMinutes(20); } // Should satisfy - 48h rest every week constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::extendedRecoveryRestPeriod) .given(given.toArray()) .penalizesBy(0); } @Test void extendedRecoveryRestPeriod_multipleEmployees_isolated() { // Employee 1: Violates ERRP Employee employee1 = new Employee("1"); employee1.setHomeAirport(new Airport("LHR")); // Employee 2: Satisfies ERRP Employee employee2 = new Employee("2"); employee2.setHomeAirport(new Airport("LHR")); LocalDateTime start = LocalDateTime.of(2025, 1, 1, 8, 0); List given = new ArrayList<>(); given.add(employee1); given.add(employee2); // Employee 1: Continuous short rests (violation) LocalDateTime time1 = start; for (int i = 0; i < 10; i++) { Flight flight = new Flight("E1F" + i, new Airport("LHR"), time1, new Airport("JFK"), time1.plusHours(8)); FlightAssignment assignment = new FlightAssignment("E1A" + i, flight); assignment.setEmployee(employee1); given.add(assignment); time1 = time1.plusHours(23).plusMinutes(20); } // Employee 2: Long rests (no violation) LocalDateTime time2 = start; for (int i = 0; i < 3; i++) { Flight flight = new Flight("E2F" + i, new Airport("LHR"), time2, new Airport("JFK"), time2.plusHours(8)); FlightAssignment assignment = new FlightAssignment("E2A" + i, flight); assignment.setEmployee(employee2); given.add(assignment); time2 = time2.plusHours(8).plusMinutes(20).plusHours(50).plusMinutes(45); } // Should only penalize employee1's violations (2 violations for employee1) constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::extendedRecoveryRestPeriod) .given(given.toArray()) .penalizesBy(2); } @Test void transferBetweenTwoFlights_withExcessiveTaxiTime() { Employee employee = new Employee("1"); Airport lhr = new Airport("LHR"); // Set taxi time to same airport as 400 minutes (> 5 hours limit) lhr.setTaxiTimeInMinutes(java.util.Map.of("LHR", 400L)); LocalDateTime now = LocalDateTime.now(); Flight firstFlight = new Flight("1", lhr, now, lhr, now.plusHours(2)); FlightAssignment firstAssignment = new FlightAssignment("1", firstFlight); firstAssignment.setEmployee(employee); // Second flight also at LHR but taxi time is excessive Flight secondFlight = new Flight("2", lhr, now.plusHours(3), lhr, now.plusHours(5)); FlightAssignment secondAssignment = new FlightAssignment("2", secondFlight); secondAssignment.setEmployee(employee); constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::transferBetweenTwoFlights) .given(firstAssignment, secondAssignment) .penalizesBy(1); // same airport but excessive taxi time } @Test void transferBetweenTwoFlights_withAcceptableTaxiTime() { Employee employee = new Employee("1"); Airport lhr = new Airport("LHR"); // Set acceptable taxi time (< 5 hours) lhr.setTaxiTimeInMinutes(java.util.Map.of("LHR", 200L)); LocalDateTime now = LocalDateTime.now(); Flight firstFlight = new Flight("1", lhr, now, lhr, now.plusHours(2)); FlightAssignment firstAssignment = new FlightAssignment("1", firstFlight); firstAssignment.setEmployee(employee); Flight secondFlight = new Flight("2", lhr, now.plusHours(3), lhr, now.plusHours(5)); FlightAssignment secondAssignment = new FlightAssignment("2", secondFlight); secondAssignment.setEmployee(employee); constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::transferBetweenTwoFlights) .given(firstAssignment, secondAssignment) .penalizesBy(0); // acceptable taxi time at same airport } @Test void minimumRestAfterLongHaul_satisfied() { Employee employee = new Employee("1"); Airport homeAirport = new Airport("LHR"); employee.setHomeAirport(homeAirport); LocalDateTime now = LocalDateTime.now(); // First flight: Long haul (9 hours) Flight longHaulFlight = new Flight("1", homeAirport, now, new Airport("JFK"), now.plusHours(9)); FlightAssignment longHaulAssignment = new FlightAssignment("1", longHaulFlight); longHaulAssignment.setEmployee(employee); // Second flight: 50 hours rest after long haul (exceeds 48h requirement) // Duty end of long haul = arrival + 20 min = now + 9h + 20min // Second duty start = duty end + 50h LocalDateTime secondDeparture = now.plusHours(9).plusMinutes(20) .plusHours(50).plusMinutes(45); Flight secondFlight = new Flight("2", homeAirport, secondDeparture, new Airport("BRU"), secondDeparture.plusHours(2)); FlightAssignment secondAssignment = new FlightAssignment("2", secondFlight); secondAssignment.setEmployee(employee); constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::minimumRestAfterLongHaul) .given(longHaulAssignment, secondAssignment) .penalizesBy(0); // 50 hours rest satisfies 48 hour minimum } @Test void minimumRestAfterLongHaul_violated() { Employee employee = new Employee("1"); Airport homeAirport = new Airport("LHR"); employee.setHomeAirport(homeAirport); LocalDateTime now = LocalDateTime.now(); // First flight: Long haul (10 hours) Flight longHaulFlight = new Flight("1", homeAirport, now, new Airport("JFK"), now.plusHours(10)); FlightAssignment longHaulAssignment = new FlightAssignment("1", longHaulFlight); longHaulAssignment.setEmployee(employee); // Second flight: Only 30 hours rest after long haul // Violates 48 hour minimum by 18 hours = 1080 minutes LocalDateTime secondDeparture = now.plusHours(10).plusMinutes(20) .plusHours(30).plusMinutes(45); Flight secondFlight = new Flight("2", homeAirport, secondDeparture, new Airport("ATL"), secondDeparture.plusHours(3)); FlightAssignment secondAssignment = new FlightAssignment("2", secondFlight); secondAssignment.setEmployee(employee); constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::minimumRestAfterLongHaul) .given(longHaulAssignment, secondAssignment) .penalizesBy(1080); // 18 hours = 1080 minutes violation } @Test void minimumRestAfterLongHaul_notApplicableToShortHaul() { Employee employee = new Employee("1"); Airport homeAirport = new Airport("LHR"); employee.setHomeAirport(homeAirport); LocalDateTime now = LocalDateTime.now(); // First flight: Short haul (3 hours, below 8h threshold) Flight shortHaulFlight = new Flight("1", homeAirport, now, new Airport("BRU"), now.plusHours(3)); FlightAssignment shortHaulAssignment = new FlightAssignment("1", shortHaulFlight); shortHaulAssignment.setEmployee(employee); // Second flight: Only 15 hours rest // Constraint should not apply because first flight is not long haul LocalDateTime secondDeparture = now.plusHours(3).plusMinutes(20) .plusHours(15).plusMinutes(45); Flight secondFlight = new Flight("2", homeAirport, secondDeparture, new Airport("ATL"), secondDeparture.plusHours(8)); FlightAssignment secondAssignment = new FlightAssignment("2", secondFlight); secondAssignment.setEmployee(employee); constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::minimumRestAfterLongHaul) .given(shortHaulAssignment, secondAssignment) .penalizesBy(0); // constraint doesn't apply to short haul flights } @Test void minimumRestAfterLongHaul_exactlyTenHours() { Employee employee = new Employee("1"); Airport homeAirport = new Airport("LHR"); employee.setHomeAirport(homeAirport); LocalDateTime now = LocalDateTime.now(); // First flight: Exactly 10 hours (boundary case - should be long haul) Flight boundaryFlight = new Flight("1", homeAirport, now, new Airport("JFK"), now.plusHours(10)); FlightAssignment boundaryAssignment = new FlightAssignment("1", boundaryFlight); boundaryAssignment.setEmployee(employee); // Second flight: Only 24 hours rest // Should violate because 10h exactly is considered long haul LocalDateTime secondDeparture = now.plusHours(10).plusMinutes(20) .plusHours(24).plusMinutes(45); Flight secondFlight = new Flight("2", homeAirport, secondDeparture, new Airport("ATL"), secondDeparture.plusHours(3)); FlightAssignment secondAssignment = new FlightAssignment("2", secondFlight); secondAssignment.setEmployee(employee); constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::minimumRestAfterLongHaul) .given(boundaryAssignment, secondAssignment) .penalizesBy(1440); // 24 hours = 1440 minutes violation } @Test void minimumRestAfterLongHaul_awayFromHomeBase() { Employee employee = new Employee("1"); Airport homeAirport = new Airport("LHR"); employee.setHomeAirport(homeAirport); Airport jfk = new Airport("JFK"); Airport atl = new Airport("ATL"); LocalDateTime now = LocalDateTime.now(); // First flight: Long haul ending at JFK (away from home) - 10 hours Flight longHaulFlight = new Flight("1", homeAirport, now, jfk, now.plusHours(10)); FlightAssignment longHaulAssignment = new FlightAssignment("1", longHaulFlight); longHaulAssignment.setEmployee(employee); // Second flight: Departing from ATL with only 36 hours rest // Should still violate 48h requirement even away from home LocalDateTime secondDeparture = now.plusHours(10).plusMinutes(20) .plusHours(36).plusMinutes(45); Flight secondFlight = new Flight("2", atl, secondDeparture, new Airport("BRU"), secondDeparture.plusHours(7)); FlightAssignment secondAssignment = new FlightAssignment("2", secondFlight); secondAssignment.setEmployee(employee); constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::minimumRestAfterLongHaul) .given(longHaulAssignment, secondAssignment) .penalizesBy(720); // 12 hours = 720 minutes violation } @Test void minimumRestAfterLongHaul_multipleLongHauls() { Employee employee = new Employee("1"); Airport homeAirport = new Airport("LHR"); employee.setHomeAirport(homeAirport); LocalDateTime now = LocalDateTime.now(); // First long haul Flight firstLongHaul = new Flight("1", homeAirport, now, new Airport("JFK"), now.plusHours(9)); FlightAssignment firstAssignment = new FlightAssignment("1", firstLongHaul); firstAssignment.setEmployee(employee); // Second long haul after 50h rest (satisfies first constraint) LocalDateTime secondDeparture = now.plusHours(9).plusMinutes(20) .plusHours(50).plusMinutes(45); Flight secondLongHaul = new Flight("2", homeAirport, secondDeparture, new Airport("ATL"), secondDeparture.plusHours(10)); FlightAssignment secondAssignment = new FlightAssignment("2", secondLongHaul); secondAssignment.setEmployee(employee); // Third flight after only 20h rest (violates second constraint) LocalDateTime thirdDeparture = secondDeparture.plusHours(10).plusMinutes(20) .plusHours(20).plusMinutes(45); Flight thirdFlight = new Flight("3", homeAirport, thirdDeparture, new Airport("BRU"), thirdDeparture.plusHours(2)); FlightAssignment thirdAssignment = new FlightAssignment("3", thirdFlight); thirdAssignment.setEmployee(employee); constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::minimumRestAfterLongHaul) .given(firstAssignment, secondAssignment, thirdAssignment) .penalizesBy(1680); // 28 hours violation for second-to-third } @Test void minimumRestAfterLongHaul_multipleEmployeesIsolated() { // Employee 1: Violates constraint Employee employee1 = new Employee("1"); employee1.setHomeAirport(new Airport("LHR")); // Employee 2: Satisfies constraint Employee employee2 = new Employee("2"); employee2.setHomeAirport(new Airport("LHR")); LocalDateTime now = LocalDateTime.now(); // Employee 1: Long haul with insufficient rest - changed to 10 hours Flight e1Flight1 = new Flight("F1", new Airport("LHR"), now, new Airport("JFK"), now.plusHours(10)); FlightAssignment e1Assignment1 = new FlightAssignment("A1", e1Flight1); e1Assignment1.setEmployee(employee1); Flight e1Flight2 = new Flight("F2", new Airport("LHR"), now.plusHours(10).plusMinutes(20).plusHours(30).plusMinutes(45), new Airport("ATL"), now.plusHours(43)); FlightAssignment e1Assignment2 = new FlightAssignment("A2", e1Flight2); e1Assignment2.setEmployee(employee1); // Employee 2: Long haul with sufficient rest Flight e2Flight1 = new Flight("F3", new Airport("LHR"), now, new Airport("BNE"), now.plusHours(20)); FlightAssignment e2Assignment1 = new FlightAssignment("A3", e2Flight1); e2Assignment1.setEmployee(employee2); Flight e2Flight2 = new Flight("F4", new Airport("LHR"), now.plusHours(20).plusMinutes(20).plusHours(60).plusMinutes(45), new Airport("JFK"), now.plusHours(88)); FlightAssignment e2Assignment2 = new FlightAssignment("A4", e2Flight2); e2Assignment2.setEmployee(employee2); constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::minimumRestAfterLongHaul) .given(employee1, employee2, e1Assignment1, e1Assignment2, e2Assignment1, e2Assignment2) .penalizesBy(1080); // Only employee1's violation (18 hours) } @Test void minimumRestAfterConsecutiveLongHaul_satisfied() { Employee employee = new Employee("1"); Airport lhr = new Airport("LHR"); Airport jfk = new Airport("JFK"); Airport lax = new Airport("LAX"); employee.setHomeAirport(lhr); LocalDateTime now = LocalDateTime.now(); // First flight: 5 hours (short haul alone) Flight firstFlight = new Flight("1", lhr, now, jfk, now.plusHours(5)); FlightAssignment firstAssignment = new FlightAssignment("1", firstFlight); firstAssignment.setEmployee(employee); // Second flight: 6 hours, consecutive with first (total 11 hours = long haul) // Departs 1 hour after first arrives (within 2 hour window) LocalDateTime secondDeparture = now.plusHours(5).plusHours(1); Flight secondFlight = new Flight("2", jfk, secondDeparture, lax, secondDeparture.plusHours(6)); FlightAssignment secondAssignment = new FlightAssignment("2", secondFlight); secondAssignment.setEmployee(employee); // Third flight: 50 hours rest after the consecutive long haul // Should satisfy 48 hour requirement LocalDateTime thirdDeparture = secondDeparture.plusHours(6).plusMinutes(20) .plusHours(50).plusMinutes(45); Flight thirdFlight = new Flight("3", lhr, thirdDeparture, lax, thirdDeparture.plusHours(3)); FlightAssignment thirdAssignment = new FlightAssignment("3", thirdFlight); thirdAssignment.setEmployee(employee); constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::minimumRestAfterConsecutiveLongHaul) .given(firstAssignment, secondAssignment, thirdAssignment) .penalizesBy(0); // 50 hours rest satisfies 48 hour minimum } @Test void minimumRestAfterConsecutiveLongHaul_violated() { Employee employee = new Employee("1"); Airport lhr = new Airport("LHR"); Airport jfk = new Airport("JFK"); Airport lax = new Airport("LAX"); employee.setHomeAirport(lhr); LocalDateTime now = LocalDateTime.now(); // First flight: 5 hours Flight firstFlight = new Flight("1", lhr, now, jfk, now.plusHours(5)); FlightAssignment firstAssignment = new FlightAssignment("1", firstFlight); firstAssignment.setEmployee(employee); // Second flight: 6 hours, consecutive (total 11 hours = long haul) LocalDateTime secondDeparture = now.plusHours(5).plusHours(1); Flight secondFlight = new Flight("2", jfk, secondDeparture, lax, secondDeparture.plusHours(6)); FlightAssignment secondAssignment = new FlightAssignment("2", secondFlight); secondAssignment.setEmployee(employee); // Third flight: Only 30 hours rest after consecutive long haul // Violates 48 hour minimum by 18 hours = 1080 minutes LocalDateTime thirdDeparture = secondDeparture.plusHours(6).plusMinutes(20) .plusHours(30).plusMinutes(45); Flight thirdFlight = new Flight("3", lhr, thirdDeparture, lax, thirdDeparture.plusHours(3)); FlightAssignment thirdAssignment = new FlightAssignment("3", thirdFlight); thirdAssignment.setEmployee(employee); constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::minimumRestAfterConsecutiveLongHaul) .given(firstAssignment, secondAssignment, thirdAssignment) .penalizesBy(1080); // 18 hours = 1080 minutes violation } @Test void minimumRestAfterConsecutiveLongHaul_notApplicableToNonConsecutive() { Employee employee = new Employee("1"); Airport lhr = new Airport("LHR"); Airport jfk = new Airport("JFK"); Airport atl = new Airport("ATL"); employee.setHomeAirport(lhr); LocalDateTime now = LocalDateTime.now(); // First flight: 5 hours Flight firstFlight = new Flight("1", lhr, now, jfk, now.plusHours(5)); FlightAssignment firstAssignment = new FlightAssignment("1", firstFlight); firstAssignment.setEmployee(employee); // Second flight: 6 hours but NOT consecutive (different airport) LocalDateTime secondDeparture = now.plusHours(5).plusHours(1); Flight secondFlight = new Flight("2", atl, secondDeparture, // ATL, not JFK lhr, secondDeparture.plusHours(6)); FlightAssignment secondAssignment = new FlightAssignment("2", secondFlight); secondAssignment.setEmployee(employee); // Third flight: Only 20 hours rest // Should NOT violate because first + second don't form consecutive long haul LocalDateTime thirdDeparture = secondDeparture.plusHours(6).plusMinutes(20) .plusHours(20).plusMinutes(45); Flight thirdFlight = new Flight("3", lhr, thirdDeparture, jfk, thirdDeparture.plusHours(5)); FlightAssignment thirdAssignment = new FlightAssignment("3", thirdFlight); thirdAssignment.setEmployee(employee); constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::minimumRestAfterConsecutiveLongHaul) .given(firstAssignment, secondAssignment, thirdAssignment) .penalizesBy(0); // constraint doesn't apply to non-consecutive flights } @Test void minimumRestAfterConsecutiveLongHaul_notApplicableToShortTotal() { Employee employee = new Employee("1"); Airport lhr = new Airport("LHR"); Airport jfk = new Airport("JFK"); Airport lax = new Airport("LAX"); employee.setHomeAirport(lhr); LocalDateTime now = LocalDateTime.now(); // First flight: 3 hours Flight firstFlight = new Flight("1", lhr, now, jfk, now.plusHours(3)); FlightAssignment firstAssignment = new FlightAssignment("1", firstFlight); firstAssignment.setEmployee(employee); // Second flight: 4 hours, consecutive (total only 7 hours = NOT long haul) LocalDateTime secondDeparture = now.plusHours(3).plusHours(1); Flight secondFlight = new Flight("2", jfk, secondDeparture, lax, secondDeparture.plusHours(4)); FlightAssignment secondAssignment = new FlightAssignment("2", secondFlight); secondAssignment.setEmployee(employee); // Third flight: Only 20 hours rest // Should NOT violate because combined duration < 10 hours LocalDateTime thirdDeparture = secondDeparture.plusHours(4).plusMinutes(20) .plusHours(20).plusMinutes(45); Flight thirdFlight = new Flight("3", lhr, thirdDeparture, lax, thirdDeparture.plusHours(3)); FlightAssignment thirdAssignment = new FlightAssignment("3", thirdFlight); thirdAssignment.setEmployee(employee); constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::minimumRestAfterConsecutiveLongHaul) .given(firstAssignment, secondAssignment, thirdAssignment) .penalizesBy(0); // constraint doesn't apply when total < 10 hours } @Test void minimumRestAfterConsecutiveLongHaul_tooLongGapBetweenFlights() { Employee employee = new Employee("1"); Airport lhr = new Airport("LHR"); Airport jfk = new Airport("JFK"); Airport lax = new Airport("LAX"); employee.setHomeAirport(lhr); LocalDateTime now = LocalDateTime.now(); // First flight: 5 hours Flight firstFlight = new Flight("1", lhr, now, jfk, now.plusHours(5)); FlightAssignment firstAssignment = new FlightAssignment("1", firstFlight); firstAssignment.setEmployee(employee); // Second flight: 6 hours but 3 hours after first (> 2 hour limit) // Not considered consecutive despite total being 11 hours LocalDateTime secondDeparture = now.plusHours(5).plusHours(3); Flight secondFlight = new Flight("2", jfk, secondDeparture, lax, secondDeparture.plusHours(6)); FlightAssignment secondAssignment = new FlightAssignment("2", secondFlight); secondAssignment.setEmployee(employee); // Third flight: Only 20 hours rest // Should NOT violate because gap between flights is too long LocalDateTime thirdDeparture = secondDeparture.plusHours(6).plusMinutes(20) .plusHours(20).plusMinutes(45); Flight thirdFlight = new Flight("3", lhr, thirdDeparture, lax, thirdDeparture.plusHours(3)); FlightAssignment thirdAssignment = new FlightAssignment("3", thirdFlight); thirdAssignment.setEmployee(employee); constraintVerifier.verifyThat(FlightCrewSchedulingConstraintProvider::minimumRestAfterConsecutiveLongHaul) .given(firstAssignment, secondAssignment, thirdAssignment) .penalizesBy(0); // constraint doesn't apply when gap > 2 hours } }