""" 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, }).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