"""Slot computation (§8.1): working hours − busy, quantization, notice, horizon.""" from datetime import date, datetime from zoneinfo import ZoneInfo from gogo.scheduling.slots import compute_slots, subtract_busy TZ = ZoneInfo("Europe/Sarajevo") def dt(day: int, h: int, m: int = 0) -> datetime: return datetime(2026, 7, day, h, m, tzinfo=TZ) WH = { "mon": [["09:00", "13:00"], ["14:00", "18:00"]], # lunch break "tue": [["09:00", "18:00"]], "wed": [["09:00", "18:00"]], "thu": [["09:00", "18:00"]], "fri": [["09:00", "18:00"]], "sat": [["09:00", "14:00"]], "sun": [], } # Mon 2026-07-13 .. Sun 2026-07-19 NOW = dt(13, 8, 0) # Monday 08:00 def test_subtract_busy_splits_interval(): free = subtract_busy((dt(13, 9), dt(13, 13)), [(dt(13, 10), dt(13, 11))]) assert free == [(dt(13, 9), dt(13, 10)), (dt(13, 11), dt(13, 13))] def test_subtract_busy_no_overlap(): free = subtract_busy((dt(13, 9), dt(13, 13)), [(dt(13, 14), dt(13, 15))]) assert free == [(dt(13, 9), dt(13, 13))] def test_slots_respect_break_and_duration(): slots = compute_slots( working_hours=WH, busy=[], duration_min=60, date_from=date(2026, 7, 13), date_to=date(2026, 7, 13), tz=TZ, now=NOW, min_notice_hours=0, max_slots=50, ) starts = [s.start for s in slots] # no slot may span the 13:00-14:00 break assert dt(13, 13) not in starts for s in slots: assert not (s.start < dt(13, 14) and s.end > dt(13, 13)) assert slots[0].start == dt(13, 9) assert all((s.end - s.start).total_seconds() == 3600 for s in slots) def test_min_notice_pushes_first_slot(): slots = compute_slots( working_hours=WH, busy=[], duration_min=30, date_from=date(2026, 7, 13), date_to=date(2026, 7, 13), tz=TZ, now=dt(13, 9, 5), min_notice_hours=2, ) assert slots[0].start >= dt(13, 11, 5) def test_busy_blocks_remove_slots(): slots = compute_slots( working_hours=WH, busy=[(dt(14, 9), dt(14, 12))], duration_min=60, date_from=date(2026, 7, 14), date_to=date(2026, 7, 14), tz=TZ, now=NOW, min_notice_hours=0, ) assert slots[0].start == dt(14, 12) def test_sunday_closed(): slots = compute_slots( working_hours=WH, busy=[], duration_min=30, date_from=date(2026, 7, 19), date_to=date(2026, 7, 19), tz=TZ, now=NOW, min_notice_hours=0, ) assert slots == [] def test_max_days_ahead_horizon(): slots = compute_slots( working_hours=WH, busy=[], duration_min=30, date_from=date(2026, 7, 13), date_to=date(2026, 8, 30), tz=TZ, now=NOW, min_notice_hours=0, max_days_ahead=2, max_slots=1000, ) assert max(s.start for s in slots).date() <= date(2026, 7, 15) def test_buffer_extends_step(): slots = compute_slots( working_hours=WH, busy=[], duration_min=45, buffer_min=15, date_from=date(2026, 7, 14), date_to=date(2026, 7, 14), tz=TZ, now=NOW, min_notice_hours=0, ) # steps of 60 (45+15), but slot end = start + 45 assert slots[0].start == dt(14, 9) assert slots[0].end == dt(14, 9, 45) assert slots[1].start == dt(14, 10)