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

73 lines
2.0 KiB
Python

"""SmsProvider interface. Console impl for dev/tests; GSM gateway impl in gateway.py (M5)."""
from __future__ import annotations
import logging
import uuid
from typing import Protocol
from sqlalchemy.ext.asyncio import AsyncSession
from gogo.config import get_settings
from gogo.models import SmsLog
log = logging.getLogger("gogo.sms")
class SmsProvider(Protocol):
async def send(self, tenant_id: uuid.UUID, to_msisdn: str, body: str) -> None:
"""Send one SMS from the tenant's assigned SIM. Raises on failure."""
...
class ConsoleSmsProvider:
"""Logs SMS instead of sending; keeps them in memory for tests."""
sent: list[tuple[str, str]]
def __init__(self) -> None:
self.sent = []
async def send(self, tenant_id: uuid.UUID, to_msisdn: str, body: str) -> None:
self.sent.append((to_msisdn, body))
log.info("SMS tenant=%s to=%s: %s", tenant_id, to_msisdn, body)
_provider: SmsProvider | None = None
def get_sms_provider() -> SmsProvider:
global _provider
if _provider is None:
if get_settings().sms_provider == "gsm_gateway":
from gogo.sms.gateway import GsmGatewaySmsProvider
_provider = GsmGatewaySmsProvider()
else:
_provider = ConsoleSmsProvider()
return _provider
def set_sms_provider(p: SmsProvider | None) -> None:
"""Test hook."""
global _provider
_provider = p
async def send_sms(
session: AsyncSession, tenant_id: uuid.UUID, to_msisdn: str, body: str, kind: str
) -> bool:
"""Send + log. Returns success. Never raises (SMS failure must not break the flow)."""
row = SmsLog(tenant_id=tenant_id, to_msisdn=to_msisdn, body=body, kind=kind)
try:
await get_sms_provider().send(tenant_id, to_msisdn, body)
row.status = "sent"
ok = True
except Exception as e: # noqa: BLE001
log.exception("SMS send failed tenant=%s to=%s", tenant_id, to_msisdn)
row.status = "failed"
row.error = str(e)
ok = False
session.add(row)
return ok