"""School registry endpoints — super admin only.""" import uuid from datetime import datetime, timezone from typing import Optional, Any from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel, EmailStr from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func, desc from slugify import slugify from app.auth.dependencies import require_super_admin, get_current_user from app.database import get_db from app.models.user import HubUser from app.models.school import School, SchoolStatus, LicenseTier from app.models.license import License, LicenseStatus router = APIRouter(prefix="/api/schools", tags=["schools"]) class SchoolCreate(BaseModel): name: str address: Optional[str] = None city: Optional[str] = None contact_name: Optional[str] = None contact_email: Optional[EmailStr] = None contact_phone: Optional[str] = None billing_email: Optional[EmailStr] = None tier: LicenseTier = LicenseTier.standard student_limit: int = 500 sms_sender_name: str = "SCHOOL" notes: Optional[str] = None class SchoolUpdate(BaseModel): name: Optional[str] = None address: Optional[str] = None city: Optional[str] = None contact_name: Optional[str] = None contact_email: Optional[EmailStr] = None contact_phone: Optional[str] = None billing_email: Optional[EmailStr] = None tier: Optional[LicenseTier] = None student_limit: Optional[int] = None sms_sender_name: Optional[str] = None status: Optional[SchoolStatus] = None notes: Optional[str] = None def _school_out(s: School, license: License | None = None) -> dict: return { "id": s.id, "name": s.name, "slug": s.slug, "address": s.address, "city": s.city, "contact_name": s.contact_name, "contact_email": s.contact_email, "contact_phone": s.contact_phone, "billing_email": s.billing_email, "status": s.status.value, "tier": s.tier.value, "student_limit": s.student_limit, "sms_sender_name": s.sms_sender_name, "sms_credits": float(s.sms_credits), "sms_credit_low_threshold": s.sms_credit_low_threshold, "created_at": s.created_at.isoformat(), "notes": s.notes, "license_key": license.key if license else None, "license_status": license.status.value if license else None, "license_expires_at": license.expires_at.isoformat() if license and license.expires_at else None, "license_last_seen": license.last_validated_at.isoformat() if license and license.last_validated_at else None, } @router.get("") async def list_schools( page: int = Query(1, ge=1), per_page: int = Query(25, ge=1, le=100), search: Optional[str] = Query(None), status: Optional[SchoolStatus] = Query(None), _admin: HubUser = Depends(require_super_admin), db: AsyncSession = Depends(get_db), ): stmt = select(School).order_by(desc(School.created_at)) if search: stmt = stmt.where(School.name.ilike(f"%{search}%")) if status: stmt = stmt.where(School.status == status) total = (await db.execute(select(func.count()).select_from(stmt.subquery()))).scalar_one() schools = (await db.execute(stmt.offset((page - 1) * per_page).limit(per_page))).scalars().all() items = [] for s in schools: lic_res = await db.execute(select(License).where(License.school_id == s.id)) lic = lic_res.scalar_one_or_none() items.append(_school_out(s, lic)) return {"items": items, "total": total, "page": page, "per_page": per_page} @router.post("", status_code=201) async def create_school( body: SchoolCreate, _admin: HubUser = Depends(require_super_admin), db: AsyncSession = Depends(get_db), ): slug = slugify(body.name) # Ensure slug uniqueness existing = (await db.execute(select(School).where(School.slug == slug))).scalar_one_or_none() if existing: slug = f"{slug}-{uuid.uuid4().hex[:6]}" school = School( name=body.name, slug=slug, address=body.address, city=body.city, contact_name=body.contact_name, contact_email=body.contact_email, contact_phone=body.contact_phone, billing_email=body.billing_email, tier=body.tier, student_limit=body.student_limit, sms_sender_name=body.sms_sender_name[:11], notes=body.notes, status=SchoolStatus.pending, ) db.add(school) await db.flush() # Auto-create license lic = License(school_id=school.id, tier=body.tier.value, max_students=body.student_limit) db.add(lic) await db.commit() await db.refresh(school) return _school_out(school, lic) @router.get("/{school_id}") async def get_school( school_id: str, _admin: HubUser = Depends(require_super_admin), db: AsyncSession = Depends(get_db), ): school = (await db.execute(select(School).where(School.id == school_id))).scalar_one_or_none() if not school: raise HTTPException(404, "School not found") lic = (await db.execute(select(License).where(License.school_id == school_id))).scalar_one_or_none() return _school_out(school, lic) @router.put("/{school_id}") async def update_school( school_id: str, body: SchoolUpdate, _admin: HubUser = Depends(require_super_admin), db: AsyncSession = Depends(get_db), ): school = (await db.execute(select(School).where(School.id == school_id))).scalar_one_or_none() if not school: raise HTTPException(404, "School not found") for field, value in body.model_dump(exclude_none=True).items(): if field == "sms_sender_name": value = value[:11] setattr(school, field, value) await db.commit() lic = (await db.execute(select(License).where(License.school_id == school_id))).scalar_one_or_none() return _school_out(school, lic) @router.post("/{school_id}/credits") async def add_sms_credits( school_id: str, amount: float, description: Optional[str] = None, _admin: HubUser = Depends(require_super_admin), db: AsyncSession = Depends(get_db), ): from app.models.sms import SmsCreditLedger, SmsCreditTx school = (await db.execute(select(School).where(School.id == school_id))).scalar_one_or_none() if not school: raise HTTPException(404, "School not found") school.sms_credits = float(school.sms_credits) + amount ledger = SmsCreditLedger( school_id=school_id, tx_type=SmsCreditTx.topup, amount=amount, balance_after=float(school.sms_credits), description=description or f"Manual top-up of {amount} credits", ) db.add(ledger) await db.commit() return {"sms_credits": float(school.sms_credits), "added": amount}