import logging from typing import Protocol import requests from django.conf import settings from django.utils.module_loading import import_string logger = logging.getLogger(__name__) class SMSError(Exception): pass class SMSBackend(Protocol): def send_otp(self, phone_number: str, code: str) -> None: ... class ConsoleSMSBackend: """Dev backend — logs the OTP instead of sending. Never use in production.""" def send_otp(self, phone_number: str, code: str) -> None: logger.warning("[SMS-CONSOLE] OTP for %s: %s", phone_number, code) class SMSIRBackend: """sms.ir template-based OTP via /v1/send/verify.""" def __init__(self): self.api_key = settings.SMSIR_API_KEY self.template_id = settings.SMSIR_TEMPLATE_ID self.param_name = settings.SMSIR_TEMPLATE_PARAM_NAME self.base_url = settings.SMSIR_BASE_URL.rstrip("/") self.timeout = 15 if not self.api_key: raise SMSError("SMSIR_API_KEY is not configured") if not self.template_id: raise SMSError("SMSIR_TEMPLATE_ID is not configured") def send_otp(self, phone_number: str, code: str) -> None: url = f"{self.base_url}/v1/send/verify" headers = {"x-api-key": self.api_key, "Content-Type": "application/json"} payload = { "mobile": phone_number, "templateId": int(self.template_id), "parameters": [{"name": self.param_name, "value": str(code)}], } try: resp = requests.post(url, json=payload, headers=headers, timeout=self.timeout) except requests.RequestException as e: raise SMSError(f"SMS network error: {e}") from e try: data = resp.json() except ValueError as e: raise SMSError(f"SMS non-JSON response (HTTP {resp.status_code})") from e if not (200 <= resp.status_code < 300) or data.get("status") != 1: msg = data.get("message") or f"HTTP {resp.status_code}" raise SMSError(f"sms.ir error: {msg}") logger.info("OTP delivered to %s via sms.ir template %s", phone_number, self.template_id) _DEFAULT_BACKEND = "apps.accounts.services.sms.ConsoleSMSBackend" def get_sms_backend() -> SMSBackend: backend_path = getattr(settings, "SMS_BACKEND", _DEFAULT_BACKEND) return import_string(backend_path)()