Finance-Accounts/ar-aging-app/backend/tests/test_aging.py

93 lines
4.4 KiB
Python

"""
A/R aging bands.
The bands measure days **past due**, not days since the transaction. An Amazon receivable is
not due when the order posts — it is due when its settlement disburses, roughly 14 days after
the settlement's last activity plus the clearing lag.
This distinction is the whole report. Banding by transaction date instead pushes a perfectly
normal biweekly settlement into 1-30 (on the Jan-2026 close that misfiled 9,556,111.11 of USA's
11,110,308 as overdue) and the aging stops meaning anything. Banding by due date keeps a healthy
month at ~100% Current — matching the Finance workbook — while a settlement Amazon is actually
holding still ages out of Current, which is the point.
"""
from __future__ import annotations
import datetime as dt
import pytest
# The test database is redirected in conftest.py, which runs before any test module is
# imported. (An earlier version set "AR_DB_URL" here — a name app/config.py does not read —
# so these tests wrote into the production database instead.)
from fastapi.testclient import TestClient # noqa: E402
from sqlalchemy.orm import Session as OrmSession # noqa: E402
from app.api.main import app # noqa: E402
from app.db import models # noqa: E402
from app.db.database import SessionLocal, init_db # noqa: E402
MONTH_END = dt.date(2026, 1, 31)
def _session_with_settlement(last_date: dt.date, amount: float = 100_000.0) -> int:
"""A processed closing holding one receivable settlement with the given last activity."""
init_db()
db: OrmSession = SessionLocal()
try:
s = models.Session(name="aging", reporting_month="2026-01", month_end_date=MONTH_END,
clearing_lag_days=2, status="processed")
db.add(s)
db.commit()
db.add(models.Settlement(
session_id=s.id, marketplace="USA", account_type="Standard Orders",
settlement_id="900", order_total=amount, transfer_total=0.0, row_count=1,
first_date=last_date, last_date=last_date, status="receivable"))
db.add(models.ReceivableResultRow(
session_id=s.id, marketplace="USA", account_type="TOTAL",
additional_sales=amount, reserve=0.0, receivable_local=amount,
fx_rate=1.0, receivable_usd=amount, currency="USD"))
db.commit()
return s.id
finally:
db.close()
@pytest.mark.parametrize("last_date,expected_band", [
# due = last activity + 14 (settlement cycle) + 2 (clearing lag); overdue vs 31 Jan 2026
(dt.date(2026, 1, 29), "Current"), # due 14 Feb — 14 days before it is even due
(dt.date(2026, 1, 15), "Current"), # due 31 Jan — due exactly today, not yet late
(dt.date(2026, 1, 14), "1-30"), # due 30 Jan — 1 day overdue
(dt.date(2025, 12, 20), "1-30"), # due 5 Jan — 26 days overdue
(dt.date(2025, 11, 20), "31-60"), # due 6 Dec — 56 days overdue
(dt.date(2025, 11, 1), "61-90"), # due 17 Nov — 75 days overdue
(dt.date(2025, 10, 15), "91-Over"), # due 31 Oct — 92 days overdue
])
def test_settlements_band_by_days_past_due(last_date, expected_band):
sid = _session_with_settlement(last_date)
with TestClient(app) as c:
row = c.get(f"/api/sessions/{sid}/aging").json()["rows"][0]
banded = {b: v for b, v in row.items() if b in
("Current", "1-30", "31-60", "61-90", "91-Over")}
hit = max(banded, key=lambda b: abs(banded[b]))
assert hit == expected_band, f"last activity {last_date} landed in {hit}, expected {expected_band}"
def test_normal_biweekly_settlement_stays_current():
"""The regression that matters: a healthy settlement must not read as overdue."""
sid = _session_with_settlement(dt.date(2026, 1, 29), amount=9_556_111.11)
with TestClient(app) as c:
row = c.get(f"/api/sessions/{sid}/aging").json()["rows"][0]
assert row["Current"] == pytest.approx(9_556_111.11, abs=0.01)
assert row["1-30"] == 0.0
def test_bands_always_tie_to_the_headline_receivable():
"""Reserve and rounding land in Current so the row still sums to the published figure."""
sid = _session_with_settlement(dt.date(2025, 11, 20), amount=100_000.0)
with TestClient(app) as c:
data = c.get(f"/api/sessions/{sid}/aging").json()
row = data["rows"][0]
assert sum(row[b] for b in data["bands"]) == pytest.approx(row["Total"], abs=0.01)
assert row["Total"] == pytest.approx(100_000.0, abs=0.01)