diff --git a/app/models.py b/app/models.py index 8e340a1..74ad50c 100644 --- a/app/models.py +++ b/app/models.py @@ -58,22 +58,41 @@ def has_no_shift_same_day(self, shift): ) def has_required_rest_time(self, shift): + new_date = datetime.date.fromisoformat(shift.date) + for assigned_shift in self.assigned_shifts: - existing_date = datetime.datetime.strptime(assigned_shift.date, "%Y-%m-%d") - new_date = datetime.datetime.strptime(shift.date, "%Y-%m-%d") + existing_date = datetime.date.fromisoformat(assigned_shift.date) - is_next_day = new_date == existing_date + datetime.timedelta(days=1) - is_evening_to_day = ( - assigned_shift.shift_type == ShiftType.EVENING - and shift.shift_type == ShiftType.DAY + # A night blocks the following day; an evening blocks the + # following morning. Check both orders because shifts are + # assigned by priority, not necessarily by chronological date. + existing_is_previous_day = ( + new_date == existing_date + datetime.timedelta(days=1) + ) + new_is_previous_day = ( + existing_date == new_date + datetime.timedelta(days=1) ) - is_night_before = assigned_shift.shift_type == ShiftType.NIGHT - if is_next_day and (is_evening_to_day or is_night_before): + if existing_is_previous_day and ( + assigned_shift.shift_type == ShiftType.NIGHT + or ( + assigned_shift.shift_type == ShiftType.EVENING + and shift.shift_type == ShiftType.DAY + ) + ): + return False + + if new_is_previous_day and ( + shift.shift_type == ShiftType.NIGHT + or ( + shift.shift_type == ShiftType.EVENING + and assigned_shift.shift_type == ShiftType.DAY + ) + ): return False return True - + def get_risk_flags(self, workload_ratio_threshold: float, average_score_threshold: float) -> list[str]: flags = [] diff --git a/tests/test_employee.py b/tests/test_employee.py index 51e4920..83d97cd 100644 --- a/tests/test_employee.py +++ b/tests/test_employee.py @@ -99,4 +99,41 @@ def test_employee_cannot_take_two_shifts_on_same_day(): result = employee.can_take_shift(shift2) # Assert - assert result is False, "An employee may not have two shifts on the same date." \ No newline at end of file + assert result is False, "An employee may not have two shifts on the same date." + +def test_employee_cannot_take_evening_before_assigned_next_day(): + employee = Employee(name="Reverse-Evening", max_shifts=5, skills=["CT"]) + day_shift = Shift(date="2026-05-12", shift_type=ShiftType.DAY, required_skill="CT") + previous_evening = Shift( + date="2026-05-11", shift_type=ShiftType.EVENING, required_skill="CT" + ) + employee.assigned_shifts.append(day_shift) + + assert employee.can_take_shift(previous_evening) is False + assert "rest time violation" in employee.get_shift_blockers(previous_evening) + + +def test_employee_cannot_take_night_before_assigned_next_day(): + employee = Employee(name="Reverse-Night", max_shifts=5, skills=["MR"]) + next_evening = Shift( + date="2026-05-12", shift_type=ShiftType.EVENING, required_skill="MR" + ) + previous_night = Shift( + date="2026-05-11", shift_type=ShiftType.NIGHT, required_skill="MR" + ) + employee.assigned_shifts.append(next_evening) + + assert employee.can_take_shift(previous_night) is False + + +def test_employee_can_take_day_before_assigned_next_day_evening(): + employee = Employee(name="Safe-Sequence", max_shifts=5, skills=["CT"]) + next_evening = Shift( + date="2026-05-12", shift_type=ShiftType.EVENING, required_skill="CT" + ) + previous_day = Shift( + date="2026-05-11", shift_type=ShiftType.DAY, required_skill="CT" + ) + employee.assigned_shifts.append(next_evening) + + assert employee.can_take_shift(previous_day) is True diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py index d4f081b..5705192 100644 --- a/tests/test_scheduler.py +++ b/tests/test_scheduler.py @@ -161,4 +161,26 @@ def test_assign_shifts_still_works_without_employee_profiles(): employee_profiles=None, ) - assert employee in shift.assigned_employees \ No newline at end of file + assert employee in shift.assigned_employees + +def test_priority_order_does_not_bypass_rest_rules(): + employee = Employee(name="Priority-Rest", max_shifts=3, skills=["CT"]) + # Later calendar date has the higher scheduling priority. + next_day = Shift( + date="2026-05-12", + shift_type=ShiftType.DAY, + required_skill="CT", + priority=1, + ) + previous_night = Shift( + date="2026-05-11", + shift_type=ShiftType.NIGHT, + required_skill="CT", + priority=2, + ) + + assign_shifts([employee], [previous_night, next_day]) + + assert employee in next_day.assigned_employees + assert previous_night.assigned_employees == [] + assert employee.assigned_shifts == [next_day]