"""0.2.39: BTC price CSV + bitcoin-denominated marks, forced default-password change, and the first-login onboarded watermark.""" import io from datetime import date from ten31portal import config from ten31portal.models import CapitalAccountStatement, Entity, EntityAccess, EntityType, UserRole from tests.conftest import make_user def _fund_with_lp(session, *, close_date=None): entity = Entity(name="LTPF X", type=EntityType.fund, close_date=close_date) session.add(entity) session.commit() lp = make_user(session, username="lp", role=UserRole.investor, name="An LP") session.add(EntityAccess(user_id=lp.id, entity_id=entity.id)) session.add(CapitalAccountStatement( entity_id=entity.id, investor_user_id=lp.id, as_of_date=date(2026, 3, 31), commitment_cents=1_000_000_00, beginning_balance_cents=0, contributions_cents=500_000_00, distributions_cents=0, ending_balance_cents=600_000_00, )) session.commit() return entity, lp def _upload_prices(client, csv_text): return client.post( "/api/import/btc-prices", files={"file": ("prices.csv", io.BytesIO(csv_text.encode()), "text/csv")}, ) def test_btc_csv_import_and_marks(auth_client, session): _fund_with_lp(session, close_date=date(2025, 6, 30)) resp = _upload_prices( auth_client, "Date,Close\n2025-06-30,60000\n2025-12-31,80000\n2026-03-31,100000.50\n", ) assert resp.status_code == 200, resp.text body = resp.json() assert body["imported"] == 3 assert body["latest_price_cents"] == 10_000_050 # The LP's statements carry both marks: as-of price and close-date price. auth_client.post("/api/auth/logout") auth_client.post("/api/auth/login", json={"login": "lp", "password": "password123"}) acct = auth_client.get("/api/capital-accounts").json()[0] assert acct["btc_price_cents"] == 10_000_050 # exact as-of match assert acct["btc_close_price_cents"] == 6_000_000 # fund close 2025-06-30 def test_btc_price_nearest_on_or_before(auth_client, session): # Prices only exist BEFORE the statement date → the newest one on-or-before is used. _fund_with_lp(session, close_date=date(2025, 6, 30)) resp = _upload_prices(auth_client, "date,price\n2025-06-28,55000\n2026-03-01,90000\n") assert resp.status_code == 200, resp.text auth_client.post("/api/auth/logout") auth_client.post("/api/auth/login", json={"login": "lp", "password": "password123"}) acct = auth_client.get("/api/capital-accounts").json()[0] assert acct["btc_price_cents"] == 9_000_000 # 2026-03-01 covers 2026-03-31 assert acct["btc_close_price_cents"] == 5_500_000 # 2025-06-28 covers the 06-30 close def test_btc_reupload_overwrites_and_bad_file_rejected(auth_client, session): assert _upload_prices(auth_client, "Date,Close\n2026-01-01,90000\n").status_code == 200 r = _upload_prices(auth_client, "Date,Close\n2026-01-01,95000\n") assert r.status_code == 200 assert r.json()["count"] == 1 # upsert, not a duplicate row assert r.json()["latest_price_cents"] == 9_500_000 assert _upload_prices(auth_client, "just some text\nwith,no,dates\n").status_code == 400 def test_default_password_forces_change(client, session): make_user(session, username="fresh", role=UserRole.investor, password=config.DEFAULT_INVESTOR_PASSWORD) resp = client.post( "/api/auth/login", json={"login": "fresh", "password": config.DEFAULT_INVESTOR_PASSWORD}, ) assert resp.status_code == 200 assert resp.json()["must_change_password"] is True # Choosing the shared default again is rejected; a real password clears the flag. r = client.post("/api/auth/change-password", json={ "current_password": config.DEFAULT_INVESTOR_PASSWORD, "new_password": config.DEFAULT_INVESTOR_PASSWORD, }) assert r.status_code == 400 r = client.post("/api/auth/change-password", json={ "current_password": config.DEFAULT_INVESTOR_PASSWORD, "new_password": "my-own-secret-1", }) assert r.status_code == 200 assert client.get("/api/auth/me").json()["must_change_password"] is False def test_onboarded_stamp(client, session): make_user(session, username="lp2", role=UserRole.investor) client.post("/api/auth/login", json={"login": "lp2", "password": "password123"}) assert client.get("/api/auth/me").json()["onboarded_at"] is None assert client.post("/api/auth/onboarded").status_code == 200 stamped = client.get("/api/auth/me").json()["onboarded_at"] assert stamped is not None # Idempotent — the first stamp wins. client.post("/api/auth/onboarded") assert client.get("/api/auth/me").json()["onboarded_at"] == stamped def test_entity_close_date_update(auth_client, session): entity = Entity(name="SPV Y", type=EntityType.spv) session.add(entity) session.commit() r = auth_client.patch(f"/api/entities/{entity.id}", json={"close_date": "2025-11-15"}) assert r.status_code == 200, r.text assert r.json()["close_date"] == "2025-11-15"