Issue 1: repo scaffold and project structure

This commit is contained in:
Johnny 5
2026-06-07 19:20:02 +00:00
parent e0b31009b7
commit a70bdeaa5e
52 changed files with 6750 additions and 0 deletions
+172
View File
@@ -0,0 +1,172 @@
/**
* Typed API client for Ten31Portal backend.
*/
// --- Types ---
export type UserRole = "approver" | "cfo" | "fund_admin" | "viewer";
export type EntityType = "fund" | "spv" | "gp" | "mgmt_co";
export type EntityStatus = "active" | "closed";
export type RoundStatus = "draft" | "submitted" | "approved" | "returned";
export interface User {
id: number;
name: string;
email: string;
role: UserRole;
is_active: boolean;
created_at: string;
}
export interface Entity {
id: number;
name: string;
type: EntityType;
vintage_year: number | null;
fund_size_cents: number | null;
status: EntityStatus;
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;
}
// --- API helpers ---
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: (email: string, password: string) =>
request<User>("/api/auth/login", {
method: "POST",
body: JSON.stringify({ email, password }),
}),
logout: () => request<{ status: string }>("/api/auth/logout", { method: "POST" }),
me: () => request<User>("/api/auth/me"),
// Entities
listEntities: () => request<Entity[]>("/api/entities"),
getEntity: (id: number) => request<Entity>(`/api/entities/${id}`),
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) }),
// 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}`);
},
};