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
+8 -3
View File
@@ -144,14 +144,17 @@ def investor_view(
col(User.id).in_({r.investor_user_id for r in cap_rows})
)
).all()) if cap_rows else {}
# Mirror the investor's own portal exactly — including exit status, so an exited
# position shows its badge here too instead of a phantom -100%.
from ten31portal.routers.capital_account_router import exit_dates
# Mirror the investor's own portal exactly — including exit status and BTC marks,
# so this view never drifts from what the LP actually sees.
from ten31portal.routers.capital_account_router import btc_marks, exit_dates
exits = exit_dates(session, cap_rows)
btc_asof, btc_close = btc_marks(session, cap_rows)
for r in cap_rows:
d = CapitalAccountResponse.model_validate(r, from_attributes=True)
d.investor_name = names.get(r.investor_user_id)
d.exited_on = exits.get((r.investor_user_id, r.entity_id))
d.btc_price_cents = btc_asof.get(r.id)
d.btc_close_price_cents = btc_close.get(r.entity_id)
caps.append(d)
doc_rows = session.exec(
@@ -373,6 +376,8 @@ def reset_password(
if user is None:
raise HTTPException(status_code=404, detail="User not found")
user.password_hash = hash_password(body.password)
# Admin handed them a real password — no forced change on next login.
user.must_change_password = False
user.login_enabled = True # setting a password enables login
session.add(user)
record_audit(session, admin.id, "reset_password", "user", user_id, None)