Files
gogo-telefon/tests/test_slots.py

97 lines
3.1 KiB
Python
Raw Normal View History

"""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)