"""SMS gateway endpoints.""" from typing import Optional from datetime import datetime, timezone from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func, desc, and_ from app.auth.dependencies import require_super_admin, require_school_admin, get_current_user from app.database import get_db from app.models.user import HubUser, UserRole from app.models.sms import SmsJob, SmsJobStatus, SmsCreditLedger from app.models.school import School router = APIRouter(prefix="/api/sms", tags=["sms"]) class SubmitSmsJob(BaseModel): """Called by on-prem TapTrack to submit SMS jobs to Hub.""" license_key: str jobs: list[dict] # [{ recipient_phone, message, trigger }] class ManualSmsRequest(BaseModel): school_id: str recipient_phone: str message: str @router.post("/submit", status_code=202) async def submit_sms_jobs( body: SubmitSmsJob, db: AsyncSession = Depends(get_db), ): """On-prem posts SMS jobs for Hub to process via Semaphore.""" from app.models.license import License lic = (await db.execute(select(License).where(License.key == body.license_key))).scalar_one_or_none() if not lic: raise HTTPException(403, "Invalid license key") school = (await db.execute(select(School).where(School.id == lic.school_id))).scalar_one_or_none() if not school or float(school.sms_credits) <= 0: raise HTTPException(402, "Insufficient SMS credits") created_ids = [] for job_data in body.jobs: job = SmsJob( school_id=school.id, recipient_phone=job_data.get("recipient_phone", ""), message=job_data.get("message", ""), sender_name=school.sms_sender_name, trigger=job_data.get("trigger"), ) db.add(job) created_ids.append(job.id) await db.commit() return {"queued": len(created_ids), "job_ids": created_ids} @router.get("/jobs") async def list_sms_jobs( school_id: Optional[str] = Query(None), status: Optional[SmsJobStatus] = Query(None), page: int = Query(1, ge=1), per_page: int = Query(50, ge=1, le=200), current_user: HubUser = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): stmt = select(SmsJob).order_by(desc(SmsJob.created_at)) if current_user.role != UserRole.super_admin: stmt = stmt.where(SmsJob.school_id == current_user.school_id) elif school_id: stmt = stmt.where(SmsJob.school_id == school_id) if status: stmt = stmt.where(SmsJob.status == status) total = (await db.execute(select(func.count()).select_from(stmt.subquery()))).scalar_one() jobs = (await db.execute(stmt.offset((page - 1) * per_page).limit(per_page))).scalars().all() return { "items": [ { "id": j.id, "school_id": j.school_id, "recipient_phone": j.recipient_phone, "message": j.message[:60] + "..." if len(j.message) > 60 else j.message, "sender_name": j.sender_name, "status": j.status.value, "trigger": j.trigger, "created_at": j.created_at.isoformat(), "sent_at": j.sent_at.isoformat() if j.sent_at else None, "retry_count": j.retry_count, "error_message": j.error_message, } for j in jobs ], "total": total, "page": page, "per_page": per_page, } @router.get("/credits/{school_id}") async def get_credit_ledger( school_id: str, page: int = Query(1, ge=1), per_page: int = Query(50), current_user: HubUser = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): if current_user.role != UserRole.super_admin and current_user.school_id != school_id: raise HTTPException(403) stmt = select(SmsCreditLedger).where(SmsCreditLedger.school_id == school_id).order_by(desc(SmsCreditLedger.created_at)) total = (await db.execute(select(func.count()).select_from(stmt.subquery()))).scalar_one() rows = (await db.execute(stmt.offset((page - 1) * per_page).limit(per_page))).scalars().all() return { "items": [ {"id": r.id, "tx_type": r.tx_type.value, "amount": float(r.amount), "balance_after": float(r.balance_after), "description": r.description, "created_at": r.created_at.isoformat()} for r in rows ], "total": total, }