"""SQLModel table definitions for Ten31Portal.""" import enum from datetime import date, datetime from decimal import Decimal from typing import Optional from sqlmodel import Field, SQLModel, Column, String, JSON, UniqueConstraint # --- Enums --- class UserRole(str, enum.Enum): # 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): fund = "fund" spv = "spv" gp = "gp" mgmt_co = "mgmt_co" carry = "carry" class EntityStatus(str, enum.Enum): active = "active" closed = "closed" class RoundStatus(str, enum.Enum): draft = "draft" submitted = "submitted" approved = "approved" returned = "returned" # --- Tables --- class User(SQLModel, table=True): __tablename__ = "users" id: int | None = Field(default=None, primary_key=True) name: str 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)) # When this investor last loaded their documents list — docs newer than this get a "New" # badge in the portal. Null until their first visit (nothing badged for brand-new logins). docs_seen_at: datetime | None = Field(default=None) # Two-factor auth (optional, per-user opt-in). The secret is set at setup time but only # counts once totp_enabled is True (enrollment is confirmed with a first valid code). totp_secret: str | None = Field(default=None) totp_enabled: bool = Field(default=False) # JSON list of sha256 hex digests of unused one-time recovery codes. totp_recovery_codes: str | None = Field(default=None) created_at: datetime = Field(default_factory=datetime.utcnow) class Entity(SQLModel, table=True): __tablename__ = "entities" id: int | None = Field(default=None, primary_key=True) name: str type: EntityType vintage_year: int | None = None fund_size_cents: int | None = None status: EntityStatus = Field(default=EntityStatus.active) # For a GP/mgmt entity that is also an LP with capital accounts (e.g. Ten31 LLC), link to # its investor account so its Assets view can pull real per-fund balances from the eNAV. linked_user_id: int | None = Field(default=None, foreign_key="users.id", index=True) created_at: datetime = Field(default_factory=datetime.utcnow) class Holding(SQLModel, table=True): __tablename__ = "holdings" id: int | None = Field(default=None, primary_key=True) entity_id: int = Field(foreign_key="entities.id", index=True) company_name: str created_at: datetime = Field(default_factory=datetime.utcnow) class Position(SQLModel, table=True): __tablename__ = "positions" id: int | None = Field(default=None, primary_key=True) holding_id: int = Field(foreign_key="holdings.id", index=True) security_name: str investment_date: date shares: str | None = None # Decimal stored as string cost_cents: int created_at: datetime = Field(default_factory=datetime.utcnow) class ValuationRound(SQLModel, table=True): __tablename__ = "valuation_rounds" __table_args__ = ( UniqueConstraint("entity_id", "quarter_end", name="uq_round_entity_quarter"), ) id: int | None = Field(default=None, primary_key=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", index=True) submitted_at: datetime | None = None 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) created_at: datetime = Field(default_factory=datetime.utcnow) class Valuation(SQLModel, table=True): __tablename__ = "valuations" __table_args__ = ( UniqueConstraint("round_id", "position_id", name="uq_valuation_round_position"), ) 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", index=True) value_cents: int created_at: datetime = Field(default_factory=datetime.utcnow) 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", index=True) action: str object_type: str object_id: int | None = None # 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) # Set when this member sold/transferred their stake (e.g. a secondary sale): the fund's # books show a $0 balance with no distribution, which is NOT a loss. An exited position # shows a badge instead of gain/loss, keeps its documents, and drops out of totals. exited_on: date | None = Field(default=None) 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) class EntityStake(SQLModel, table=True): """A holder entity's ownership stake in a fund/SPV it manages. Models the assets of a GP or management company: its interest in each fund it manages, which are entities in their own right rather than portfolio-company holdings. """ __tablename__ = "entity_stakes" __table_args__ = ( UniqueConstraint("holder_entity_id", "fund_entity_id", name="uq_stake_holder_fund"), ) id: int | None = Field(default=None, primary_key=True) holder_entity_id: int = Field(foreign_key="entities.id", index=True) # the GP / mgmt co fund_entity_id: int = Field(foreign_key="entities.id", index=True) # the fund/SPV held ownership_pct: float | None = None # e.g. 20.0 for a 20% interest value_cents: int | None = None # optional current value of the stake note: str | None = None created_at: datetime = Field(default_factory=datetime.utcnow)