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.
634 lines
20 KiB
TypeScript
634 lines
20 KiB
TypeScript
/**
|
|
* Typed API client for Ten31Portal backend.
|
|
*/
|
|
|
|
// --- Types ---
|
|
|
|
export type UserRole =
|
|
| "approver"
|
|
| "operations"
|
|
| "cfo"
|
|
| "fund_admin"
|
|
| "viewer"
|
|
| "investor"
|
|
| "fund_administrator";
|
|
export type EntityType = "fund" | "spv" | "gp" | "mgmt_co" | "carry";
|
|
export type EntityStatus = "active" | "closed";
|
|
export type RoundStatus = "draft" | "submitted" | "approved" | "returned";
|
|
export type DocumentCategory =
|
|
| "capital_account"
|
|
| "k1"
|
|
| "statement"
|
|
| "tax"
|
|
| "other";
|
|
|
|
export const INTERNAL_ROLES: UserRole[] = ["approver", "operations", "cfo", "fund_admin", "viewer"];
|
|
export const EXTERNAL_ROLES: UserRole[] = ["investor", "fund_administrator"];
|
|
// Roles that can administer users, documents, and capital accounts (everything except sign-off).
|
|
export const ADMIN_ROLES: UserRole[] = ["approver", "operations", "cfo"];
|
|
|
|
// Roles that can create/edit entities and valuation rounds (staff, excluding read-only viewer).
|
|
export const WRITER_ROLES: UserRole[] = ["fund_admin", "operations", "cfo", "approver"];
|
|
|
|
export const isInternal = (role: UserRole) => INTERNAL_ROLES.includes(role);
|
|
export const isAdmin = (role: UserRole) => ADMIN_ROLES.includes(role);
|
|
export const canEditRound = (role: UserRole) => WRITER_ROLES.includes(role);
|
|
export const isApprover = (role: UserRole) => role === "approver";
|
|
// The external Administrator: full management, but only inside their granted entities.
|
|
export const isAdministrator = (role: UserRole) => role === "fund_administrator";
|
|
// Can run the admin screens (users, documents, capital accounts, imports).
|
|
export const isManager = (role: UserRole) => isAdmin(role) || isAdministrator(role);
|
|
// Can edit entity records (partners, exits, fund details) — internal writers + Administrator.
|
|
export const canManageEntity = (role: UserRole) =>
|
|
WRITER_ROLES.includes(role) || isAdministrator(role);
|
|
|
|
export interface User {
|
|
id: number;
|
|
name: string;
|
|
username: string;
|
|
email: string | null;
|
|
role: UserRole;
|
|
is_active: boolean;
|
|
is_service_admin: boolean;
|
|
primary_account_id: number | null;
|
|
totp_enabled: boolean;
|
|
must_change_password: boolean;
|
|
onboarded_at: string | null;
|
|
created_at: string;
|
|
}
|
|
|
|
/** Password was accepted but the account needs its authenticator code to finish signing in. */
|
|
export interface LoginPending2FA {
|
|
requires_2fa: true;
|
|
}
|
|
|
|
export interface LinkedAccount {
|
|
id: number;
|
|
name: string;
|
|
username: string;
|
|
}
|
|
|
|
export interface UserDetail extends User {
|
|
primary_account_name: string | null;
|
|
linked_accounts: LinkedAccount[];
|
|
entity_ids: number[];
|
|
}
|
|
|
|
export interface PortalDocument {
|
|
id: number;
|
|
entity_id: number;
|
|
investor_user_id: number | null;
|
|
category: DocumentCategory;
|
|
title: string;
|
|
original_filename: string;
|
|
content_type: string;
|
|
size_bytes: number;
|
|
uploaded_by: number | null;
|
|
created_at: string;
|
|
is_new?: boolean;
|
|
}
|
|
|
|
export interface Partner {
|
|
user_id: number;
|
|
name: string;
|
|
username: string;
|
|
external_investor_id: string | null;
|
|
is_active: boolean;
|
|
login_enabled: boolean;
|
|
latest_commitment_cents: number | null;
|
|
latest_contributions_cents: number | null;
|
|
latest_distributions_cents: number | null;
|
|
latest_value_cents: number | null;
|
|
latest_as_of: string | null;
|
|
statements_count: number;
|
|
exited_on: string | null;
|
|
}
|
|
|
|
export interface AccessGrant {
|
|
user_id: number;
|
|
entity_id: number;
|
|
}
|
|
|
|
export interface AccessMatrix {
|
|
users: User[];
|
|
entities: Entity[];
|
|
grants: AccessGrant[];
|
|
}
|
|
|
|
export interface ImportValueRow {
|
|
row_index: number;
|
|
label: string;
|
|
}
|
|
|
|
export interface ImportInvestorPreview {
|
|
source_name: string;
|
|
column_index: number;
|
|
value_dollars: number;
|
|
commitment_dollars: number;
|
|
contributions_dollars: number;
|
|
distributions_dollars: number;
|
|
external_id: string | null;
|
|
matched_user_id: number | null;
|
|
matched_username: string | null;
|
|
suggested_username: string | null;
|
|
}
|
|
|
|
export interface CapitalImportPreview {
|
|
as_of_date: string | null;
|
|
value_rows: ImportValueRow[];
|
|
chosen_row_index: number;
|
|
investors: ImportInvestorPreview[];
|
|
}
|
|
|
|
export interface CapitalImportResult {
|
|
status: string;
|
|
created_accounts: number;
|
|
matched_accounts_updated: number;
|
|
statements_written: number;
|
|
}
|
|
|
|
export interface BatchCapitalFileResult {
|
|
filename: string;
|
|
as_of_date: string | null;
|
|
matched: number;
|
|
statements_written: number;
|
|
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 {
|
|
entity_id: number;
|
|
files: BatchCapitalFileResult[];
|
|
total_statements: number;
|
|
}
|
|
|
|
export interface BtcPricesStatus {
|
|
count: number;
|
|
first_date: string | null;
|
|
last_date: string | null;
|
|
latest_price_cents: number | null;
|
|
}
|
|
|
|
export interface BtcPricesImportResult extends BtcPricesStatus {
|
|
imported: number;
|
|
skipped_rows: number;
|
|
}
|
|
|
|
export interface CapitalAccount {
|
|
id: number;
|
|
entity_id: number;
|
|
investor_user_id: number;
|
|
investor_name: string | null;
|
|
as_of_date: string;
|
|
commitment_cents: number;
|
|
beginning_balance_cents: number;
|
|
contributions_cents: number;
|
|
distributions_cents: number;
|
|
ending_balance_cents: number;
|
|
document_id: number | null;
|
|
created_at: string;
|
|
exited_on: string | null;
|
|
// BTC/USD marks for the bitcoin-denominated view (null = no price data / no close date)
|
|
btc_price_cents: number | null;
|
|
btc_close_price_cents: number | null;
|
|
}
|
|
|
|
export interface Entity {
|
|
id: number;
|
|
name: string;
|
|
type: EntityType;
|
|
vintage_year: number | null;
|
|
fund_size_cents: number | null;
|
|
status: EntityStatus;
|
|
linked_user_id: number | null;
|
|
close_date: string | null;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface Holding {
|
|
id: number;
|
|
entity_id: number;
|
|
company_name: string;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface Position {
|
|
id: number;
|
|
holding_id: number;
|
|
security_name: string;
|
|
investment_date: string;
|
|
shares: string | null;
|
|
cost_cents: number;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface Valuation {
|
|
id: number;
|
|
round_id: number;
|
|
position_id: number;
|
|
value_cents: number;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface ValuationRound {
|
|
id: number;
|
|
entity_id: number;
|
|
quarter_end: string;
|
|
status: RoundStatus;
|
|
submitted_by: number | null;
|
|
submitted_at: string | null;
|
|
approved_by: number | null;
|
|
approved_at: string | null;
|
|
return_note: string | null;
|
|
is_seed: boolean;
|
|
created_at: string;
|
|
valuations: Valuation[];
|
|
}
|
|
|
|
export interface AuditEntry {
|
|
id: number;
|
|
actor_user_id: number | null;
|
|
action: string;
|
|
object_type: string;
|
|
object_id: number | null;
|
|
detail: unknown;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface EntityStake {
|
|
id: number;
|
|
holder_entity_id: number;
|
|
fund_entity_id: number;
|
|
fund_name: string | null;
|
|
fund_type: EntityType | null;
|
|
ownership_pct: number | null;
|
|
value_cents: number | null;
|
|
note: string | null;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface InvestorView {
|
|
user: User;
|
|
entities: Entity[];
|
|
capital_accounts: CapitalAccount[];
|
|
documents: PortalDocument[];
|
|
}
|
|
|
|
export interface AssetBalances {
|
|
linked_user_id: number | null;
|
|
linked_name: string | null;
|
|
balances: CapitalAccount[];
|
|
}
|
|
|
|
// --- API helpers ---
|
|
|
|
export class ApiError extends Error {
|
|
status: number;
|
|
constructor(status: number, message: string) {
|
|
super(message);
|
|
this.status = status;
|
|
}
|
|
}
|
|
|
|
async function request<T>(path: string, options?: RequestInit): Promise<T> {
|
|
const res = await fetch(path, {
|
|
...options,
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
...options?.headers,
|
|
},
|
|
});
|
|
if (!res.ok) {
|
|
const body = await res.json().catch(() => ({ detail: res.statusText }));
|
|
throw new ApiError(res.status, body.detail || res.statusText);
|
|
}
|
|
return res.json();
|
|
}
|
|
|
|
// --- Auth ---
|
|
|
|
export const api = {
|
|
login: (login: string, password: string) =>
|
|
request<User | LoginPending2FA>("/api/auth/login", {
|
|
method: "POST",
|
|
body: JSON.stringify({ login, password }),
|
|
}),
|
|
|
|
verifyTotp: (code: string) =>
|
|
request<User>("/api/auth/login/verify-totp", {
|
|
method: "POST",
|
|
body: JSON.stringify({ code }),
|
|
}),
|
|
|
|
logout: () => request<{ status: string }>("/api/auth/logout", { method: "POST" }),
|
|
|
|
me: () => request<User>("/api/auth/me"),
|
|
|
|
changePassword: (current_password: string, new_password: string) =>
|
|
request<{ status: string }>("/api/auth/change-password", {
|
|
method: "POST",
|
|
body: JSON.stringify({ current_password, new_password }),
|
|
}),
|
|
|
|
totpSetup: () =>
|
|
request<{ secret: string; otpauth_uri: string; qr_svg: string }>("/api/auth/totp/setup", {
|
|
method: "POST",
|
|
}),
|
|
|
|
totpConfirm: (code: string) =>
|
|
request<{ recovery_codes: string[] }>("/api/auth/totp/confirm", {
|
|
method: "POST",
|
|
body: JSON.stringify({ code }),
|
|
}),
|
|
|
|
totpDisable: (password: string) =>
|
|
request<{ status: string }>("/api/auth/totp/disable", {
|
|
method: "POST",
|
|
body: JSON.stringify({ password }),
|
|
}),
|
|
|
|
// Entities
|
|
listEntities: () => request<Entity[]>("/api/entities"),
|
|
listEntityRollup: () =>
|
|
request<{
|
|
id: number;
|
|
name: string;
|
|
type: EntityType;
|
|
vintage_year: number | null;
|
|
fund_size_cents: number | null;
|
|
status: EntityStatus;
|
|
committed_cents: number;
|
|
invested_cents: number;
|
|
last_signed_value_cents: number;
|
|
}[]>("/api/entities/rollup"),
|
|
getEntity: (id: number) => request<Entity>(`/api/entities/${id}`),
|
|
listPartners: (entityId: number) => request<Partner[]>(`/api/entities/${entityId}/partners`),
|
|
clearPartners: (entityId: number) =>
|
|
request<{ statements: number; access_grants: number }>(
|
|
`/api/entities/${entityId}/partners`,
|
|
{ method: "DELETE" },
|
|
),
|
|
setPartnerExited: (entityId: number, userId: number, exitedOn: string | null) =>
|
|
request<Partner>(`/api/entities/${entityId}/partners/${userId}/exited`, {
|
|
method: "PUT",
|
|
body: JSON.stringify({ exited_on: exitedOn }),
|
|
}),
|
|
createEntity: (data: Partial<Entity>) =>
|
|
request<Entity>("/api/entities", { method: "POST", body: JSON.stringify(data) }),
|
|
updateEntity: (id: number, data: Partial<Entity>) =>
|
|
request<Entity>(`/api/entities/${id}`, { method: "PATCH", body: JSON.stringify(data) }),
|
|
|
|
// Entity stakes (a GP/mgmt entity's interest in the funds it manages)
|
|
listStakes: (entityId: number) => request<EntityStake[]>(`/api/entities/${entityId}/stakes`),
|
|
createStake: (
|
|
entityId: number,
|
|
data: { fund_entity_id: number; ownership_pct?: number | null; value_dollars?: number | null; note?: string | null },
|
|
) => request<EntityStake>(`/api/entities/${entityId}/stakes`, { method: "POST", body: JSON.stringify(data) }),
|
|
deleteStake: (entityId: number, stakeId: number) =>
|
|
request<{ status: string }>(`/api/entities/${entityId}/stakes/${stakeId}`, { method: "DELETE" }),
|
|
|
|
// A GP/mgmt entity's assets: its linked account's capital balances across the funds.
|
|
entityAssetBalances: (entityId: number) =>
|
|
request<AssetBalances>(`/api/entities/${entityId}/asset-balances`),
|
|
|
|
// Investor View (admin read-only reconstruction of an investor's portal)
|
|
investorView: (userId: number) => request<InvestorView>(`/api/users/${userId}/investor-view`),
|
|
|
|
// Holdings
|
|
listHoldings: (entityId: number) =>
|
|
request<Holding[]>(`/api/entities/${entityId}/holdings`),
|
|
createHolding: (entityId: number, data: { company_name: string }) =>
|
|
request<Holding>(`/api/entities/${entityId}/holdings`, {
|
|
method: "POST",
|
|
body: JSON.stringify(data),
|
|
}),
|
|
|
|
// Positions
|
|
listPositions: (holdingId: number) =>
|
|
request<Position[]>(`/api/holdings/${holdingId}/positions`),
|
|
|
|
// Rounds
|
|
listRounds: (entityId: number) =>
|
|
request<ValuationRound[]>(`/api/entities/${entityId}/rounds`),
|
|
getRound: (roundId: number) => request<ValuationRound>(`/api/rounds/${roundId}`),
|
|
createRound: (entityId: number, quarterEnd: string) =>
|
|
request<ValuationRound>(`/api/entities/${entityId}/rounds`, {
|
|
method: "POST",
|
|
body: JSON.stringify({ quarter_end: quarterEnd }),
|
|
}),
|
|
updateValuations: (roundId: number, valuations: { position_id: number; value_cents: number }[]) =>
|
|
request<ValuationRound>(`/api/rounds/${roundId}/valuations`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify({ valuations }),
|
|
}),
|
|
submitRound: (roundId: number) =>
|
|
request<ValuationRound>(`/api/rounds/${roundId}/submit`, { method: "POST" }),
|
|
approveRound: (roundId: number) =>
|
|
request<ValuationRound>(`/api/rounds/${roundId}/approve`, { method: "POST" }),
|
|
returnRound: (roundId: number, note: string) =>
|
|
request<ValuationRound>(`/api/rounds/${roundId}/return`, {
|
|
method: "POST",
|
|
body: JSON.stringify({ note }),
|
|
}),
|
|
|
|
// Audit
|
|
listAudit: (params?: { object_type?: string; object_id?: number; page?: number }) => {
|
|
const q = new URLSearchParams();
|
|
if (params?.object_type) q.set("object_type", params.object_type);
|
|
if (params?.object_id) q.set("object_id", String(params.object_id));
|
|
if (params?.page) q.set("page", String(params.page));
|
|
return request<AuditEntry[]>(`/api/audit?${q}`);
|
|
},
|
|
|
|
// Users (admin)
|
|
listUsers: () => request<User[]>("/api/users"),
|
|
investorsForEntity: (entityId: number) =>
|
|
request<User[]>(`/api/users/investors-for-entity/${entityId}`),
|
|
accessMatrix: () => request<AccessMatrix>("/api/users/access-matrix"),
|
|
grantAccess: (userId: number, entityId: number) =>
|
|
request<{ status: string }>(`/api/users/${userId}/access/${entityId}`, { method: "PUT" }),
|
|
revokeAccess: (userId: number, entityId: number) =>
|
|
request<{ status: string }>(`/api/users/${userId}/access/${entityId}`, { method: "DELETE" }),
|
|
|
|
// Holdings / NAV import (eNAV HLD tab)
|
|
scheduleImport: async (
|
|
file: File,
|
|
opts: {
|
|
commit: boolean;
|
|
entityId?: number | null;
|
|
password?: string;
|
|
asOf?: string;
|
|
createType?: EntityType;
|
|
vintageYear?: string;
|
|
replaceExisting?: boolean;
|
|
},
|
|
): Promise<any> => {
|
|
const form = new FormData();
|
|
form.set("file", file);
|
|
if (opts.password) form.set("password", opts.password);
|
|
const p = new URLSearchParams();
|
|
p.set("commit", String(opts.commit));
|
|
if (opts.replaceExisting) p.set("replace_existing", "true");
|
|
if (opts.asOf) p.set("as_of", opts.asOf);
|
|
if (opts.entityId != null) {
|
|
p.set("entity_id", String(opts.entityId));
|
|
} else {
|
|
p.set("create_entity_type", opts.createType || "fund");
|
|
if (opts.vintageYear) p.set("create_vintage_year", opts.vintageYear);
|
|
}
|
|
const res = await fetch(`/api/import/schedule?${p}`, { method: "POST", body: form });
|
|
const data = await res.json().catch(() => ({ detail: res.statusText }));
|
|
if (!res.ok) throw new ApiError(res.status, data.detail || "Import failed");
|
|
return data;
|
|
},
|
|
|
|
// Capital-account import (fund-admin spreadsheet)
|
|
capitalImportPreview: async (
|
|
file: File,
|
|
entityId: number,
|
|
rowIndex?: number,
|
|
password?: string,
|
|
): Promise<CapitalImportPreview> => {
|
|
const form = new FormData();
|
|
form.set("file", file);
|
|
form.set("entity_id", String(entityId));
|
|
if (rowIndex != null) form.set("row_index", String(rowIndex));
|
|
if (password) form.set("password", password);
|
|
const res = await fetch("/api/import/capital-accounts/preview", { method: "POST", body: form });
|
|
if (!res.ok) {
|
|
const body = await res.json().catch(() => ({ detail: res.statusText }));
|
|
throw new ApiError(res.status, body.detail || res.statusText);
|
|
}
|
|
return res.json();
|
|
},
|
|
capitalImportCommit: (data: {
|
|
entity_id: number;
|
|
as_of_date: string;
|
|
investors: {
|
|
action: "match" | "create" | "skip";
|
|
value_dollars: number;
|
|
commitment_dollars?: number;
|
|
contributions_dollars?: number;
|
|
distributions_dollars?: number;
|
|
user_id?: number | null;
|
|
name?: string | null;
|
|
username?: string | null;
|
|
email?: string | null;
|
|
password?: string | null;
|
|
external_id?: string | null;
|
|
}[];
|
|
}) =>
|
|
request<CapitalImportResult>("/api/import/capital-accounts/commit", {
|
|
method: "POST",
|
|
body: JSON.stringify(data),
|
|
}),
|
|
|
|
// Batch backfill: several historical eNAV files → capital statements per quarter, auto-matched.
|
|
capitalImportBatch: async (
|
|
entityId: number,
|
|
files: File[],
|
|
password?: string,
|
|
): Promise<BatchCapitalImportResult> => {
|
|
const form = new FormData();
|
|
form.set("entity_id", String(entityId));
|
|
if (password) form.set("password", password);
|
|
for (const f of files) form.append("files", f);
|
|
const res = await fetch("/api/import/capital-accounts/batch", { method: "POST", body: form });
|
|
const data = await res.json().catch(() => ({ detail: res.statusText }));
|
|
if (!res.ok) throw new ApiError(res.status, data.detail || "Batch import failed");
|
|
return data;
|
|
},
|
|
// BTC prices (bitcoin-denominated view)
|
|
btcPricesStatus: () => request<BtcPricesStatus>("/api/import/btc-prices"),
|
|
importBtcPrices: async (file: File): Promise<BtcPricesImportResult> => {
|
|
const form = new FormData();
|
|
form.set("file", file);
|
|
const res = await fetch("/api/import/btc-prices", { method: "POST", body: form });
|
|
const data = await res.json().catch(() => ({ detail: res.statusText }));
|
|
if (!res.ok) throw new ApiError(res.status, data.detail || "Price import failed");
|
|
return data;
|
|
},
|
|
|
|
markOnboarded: () =>
|
|
request<{ status: string }>("/api/auth/onboarded", { method: "POST" }),
|
|
|
|
getUser: (id: number) => request<UserDetail>(`/api/users/${id}`),
|
|
createUser: (data: {
|
|
name: string;
|
|
username: string;
|
|
password: string;
|
|
role: UserRole;
|
|
email?: string | null;
|
|
entity_ids?: number[];
|
|
}) => request<UserDetail>("/api/users", { method: "POST", body: JSON.stringify(data) }),
|
|
updateUser: (
|
|
id: number,
|
|
data: Partial<{
|
|
name: string;
|
|
username: string;
|
|
email: string | null;
|
|
role: UserRole;
|
|
is_active: boolean;
|
|
entity_ids: number[];
|
|
}>,
|
|
) => request<UserDetail>(`/api/users/${id}`, { method: "PATCH", body: JSON.stringify(data) }),
|
|
resetPassword: (id: number, password: string) =>
|
|
request<{ status: string }>(`/api/users/${id}/reset-password`, {
|
|
method: "POST",
|
|
body: JSON.stringify({ password }),
|
|
}),
|
|
linkAccount: (id: number, primary_account_id: number | null) =>
|
|
request<UserDetail>(`/api/users/${id}/primary-account`, {
|
|
method: "PUT",
|
|
body: JSON.stringify({ primary_account_id }),
|
|
}),
|
|
deleteUser: (id: number) =>
|
|
request<{ status: string }>(`/api/users/${id}`, { method: "DELETE" }),
|
|
|
|
// Documents
|
|
listDocuments: (params?: { entity_id?: number; investor_user_id?: number }) => {
|
|
const q = new URLSearchParams();
|
|
if (params?.entity_id != null) q.set("entity_id", String(params.entity_id));
|
|
if (params?.investor_user_id != null) q.set("investor_user_id", String(params.investor_user_id));
|
|
const qs = q.toString();
|
|
return request<PortalDocument[]>(`/api/documents${qs ? `?${qs}` : ""}`);
|
|
},
|
|
uploadDocument: async (form: FormData): Promise<PortalDocument> => {
|
|
const res = await fetch("/api/documents", { method: "POST", body: form });
|
|
if (!res.ok) {
|
|
const body = await res.json().catch(() => ({ detail: res.statusText }));
|
|
throw new ApiError(res.status, body.detail || res.statusText);
|
|
}
|
|
return res.json();
|
|
},
|
|
downloadUrl: (id: number) => `/api/documents/${id}/download`,
|
|
deleteDocument: (id: number) =>
|
|
request<{ status: string }>(`/api/documents/${id}`, { method: "DELETE" }),
|
|
|
|
// Capital accounts
|
|
listCapitalAccounts: (params?: { entity_id?: number; investor_user_id?: number }) => {
|
|
const q = new URLSearchParams();
|
|
if (params?.entity_id != null) q.set("entity_id", String(params.entity_id));
|
|
if (params?.investor_user_id != null) q.set("investor_user_id", String(params.investor_user_id));
|
|
const qs = q.toString();
|
|
return request<CapitalAccount[]>(`/api/capital-accounts${qs ? `?${qs}` : ""}`);
|
|
},
|
|
createCapitalAccount: (data: {
|
|
entity_id: number;
|
|
investor_user_id: number;
|
|
as_of_date: string;
|
|
beginning_balance_dollars: number;
|
|
contributions_dollars: number;
|
|
distributions_dollars: number;
|
|
ending_balance_dollars: number;
|
|
document_id?: number | null;
|
|
}) => request<CapitalAccount>("/api/capital-accounts", { method: "POST", body: JSON.stringify(data) }),
|
|
deleteCapitalAccount: (id: number) =>
|
|
request<{ status: string }>(`/api/capital-accounts/${id}`, { method: "DELETE" }),
|
|
};
|