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

174 lines
7.9 KiB
Python

"""Read endpoints for the dashboard: summary, settlements, transactions, exceptions, etc."""
from __future__ import annotations
import json
from fastapi import APIRouter, Body, Depends, Query
from sqlalchemy import func
from sqlalchemy.orm import Session as OrmSession
from ...core.receivable import AGING_BANDS
from ...db import models
from ..deps import db_dep, get_session_or_404, to_dict
router = APIRouter(prefix="/api/sessions", tags=["results"])
@router.get("/{session_id}/summary")
def summary(session_id: int, db: OrmSession = Depends(db_dep)) -> dict:
get_session_or_404(session_id, db)
setts = db.query(models.Settlement).filter(models.Settlement.session_id == session_id).all()
recon = db.query(models.ReconciliationRow).filter(
models.ReconciliationRow.session_id == session_id).first()
rec_rows = db.query(models.ReceivableResultRow).filter(
models.ReceivableResultRow.session_id == session_id,
models.ReceivableResultRow.account_type == "TOTAL").all()
exc = db.query(models.Exception_.severity, func.count()).filter(
models.Exception_.session_id == session_id).group_by(models.Exception_.severity).all()
n_txn = db.query(func.count(models.Transaction.id)).filter(
models.Transaction.session_id == session_id).scalar() or 0
n_recv_txn = db.query(func.count(models.Transaction.id)).filter(
models.Transaction.session_id == session_id,
models.Transaction.receivable_flag == True).scalar() or 0 # noqa: E712
receivable_settlements = [s for s in setts if s.status == "receivable"]
return {
"closing_receivable_usd": recon.final_receivable_usd if recon else None,
"reconciliation_status": recon.status if recon else None,
"reserve_total": recon.reserve_total if recon else 0.0,
"transfers_total": recon.transfers_total if recon else 0.0,
"receivable_orders": recon.receivable_orders if recon else 0.0,
"paid_orders": recon.paid_orders if recon else 0.0,
"num_settlements": len(setts),
"num_receivable_settlements": len(receivable_settlements),
"num_paid_settlements": len(setts) - len(receivable_settlements),
"num_transactions": n_txn,
"num_receivable_transactions": n_recv_txn,
"exceptions_by_severity": {sev: c for sev, c in exc},
"receivable_by_marketplace": [
{"marketplace": r.marketplace, "receivable_local": round(r.receivable_local),
"receivable_usd": r.receivable_usd, "currency": r.currency} for r in rec_rows
],
}
@router.get("/{session_id}/receivable")
def receivable(session_id: int, db: OrmSession = Depends(db_dep)) -> list[dict]:
get_session_or_404(session_id, db)
rows = db.query(models.ReceivableResultRow).filter(
models.ReceivableResultRow.session_id == session_id).all()
return [to_dict(r, ["marketplace", "account_type", "additional_sales", "reserve",
"receivable_local", "fx_rate", "receivable_usd", "currency"]) for r in rows]
@router.get("/{session_id}/settlements")
def settlements(session_id: int, db: OrmSession = Depends(db_dep)) -> list[dict]:
get_session_or_404(session_id, db)
rows = db.query(models.Settlement).filter(
models.Settlement.session_id == session_id).order_by(
models.Settlement.marketplace, models.Settlement.account_type,
models.Settlement.settlement_id).all()
return [to_dict(r, ["marketplace", "account_type", "settlement_id", "order_total",
"transfer_total", "transfer_amount", "transfer_date", "transfer_received",
"row_count", "first_date", "last_date", "status"]) for r in rows]
@router.get("/{session_id}/transactions")
def transactions(session_id: int, db: OrmSession = Depends(db_dep),
limit: int = Query(100, le=1000), offset: int = 0,
settlement_id: str | None = None, txn_type: str | None = None,
marketplace: str | None = None, receivable: bool | None = None,
storage: bool | None = None, search: str | None = None) -> dict:
get_session_or_404(session_id, db)
q = db.query(models.Transaction).filter(models.Transaction.session_id == session_id)
if settlement_id:
q = q.filter(models.Transaction.settlement_id == settlement_id)
if txn_type:
# match either the original (possibly localized) type or the canonical English one
q = q.filter((models.Transaction.txn_type == txn_type)
| (models.Transaction.txn_type_en == txn_type))
if marketplace:
q = q.filter(models.Transaction.marketplace == marketplace)
if receivable is not None:
q = q.filter(models.Transaction.receivable_flag == receivable)
if storage is not None:
q = q.filter(models.Transaction.storage_flag == storage)
if search:
like = f"%{search}%"
q = q.filter((models.Transaction.order_id.like(like)) | (models.Transaction.sku.like(like)))
total = q.count()
rows = q.order_by(models.Transaction.id).offset(offset).limit(limit).all()
return {
"total": total, "limit": limit, "offset": offset,
"rows": [to_dict(r, ["id", "source_file", "source_row", "marketplace", "settlement_id",
"order_id", "sku", "txn_type", "txn_type_en", "account_type",
"posted_date", "total", "currency", "settlement_status",
"receivable_flag", "storage_flag"])
for r in rows],
}
@router.get("/{session_id}/exceptions")
def exceptions(session_id: int, db: OrmSession = Depends(db_dep)) -> list[dict]:
get_session_or_404(session_id, db)
rows = db.query(models.Exception_).filter(
models.Exception_.session_id == session_id).all()
return [to_dict(r, ["category", "severity", "detail", "source"]) for r in rows]
@router.get("/{session_id}/reconciliation")
def reconciliation(session_id: int, db: OrmSession = Depends(db_dep)) -> dict:
get_session_or_404(session_id, db)
r = db.query(models.ReconciliationRow).filter(
models.ReconciliationRow.session_id == session_id).first()
if not r:
return {}
d = to_dict(r, ["uploaded_total", "receivable_orders", "paid_orders", "transfers_total",
"reserve_total", "manual_adjustments", "final_receivable_usd",
"identity_difference", "status"])
try:
d["notes"] = json.loads(r.notes) if r.notes else []
except Exception:
d["notes"] = []
return d
@router.get("/{session_id}/journal")
def journal(session_id: int, db: OrmSession = Depends(db_dep)) -> dict:
get_session_or_404(session_id, db)
j = db.query(models.JournalEntry).filter(
models.JournalEntry.session_id == session_id).first()
if not j or not j.data:
return {"available": False}
data = json.loads(j.data)
data["available"] = True
data["entry_no"] = j.entry_no or ""
return data
@router.put("/{session_id}/journal/entry-no")
def set_journal_entry_no(session_id: int, entry_no: str = Body(..., embed=True),
db: OrmSession = Depends(db_dep)) -> dict:
get_session_or_404(session_id, db)
j = db.query(models.JournalEntry).filter(
models.JournalEntry.session_id == session_id).first()
if j:
j.entry_no = entry_no
db.commit()
return {"entry_no": entry_no}
@router.get("/{session_id}/aging")
def aging(session_id: int, db: OrmSession = Depends(db_dep)) -> dict:
get_session_or_404(session_id, db)
rows = db.query(models.ReceivableResultRow).filter(
models.ReceivableResultRow.session_id == session_id,
models.ReceivableResultRow.account_type == "TOTAL").all()
matrix = []
for r in rows:
band = {b: 0.0 for b in AGING_BANDS}
band["Current"] = r.receivable_usd # Amazon receivable is all Current
matrix.append({"marketplace": r.marketplace, **band,
"Total": r.receivable_usd})
return {"bands": list(AGING_BANDS), "rows": matrix}