0.2.43: historical NAV backfill without touching current holdings
The batch history import now also records each quarter's NAV in the fund's valuation history: the old file's HLD rows are matched by issuer and security name against the book as it exists today, matched rows write that quarter's valuations, unmatched rows are counted and reported, and nothing outside the round is created or modified. A manually signed quarter is never overwritten. The single-file wizard automatically takes the same history-only path when the file is older than the fund's newest round. Previously that import would regress position cost basis to the old file's values and resurrect since-exited positions, corrupting the fund's Invested total.
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
// - content-hashed /assets/* are cache-first (immutable, safe forever)
|
||||
// - /api/* is never cached
|
||||
// Bump CACHE on each release so old entries are purged.
|
||||
const CACHE = 'ten31-portal-0.2.42'
|
||||
const CACHE = 'ten31-portal-0.2.43'
|
||||
|
||||
self.addEventListener('install', () => self.skipWaiting())
|
||||
|
||||
|
||||
@@ -155,6 +155,10 @@ export interface BatchCapitalFileResult {
|
||||
updated: number;
|
||||
skipped: string[];
|
||||
error: string | null;
|
||||
nav_status: "added" | "updated" | "kept-signed" | "no-match" | "no-hld" | "error" | null;
|
||||
nav_matched: number;
|
||||
nav_unmatched: number;
|
||||
nav_cents: number;
|
||||
}
|
||||
|
||||
export interface BatchCapitalImportResult {
|
||||
|
||||
@@ -165,13 +165,22 @@ export default function Import() {
|
||||
await api.capitalImportCommit({ entity_id: resolvedEntityId, ...memberPayload });
|
||||
setStep(replaceExisting ? "Replacing holdings…" : "Loading holdings…");
|
||||
try {
|
||||
await api.scheduleImport(file, {
|
||||
const sres = await api.scheduleImport(file, {
|
||||
commit: true,
|
||||
entityId: resolvedEntityId,
|
||||
password: password || undefined,
|
||||
asOf,
|
||||
replaceExisting,
|
||||
});
|
||||
if (sres.history_only) {
|
||||
const total = (sres.matched ?? 0) + (sres.unmatched ?? 0);
|
||||
holdingsNote =
|
||||
sres.status === "kept-signed"
|
||||
? "This quarter already has a signed valuation round; it was left unchanged."
|
||||
: sres.status === "no-match"
|
||||
? "Older file: none of its holdings match the current book, so no NAV was recorded for that quarter."
|
||||
: `Older file: recorded as a historical quarter in valuation history (${sres.matched} of ${total} holdings matched). Current holdings and cost basis untouched.`;
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (String(e.message).toLowerCase().includes("already exists")) {
|
||||
holdingsNote = "A signed valuation round exists for this quarter — holdings left unchanged.";
|
||||
@@ -458,8 +467,10 @@ function BatchBackfill({ entities }: { entities: Entity[] }) {
|
||||
<p className="text-sm text-gray-500 mb-4 max-w-3xl">
|
||||
Load several past quarters at once to build investors' trend-lines. Drop the eNAV
|
||||
workbooks for one fund; each file's members are matched to existing accounts and their
|
||||
capital statement is saved at that file's own as-of date. The latest figures are never
|
||||
replaced, and members not already in the portal are skipped (not created).
|
||||
capital statement is saved at that file's own as-of date, and the quarter's NAV is
|
||||
recorded in the fund's valuation history. The latest figures are never replaced,
|
||||
current holdings are never modified, and members not already in the portal are
|
||||
skipped (not created).
|
||||
</p>
|
||||
|
||||
{error && <div className="p-3 bg-red-50 border border-red-200 rounded text-sm text-red-700 mb-4">{error}</div>}
|
||||
@@ -518,6 +529,7 @@ function BatchBackfill({ entities }: { entities: Entity[] }) {
|
||||
<th className="px-4 py-2 font-medium">File</th>
|
||||
<th className="px-4 py-2 font-medium">As-of</th>
|
||||
<th className="px-4 py-2 font-medium text-right">Loaded</th>
|
||||
<th className="px-4 py-2 font-medium text-right">NAV</th>
|
||||
<th className="px-4 py-2 font-medium">Skipped (no account)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -529,6 +541,28 @@ function BatchBackfill({ entities }: { entities: Entity[] }) {
|
||||
<td className="px-4 py-2 text-right text-gray-900">
|
||||
{f.error ? "—" : f.statements_written}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-right text-gray-600 whitespace-nowrap">
|
||||
{f.error || !f.nav_status ? (
|
||||
"—"
|
||||
) : f.nav_status === "added" || f.nav_status === "updated" ? (
|
||||
<>
|
||||
{formatMoneyExact(f.nav_cents)}
|
||||
{f.nav_unmatched > 0 && (
|
||||
<span className="text-xs text-amber-600 block">
|
||||
{f.nav_unmatched} row{f.nav_unmatched === 1 ? "" : "s"} not in current book
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
) : f.nav_status === "kept-signed" ? (
|
||||
<span className="text-xs text-gray-400">signed, kept</span>
|
||||
) : f.nav_status === "no-match" ? (
|
||||
<span className="text-xs text-amber-600">no holdings matched</span>
|
||||
) : f.nav_status === "no-hld" ? (
|
||||
<span className="text-xs text-gray-400">no HLD tab</span>
|
||||
) : (
|
||||
<span className="text-xs text-red-600">NAV failed</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-gray-500">
|
||||
{f.error ? (
|
||||
<span className="text-red-600">{f.error}</span>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Bumped each release so the running build is visible in the UI.
|
||||
// If the number shown in the app doesn't match the installed s9pk version,
|
||||
// the new frontend isn't actually being served.
|
||||
export const APP_VERSION = "0.2.42";
|
||||
export const APP_VERSION = "0.2.43";
|
||||
|
||||
Reference in New Issue
Block a user