from fastapi import HTTPException, status from pydantic import BaseModel, Field from sqlalchemy import select from sqlalchemy.orm import Session from .log_service import record_otp_log from .message_send_service import SendMessageError, normalize_recipients from .models import UrmiOtpProject, UserAccount from .ultronsms_client import UltronsmsClient from .urmiotp_catalog import OTP_TEMPLATES, URMI_OTP_SMS_CONFIG, build_dlt_otp_message TEMPLATE_TYPES = {template["type"] for template in OTP_TEMPLATES} class GenerateOtpPayload(BaseModel): project_name: str = Field(min_length=3, max_length=30) template_type: str = Field(min_length=3, max_length=40) to: str = Field(min_length=10, max_length=15) variables: dict[str, str] = Field(default_factory=dict) def _require_otp_variables(variables: dict[str, str]) -> None: otp_value = variables.get("otp", "").strip() validity = variables.get("validity_minutes", "").strip() if not otp_value or not validity: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="variables.otp and variables.validity_minutes are required", ) def generate_otp_for_user( db: Session, user: UserAccount, payload: GenerateOtpPayload, ) -> dict: if payload.template_type not in TEMPLATE_TYPES: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=f"template_type must be one of: {', '.join(sorted(TEMPLATE_TYPES))}", ) project = db.scalar( select(UrmiOtpProject).where( UrmiOtpProject.user_id == user.id, UrmiOtpProject.project_name == payload.project_name.strip(), ) ) if not project: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="UrmiOTP project not found") if project.status != "approved": raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="UrmiOTP project is not approved yet", ) try: recipients = normalize_recipients([payload.to]) except SendMessageError as exc: raise HTTPException(status_code=exc.status_code, detail=exc.detail) from exc recipient = recipients[0] _require_otp_variables(payload.variables) try: message = build_dlt_otp_message(payload.variables, project.message_label) except ValueError as exc: raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) from exc client = UltronsmsClient() try: send_result = client.send_template_sms( mobile=recipient, message=message, sender_id=URMI_OTP_SMS_CONFIG["sender_id"], pe_id=URMI_OTP_SMS_CONFIG["pe_id"], dlt_template_id=URMI_OTP_SMS_CONFIG["dlt_template_id"], sender_type="transactional", ) except RuntimeError as exc: entry = record_otp_log( db, user, project_name=project.project_name, template_type=payload.template_type, to_number=recipient, status="failed", error_message=str(exc), ) raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=str(exc), ) from exc if not send_result.success: error_message = send_result.raw.get("ErrorMessage") or "SMS gateway rejected the request" entry = record_otp_log( db, user, project_name=project.project_name, template_type=payload.template_type, to_number=recipient, status="failed", error_message=error_message, ) raise HTTPException( status_code=status.HTTP_502_BAD_GATEWAY, detail=error_message, ) entry = record_otp_log( db, user, project_name=project.project_name, template_type=payload.template_type, to_number=recipient, status="sent", job_id=send_result.job_id, message_id=send_result.message_id, ) return { "success": True, "log_id": str(entry.id), "project_name": project.project_name, "template_type": payload.template_type, "to": send_result.number or recipient, "message_preview": message, "status": entry.status, "job_id": send_result.job_id, "message_id": send_result.message_id, }