"""Shared intern/leader self-service operations on attendance reports. Single source of truth for "what can this viewer see/do on this report", reused by the H5 HTTP endpoints (``me_router``) and the MCP tools so the authorization + state logic can't drift between surfaces. None of these commit — the caller (router endpoint / MCP ``tool_runtime``) owns the transaction boundary. """ from __future__ import annotations from uuid import UUID from sqlalchemy import or_, select from sqlalchemy.ext.asyncio import AsyncSession from app.core.exceptions import ForbiddenError, NotFoundError from app.domains.attendance import constants as c from app.domains.attendance.lifecycle import AttendanceLifecycleService from app.domains.attendance.models import AttendanceReport from app.domains.attendance.serializers import ( load_person_index, serialize_event, serialize_report, ) from app.domains.attendance.service import AttendanceService from app.identity.person_models import IdentityPerson def viewer_role(report: AttendanceReport, me: UUID) -> str: if report.person_id == me: return c.SIDE_INTERN if report.leader_person_id == me: return c.SIDE_LEADER raise ForbiddenError("你无权查看该考勤报告") def can_act(report: AttendanceReport, role: str) -> dict: if role == c.SIDE_INTERN: return { "can_confirm": report.status == c.STATUS_PENDING_INTERN, "can_return": report.status == c.STATUS_PENDING_INTERN, "can_undo": report.status == c.STATUS_PENDING_LEADER, } return { "can_confirm": report.status == c.STATUS_PENDING_LEADER, "can_return": report.status == c.STATUS_PENDING_LEADER, "can_undo": False, } async def list_my_reports(db: AsyncSession, *, person: IdentityPerson) -> list[dict]: me = person.id rows = ( await db.execute( select(AttendanceReport) .where(or_( AttendanceReport.person_id == me, AttendanceReport.leader_person_id == me, )) .order_by(AttendanceReport.updated_at.desc()) .limit(200) ) ).scalars().all() person_ids = {r.person_id for r in rows} person_ids |= {r.leader_person_id for r in rows if r.leader_person_id} person_index = await load_person_index(db, person_ids) svc = AttendanceService(db) cycle_index = await svc.get_cycles({r.cycle_id for r in rows}) cycle_keys: dict[UUID, str] = { cid: cyc.cycle_key for cid, cyc in cycle_index.items() } out: list[dict] = [] for r in rows: role = c.SIDE_INTERN if r.person_id == me else c.SIDE_LEADER item = serialize_report( r, person_index=person_index, cycle_key=cycle_keys.get(r.cycle_id), ) item["viewer_role"] = role item.update(can_act(r, role)) out.append(item) return out async def report_detail( db: AsyncSession, *, report_id: UUID, person: IdentityPerson, ) -> dict: svc = AttendanceService(db) report = await svc.get_report(report_id) if report is None: raise NotFoundError("考勤报告不存在") role = viewer_role(report, person.id) events = await svc.get_report_events(report_id) cycle = await svc.get_cycle(report.cycle_id) person_ids: set[UUID] = {report.person_id} if report.leader_person_id: person_ids.add(report.leader_person_id) person_ids |= {e.actor_person_id for e in events if e.actor_person_id} person_index = await load_person_index(db, person_ids) data = serialize_report( report, person_index=person_index, cycle_key=cycle.cycle_key if cycle else None, include_snapshot=True, ) data["viewer_role"] = role data.update(can_act(report, role)) data["events"] = [serialize_event(e, person_index=person_index) for e in events] return data async def confirm( db: AsyncSession, *, report_id: UUID, actor: IdentityPerson, trace_id: str = "", ) -> dict: svc = AttendanceLifecycleService(db) report = await svc.load_report(report_id, lock=True) role = viewer_role(report, actor.id) if role == c.SIDE_INTERN: await svc.intern_confirm(report, actor_person_id=actor.id, trace_id=trace_id) else: await svc.leader_confirm(report, actor_person_id=actor.id, trace_id=trace_id) return {"id": str(report.id), "status": report.status, "role": role} async def return_to_hr( db: AsyncSession, *, report_id: UUID, actor: IdentityPerson, reason: str, trace_id: str = "", ) -> dict: svc = AttendanceLifecycleService(db) report = await svc.load_report(report_id, lock=True) role = viewer_role(report, actor.id) if role == c.SIDE_INTERN: await svc.intern_return(report, actor_person_id=actor.id, reason=reason, trace_id=trace_id) else: await svc.leader_return(report, actor_person_id=actor.id, reason=reason, trace_id=trace_id) return {"id": str(report.id), "status": report.status, "role": role} async def undo( db: AsyncSession, *, report_id: UUID, actor: IdentityPerson, trace_id: str = "", ) -> dict: svc = AttendanceLifecycleService(db) report = await svc.load_report(report_id, lock=True) role = viewer_role(report, actor.id) if role != c.SIDE_INTERN: raise ForbiddenError("只有实习生本人可以撤销确认") await svc.intern_undo(report, actor_person_id=actor.id, trace_id=trace_id) return {"id": str(report.id), "status": report.status, "role": role}