"""Outbound send guard: per-UTC-day counter + structured logging. The voip.ms API caps sending at 100 SMS/MMS per day. This guard refuses sends beyond a configurable limit (default 100) before they hit the network, and logs every outbound send. Note: this counter is per-process. If you run multiple instances behind a load balancer, replace it with a shared store (Redis, etc.). """ from __future__ import annotations import logging from datetime import datetime, timezone from .exceptions import VoipMsRateLimitError logger = logging.getLogger("voipms.sms.guard") class DailySendCounter: """Counts outbound sends per UTC day, refusing sends past the limit.""" def __init__(self, limit: int) -> None: self._limit = limit self._date: str | None = None self._count = 0 @staticmethod def _today() -> str: return datetime.now(timezone.utc).strftime("%Y-%m-%d") @property def used_today(self) -> int: if self._date != self._today(): return 0 return self._count @property def limit(self) -> int: return self._limit def check_and_increment(self) -> int: """Raise VoipMsRateLimitError if the daily cap is hit; otherwise increment. Returns the count after this send. """ today = self._today() if self._date != today: self._date = today self._count = 0 if self._count >= self._limit: raise VoipMsRateLimitError( f"daily send limit ({self._limit}) reached", limit=self._limit, used_today=self._count, ) self._count += 1 return self._count def log_send(self, *, kind: str, did: str, dst: str, chars: int) -> None: remaining = self._limit - self._count logger.info( "voipms_send kind=%s did=%s dst=%s chars=%d used_today=%d limit=%d remaining=%d", kind, did, dst, chars, self._count, self._limit, remaining, )