"""SR / Task CRUD + status transition endpoints.""" from datetime import datetime from typing import List, Optional from uuid import uuid4 from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession from database import get_db from models import ( AuditLog, Institution, SRCreate, SROut, SRRequest, SRStatus, SRStatusUpdate, SRType, compute_log_hash ) router = APIRouter(prefix="/api/tasks", tags=["tasks"]) # Valid state transitions _TRANSITIONS: dict[str, list[str]] = { SRStatus.RECEIVED: [SRStatus.PARSED, SRStatus.REJECTED], SRStatus.PARSED: [SRStatus.PENDING_APPROVAL, SRStatus.REJECTED], SRStatus.PENDING_APPROVAL: [SRStatus.APPROVED, SRStatus.REJECTED], SRStatus.APPROVED: [SRStatus.IN_PROGRESS, SRStatus.REJECTED], SRStatus.IN_PROGRESS: [SRStatus.PENDING_PM_VALIDATION, SRStatus.FAILED_ROLLBACK], SRStatus.PENDING_PM_VALIDATION:[SRStatus.COMPLETED, SRStatus.FAILED_ROLLBACK], SRStatus.COMPLETED: [], SRStatus.FAILED_ROLLBACK: [], SRStatus.REJECTED: [], } def _new_sr_id() -> str: return f"SR-{datetime.now().strftime('%Y%m%d')}-{str(uuid4())[:6].upper()}" async def _write_audit(db: AsyncSession, sr_id: str, actor: str, action: str, detail: str) -> None: from sqlalchemy import select as sel result = await db.execute( sel(AuditLog).where(AuditLog.sr_id == sr_id) .order_by(AuditLog.id.desc()).limit(1) ) last = result.scalars().first() prev_hash = last.log_hash if last else None ts = datetime.now().isoformat() log_hash = compute_log_hash(prev_hash, actor, action, detail, ts) db.add(AuditLog( sr_id=sr_id, actor=actor, action=action, detail=detail, prev_hash=prev_hash, log_hash=log_hash )) @router.get("", response_model=List[SROut]) async def list_tasks( status: Optional[str] = Query(None), sr_type: Optional[str] = Query(None), priority: Optional[str]= Query(None), keyword: Optional[str] = Query(None), skip: int = 0, limit: int = 50, db: AsyncSession = Depends(get_db), ): q = select(SRRequest).order_by(SRRequest.created_at.desc()) if status: q = q.where(SRRequest.status == status) if sr_type: q = q.where(SRRequest.sr_type == sr_type) if priority: q = q.where(SRRequest.priority == priority) if keyword: q = q.where(SRRequest.title.contains(keyword)) q = q.offset(skip).limit(limit) result = await db.execute(q) return result.scalars().all() @router.get("/stats") async def get_stats(db: AsyncSession = Depends(get_db)): total = (await db.execute(select(func.count(SRRequest.id)))).scalar() by_status: dict[str, int] = {} for s in SRStatus: cnt = (await db.execute( select(func.count(SRRequest.id)).where(SRRequest.status == s) )).scalar() if cnt: by_status[s.value] = cnt by_type: dict[str, int] = {} for t in SRType: cnt = (await db.execute( select(func.count(SRRequest.id)).where(SRRequest.sr_type == t) )).scalar() if cnt: by_type[t.value] = cnt return {"total": total, "by_status": by_status, "by_type": by_type} @router.get("/{sr_id}", response_model=SROut) async def get_task(sr_id: str, db: AsyncSession = Depends(get_db)): result = await db.execute(select(SRRequest).where(SRRequest.sr_id == sr_id)) sr = result.scalars().first() if not sr: raise HTTPException(404, detail="SR을 찾을 수 없습니다.") return sr @router.post("", response_model=SROut, status_code=201) async def create_task(payload: SRCreate, db: AsyncSession = Depends(get_db)): inst_id = None if payload.inst_code: r = await db.execute( select(Institution).where(Institution.inst_code == payload.inst_code) ) inst = r.scalars().first() if inst: inst_id = inst.id sr = SRRequest( sr_id=_new_sr_id(), inst_id=inst_id, sr_type=payload.sr_type, title=payload.title, description=payload.description, status=SRStatus.RECEIVED, priority=payload.priority, requested_by=payload.requested_by, assigned_to=payload.assigned_to, target_server=payload.target_server, ) db.add(sr) await db.flush() await _write_audit(db, sr.sr_id, payload.requested_by, "SR_CREATED", f"SR 생성: {payload.title}") await db.commit() await db.refresh(sr) return sr @router.patch("/{sr_id}/status", response_model=SROut) async def update_status(sr_id: str, payload: SRStatusUpdate, db: AsyncSession = Depends(get_db)): result = await db.execute(select(SRRequest).where(SRRequest.sr_id == sr_id)) sr = result.scalars().first() if not sr: raise HTTPException(404, detail="SR을 찾을 수 없습니다.") allowed = _TRANSITIONS.get(SRStatus(sr.status), []) if payload.status not in allowed: raise HTTPException(400, detail=f"'{sr.status}' → '{payload.status}' 전이는 허용되지 않습니다.") old_status = sr.status sr.status = payload.status sr.updated_at = datetime.now() detail = payload.comment or f"상태 변경: {old_status} → {payload.status}" await _write_audit(db, sr_id, payload.actor, "STATUS_CHANGED", detail) await db.commit() await db.refresh(sr) return sr