"""Month-end controls: view, re-run, and confirm the FX rates control C5 requires.""" from __future__ import annotations import datetime as dt from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from sqlalchemy.orm import Session as OrmSession from ...db import models from ...services.controls_run import payload, run_and_persist from ..deps import db_dep, get_session_or_404 router = APIRouter(prefix="/api/sessions", tags=["controls"]) @router.get("/{session_id}/controls") def get_controls(session_id: int, db: OrmSession = Depends(db_dep)) -> dict: get_session_or_404(session_id, db) return payload(db, session_id) @router.post("/{session_id}/controls/run") def rerun_controls(session_id: int, db: OrmSession = Depends(db_dep)) -> dict: """ Re-evaluate the controls without re-parsing the source files. Changing an opening balance, an FX rate or a reserve changes what the controls should say, so this is what clears (or re-applies) a block after a fix. """ s = get_session_or_404(session_id, db) if s.status == "processing": raise HTTPException(409, "This closing is still processing — wait for it to finish.") return run_and_persist(db, session_id) class FxConfirmIn(BaseModel): marketplace: str rate: float | None = None # optionally correct the rate while confirming it currency: str | None = None confirmed_by: str @router.post("/{session_id}/fx/confirm") def confirm_fx(session_id: int, body: FxConfirmIn, db: OrmSession = Depends(db_dep)) -> dict: """ Record that a human confirmed this marketplace's rate FOR THIS REPORTING MONTH. Control C5 treats a seeded default as missing, because the seeded table is a Jan-2026 snapshot and would otherwise value any later month at January's rates in silence. """ s = get_session_or_404(session_id, db) if not body.confirmed_by.strip(): raise HTTPException(400, "confirmed_by is required — a rate is confirmed by a person.") row = db.query(models.FxRate).filter( models.FxRate.session_id == session_id, models.FxRate.marketplace == body.marketplace).first() if row is None: row = models.FxRate(session_id=session_id, marketplace=body.marketplace) db.add(row) if body.rate is not None: row.rate = body.rate if body.currency: row.currency = body.currency row.confirmed_by = body.confirmed_by.strip() row.confirmed_at = dt.datetime.utcnow() row.confirmed_month = s.reporting_month or "" row.source = f"confirmed by {row.confirmed_by}" db.commit() return run_and_persist(db, session_id) class FxConfirmAllIn(BaseModel): confirmed_by: str @router.post("/{session_id}/fx/confirm-all") def confirm_all_fx(session_id: int, body: FxConfirmAllIn, db: OrmSession = Depends(db_dep)) -> dict: """Confirm every rate on the closing as-is (after reviewing them on the Settings tab).""" s = get_session_or_404(session_id, db) who = body.confirmed_by.strip() if not who: raise HTTPException(400, "confirmed_by is required — a rate is confirmed by a person.") now = dt.datetime.utcnow() n = 0 for row in db.query(models.FxRate).filter(models.FxRate.session_id == session_id): row.confirmed_by = who row.confirmed_at = now row.confirmed_month = s.reporting_month or "" n += 1 db.commit() out = run_and_persist(db, session_id) out["confirmed"] = n return out