Files
gogo-telefon/gogo/hours.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

80 lines
2.3 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.
"""Working-hours helpers.
Working hours are stored per tenant as:
{"mon": [["09:00", "13:00"], ["14:00", "18:00"]], ..., "sun": []}
Multiple intervals per day model lunch breaks. Missing/empty day = closed.
All computations happen in the tenant's timezone.
"""
from __future__ import annotations
from datetime import date, datetime, time, timedelta
from zoneinfo import ZoneInfo
WEEKDAY_KEYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
# Bosnian day names (Latin) for UI / agent context
DAY_NAMES_BS = {
"mon": "ponedjeljak",
"tue": "utorak",
"wed": "srijeda",
"thu": "četvrtak",
"fri": "petak",
"sat": "subota",
"sun": "nedjelja",
}
DEFAULT_WORKING_HOURS: dict[str, list[list[str]]] = {
"mon": [["09:00", "18:00"]],
"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": [],
}
def _parse_hhmm(s: str) -> time:
h, m = s.split(":")
return time(int(h), int(m))
def day_intervals(
working_hours: dict, day: date, tz: ZoneInfo
) -> list[tuple[datetime, datetime]]:
"""Open intervals for a calendar day as tz-aware datetimes."""
key = WEEKDAY_KEYS[day.weekday()]
out = []
for start_s, end_s in working_hours.get(key, []):
start = datetime.combine(day, _parse_hhmm(start_s), tzinfo=tz)
end = datetime.combine(day, _parse_hhmm(end_s), tzinfo=tz)
if end > start:
out.append((start, end))
return out
def is_open_at(working_hours: dict, at: datetime, tz: ZoneInfo) -> bool:
at = at.astimezone(tz)
return any(s <= at < e for s, e in day_intervals(working_hours, at.date(), tz))
def hours_summary_bs(working_hours: dict) -> str:
"""Human-readable Bosnian working-hours summary for prompts/emails."""
parts = []
for key in WEEKDAY_KEYS:
intervals = working_hours.get(key, [])
if not intervals:
parts.append(f"{DAY_NAMES_BS[key]}: zatvoreno")
else:
spans = ", ".join(f"{a}{b}" for a, b in intervals)
parts.append(f"{DAY_NAMES_BS[key]}: {spans}")
return "; ".join(parts)
def iter_days(start: date, end: date):
d = start
while d <= end:
yield d
d += timedelta(days=1)