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:
co-authored by
Claude Fable 5
parent
0822eca887
commit
eac3262f29
@@ -1,5 +1,7 @@
|
||||
"""Capital account statements: admin entry, investor read of their own figures."""
|
||||
|
||||
from bisect import bisect_right
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlmodel import Session, select, col
|
||||
|
||||
@@ -10,7 +12,7 @@ from ten31portal.auth import (
|
||||
)
|
||||
from ten31portal.database import get_session
|
||||
from ten31portal.models import (
|
||||
CapitalAccountStatement, Entity, EntityAccess, User, UserRole,
|
||||
BtcPrice, CapitalAccountStatement, Entity, EntityAccess, User, UserRole,
|
||||
)
|
||||
from ten31portal.schemas import CapitalAccountCreate, CapitalAccountResponse
|
||||
|
||||
@@ -37,6 +39,33 @@ def exit_dates(session: Session, rows) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def btc_marks(session: Session, rows) -> tuple[dict, dict]:
|
||||
"""BTC/USD marks for the bitcoin-denominated view.
|
||||
|
||||
Returns ({statement_id: price at its as-of date}, {entity_id: price at the fund's
|
||||
close date}) — "price at" meaning the newest uploaded price on or before that date,
|
||||
so a quarter-end-only CSV is enough. Empty when no prices are uploaded; a fund without
|
||||
a close_date has no entry and the portal hides its BTC view.
|
||||
"""
|
||||
if not rows:
|
||||
return {}, {}
|
||||
prices = session.exec(select(BtcPrice).order_by(col(BtcPrice.date))).all()
|
||||
if not prices:
|
||||
return {}, {}
|
||||
dates = [p.date for p in prices]
|
||||
|
||||
def price_at(d):
|
||||
i = bisect_right(dates, d)
|
||||
return prices[i - 1].price_cents if i else None
|
||||
|
||||
asof = {r.id: price_at(r.as_of_date) for r in rows}
|
||||
entities = session.exec(
|
||||
select(Entity).where(col(Entity.id).in_({r.entity_id for r in rows}))
|
||||
).all()
|
||||
close = {e.id: price_at(e.close_date) for e in entities if e.close_date is not None}
|
||||
return asof, close
|
||||
|
||||
|
||||
@router.get("")
|
||||
def list_statements(
|
||||
entity_id: int | None = None,
|
||||
@@ -74,11 +103,14 @@ def list_statements(
|
||||
)
|
||||
).all()) if rows else {}
|
||||
exits = exit_dates(session, rows)
|
||||
btc_asof, btc_close = btc_marks(session, rows)
|
||||
out: list[CapitalAccountResponse] = []
|
||||
for r in rows:
|
||||
data = CapitalAccountResponse.model_validate(r, from_attributes=True)
|
||||
data.investor_name = names.get(r.investor_user_id)
|
||||
data.exited_on = exits.get((r.investor_user_id, r.entity_id))
|
||||
data.btc_price_cents = btc_asof.get(r.id)
|
||||
data.btc_close_price_cents = btc_close.get(r.entity_id)
|
||||
out.append(data)
|
||||
return out
|
||||
|
||||
|
||||
Reference in New Issue
Block a user