Files
Ten31-Portal/backend/tests/test_exited.py
T
Jonathan KirkwoodandClaude Opus 4.8 a639ba14cf 0.2.33: exited positions (secondary-sale edge case)
A member who sold/transferred their stake showed a phantom -100% loss
($0 ending balance, no distribution through the fund). Now:
- entity_access.exited_on (migration e1f2a3b4c5d6), set/cleared from the
  Partners tab (writer-only, inline date picker, audited)
- LP portal card shows a quiet "Exited <date>" badge, keeps documents,
  hides balance/gain
- portfolio summary excludes exited positions ("Excludes N exited")
- fund committed totals (rollup + Partners tab) skip exited members so
  seller + buyer are not double-counted

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

100 lines
3.8 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_requires_membership(auth_client, session):
entity, _ = _setup_fund(session)
stranger = make_user(session, username="stranger", role=UserRole.investor)
resp = auth_client.put(
f"/api/entities/{entity.id}/partners/{stranger.id}/exited",
json={"exited_on": "2026-05-15"},
)
assert resp.status_code == 404