101 lines
3.8 KiB
Python
101 lines
3.8 KiB
Python
"""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, Request
|
|
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 ..auth import actor_name
|
|
from ..deps import db_dep, ensure_editable, 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 = "" # ignored when signed in — the verified identity wins
|
|
|
|
|
|
@router.post("/{session_id}/fx/confirm")
|
|
def confirm_fx(session_id: int, body: FxConfirmIn, request: Request,
|
|
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)
|
|
ensure_editable(s)
|
|
who = actor_name(request, body.confirmed_by)
|
|
if not who:
|
|
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 = who
|
|
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 = "" # ignored when signed in — the verified identity wins
|
|
|
|
|
|
@router.post("/{session_id}/fx/confirm-all")
|
|
def confirm_all_fx(session_id: int, body: FxConfirmAllIn, request: Request,
|
|
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)
|
|
ensure_editable(s)
|
|
who = actor_name(request, body.confirmed_by)
|
|
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
|