152 lines
6.2 KiB
Python
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
|