0.2.27-0.2.32: LP portal polish, brand palette, default investor logins
Cumulative checkpoint since 0.2.26:
- 0.2.27/28: entity valuation-history table; investor gain/loss = NAV +
distributions vs paid-in
- 0.2.29: Reset Fund Partners (endpoint, Partners-tab button, CLI, action)
- 0.2.30: "Current Capital Balance" label, %-only gain/loss
- 0.2.31: Management Entities rename, Carry Vehicle type, chart
distributions-line gate
- 0.2.32: LP-facing polish pass
* Ten31 brand palette from the logo (navy/mint); orange retired
* portfolio summary card across funds; gain labeled "net of paid-in"
* whole-dollar headline figures; "History · N quarters" toggle
* documents grouped by year with a "New" badge (users.docs_seen_at)
* eNAV-created members start on default password with login enabled;
enable-investor-logins CLI + StartOS action for existing accounts
* password minimum raised to 8 chars; login help line (Portal@ten31.xyz)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
69f12b0519
commit
4215d4478f
@@ -6,20 +6,19 @@ Fallback: a subaccounts sheet (investor names across columns, per-vehicle value
|
||||
|
||||
Encrypted workbooks are decrypted with the open password. Nothing is written on preview.
|
||||
Commit matches existing members (by fund-admin investor ID, else name), creates new ones
|
||||
(without a login unless a password is given), grants entity access, and loads each member's
|
||||
capital-account statement (commitment, contributions, distributions, current value).
|
||||
(on the shared default password unless one is given), grants entity access, and loads each
|
||||
member's capital-account statement (commitment, contributions, distributions, current value).
|
||||
"""
|
||||
|
||||
import io
|
||||
import re
|
||||
import secrets
|
||||
from datetime import date, datetime
|
||||
|
||||
import openpyxl
|
||||
from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from ten31portal import storage
|
||||
from ten31portal import config, storage
|
||||
from ten31portal.audit import record_audit
|
||||
from ten31portal.auth import hash_password, require_internal_admin
|
||||
from ten31portal.database import get_session
|
||||
@@ -38,6 +37,39 @@ MAX_ROWS = 400
|
||||
MAX_COLS = 90
|
||||
|
||||
|
||||
def reset_entity_partners(entity_id: int, session: Session) -> dict[str, int]:
|
||||
"""Remove every partner from one fund: delete its capital-account statements and the
|
||||
investors' access grants to it. The investor *accounts* are kept — they usually also
|
||||
belong to other funds — only their membership of THIS entity is cleared. Use to undo a
|
||||
wrong members/ALLOC-SI import (e.g. Fund II's roster loaded into Fund III). Holdings/NAV
|
||||
are untouched (see reset_entity_holdings for those). Caller commits.
|
||||
"""
|
||||
statements = 0
|
||||
for s in session.exec(
|
||||
select(CapitalAccountStatement).where(
|
||||
CapitalAccountStatement.entity_id == entity_id
|
||||
)
|
||||
).all():
|
||||
session.delete(s)
|
||||
statements += 1
|
||||
|
||||
# Only drop investor memberships; a fund_administrator's access is not a "partner".
|
||||
investor_ids = {
|
||||
u.id for u in session.exec(
|
||||
select(User).where(User.role == UserRole.investor)
|
||||
).all()
|
||||
}
|
||||
access = 0
|
||||
for a in session.exec(
|
||||
select(EntityAccess).where(EntityAccess.entity_id == entity_id)
|
||||
).all():
|
||||
if a.user_id in investor_ids:
|
||||
session.delete(a)
|
||||
access += 1
|
||||
|
||||
return {"statements": statements, "access_grants": access}
|
||||
|
||||
|
||||
def _slug_username(name: str) -> str:
|
||||
base = re.sub(r"[^a-z0-9]+", "", name.lower())
|
||||
return base or "investor"
|
||||
@@ -282,14 +314,16 @@ def commit_import(
|
||||
raise HTTPException(status_code=409, detail=f"Username '{inv.username}' already taken.")
|
||||
if inv.email and session.exec(select(User).where(User.email == inv.email)).first():
|
||||
raise HTTPException(status_code=409, detail=f"Email '{inv.email}' already in use.")
|
||||
pw = inv.password or secrets.token_urlsafe(32)
|
||||
# New members start on the shared default password (login enabled) so the admin
|
||||
# can send credentials right away; each investor rotates it in the portal.
|
||||
pw = inv.password or config.DEFAULT_INVESTOR_PASSWORD
|
||||
user = User(
|
||||
name=inv.name,
|
||||
username=inv.username,
|
||||
email=inv.email or None,
|
||||
password_hash=hash_password(pw),
|
||||
role=UserRole.investor,
|
||||
login_enabled=bool(inv.password),
|
||||
login_enabled=True,
|
||||
external_investor_id=inv.external_id,
|
||||
)
|
||||
session.add(user)
|
||||
|
||||
Reference in New Issue
Block a user