114 lines
5.1 KiB
Python
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
|