"""NAV history backfill (0.2.43): old eNAV files add past quarters to valuation history WITHOUT touching current holdings — no cost-basis regression, no resurrected positions.""" import io from datetime import date, datetime import openpyxl from sqlmodel import select from ten31portal.models import ( Entity, EntityType, Holding, Position, RoundStatus, Valuation, ValuationRound, ) def _enav_file(report: datetime, hld_rows: list[tuple], alloc_rows: list[dict] | None = None) -> bytes: """Minimal eNAV workbook: an HLD sheet (and optionally an ALLOC SI roster). hld_rows: (security_name, quantity, cost, value) tuples. """ wb = openpyxl.Workbook() ws = wb.active ws.title = "HLD" ws["A1"] = report # _enav_as_of scans the top-left cells for the report date ws.append([None]) ws.append(["SECURITY NAME", "QUANTITY", "COST BASIS - BOOK", "MARKET VALUE (BOOK)"]) for name, qty, cost, value in hld_rows: ws.append([name, qty, cost, value]) if alloc_rows is not None: alloc = wb.create_sheet("ALLOC SI") alloc.append(["INVESTOR ID", "INVESTOR TYPE", "INVESTOR NAME", "COMMITTED CAPITAL", "CONTRIBUTIONS", "(DISTRIBUTIONS)", "ENDING BALANCE"]) for r in alloc_rows: alloc.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 _fund_with_current_book(session): """A fund holding one position (cost $100) with its NAV already signed for Q1 2026.""" fund = Entity(name="LTPF I", type=EntityType.fund) session.add(fund) session.commit() session.refresh(fund) holding = Holding(entity_id=fund.id, company_name="Acme") session.add(holding) session.commit() pos = Position(holding_id=holding.id, security_name="Acme - Series A", investment_date=date(2023, 1, 1), cost_cents=100_00) session.add(pos) session.commit() session.refresh(pos) rnd = ValuationRound(entity_id=fund.id, quarter_end=date(2026, 3, 31), status=RoundStatus.approved, is_seed=True) session.add(rnd) session.commit() session.refresh(rnd) session.add(Valuation(round_id=rnd.id, position_id=pos.id, value_cents=900_00)) session.commit() return fund, pos def test_old_file_becomes_history_round_without_touching_book(auth_client, session): fund, pos = _fund_with_current_book(session) old = _enav_file(datetime(2025, 9, 30), [ ("Acme - Series A", 10, 123.0, 555.0), # matches today's book ("Ghost - SAFE", 5, 999.0, 999.0), # sold since; must NOT be created ]) resp = auth_client.post( f"/api/import/schedule?entity_id={fund.id}&commit=true", files={"file": ("old.xlsx", io.BytesIO(old), "application/octet-stream")}, ) assert resp.status_code == 200, resp.text body = resp.json() assert body["history_only"] is True assert body["status"] == "added" assert body["matched"] == 1 and body["unmatched"] == 1 session.expire_all() # The quarter landed in valuation history with the matched row's value only. hist_round = session.exec(select(ValuationRound).where( ValuationRound.entity_id == fund.id, ValuationRound.quarter_end == date(2025, 9, 30) )).first() assert hist_round is not None vals = session.exec(select(Valuation).where(Valuation.round_id == hist_round.id)).all() assert [(v.position_id, v.value_cents) for v in vals] == [(pos.id, 555_00)] # Today's book is untouched: cost basis kept, no ghost position resurrected. assert session.get(Position, pos.id).cost_cents == 100_00 assert session.exec(select(Holding).where(Holding.company_name == "Ghost")).first() is None def test_signed_round_is_kept(auth_client, session): fund, pos = _fund_with_current_book(session) signed = ValuationRound(entity_id=fund.id, quarter_end=date(2025, 9, 30), status=RoundStatus.approved, is_seed=False) session.add(signed) session.commit() old = _enav_file(datetime(2025, 9, 30), [("Acme - Series A", 10, 123.0, 555.0)]) resp = auth_client.post( f"/api/import/schedule?entity_id={fund.id}&commit=true", files={"file": ("old.xlsx", io.BytesIO(old), "application/octet-stream")}, ) assert resp.status_code == 200, resp.text assert resp.json()["status"] == "kept-signed" session.expire_all() assert session.exec(select(Valuation).join( ValuationRound, Valuation.round_id == ValuationRound.id ).where(ValuationRound.quarter_end == date(2025, 9, 30))).all() == [] def test_batch_backfill_also_records_nav(auth_client, session): from tests.conftest import make_user from ten31portal.models import CapitalAccountStatement, EntityAccess, UserRole fund, pos = _fund_with_current_book(session) lp = make_user(session, username="lp", name="Alice Trust", role=UserRole.investor) session.add(EntityAccess(user_id=lp.id, entity_id=fund.id)) session.commit() old = _enav_file( datetime(2025, 9, 30), [("Acme - Series A", 10, 123.0, 555.0), ("Ghost - SAFE", 5, 999.0, 999.0)], alloc_rows=[{"id": "INV-1", "name": "Alice Trust", "commit": 1000, "contrib": 400, "ending": 420}], ) for expected_status in ("added", "updated"): # second pass proves idempotency resp = auth_client.post( "/api/import/capital-accounts/batch", data={"entity_id": str(fund.id)}, files=[("files", ("old.xlsx", io.BytesIO(old), "application/octet-stream"))], ) assert resp.status_code == 200, resp.text f = resp.json()["files"][0] assert f["error"] is None and f["skipped"] == [] assert f["statements_written"] == 1 assert f["nav_status"] == expected_status assert f["nav_matched"] == 1 and f["nav_unmatched"] == 1 assert f["nav_cents"] == 555_00 session.expire_all() stmts = session.exec(select(CapitalAccountStatement).where( CapitalAccountStatement.entity_id == fund.id )).all() assert len(stmts) == 1 # upserted, not duplicated rounds = session.exec(select(ValuationRound).where( ValuationRound.entity_id == fund.id, ValuationRound.quarter_end == date(2025, 9, 30) )).all() assert len(rounds) == 1 assert session.get(Position, pos.id).cost_cents == 100_00