93 lines
4.4 KiB
Python
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)
|