131 lines
4.4 KiB
Python
131 lines
4.4 KiB
Python
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,
|
|
}
|