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

152 lines
6.2 KiB
Python

"""
Session deletion (cascade) and the three opening-balance modes.
Both were user-reported issues:
* deleting a closing failed with a FOREIGN KEY error because child rows blocked it
* the opening balance auto-seeded, with no way to choose zero / carry-forward / manual
"""
from __future__ import annotations
import os
import tempfile
_TMP = tempfile.mkdtemp(prefix="ar_lifecycle_test_")
os.environ["AR_DATA_DIR"] = _TMP
from fastapi.testclient import TestClient # noqa: E402
from app.api.main import app # noqa: E402
from app.db import models # noqa: E402
from app.db.database import SessionLocal, init_db # noqa: E402
from tests.test_excel_export import make_amazon_xlsx # noqa: E402
def _new(c, name: str, month_end: str, **kw) -> int:
body = {"name": name, "month_end_date": month_end, "clearing_lag_days": 2, **kw}
r = c.post("/api/sessions", json=body)
assert r.status_code == 200, r.text
return r.json()["id"]
def _processed_session(c, name: str, month_end: str, **kw) -> int:
sid = _new(c, name, month_end, **kw)
path = os.path.join(_TMP, f"USA {name}.xlsx")
make_amazon_xlsx(path, order_rows=6)
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
# --------------------------------------------------------------------- deletion
def test_delete_removes_session_and_all_children():
init_db()
with TestClient(app) as c:
sid = _processed_session(c, "to delete", "2026-01-31")
db = SessionLocal()
assert db.query(models.Transaction).filter_by(session_id=sid).count() > 0
db.close()
r = c.delete(f"/api/sessions/{sid}")
assert r.status_code == 200, r.text
assert r.json()["deleted"] == sid
assert r.json()["rows_deleted"] # reported what it removed
assert c.get(f"/api/sessions/{sid}").status_code == 404
db = SessionLocal()
for model in (models.Transaction, models.Settlement, models.ReceivableResultRow,
models.ReconciliationRow, models.MarketPayout, models.Exception_,
models.OpeningBalance, models.FxRate, models.Reserve,
models.JournalEntry, models.SessionFile):
assert db.query(model).filter_by(session_id=sid).count() == 0, model.__tablename__
db.close()
def test_delete_blocked_while_processing():
init_db()
with TestClient(app) as c:
sid = _new(c, "busy", "2026-01-31")
db = SessionLocal()
db.get(models.Session, sid).status = "processing"
db.commit()
db.close()
assert c.delete(f"/api/sessions/{sid}").status_code == 409
db = SessionLocal() # tidy up
db.get(models.Session, sid).status = "draft"
db.commit()
db.close()
assert c.delete(f"/api/sessions/{sid}").status_code == 200
# --------------------------------------------------------- opening-balance modes
def test_opening_mode_zero_is_the_default():
init_db()
with TestClient(app) as c:
prior = _processed_session(c, "prior for zero", "2026-01-31")
c.put(f"/api/sessions/{prior}/opening-balances",
json=[{"marketplace": "USA", "amount": 5000.0, "reason": "x", "source": "manual"}])
sid = _new(c, "zero month", "2026-02-28") # no opening_mode passed
assert c.get(f"/api/sessions/{sid}").json()["opening_mode"] == "zero"
assert all(o["amount"] == 0.0 for o in c.get(f"/api/sessions/{sid}/opening-balances").json())
def test_opening_mode_carry_forward_at_creation():
init_db()
with TestClient(app) as c:
prior = _processed_session(c, "prior carry", "2026-03-31")
closing = c.get(f"/api/sessions/{prior}/ar-movement").json()["closing"]
assert closing != 0
sid = _new(c, "carry month", "2026-04-30",
opening_mode="carry_forward", opening_source_session_id=prior)
s = c.get(f"/api/sessions/{sid}").json()
assert s["opening_mode"] == "carry_forward"
usa = [o for o in c.get(f"/api/sessions/{sid}/opening-balances").json()
if o["marketplace"] == "USA"][0]
assert usa["amount"] == closing
assert usa["source"] == "carried_forward"
def test_carry_forward_and_reset_endpoints():
init_db()
with TestClient(app) as c:
prior = _processed_session(c, "prior endpoints", "2026-05-31")
closing = c.get(f"/api/sessions/{prior}/ar-movement").json()["closing"]
sid = _processed_session(c, "target endpoints", "2026-06-30") # starts at zero
assert c.get(f"/api/sessions/{sid}/ar-movement").json()["opening"] == 0.0
cands = c.get(f"/api/sessions/{sid}/opening-candidates").json()
assert any(x["session_id"] == prior for x in cands["candidates"])
r = c.post(f"/api/sessions/{sid}/opening-balances/carry-forward",
json={"from_session_id": prior})
assert r.status_code == 200, r.text
assert r.json()["applied"] >= 1
mv = c.get(f"/api/sessions/{sid}/ar-movement").json()
assert mv["opening"] == closing
# the roll-forward must move with the new opening
assert round(mv["closing"], 2) == round(
closing + mv["net_revenue"] + mv["received_payouts"], 2)
assert c.post(f"/api/sessions/{sid}/opening-balances/reset").status_code == 200
assert c.get(f"/api/sessions/{sid}/ar-movement").json()["opening"] == 0.0
assert c.get(f"/api/sessions/{sid}").json()["opening_mode"] == "zero"
def test_carry_forward_with_no_prior_falls_back_to_zero():
"""A first-ever closing that asks to carry forward must still be creatable."""
init_db()
with TestClient(app) as c:
# a month earlier than every processed closing -> no candidates
sid = _new(c, "earliest", "2020-01-31", opening_mode="carry_forward")
s = c.get(f"/api/sessions/{sid}").json()
assert s["opening_mode"] == "zero"
assert c.delete(f"/api/sessions/{sid}").status_code == 200