"""Self-service reads/writes for close-task owners (the `/me` surface + MCP). An owner (a finance-team member who owns close tasks) can see and tick off *their own* tasks without the full admin console. Scoped strictly to the caller's ``owner_person_id`` — no cross-owner visibility, no finance-role requirement (owning the task is the authority). Status writes reuse the same ``close_task_service`` path the console uses. """ from __future__ import annotations from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from app.core.exceptions import ForbiddenError from app.domains.finance import close_service, close_task_service from app.domains.finance.close_models import ( RUN_CLOSED, RUN_OPEN, TASK_DONE_STATUSES, FinanceCloseRun, FinanceCloseTask, ) async def _latest_period_for_owner(db: AsyncSession, person_id: str) -> str | None: """The most recent period with a task owned by this person. Prefers the latest period whose close run is NOT closed (the owner's live work) — so a future period that was already rolled forward and closed doesn't shadow the current open period. Falls back to the overall latest if every owned period is closed (or has no run row). """ open_latest = ( await db.execute( select(func.max(FinanceCloseTask.period)) .select_from(FinanceCloseTask) .outerjoin(FinanceCloseRun, FinanceCloseRun.period == FinanceCloseTask.period) .where( FinanceCloseTask.owner_person_id == person_id, func.coalesce(FinanceCloseRun.status, RUN_OPEN) != RUN_CLOSED, ) ) ).scalar() if open_latest is not None: return open_latest return ( await db.execute( select(func.max(FinanceCloseTask.period)).where( FinanceCloseTask.owner_person_id == person_id ) ) ).scalar() async def my_close_tasks( db: AsyncSession, *, person_id: str, period: str | None = None ) -> dict: """The caller's close tasks for a period (default: their latest period). Returns a warm, self-contained payload: the period label + close date, the caller's tasks (sorted by deadline then sort order), and a small status rollup — enough for a `/me` card or an MCP answer. """ person_id = str(person_id) if period is None: period = await _latest_period_for_owner(db, person_id) if period is None: return {"period": None, "label": None, "close_date": None, "tasks": [], "counts": {}} close_service.validate_period(period) rows = ( await db.execute( select(FinanceCloseTask) .where( FinanceCloseTask.period == period, FinanceCloseTask.owner_person_id == person_id, ) .order_by(FinanceCloseTask.deadline.is_(None), FinanceCloseTask.deadline, FinanceCloseTask.sort_order, FinanceCloseTask.id) ) ).scalars().all() close_date = None label = close_service.period_label(period) run = await close_service.get_run(db, period) if run is not None: window = await close_service.compute_window(db, run) close_date = close_service.effective_close_date(run, window).isoformat() label = run.label counts: dict[str, int] = {} for tk in rows: counts[tk.status] = counts.get(tk.status, 0) + 1 open_count = sum(1 for tk in rows if tk.status not in TASK_DONE_STATUSES) return { "period": period, "label": label, "close_date": close_date, "open_count": open_count, "total": len(rows), "counts": counts, "tasks": rows, } async def _owned_task(db: AsyncSession, person_id: str, task_id: int) -> FinanceCloseTask: task = await close_task_service.get_task(db, task_id) if str(task.owner_person_id) != str(person_id): raise ForbiddenError("you can only update your own close tasks") return task async def set_my_task_status( db: AsyncSession, *, person_id: str, task_id: int, status: str ) -> FinanceCloseTask: """Owner updates the status of one of their own close tasks.""" await _owned_task(db, person_id, task_id) return await close_task_service.set_status(db, task_id, status=status, actor_id=str(person_id)) async def set_my_task_comment( db: AsyncSession, *, person_id: str, person_label: str | None, task_id: int, comment: str | None ) -> FinanceCloseTask: """Owner annotates one of their own close tasks.""" await _owned_task(db, person_id, task_id) return await close_task_service.set_comment( db, task_id, comment=comment, commenter_person_id=str(person_id), commenter_label=person_label, )