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

114 lines
5.1 KiB
Python

"""Fast unit tests for the engine (synthetic data, no large files)."""
from __future__ import annotations
from datetime import date
from app.core.dates import parse_amazon_date, parse_amazon_datetime
from app.core.column_map import build_mapping, normalize_header
from app.core.settlements import aggregate, classify
from app.core.receivable import compute_receivable, classify_aging
# --------------------------------------------------------------------------- dates
def test_parse_amazon_dates():
assert parse_amazon_date("Jan 15, 2026 5:45:40 PM PST") == date(2026, 1, 15)
assert parse_amazon_date("Jan 30, 2026 9:40:25 PM PST") == date(2026, 1, 30)
assert parse_amazon_date("Dec 1, 2025 12:00:00 AM PST") == date(2025, 12, 1)
assert parse_amazon_date("2026-01-31 23:59:59") == date(2026, 1, 31)
assert parse_amazon_date("garbage") is None
dt, tz = parse_amazon_datetime("Jan 15, 2026 5:45:40 PM PST")
assert dt.hour == 17 and tz == "PST"
# --------------------------------------------------------------------------- mapping
def test_column_mapping_normalizes_headers():
assert normalize_header("Order-ID") == "order id"
assert normalize_header(" Total ") == "total"
header = [("A", "date/time"), ("B", "settlement id"), ("C", "type"),
("I", "account type"), ("AD", "total"), ("Z", "SomeUnknownCol")]
m = build_mapping(header, header_row=8)
assert m.is_valid
assert m.field_to_col["settlement_id"] == "B"
assert m.field_to_col["total"] == "AD"
assert "Z" in m.unmapped
def test_column_mapping_missing_required():
m = build_mapping([("A", "date/time"), ("B", "sku")], header_row=8)
assert not m.is_valid
assert "settlement_id" in m.missing_required
assert "total" in m.missing_required
# --------------------------------------------------------------------------- aging
def test_aging_bands():
assert classify_aging(None) == "Current"
assert classify_aging(0) == "Current"
assert classify_aging(15) == "1-30"
assert classify_aging(45) == "31-60"
assert classify_aging(75) == "61-90"
assert classify_aging(120) == "91-Over"
# --------------------------------------------------------------------------- settlements
def _rec(total, ttype, acct, sid, d, mkt="USA"):
return {
"total": total, "txn_type": ttype, "account_type": acct,
"settlement_id": sid, "_date": d, "_marketplace": mkt,
"date_time": d.isoformat() if d else None,
}
def _scenario():
# Standard: 100 paid (payout tagged 200, Jan 6 received),
# 200 receivable (payout tagged 300, Jan 30 in-transit),
# 300 receivable (no payout yet).
recs = []
recs += [_rec(1000.0, "Order", "Standard Orders", "100", date(2026, 1, 5))]
recs += [_rec(2000.0, "Order", "Standard Orders", "200", date(2026, 1, 15))]
recs += [_rec(500.0, "Order", "Standard Orders", "300", date(2026, 1, 31))]
# transfers (tagged to the next/open settlement)
recs += [_rec(-1000.0, "Transfer", "Standard Orders", "200", date(2026, 1, 6))]
recs += [_rec(-2000.0, "Transfer", "Standard Orders", "300", date(2026, 1, 30))]
# an Invoiced chain, independent
recs += [_rec(300.0, "Order", "Invoiced Orders", "150", date(2026, 1, 10))]
recs += [_rec(-300.0, "Transfer", "Invoiced Orders", "250", date(2026, 1, 12))]
recs += [_rec(80.0, "Order", "Invoiced Orders", "250", date(2026, 1, 20))]
return recs
def test_classification_and_receivable():
agg = aggregate(_scenario())
cls = classify(agg, month_end=date(2026, 1, 31), clearing_lag_days=2)
def status(acct, sid):
return agg.settlements[("USA", acct, sid)].status
assert status("Standard Orders", "100") == "paid"
assert status("Standard Orders", "200") == "receivable"
assert status("Standard Orders", "300") == "receivable"
assert status("Invoiced Orders", "150") == "paid"
assert status("Invoiced Orders", "250") == "receivable"
# Jan-30 transfer is in-transit; Jan-6 transfer received.
by_sid = {(t.account_type, t.settlement_id): t for t in agg.transfers}
assert by_sid[("Standard Orders", "300")].received is False
assert by_sid[("Standard Orders", "200")].received is True
rec = compute_receivable(agg, cls)
usa = rec.marketplaces["USA"]
assert usa.accounts["Standard Orders"].additional_sales == 2500.0 # 2000 + 500
assert usa.accounts["Invoiced Orders"].additional_sales == 80.0
assert usa.receivable_local == 2580 # rounded total
def test_clearing_lag_override_moves_boundary():
# With lag=0, the Jan-30 transfer counts as received -> settlement 300 becomes paid.
agg = aggregate(_scenario())
cls = classify(agg, month_end=date(2026, 1, 31), clearing_lag_days=0)
assert agg.settlements[("USA", "Standard Orders", "300")].status == "receivable"
# boundary = max received tagged = 300 -> only >=300 receivable; 200 becomes paid
assert agg.settlements[("USA", "Standard Orders", "200")].status == "paid"
rec = compute_receivable(agg, cls)
assert rec.marketplaces["USA"].accounts["Standard Orders"].additional_sales == 500.0