- 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>
66 lines
1.6 KiB
Python
66 lines
1.6 KiB
Python
"""Bosnian (Latin) formatting helpers for client- and owner-facing text."""
|
||
|
||
from __future__ import annotations
|
||
|
||
from datetime import datetime
|
||
from zoneinfo import ZoneInfo
|
||
|
||
DAY_NAMES = [
|
||
"ponedjeljak",
|
||
"utorak",
|
||
"srijeda",
|
||
"četvrtak",
|
||
"petak",
|
||
"subota",
|
||
"nedjelja",
|
||
]
|
||
|
||
MONTH_NAMES = [
|
||
"januar",
|
||
"februar",
|
||
"mart",
|
||
"april",
|
||
"maj",
|
||
"juni",
|
||
"juli",
|
||
"august",
|
||
"septembar",
|
||
"oktobar",
|
||
"novembar",
|
||
"decembar",
|
||
]
|
||
|
||
|
||
def fmt_slot(dt: datetime, tz: str = "Europe/Sarajevo") -> str:
|
||
"""'srijeda, 15.07. u 17:00'"""
|
||
local = dt.astimezone(ZoneInfo(tz))
|
||
day = DAY_NAMES[local.weekday()]
|
||
return f"{day}, {local.day:02d}.{local.month:02d}. u {local:%H:%M}"
|
||
|
||
|
||
def fmt_date(dt: datetime, tz: str = "Europe/Sarajevo") -> str:
|
||
local = dt.astimezone(ZoneInfo(tz))
|
||
return f"{local.day:02d}.{local.month:02d}.{local.year}."
|
||
|
||
|
||
def fmt_time(dt: datetime, tz: str = "Europe/Sarajevo") -> str:
|
||
local = dt.astimezone(ZoneInfo(tz))
|
||
return f"{local:%H:%M}"
|
||
|
||
|
||
def fmt_price(price_min: float | None, price_max: float | None, currency: str = "KM") -> str:
|
||
cur = "KM" if currency == "BAM" else currency
|
||
if price_min is None and price_max is None:
|
||
return "cijena na upit"
|
||
if price_max is None or price_min == price_max:
|
||
return f"{_num(price_min)} {cur}"
|
||
if price_min is None:
|
||
return f"do {_num(price_max)} {cur}"
|
||
return f"{_num(price_min)}–{_num(price_max)} {cur}"
|
||
|
||
|
||
def _num(x: float | None) -> str:
|
||
if x is None:
|
||
return "?"
|
||
return str(int(x)) if float(x).is_integer() else f"{x:.2f}".replace(".", ",")
|