Finance-Accounts/ar-aging-app/backend/app/api/routes/controls.py

96 lines
3.5 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
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