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

109 lines
3.9 KiB
Python

"""Per-session settings (reserves, FX rates) + global header-mapping rules."""
from __future__ import annotations
from datetime import date
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy.orm import Session as OrmSession
from ...core.column_map import FIELD_ORDER, normalize_header
from ...db import models
from ..deps import db_dep, get_session_or_404, to_dict
router = APIRouter(prefix="/api/sessions", tags=["settings"])
rules_router = APIRouter(prefix="/api/mapping-rules", tags=["mapping"])
class RuleIn(BaseModel):
header: str # raw or normalized source header
field: str # canonical internal field
@rules_router.get("")
def list_rules(db: OrmSession = Depends(db_dep)) -> dict:
rows = db.query(models.MappingRule).order_by(models.MappingRule.normalized_header).all()
return {
"fields": FIELD_ORDER,
"rules": [{"id": r.id, "normalized_header": r.normalized_header, "field": r.field}
for r in rows],
}
@rules_router.put("")
def upsert_rules(items: list[RuleIn], db: OrmSession = Depends(db_dep)) -> dict:
"""Save admin header-alias rules; applied to every future parse (no code change needed)."""
for it in items:
if it.field not in FIELD_ORDER:
raise HTTPException(400, f"Unknown field '{it.field}'.")
norm = normalize_header(it.header)
if not norm:
raise HTTPException(400, "Empty header.")
row = db.query(models.MappingRule).filter(
models.MappingRule.normalized_header == norm).first()
if row is None:
db.add(models.MappingRule(normalized_header=norm, field=it.field))
else:
row.field = it.field
db.commit()
return list_rules(db)
@rules_router.delete("/{rule_id}")
def delete_rule(rule_id: int, db: OrmSession = Depends(db_dep)) -> dict:
db.query(models.MappingRule).filter(models.MappingRule.id == rule_id).delete()
db.commit()
return list_rules(db)
class ReserveIn(BaseModel):
marketplace: str
account_type: str
amount: float = 0.0
class FxIn(BaseModel):
marketplace: str
currency: str = "USD"
rate: float = 1.0
source: str = "manual"
rate_date: date | None = None
@router.get("/{session_id}/reserves")
def get_reserves(session_id: int, db: OrmSession = Depends(db_dep)) -> list[dict]:
get_session_or_404(session_id, db)
rows = db.query(models.Reserve).filter(models.Reserve.session_id == session_id).all()
return [to_dict(r, ["marketplace", "account_type", "amount"]) for r in rows]
@router.put("/{session_id}/reserves")
def put_reserves(session_id: int, items: list[ReserveIn],
db: OrmSession = Depends(db_dep)) -> list[dict]:
get_session_or_404(session_id, db)
db.query(models.Reserve).filter(models.Reserve.session_id == session_id).delete()
for it in items:
db.add(models.Reserve(session_id=session_id, marketplace=it.marketplace,
account_type=it.account_type, amount=it.amount))
db.commit()
return get_reserves(session_id, db)
@router.get("/{session_id}/fx")
def get_fx(session_id: int, db: OrmSession = Depends(db_dep)) -> list[dict]:
get_session_or_404(session_id, db)
rows = db.query(models.FxRate).filter(models.FxRate.session_id == session_id).all()
return [to_dict(r, ["marketplace", "currency", "rate", "source", "rate_date"]) for r in rows]
@router.put("/{session_id}/fx")
def put_fx(session_id: int, items: list[FxIn], db: OrmSession = Depends(db_dep)) -> list[dict]:
get_session_or_404(session_id, db)
db.query(models.FxRate).filter(models.FxRate.session_id == session_id).delete()
for it in items:
db.add(models.FxRate(session_id=session_id, marketplace=it.marketplace,
currency=it.currency, rate=it.rate, source=it.source,
rate_date=it.rate_date))
db.commit()
return get_fx(session_id, db)