164 lines
7.6 KiB
Python
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
|