"""Self-service attendance endpoints for interns and their leaders (H5). A report is visible to its intern (``person_id``) and to that intern's leader (``leader_person_id``). The actor's relationship to the report decides which action runs — intern vs leader confirm/return; only the intern can undo, and only before the leader acts. All logic lives in ``self_service`` so the MCP tools stay in lockstep. """ from __future__ import annotations from uuid import UUID from fastapi import APIRouter, Depends, Request from sqlalchemy.ext.asyncio import AsyncSession from app.audit.service import AuditService from app.core.database import get_db from app.core.dependencies import get_current_user from app.core.response import success_response from app.domains.attendance import self_service as ss from app.domains.attendance.schemas import ReturnReportRequest from app.identity.person_models import IdentityPerson # NO ``require_active_employee_for_mutations`` here, unlike the other # self-service routers (vpn_application / ai_tool_application / # announcement_request / domain_registration). That is a DECISION, not # an omission (#2160): the attendance monthly report is the payroll # basis for a period the person has already worked, so an intern in # ``leaver_pending`` — last day booked, still employed — has to be able # to sign off the final one. Their MCP twins carry # ``leaver_allowed=True`` for the same reason, so both surfaces answer # the same state the same way. Anything genuinely irreversible added to # this router must gate itself explicitly. router = APIRouter(prefix="/api/v1/attendance/me", tags=["attendance"]) @router.get("/reports") async def list_my_reports( request: Request, current_user: IdentityPerson = Depends(get_current_user), db: AsyncSession = Depends(get_db), ) -> dict: data = await ss.list_my_reports(db, person=current_user) return success_response(data=data, request=request) @router.get("/reports/{report_id}") async def get_my_report( report_id: UUID, request: Request, current_user: IdentityPerson = Depends(get_current_user), db: AsyncSession = Depends(get_db), ) -> dict: data = await ss.report_detail(db, report_id=report_id, person=current_user) return success_response(data=data, request=request) async def _audit( db: AsyncSession, request: Request, current_user: IdentityPerson, action: str, result: dict, ) -> None: await AuditService(db).log( trace_id=getattr(request.state, "trace_id", None) or "", actor_type="user", actor_user_id=current_user.id, action=action, status="success", resource_type="attendance_report", resource_id=result["id"], detail={k: v for k, v in result.items() if k != "id"}, ip_address=request.client.host if request.client else None, ) @router.post("/reports/{report_id}/confirm") async def confirm_my_report( report_id: UUID, request: Request, current_user: IdentityPerson = Depends(get_current_user), db: AsyncSession = Depends(get_db), ) -> dict: trace_id = str(getattr(request.state, "trace_id", "") or "") result = await ss.confirm(db, report_id=report_id, actor=current_user, trace_id=trace_id) await _audit(db, request, current_user, "attendance.report.confirm", result) await db.commit() return success_response(data=result, request=request) @router.post("/reports/{report_id}/return") async def return_my_report( report_id: UUID, body: ReturnReportRequest, request: Request, current_user: IdentityPerson = Depends(get_current_user), db: AsyncSession = Depends(get_db), ) -> dict: trace_id = str(getattr(request.state, "trace_id", "") or "") result = await ss.return_to_hr( db, report_id=report_id, actor=current_user, reason=body.reason, trace_id=trace_id, ) await _audit(db, request, current_user, "attendance.report.return", result) await db.commit() return success_response(data=result, request=request) @router.post("/reports/{report_id}/undo") async def undo_my_confirmation( report_id: UUID, request: Request, current_user: IdentityPerson = Depends(get_current_user), db: AsyncSession = Depends(get_db), ) -> dict: trace_id = str(getattr(request.state, "trace_id", "") or "") result = await ss.undo(db, report_id=report_id, actor=current_user, trace_id=trace_id) await _audit(db, request, current_user, "attendance.report.undo", result) await db.commit() return success_response(data=result, request=request)