"""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}