0.2.39: bitcoin-denominated view, first-login flow, unfunded + tax center

- BTC prices: btc_prices table, CSV upload on Import page (auto-detected
  date/close columns, upsert by date), entities.close_date as the BTC entry
  mark; statements carry btc_price_cents (as-of) + btc_close_price_cents.
  LP capital blocks show paid-in vs current value in bitcoin terms.
- First login: accounts on the shared default password are flagged
  (must_change_password) and blocked behind a full-screen password change;
  external accounts then get a one-time welcome tour with a 2FA offer
  (users.onboarded_at).
- LP portal: Unfunded (callable commitment) metric; Tax documents center
  aggregating K-1/tax docs across funds, grouped by year.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jonathan Kirkwood
2026-07-12 13:01:50 +02:00
co-authored by Claude Fable 5
parent 0822eca887
commit eac3262f29
22 changed files with 823 additions and 12 deletions
+23 -1
View File
@@ -2,10 +2,11 @@
import enum
from datetime import date, datetime
from datetime import date as _date # for fields literally named "date"
from decimal import Decimal
from typing import Optional
from sqlmodel import Field, SQLModel, Column, String, JSON, UniqueConstraint
from sqlmodel import Field, SQLModel, Column, Date, String, JSON, UniqueConstraint
# --- Enums ---
@@ -85,6 +86,11 @@ class User(SQLModel, table=True):
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)
# True while the account is on the shared default password — the portal forces a
# password change before anything else. Cleared by change-password / admin reset.
must_change_password: bool = Field(default=False)
# When the investor finished (or skipped) the first-login welcome flow. Null = show it.
onboarded_at: datetime | None = Field(default=None)
created_at: datetime = Field(default_factory=datetime.utcnow)
@@ -100,9 +106,25 @@ class Entity(SQLModel, table=True):
# 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)
# Final close of the fund/SPV — the BTC entry mark: paid-in capital is valued at the BTC
# price on this date for the bitcoin-denominated view. Null = no BTC view for this fund.
close_date: date | None = Field(default=None)
created_at: datetime = Field(default_factory=datetime.utcnow)
class BtcPrice(SQLModel, table=True):
"""Daily (or as-uploaded) BTC/USD closing prices from the admin's CSV.
Statements are valued at the newest price on or before their as-of date, so the CSV
doesn't need every calendar day — quarter-end rows are enough."""
__tablename__ = "btc_prices"
id: int | None = Field(default=None, primary_key=True)
date: _date = Field(sa_column=Column(Date, unique=True, nullable=False))
price_cents: int
class Holding(SQLModel, table=True):
__tablename__ = "holdings"