Release 0.2.22: capital chart, Investor View, GP stakes, doc folders
Snapshot commit bringing the uncommitted phase-2 work into version control
together with four new features and the 0.2.22 version bump.
New features:
- Investor capital-over-time chart (value, paid-in, distributions per
quarter), rendered from existing capital-account history.
- Admin Investor View: read-only reconstruction of an investor's portal
(GET /api/users/{id}/investor-view), reusing the investor portal UI.
- Document upload scoped to the selected fund's own investors, with an
explicit upload-target confirmation to prevent mis-attaching.
- GP/mgmt entities gain an Assets tab listing their stakes in the funds
they manage (new entity_stakes table + /api/entities/{id}/stakes).
- Edit-entity form (change type/status/etc.), so GP entities can be
categorized correctly.
Verified: 11/11 backend tests pass; alembic upgrades to head b8c9d0e1f2a3;
frontend tsc + vite build clean; s9pk packs at 0.2.22:0 (x86_64).
Also: ignore .DS_Store and *.s9pk artifacts.
This commit is contained in:
@@ -5,25 +5,85 @@ from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ten31portal.models import UserRole, EntityType, EntityStatus, RoundStatus
|
||||
from ten31portal.models import (
|
||||
UserRole, EntityType, EntityStatus, RoundStatus, DocumentCategory,
|
||||
)
|
||||
|
||||
|
||||
# --- Auth ---
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
email: str
|
||||
login: str # username or email
|
||||
password: str
|
||||
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
email: str
|
||||
username: str
|
||||
email: str | None
|
||||
role: UserRole
|
||||
is_active: bool
|
||||
is_service_admin: bool = False
|
||||
primary_account_id: int | None = None # set when this account logs in under another
|
||||
created_at: datetime
|
||||
|
||||
|
||||
# --- User administration ---
|
||||
|
||||
class UserCreate(BaseModel):
|
||||
name: str
|
||||
username: str
|
||||
password: str
|
||||
role: UserRole
|
||||
email: str | None = None
|
||||
entity_ids: list[int] = []
|
||||
|
||||
|
||||
class UserUpdate(BaseModel):
|
||||
name: str | None = None
|
||||
username: str | None = None
|
||||
email: str | None = None
|
||||
role: UserRole | None = None
|
||||
is_active: bool | None = None
|
||||
entity_ids: list[int] | None = None # full replacement of grants when provided
|
||||
|
||||
|
||||
class PasswordReset(BaseModel):
|
||||
password: str
|
||||
|
||||
|
||||
class ChangePasswordRequest(BaseModel):
|
||||
current_password: str
|
||||
new_password: str
|
||||
|
||||
|
||||
class LinkedAccount(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
username: str
|
||||
|
||||
|
||||
class UserDetailResponse(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
username: str
|
||||
email: str | None
|
||||
role: UserRole
|
||||
is_active: bool
|
||||
is_service_admin: bool = False
|
||||
primary_account_id: int | None = None
|
||||
primary_account_name: str | None = None # the login this account is linked under, if any
|
||||
linked_accounts: list[LinkedAccount] = [] # secondary names that log in under this account
|
||||
created_at: datetime
|
||||
entity_ids: list[int] = []
|
||||
|
||||
|
||||
class AccountLink(BaseModel):
|
||||
# null detaches the account so it logs in on its own again
|
||||
primary_account_id: int | None = None
|
||||
|
||||
|
||||
# --- Entity ---
|
||||
|
||||
class EntityCreate(BaseModel):
|
||||
@@ -136,6 +196,157 @@ class RoundResponse(BaseModel):
|
||||
valuations: list[ValuationResponse] = []
|
||||
|
||||
|
||||
# --- Document ---
|
||||
|
||||
class DocumentResponse(BaseModel):
|
||||
id: int
|
||||
entity_id: int
|
||||
investor_user_id: int | None
|
||||
category: DocumentCategory
|
||||
title: str
|
||||
original_filename: str
|
||||
content_type: str
|
||||
size_bytes: int
|
||||
uploaded_by: int | None
|
||||
created_at: datetime
|
||||
|
||||
|
||||
# --- Capital account ---
|
||||
|
||||
class CapitalAccountCreate(BaseModel):
|
||||
entity_id: int
|
||||
investor_user_id: int
|
||||
as_of_date: date
|
||||
commitment_dollars: float = 0
|
||||
beginning_balance_dollars: float = 0
|
||||
contributions_dollars: float = 0
|
||||
distributions_dollars: float = 0
|
||||
ending_balance_dollars: float = 0
|
||||
document_id: int | None = None
|
||||
|
||||
|
||||
class CapitalAccountResponse(BaseModel):
|
||||
id: int
|
||||
entity_id: int
|
||||
investor_user_id: int
|
||||
investor_name: str | None = None # legal name of the account this statement belongs to
|
||||
as_of_date: date
|
||||
commitment_cents: int
|
||||
beginning_balance_cents: int
|
||||
contributions_cents: int
|
||||
distributions_cents: int
|
||||
ending_balance_cents: int
|
||||
document_id: int | None
|
||||
created_at: datetime
|
||||
|
||||
|
||||
# --- Partners (members of an entity) ---
|
||||
|
||||
class PartnerResponse(BaseModel):
|
||||
user_id: int
|
||||
name: str
|
||||
username: str
|
||||
external_investor_id: str | None
|
||||
is_active: bool
|
||||
login_enabled: bool
|
||||
latest_commitment_cents: int | None = None
|
||||
latest_contributions_cents: int | None = None
|
||||
latest_distributions_cents: int | None = None
|
||||
latest_value_cents: int | None
|
||||
latest_as_of: date | None
|
||||
statements_count: int
|
||||
|
||||
|
||||
# --- Access matrix ---
|
||||
|
||||
class AccessGrant(BaseModel):
|
||||
user_id: int
|
||||
entity_id: int
|
||||
|
||||
|
||||
class AccessMatrixResponse(BaseModel):
|
||||
users: list[UserResponse]
|
||||
entities: list[EntityResponse]
|
||||
grants: list[AccessGrant]
|
||||
|
||||
|
||||
# --- Capital account import (review-and-confirm) ---
|
||||
|
||||
class ImportInvestorPreview(BaseModel):
|
||||
source_name: str # name as it appears in the spreadsheet
|
||||
column_index: int # 0-based column it was read from
|
||||
value_dollars: float # current capital value (ending balance)
|
||||
commitment_dollars: float = 0
|
||||
contributions_dollars: float = 0
|
||||
distributions_dollars: float = 0
|
||||
external_id: str | None = None # fund-admin INVESTOR ID, when present
|
||||
matched_user_id: int | None = None
|
||||
matched_username: str | None = None
|
||||
suggested_username: str | None = None # for unmatched: a safe default
|
||||
|
||||
|
||||
class ImportValueRow(BaseModel):
|
||||
row_index: int # 0-based sheet row
|
||||
label: str # col A label (e.g. "LTPF1")
|
||||
|
||||
|
||||
class CapitalImportPreview(BaseModel):
|
||||
as_of_date: date | None
|
||||
value_rows: list[ImportValueRow] # candidate rows holding per-investor balances
|
||||
chosen_row_index: int # the row used for the values below
|
||||
investors: list[ImportInvestorPreview]
|
||||
|
||||
|
||||
class ImportCommitInvestor(BaseModel):
|
||||
action: str # "match" | "create" | "skip"
|
||||
value_dollars: float # current capital value (ending balance)
|
||||
commitment_dollars: float = 0
|
||||
contributions_dollars: float = 0
|
||||
distributions_dollars: float = 0
|
||||
user_id: int | None = None # for action=match
|
||||
name: str | None = None # for action=create
|
||||
username: str | None = None # for action=create
|
||||
email: str | None = None
|
||||
password: str | None = None # for action=create; omit to create without a login
|
||||
external_id: str | None = None # fund-admin INVESTOR ID, stored for re-import matching
|
||||
|
||||
|
||||
class CapitalImportCommit(BaseModel):
|
||||
entity_id: int
|
||||
as_of_date: date
|
||||
investors: list[ImportCommitInvestor]
|
||||
|
||||
|
||||
# --- Entity stakes (a GP/mgmt entity's interest in the funds it manages) ---
|
||||
|
||||
class EntityStakeCreate(BaseModel):
|
||||
fund_entity_id: int
|
||||
ownership_pct: float | None = None
|
||||
value_dollars: float | None = None
|
||||
note: str | None = None
|
||||
|
||||
|
||||
class EntityStakeResponse(BaseModel):
|
||||
id: int
|
||||
holder_entity_id: int
|
||||
fund_entity_id: int
|
||||
fund_name: str | None = None
|
||||
fund_type: EntityType | None = None
|
||||
ownership_pct: float | None
|
||||
value_cents: int | None
|
||||
note: str | None
|
||||
created_at: datetime
|
||||
|
||||
|
||||
# --- Investor View (admin reconstruction of what one investor sees) ---
|
||||
|
||||
class InvestorViewResponse(BaseModel):
|
||||
user: UserResponse
|
||||
entities: list[EntityResponse] = []
|
||||
capital_accounts: list[CapitalAccountResponse] = []
|
||||
documents: list[DocumentResponse] = []
|
||||
|
||||
|
||||
# --- Audit ---
|
||||
|
||||
class AuditLogResponse(BaseModel):
|
||||
|
||||
Reference in New Issue
Block a user