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

164 lines
7.6 KiB
Python

"""
Bank receipts for Amazon payouts.
Amazon's Transfer row says when a payout was INITIATED; the bank credit lands 3-5 working
days later. Finance records the bank date per payout, and that record decides received vs
in-transit (received ⇔ bank date ≤ month-end) — overriding the clearing-lag heuristic.
payout_mode=manual turns the heuristic off entirely: no receipt means not received.
Fixture (make_amazon_xlsx, month-end 2026-01-31, lag 2 → cutoff Jan 29):
Standard: transfers sid 200 (Jan 6, -1000, received) · sid 300 (Jan 30, in transit)
orders sid 100 (1000, paid) · 200 (2000) · 300 (500)
Invoiced: transfer sid 250 (Jan 12, -300, received) · orders 150 (300, paid) · 250 (80)
→ default receivable local = 2000 + 500 + 80 = 2580
"""
from __future__ import annotations
import os
import tempfile
from fastapi.testclient import TestClient
from app.api.main import app
from app.db.database import init_db
from tests.test_excel_export import make_amazon_xlsx
_TMP = tempfile.mkdtemp(prefix="ar_receipt_test_")
STD, INV = "Standard Orders", "Invoiced Orders"
def _fresh(c, name: str) -> int:
sid = c.post("/api/sessions", json={
"name": name, "reporting_month": "2026-01",
"month_end_date": "2026-01-31", "clearing_lag_days": 2,
"allow_duplicate": True, # suite shares one DB; the guard has its own test
}).json()["id"]
path = os.path.join(_TMP, f"USA {name}.xlsx")
make_amazon_xlsx(path, order_rows=4)
with open(path, "rb") as fh:
assert c.post(f"/api/sessions/{sid}/files",
files={"files": (os.path.basename(path), fh)}).status_code == 200
assert c.post(f"/api/sessions/{sid}/process").status_code == 200
assert c.get(f"/api/sessions/{sid}/status").json()["status"] == "processed"
return sid
def _reprocess(c, sid: int) -> None:
assert c.post(f"/api/sessions/{sid}/process").status_code == 200
assert c.get(f"/api/sessions/{sid}/status").json()["status"] == "processed"
def _usa_receivable(c, sid: int) -> float:
rows = c.get(f"/api/sessions/{sid}/receivable").json()
return round(sum(r["receivable_local"] for r in rows
if r["marketplace"] == "USA" and r["account_type"] != "TOTAL"), 2)
def test_payout_list_shows_amazon_dates_and_current_status():
init_db()
with TestClient(app) as c:
sid = _fresh(c, "list payouts")
data = c.get(f"/api/sessions/{sid}/payouts").json()
assert data["payout_mode"] == "auto" and not data["needs_reprocess"]
by_sid = {p["settlement_id"]: p for p in data["payouts"]}
assert set(by_sid) == {"200", "250", "300"}
assert by_sid["200"]["amount"] == -1000.0
assert by_sid["200"]["amazon_date"] == "2026-01-06"
assert by_sid["200"]["received_now"] is True # lag heuristic
assert by_sid["300"]["received_now"] is False # Jan 30 > cutoff Jan 29
assert by_sid["200"]["bank_date"] is None
def test_bank_date_overrides_the_lag_in_both_directions():
"""A February bank date pulls a 'received' payout back to in-transit (and the
receivable up); a Jan-31 bank date marks a payout received that the lag called
in-transit — Amazon initiated it Jan 30, the bank got it a day later."""
init_db()
with TestClient(app) as c:
sid = _fresh(c, "override both ways")
assert _usa_receivable(c, sid) == 2580.0
# Amazon initiated sid=200's payout Jan 6, but the bank only got it Feb 4:
# the payout was NOT received this month, so settlement 100 also stays open.
r = c.put(f"/api/sessions/{sid}/payouts/receipts", json=[
{"marketplace": "USA", "account_type": STD, "settlement_id": "200",
"bank_date": "2026-02-04", "entered_by": "tester"},
]).json()
assert r["saved"] == 1 and r["needs_reprocess"]
assert c.get(f"/api/sessions/{sid}/status").json()["session"]["needs_reprocess"] is True
# The list previews the effect before re-processing…
p = {x["settlement_id"]: x for x in
c.get(f"/api/sessions/{sid}/payouts").json()["payouts"]}
assert p["200"]["received_now"] is True and p["200"]["received_next_run"] is False
# …and re-processing applies it: Std boundary gone → 1000+2000+500+80 = 3580.
_reprocess(c, sid)
assert _usa_receivable(c, sid) == 3580.0
assert c.get(f"/api/sessions/{sid}/status").json()["session"]["needs_reprocess"] is False
# Remove that receipt; enter one for sid=300: initiated Jan 30, bank Jan 31 —
# received by month-end even though the lag heuristic said in-transit.
c.put(f"/api/sessions/{sid}/payouts/receipts", json=[
{"marketplace": "USA", "account_type": STD, "settlement_id": "200",
"bank_date": None},
{"marketplace": "USA", "account_type": STD, "settlement_id": "300",
"bank_date": "2026-01-31"},
])
_reprocess(c, sid)
# Boundary Std moves to 300 → only sid 300 receivable (500) + Invoiced 80.
assert _usa_receivable(c, sid) == 580.0
def test_manual_mode_counts_only_bank_dated_payouts():
"""The user's 'remove the lag' mode: without a receipt a payout is not received."""
init_db()
with TestClient(app) as c:
sid = _fresh(c, "manual mode")
r = c.put(f"/api/sessions/{sid}/payouts/mode", json={"mode": "manual"}).json()
assert r["payout_mode"] == "manual" and r["needs_reprocess"]
_reprocess(c, sid)
# No receipts: nothing received → no boundary → everything is receivable.
assert _usa_receivable(c, sid) == 3880.0 # Std 1000+2000+500, Inv 300+80
# Record the two January bank credits; the Jan-30 payout stays in transit.
c.put(f"/api/sessions/{sid}/payouts/receipts", json=[
{"marketplace": "USA", "account_type": STD, "settlement_id": "200",
"bank_date": "2026-01-08", "bank_amount": -1000.0},
{"marketplace": "USA", "account_type": INV, "settlement_id": "250",
"bank_date": "2026-01-14"},
])
_reprocess(c, sid)
assert _usa_receivable(c, sid) == 2580.0 # back to the default split
def test_daily_ledger_places_payout_on_its_bank_date():
init_db()
with TestClient(app) as c:
sid = _fresh(c, "ledger bank date")
# Amazon initiated Jan 6; the bank received it Jan 8.
c.put(f"/api/sessions/{sid}/payouts/receipts", json=[
{"marketplace": "USA", "account_type": STD, "settlement_id": "200",
"bank_date": "2026-01-08"},
])
detail = c.get(f"/api/sessions/{sid}/ledger-detail").json()
per = {p["key"]: p for p in detail["periods"]}
assert per["2026-01-08"]["payouts_received"] == -1000.0
assert per["2026-01-08"]["bank_dated"] == -1000.0
assert "2026-01-06" not in per or per["2026-01-06"]["payouts_received"] == 0.0
def test_bank_amount_variance_raises_a_warning():
init_db()
with TestClient(app) as c:
sid = _fresh(c, "variance")
c.put(f"/api/sessions/{sid}/payouts/receipts", json=[
{"marketplace": "USA", "account_type": STD, "settlement_id": "200",
"bank_date": "2026-01-09", "bank_amount": -987.65}, # bank fee shaved it
{"marketplace": "USA", "account_type": STD, "settlement_id": "9999999",
"bank_date": "2026-01-09"}, # typo'd settlement id
])
_reprocess(c, sid)
cats = [e["category"] for e in c.get(f"/api/sessions/{sid}/exceptions").json()]
assert "bank_amount_variance" in cats
assert "unmatched_bank_receipt" in cats