Implement adjudicated DO items across backend, frontend, deploy
From the ROADMAP adjudication (12 of 13 DO items; D2 is a commit action). Backend: - B3: pytest suite (auth, entity CRUD, rollup) + dev deps + pytest config - B4: cap document uploads at TEN31_MAX_UPLOAD_SIZE (default 50MB), stream- checked with partial-file cleanup, 413 on overflow - B7: type AuditLog.detail as dict|list|str|None to match the JSON column - B10: index foreign-key columns (migration a7b8c9d0e1f2 + index=True) - B11: cli delete-user logs file-removal errors instead of swallowing them Frontend: - F2: distinguish "server unreachable" from "logged out"; retry prompt - F4: confirm before destructive holdings-replace on import; step progress - F6: expandable audit-log detail with full JSON - F7: empty-state on the Investments page - F8: shared role helpers (WRITER_ROLES/canEditRound/isApprover), used by EntitiesList, AuditLog, Import, ValuationWorkflow Deploy: - D5: run tsc --noEmit before packaging (build script) - D6: TEN31_LOG_LEVEL env var (defaults to info) Verified: 8/8 backend tests pass; alembic upgrades to head with 13 FK indexes; upload limit rejects oversized + cleans up; frontend tsc + vite build clean; dev server serves and proxies to the API.
This commit is contained in:
@@ -11,10 +11,27 @@ from sqlmodel import Field, SQLModel, Column, String, JSON, UniqueConstraint
|
||||
# --- Enums ---
|
||||
|
||||
class UserRole(str, enum.Enum):
|
||||
approver = "approver"
|
||||
# Internal staff
|
||||
approver = "approver" # "Managing Partner" — full access incl. valuation sign-off
|
||||
operations = "operations" # full access except final sign-off
|
||||
cfo = "cfo"
|
||||
fund_admin = "fund_admin"
|
||||
viewer = "viewer"
|
||||
# External accounts (entity-scoped via EntityAccess)
|
||||
investor = "investor"
|
||||
fund_administrator = "fund_administrator"
|
||||
|
||||
|
||||
# External roles see only the entities granted to them.
|
||||
EXTERNAL_ROLES = (UserRole.investor, UserRole.fund_administrator)
|
||||
|
||||
|
||||
class DocumentCategory(str, enum.Enum):
|
||||
capital_account = "capital_account"
|
||||
k1 = "k1"
|
||||
statement = "statement"
|
||||
tax = "tax"
|
||||
other = "other"
|
||||
|
||||
|
||||
class EntityType(str, enum.Enum):
|
||||
@@ -43,10 +60,21 @@ class User(SQLModel, table=True):
|
||||
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
name: str
|
||||
email: str = Field(sa_column=Column(String, unique=True, nullable=False))
|
||||
username: str = Field(sa_column=Column(String, unique=True, nullable=False))
|
||||
email: str | None = Field(default=None, sa_column=Column(String, unique=True, nullable=True))
|
||||
password_hash: str
|
||||
role: UserRole
|
||||
is_active: bool = Field(default=True)
|
||||
# The built-in Service Admin (bootstrap account). Can be reset but never deleted.
|
||||
is_service_admin: bool = Field(default=False)
|
||||
# False for members imported without a password; set True when an admin sets one.
|
||||
login_enabled: bool = Field(default=True)
|
||||
# When an investor invests under several legal names (one per Partner/vehicle), each name
|
||||
# is its own account. Linking the secondary accounts to one "primary" lets that person sign
|
||||
# in once and see every name's investments. Null = this account logs in on its own.
|
||||
primary_account_id: int | None = Field(default=None, foreign_key="users.id", index=True)
|
||||
# Fund-administrator investor ID (from the eNAV ALLOC SI tab) for idempotent re-import.
|
||||
external_investor_id: str | None = Field(default=None, sa_column=Column(String, nullable=True))
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
|
||||
|
||||
@@ -66,7 +94,7 @@ class Holding(SQLModel, table=True):
|
||||
__tablename__ = "holdings"
|
||||
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
entity_id: int = Field(foreign_key="entities.id")
|
||||
entity_id: int = Field(foreign_key="entities.id", index=True)
|
||||
company_name: str
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
|
||||
@@ -75,7 +103,7 @@ class Position(SQLModel, table=True):
|
||||
__tablename__ = "positions"
|
||||
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
holding_id: int = Field(foreign_key="holdings.id")
|
||||
holding_id: int = Field(foreign_key="holdings.id", index=True)
|
||||
security_name: str
|
||||
investment_date: date
|
||||
shares: str | None = None # Decimal stored as string
|
||||
@@ -93,9 +121,9 @@ class ValuationRound(SQLModel, table=True):
|
||||
entity_id: int = Field(foreign_key="entities.id")
|
||||
quarter_end: date
|
||||
status: RoundStatus = Field(default=RoundStatus.draft)
|
||||
submitted_by: int | None = Field(default=None, foreign_key="users.id")
|
||||
submitted_by: int | None = Field(default=None, foreign_key="users.id", index=True)
|
||||
submitted_at: datetime | None = None
|
||||
approved_by: int | None = Field(default=None, foreign_key="users.id")
|
||||
approved_by: int | None = Field(default=None, foreign_key="users.id", index=True)
|
||||
approved_at: datetime | None = None
|
||||
return_note: str | None = None
|
||||
is_seed: bool = Field(default=False)
|
||||
@@ -110,7 +138,7 @@ class Valuation(SQLModel, table=True):
|
||||
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
round_id: int = Field(foreign_key="valuation_rounds.id")
|
||||
position_id: int = Field(foreign_key="positions.id")
|
||||
position_id: int = Field(foreign_key="positions.id", index=True)
|
||||
value_cents: int
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
|
||||
@@ -119,9 +147,63 @@ class AuditLog(SQLModel, table=True):
|
||||
__tablename__ = "audit_log"
|
||||
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
actor_user_id: int | None = Field(default=None, foreign_key="users.id")
|
||||
actor_user_id: int | None = Field(default=None, foreign_key="users.id", index=True)
|
||||
action: str
|
||||
object_type: str
|
||||
object_id: int | None = None
|
||||
detail: str | None = Field(default=None, sa_column=Column(JSON, nullable=True))
|
||||
# An action-specific JSON payload describing the change (stored in a JSON column). Most
|
||||
# callers pass a dict of changed fields (e.g. an entity update); some pass an identifying
|
||||
# field on delete, and some pass None. Matches AuditLogResponse.detail in schemas.py.
|
||||
detail: dict | list | str | None = Field(default=None, sa_column=Column(JSON, nullable=True))
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
|
||||
|
||||
class EntityAccess(SQLModel, table=True):
|
||||
"""Which entities an external account may view."""
|
||||
__tablename__ = "entity_access"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("user_id", "entity_id", name="uq_access_user_entity"),
|
||||
)
|
||||
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
user_id: int = Field(foreign_key="users.id")
|
||||
entity_id: int = Field(foreign_key="entities.id", index=True)
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
|
||||
|
||||
class Document(SQLModel, table=True):
|
||||
"""An uploaded file. Shared to a fund (investor_user_id null) or private to one investor."""
|
||||
__tablename__ = "documents"
|
||||
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
entity_id: int = Field(foreign_key="entities.id", index=True)
|
||||
investor_user_id: int | None = Field(default=None, foreign_key="users.id", index=True)
|
||||
category: DocumentCategory = Field(default=DocumentCategory.other)
|
||||
title: str
|
||||
original_filename: str
|
||||
content_type: str
|
||||
size_bytes: int
|
||||
storage_path: str # opaque filename on the data volume, relative to DOCS_DIR
|
||||
uploaded_by: int | None = Field(default=None, foreign_key="users.id", index=True)
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
|
||||
|
||||
class CapitalAccountStatement(SQLModel, table=True):
|
||||
"""An investor's capital-account figures for one fund as of a date."""
|
||||
__tablename__ = "capital_account_statements"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("entity_id", "investor_user_id", "as_of_date",
|
||||
name="uq_capacct_entity_investor_date"),
|
||||
)
|
||||
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
entity_id: int = Field(foreign_key="entities.id")
|
||||
investor_user_id: int = Field(foreign_key="users.id", index=True)
|
||||
as_of_date: date
|
||||
commitment_cents: int = 0 # initial capital commitment
|
||||
beginning_balance_cents: int = 0
|
||||
contributions_cents: int = 0 # paid-in capital
|
||||
distributions_cents: int = 0 # capital returned (for DPI)
|
||||
ending_balance_cents: int = 0 # current capital value
|
||||
document_id: int | None = Field(default=None, foreign_key="documents.id", index=True)
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
|
||||
Reference in New Issue
Block a user