- 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>
68 lines
2.3 KiB
Python
68 lines
2.3 KiB
Python
"""Small unit tests: working hours, Bosnian formatting, SMS templates, crypto."""
|
||
|
||
from datetime import datetime
|
||
from zoneinfo import ZoneInfo
|
||
|
||
from gogo import crypto
|
||
from gogo.hours import DEFAULT_WORKING_HOURS, hours_summary_bs, is_open_at
|
||
from gogo.i18n import fmt_price, fmt_slot
|
||
from gogo.models import Tenant
|
||
from gogo.sms.templates import render_sms
|
||
|
||
TZ = ZoneInfo("Europe/Sarajevo")
|
||
|
||
|
||
def test_is_open_at():
|
||
wh = DEFAULT_WORKING_HOURS
|
||
assert is_open_at(wh, datetime(2026, 7, 15, 10, 0, tzinfo=TZ), TZ) # Wed 10:00
|
||
assert not is_open_at(wh, datetime(2026, 7, 15, 20, 0, tzinfo=TZ), TZ) # Wed 20:00
|
||
assert not is_open_at(wh, datetime(2026, 7, 19, 10, 0, tzinfo=TZ), TZ) # Sun
|
||
assert is_open_at(wh, datetime(2026, 7, 18, 13, 59, tzinfo=TZ), TZ) # Sat 13:59
|
||
assert not is_open_at(wh, datetime(2026, 7, 18, 14, 0, tzinfo=TZ), TZ) # Sat 14:00
|
||
|
||
|
||
def test_hours_summary_bs():
|
||
s = hours_summary_bs(DEFAULT_WORKING_HOURS)
|
||
assert "ponedjeljak: 09:00–18:00" in s
|
||
assert "nedjelja: zatvoreno" in s
|
||
|
||
|
||
def test_fmt_slot_bosnian():
|
||
dt = datetime(2026, 7, 15, 17, 0, tzinfo=TZ) # Wednesday
|
||
assert fmt_slot(dt) == "srijeda, 15.07. u 17:00"
|
||
|
||
|
||
def test_fmt_price():
|
||
assert fmt_price(60, 90) == "60–90 KM"
|
||
assert fmt_price(25, 25) == "25 KM"
|
||
assert fmt_price(30, None) == "30 KM"
|
||
assert fmt_price(None, None) == "cijena na upit"
|
||
|
||
|
||
def make_tenant(**kw):
|
||
defaults = dict(name="Salon Merima", sms_templates={})
|
||
defaults.update(kw)
|
||
return Tenant(**defaults)
|
||
|
||
|
||
def test_sms_default_templates():
|
||
t = make_tenant()
|
||
body = render_sms(
|
||
t, "confirmation", usluga="Manikir", dan="srijeda", datum="15.07.2026.", vrijeme="17:00"
|
||
)
|
||
assert body == "Potvrđen termin: Manikir, srijeda 15.07.2026. u 17:00h — Salon Merima."
|
||
|
||
|
||
def test_sms_tenant_override():
|
||
t = make_tenant(sms_templates={"rejection": "{salon}: ne može, žao nam je."})
|
||
assert render_sms(t, "rejection") == "Salon Merima: ne može, žao nam je."
|
||
|
||
|
||
def test_sms_unknown_placeholder_left_intact():
|
||
t = make_tenant(sms_templates={"missed_call": "Pozovite {salon} — {nepoznato}"})
|
||
assert render_sms(t, "missed_call", link="x") == "Pozovite Salon Merima — {nepoznato}"
|
||
|
||
|
||
def test_crypto_roundtrip():
|
||
assert crypto.decrypt(crypto.encrypt("tajna-lozinka")) == "tajna-lozinka"
|