"""Batch historical capital backfill: several eNAV files → per-quarter statements, auto-matched.""" import io from datetime import date, datetime import openpyxl from sqlmodel import select from ten31portal.models import CapitalAccountStatement, Entity, EntityType, User, UserRole from tests.conftest import make_user def _alloc_si_file(report: datetime, rows: list[dict]) -> bytes: """Build a minimal eNAV workbook with an ALLOC SI roster and a report date in A1.""" wb = openpyxl.Workbook() ws = wb.active ws.title = "ALLOC SI" ws["A1"] = report # _enav_as_of reads the report date from the top-left cells header = ["INVESTOR ID", "INVESTOR TYPE", "INVESTOR NAME", "COMMITTED CAPITAL", "CONTRIBUTIONS", "(DISTRIBUTIONS)", "ENDING BALANCE"] ws.append([None] * 7) # row 2 spacer ws.append(header) # row 3 header for r in rows: ws.append([ r.get("id"), "LP", r["name"], r.get("commit", 0), r.get("contrib", 0), r.get("distrib", 0), r["ending"], ]) out = io.BytesIO() wb.save(out) return out.getvalue() def test_batch_backfill_builds_history(auth_client, session): fund = Entity(name="LTPF III", type=EntityType.fund) session.add(fund) session.commit() session.refresh(fund) # One member matched by fund-admin ID, one by name; both already exist. alice = make_user(session, username="alice", name="Alice Trust", role=UserRole.investor, external_investor_id="INV-100") make_user(session, username="bob", name="Bob Llc", role=UserRole.investor) q3 = _alloc_si_file(datetime(2025, 9, 30), [ {"id": "INV-100", "name": "Alice Trust", "commit": 1_000_000, "contrib": 400_000, "ending": 420_000}, {"id": "INV-200", "name": "Bob LLC", "commit": 500_000, "contrib": 200_000, "ending": 205_000}, {"id": "INV-999", "name": "Ghost Capital", "commit": 999, "contrib": 999, "ending": 999}, # no account → skipped ]) q4 = _alloc_si_file(datetime(2025, 12, 31), [ {"id": "INV-100", "name": "Alice Trust", "commit": 1_000_000, "contrib": 400_000, "ending": 455_000}, {"id": "INV-200", "name": "Bob LLC", "commit": 500_000, "contrib": 200_000, "ending": 210_000}, ]) resp = auth_client.post( "/api/import/capital-accounts/batch", data={"entity_id": fund.id}, files=[ ("files", ("LTPF_III_Q3.xlsx", q3, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")), ("files", ("LTPF_III_Q4.xlsx", q4, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")), ], ) assert resp.status_code == 200, resp.text body = resp.json() assert body["total_statements"] == 4 # 2 members × 2 quarters q3res = next(f for f in body["files"] if "Q3" in f["filename"]) assert q3res["as_of_date"] == "2025-09-30" assert q3res["matched"] == 2 assert q3res["skipped"] == ["Ghost Capital"] # unknown member reported, not created # No account was created for the unknown roster name. assert session.exec(select(User).where(User.name == "Ghost Capital")).first() is None # Alice now has two statements — a real trend line — and the latest (Q4) is intact. stmts = session.exec( select(CapitalAccountStatement) .where(CapitalAccountStatement.investor_user_id == alice.id) .order_by(CapitalAccountStatement.as_of_date) # type: ignore[arg-type] ).all() assert [s.as_of_date for s in stmts] == [date(2025, 9, 30), date(2025, 12, 31)] assert [s.ending_balance_cents for s in stmts] == [42_000_000, 45_500_000] def test_batch_reimport_updates_in_place(auth_client, session): fund = Entity(name="LTPF IV", type=EntityType.fund) session.add(fund) session.commit() session.refresh(fund) carol = make_user(session, username="carol", name="Carol Ira", role=UserRole.investor) def one_file(ending: int) -> bytes: return _alloc_si_file(datetime(2025, 12, 31), [{"id": "INV-1", "name": "Carol Ira", "ending": ending}]) for ending in (300_000, 315_000): # corrected figure re-imported at the same quarter resp = auth_client.post( "/api/import/capital-accounts/batch", data={"entity_id": fund.id}, files=[("files", ("f.xlsx", one_file(ending), "application/octet-stream"))], ) assert resp.status_code == 200, resp.text stmts = session.exec( select(CapitalAccountStatement).where(CapitalAccountStatement.investor_user_id == carol.id) ).all() assert len(stmts) == 1 # upsert, not a duplicate assert stmts[0].ending_balance_cents == 31_500_000