Files
Ten31-Portal/backend/tests/test_exited.py
T
Jonathan KirkwoodandClaude Opus 4.8 6313d781b4 0.2.34: manage exited status from the Capital Accounts view
The eNAV keeps listing exited members each quarter, so the admin needs
the exit control where the statements live:
- Capital Accounts table gains a Status column with the same
  Exited-badge / mark / undo flow as the Partners tab (keyed per
  investor+fund pair — marking any statement row marks them all)
- set_partner_exited now creates the access row (flag set) when a
  manually-entered investor has statements but no roster entry yet;
  clearing a never-set exit stays 404

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 09:03:49 -05:00

117 lines
4.5 KiB
Python

"""Exited positions (0.2.33): a member who sold/transferred their stake shows as exited
instead of a phantom -100% loss, and drops out of committed totals."""
from datetime import date
from sqlmodel import select
from ten31portal.models import (
CapitalAccountStatement, Entity, EntityAccess, EntityType, UserRole,
)
from tests.conftest import make_user
def _setup_fund(session, *, commitment=4_000_000_00, balance=0):
entity = Entity(name="Pawn Fund", type=EntityType.spv)
session.add(entity)
session.commit()
lp = make_user(session, username="seller", role=UserRole.investor, name="Seller LP")
session.add(EntityAccess(user_id=lp.id, entity_id=entity.id))
session.add(CapitalAccountStatement(
entity_id=entity.id, investor_user_id=lp.id, as_of_date=date(2026, 3, 31),
commitment_cents=commitment, beginning_balance_cents=0,
contributions_cents=commitment, distributions_cents=0,
ending_balance_cents=balance,
))
session.commit()
return entity, lp
def test_mark_exited_flows_to_partners_and_statements(auth_client, session):
entity, lp = _setup_fund(session)
resp = auth_client.put(
f"/api/entities/{entity.id}/partners/{lp.id}/exited",
json={"exited_on": "2026-05-15"},
)
assert resp.status_code == 200, resp.text
assert resp.json()["exited_on"] == "2026-05-15"
partners = auth_client.get(f"/api/entities/{entity.id}/partners").json()
assert partners[0]["exited_on"] == "2026-05-15"
# The LP's own capital-account view carries the exit date.
lp_client = auth_client
lp_client.post("/api/auth/logout")
assert lp_client.post(
"/api/auth/login", json={"login": "seller", "password": "password123"}
).status_code == 200
accounts = lp_client.get("/api/capital-accounts").json()
assert accounts[0]["exited_on"] == "2026-05-15"
# Undo clears it.
lp_client.post("/api/auth/logout")
assert lp_client.post(
"/api/auth/login", json={"login": "approver", "password": "password123"}
).status_code == 200
resp = lp_client.put(
f"/api/entities/{entity.id}/partners/{lp.id}/exited", json={"exited_on": None}
)
assert resp.status_code == 200
assert resp.json()["exited_on"] is None
def test_exited_member_excluded_from_rollup_committed(auth_client, session):
entity, seller = _setup_fund(session)
# The buyer joins the roster with the same commitment (they bought the stake).
buyer = make_user(session, username="buyer", role=UserRole.investor, name="Buyer LP")
session.add(EntityAccess(user_id=buyer.id, entity_id=entity.id))
session.add(CapitalAccountStatement(
entity_id=entity.id, investor_user_id=buyer.id, as_of_date=date(2026, 6, 30),
commitment_cents=4_000_000_00, beginning_balance_cents=0,
contributions_cents=4_000_000_00, distributions_cents=0,
ending_balance_cents=4_200_000_00,
))
session.commit()
# Before the exit both commitments count — the double-count problem.
rollup = auth_client.get("/api/entities/rollup").json()
row = next(r for r in rollup if r["id"] == entity.id)
assert row["committed_cents"] == 8_000_000_00
assert auth_client.put(
f"/api/entities/{entity.id}/partners/{seller.id}/exited",
json={"exited_on": "2026-05-15"},
).status_code == 200
rollup = auth_client.get("/api/entities/rollup").json()
row = next(r for r in rollup if r["id"] == entity.id)
assert row["committed_cents"] == 4_000_000_00
def test_exited_without_access_row(auth_client, session):
"""A manually-entered investor may have statements but no access grant yet — marking
them exited from the Capital Accounts screen creates the roster row with the flag set.
Clearing an exit that was never set stays a 404."""
entity, _ = _setup_fund(session)
manual = make_user(session, username="manual-lp", role=UserRole.investor)
resp = auth_client.put(
f"/api/entities/{entity.id}/partners/{manual.id}/exited",
json={"exited_on": None},
)
assert resp.status_code == 404
resp = auth_client.put(
f"/api/entities/{entity.id}/partners/{manual.id}/exited",
json={"exited_on": "2026-05-15"},
)
assert resp.status_code == 200, resp.text
assert resp.json()["exited_on"] == "2026-05-15"
access = session.exec(
select(EntityAccess).where(
EntityAccess.entity_id == entity.id, EntityAccess.user_id == manual.id
)
).one()
assert str(access.exited_on) == "2026-05-15"