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>
This commit is contained in:
Jonathan Kirkwood
2026-07-03 09:03:49 -05:00
co-authored by Claude Opus 4.8
parent a639ba14cf
commit 6313d781b4
8 changed files with 115 additions and 15 deletions
+14 -4
View File
@@ -171,16 +171,26 @@ def set_partner_exited(
Their statements and documents stay; the portal shows an Exited badge instead of a
phantom -100% and drops the position from portfolio and fund committed totals.
"""
if session.get(Entity, entity_id) is None:
raise HTTPException(status_code=404, detail="Entity not found")
member = session.get(User, user_id)
if member is None or member.role != UserRole.investor:
raise HTTPException(status_code=400, detail="Only investor members can be marked exited")
access = session.exec(
select(EntityAccess).where(
EntityAccess.entity_id == entity_id, EntityAccess.user_id == user_id
)
).first()
if access is None:
raise HTTPException(status_code=404, detail="That member has no access to this fund")
member = session.get(User, user_id)
if member is None or member.role != UserRole.investor:
raise HTTPException(status_code=400, detail="Only investor members can be marked exited")
# A manually-entered investor may have statements without an access grant yet
# (e.g. from the Capital Accounts screen). Marking them exited creates the roster
# row with the flag set; clearing an exit that doesn't exist stays a 404.
if body.exited_on is None:
raise HTTPException(status_code=404, detail="That member has no access to this fund")
access = EntityAccess(user_id=user_id, entity_id=entity_id)
session.add(access)
session.flush()
access.exited_on = body.exited_on
session.add(access)
+21 -4
View File
@@ -89,11 +89,28 @@ def test_exited_member_excluded_from_rollup_committed(auth_client, session):
assert row["committed_cents"] == 4_000_000_00
def test_exited_requires_membership(auth_client, session):
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)
stranger = make_user(session, username="stranger", role=UserRole.investor)
manual = make_user(session, username="manual-lp", role=UserRole.investor)
resp = auth_client.put(
f"/api/entities/{entity.id}/partners/{stranger.id}/exited",
json={"exited_on": "2026-05-15"},
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"