"""One closing per month (guarded), completed closings are read-only, reopen unlocks.""" from __future__ import annotations import os import tempfile from fastapi.testclient import TestClient from app.api.main import app from app.db import models from app.db.database import SessionLocal, init_db from tests.test_excel_export import make_amazon_xlsx _TMP = tempfile.mkdtemp(prefix="ar_lock_test_") def _processed(c, name: str, month_end: str) -> int: sid = c.post("/api/sessions", json={"name": name, "month_end_date": month_end, "allow_duplicate": True}).json()["id"] path = os.path.join(_TMP, f"USA {name}.xlsx") make_amazon_xlsx(path, order_rows=5) 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 _force_complete(sid: int) -> None: db = SessionLocal() try: db.get(models.Session, sid).status = "completed" db.commit() finally: db.close() def test_duplicate_month_is_refused_unless_explicit(): init_db() with TestClient(app) as c: first = c.post("/api/sessions", json={"name": "dup guard A", "month_end_date": "2028-01-31"}) assert first.status_code == 200, first.text again = c.post("/api/sessions", json={"name": "dup guard B", "month_end_date": "2028-01-31"}) assert again.status_code == 409 assert "2028-01" in again.json()["detail"] assert "dup guard A" in again.json()["detail"] # names the existing closing forced = c.post("/api/sessions", json={"name": "dup guard C", "month_end_date": "2028-01-31", "allow_duplicate": True}) assert forced.status_code == 200 # The listing flags both sessions as sharing a month. rows = c.get("/api/sessions").json() flagged = [s for s in rows if s["reporting_month"] == "2028-01"] assert len(flagged) == 2 and all(s["duplicate_month"] for s in flagged) def test_listing_is_month_ordered_with_publish_flag(): init_db() with TestClient(app) as c: c.post("/api/sessions", json={"name": "older", "month_end_date": "2028-02-29", "allow_duplicate": True}) c.post("/api/sessions", json={"name": "newer", "month_end_date": "2028-03-31", "allow_duplicate": True}) rows = c.get("/api/sessions").json() months = [s["reporting_month"] for s in rows if s["reporting_month"]] assert months == sorted(months, reverse=True) assert all("journal_approved" in s for s in rows) def test_completed_closing_is_locked_and_reopen_unlocks(): init_db() with TestClient(app) as c: sid = _processed(c, "lock me", "2028-04-30") _force_complete(sid) # Every mutating surface answers 409 while completed. path = os.path.join(_TMP, "USA lock me.xlsx") with open(path, "rb") as fh: up = c.post(f"/api/sessions/{sid}/files", files={"files": ("late file.xlsx", fh)}) assert up.status_code == 409 assert c.post(f"/api/sessions/{sid}/process").status_code == 409 assert c.put(f"/api/sessions/{sid}/fx", json=[{"marketplace": "USA", "currency": "USD", "rate": 1.0}]).status_code == 409 assert c.put(f"/api/sessions/{sid}/opening-balances", json=[{"marketplace": "USA", "amount": 1.0}]).status_code == 409 assert c.post(f"/api/sessions/{sid}/journal/reset-signoff").status_code == 409 assert c.patch(f"/api/sessions/{sid}", json={"clearing_lag_days": 5}).status_code == 409 # …but a rename stays allowed, and reads still work. assert c.patch(f"/api/sessions/{sid}", json={"name": "renamed"}).status_code == 200 assert c.get(f"/api/sessions/{sid}/summary").status_code == 200 # Reopen restores editability. assert c.post(f"/api/sessions/{sid}/reopen").status_code == 200 assert c.get(f"/api/sessions/{sid}").json()["status"] == "processed" assert c.put(f"/api/sessions/{sid}/opening-balances", json=[{"marketplace": "USA", "amount": 1.0}]).status_code == 200 # Reopen on a non-completed closing is refused. assert c.post(f"/api/sessions/{sid}/reopen").status_code == 409 def test_accounts_summary_lists_unpublished_months_as_pending(): init_db() with TestClient(app) as c: sid = _processed(c, "pending month", "2028-05-31") summ = c.get("/api/accounts-summary").json() mine = [p for p in summ["pending"] if p["session_id"] == sid] assert len(mine) == 1 assert mine[0]["month"] == "2028-05" assert "approv" in mine[0]["reason"] # explains HOW to publish it