Files
gogo-telefon/tests/test_slots.py
Senad Uka e855650f09 M1: backend core + proposal engine
- FastAPI skeleton, SQLAlchemy models (§13), Alembic initial migration
- SchedulingProvider interface with google_calendar (free/busy read-only),
  partner_api (Appendix B client) and mock implementations
- Proposal engine: create → provider-routed delivery → owner actions
  (resolve/confirm+SMS/reject) → expiry + reminders (§9)
- Signed single-use action links, .ics METHOD:REQUEST attachment
- Partner outcome webhook with HMAC verification + polling fallback
- SmsProvider (console) with Bosnian templates (§5.5), EmailProvider (console/SMTP)
- Fake partner API server in tests/ — Appendix B reference implementation
- 43 tests: slot math, proposal lifecycle, action links, partner contract

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 09:45:06 +02:00

97 lines
3.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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)