The loan-aware NAV fix (0.2.47) only affects new imports; an already-signed quarter keeps its old holdings until re-imported. Add a boot-time queue: an operator stages an eNAV plus a small JSON descriptor under data/pending_reimports and the app re-imports it on startup via the same code path as the admin Import screen, then removes the processed files. Guarded so a queue problem never blocks boot. Bumps the package to 0.2.48. Used to refresh SATS Credit Fund LP Q4 2025 to its full NAV including the Fold Holdings loan.
52 lines
1.9 KiB
Python
52 lines
1.9 KiB
Python
"""The startup re-import queue: a staged eNAV is imported on boot, then deleted.
|
|
|
|
This is how an importer fix (e.g. loan-schedule parsing) gets applied to an
|
|
already-signed round without a manual admin re-upload.
|
|
"""
|
|
|
|
import json
|
|
|
|
from sqlmodel import select
|
|
|
|
import ten31portal.database as db
|
|
from ten31portal import config
|
|
from ten31portal.models import Entity, EntityType, UserRole, Valuation, ValuationRound
|
|
from ten31portal.pending_reimports import process_pending_reimports
|
|
from tests.conftest import make_user
|
|
from tests.test_loan_import import _enav_with_loan
|
|
|
|
|
|
def test_pending_reimport_drains_and_applies(session, monkeypatch, tmp_path):
|
|
monkeypatch.setattr(config, "DB_PATH", str(tmp_path / "portal.db"))
|
|
monkeypatch.setattr(db, "engine", session.get_bind())
|
|
|
|
make_user(session, username="approver", role=UserRole.approver)
|
|
fund = Entity(name="SATS Credit Fund LP", type=EntityType.fund)
|
|
session.add(fund)
|
|
session.commit()
|
|
session.refresh(fund)
|
|
|
|
queue = tmp_path / "pending_reimports"
|
|
queue.mkdir()
|
|
(queue / "sats.xlsx").write_bytes(_enav_with_loan())
|
|
(queue / "sats.json").write_text(json.dumps({"entity_id": fund.id}))
|
|
|
|
process_pending_reimports()
|
|
|
|
# The round now includes the loan valuation; NAV = securities + loan (principal + accrued).
|
|
rnd = session.exec(select(ValuationRound).where(ValuationRound.entity_id == fund.id)).one()
|
|
nav = sum(
|
|
v.value_cents
|
|
for v in session.exec(select(Valuation).where(Valuation.round_id == rnd.id)).all()
|
|
)
|
|
assert nav == 1_639_942_63 + 0 + 46_494_185_46
|
|
|
|
# Processed files are removed so the import runs exactly once (the JSON carries a password).
|
|
assert not (queue / "sats.xlsx").exists()
|
|
assert not (queue / "sats.json").exists()
|
|
|
|
|
|
def test_no_queue_dir_is_a_noop(monkeypatch, tmp_path):
|
|
monkeypatch.setattr(config, "DB_PATH", str(tmp_path / "portal.db"))
|
|
process_pending_reimports() # must not raise when the queue dir is absent
|