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