""" 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