174 lines
7.9 KiB
Python
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}
|