"""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