mirror of
https://github.com/hanzoai/captable.git
synced 2026-10-16 14:21:29 +00:00
capTable summed ALL option rows into optionShares / per-stakeholder options
regardless of status, so EXERCISED, EXPIRED and CANCELLED grants inflated
fullyDilutedShares and understated every real ownership %. A cap-table surface
must not do this.
Filter both dilution queries (the totals sum and the per-stakeholder subquery)
to OUTSTANDING options only: status NOT IN ('EXERCISED','EXPIRED','CANCELLED').
EXERCISED options already became shares (counted in `share`) — counting the
option too double-counts; EXPIRED/CANCELLED are void. DRAFT + ACTIVE still dilute.
Rebuilt goja/bundle.js. Existing goja bundle tests pass.
Co-authored-by: hanzo-dev <[email protected]>
Co-authored-by: Hanzo Dev <[email protected]>
1304 lines
44 KiB
JavaScript
1304 lines
44 KiB
JavaScript
// @hanzo/captable — goja bundle (GENERATED by goja/build.mjs; edit src/*.ts).
|
|
// Self-contained, ESM-free; runs in dop251/goja inside hanzoai/cloud (HIP-0106).
|
|
"use strict";
|
|
(() => {
|
|
var __defProp = Object.defineProperty;
|
|
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
var __spreadValues = (a, b) => {
|
|
for (var prop in b || (b = {}))
|
|
if (__hasOwnProp.call(b, prop))
|
|
__defNormalProp(a, prop, b[prop]);
|
|
if (__getOwnPropSymbols)
|
|
for (var prop of __getOwnPropSymbols(b)) {
|
|
if (__propIsEnum.call(b, prop))
|
|
__defNormalProp(a, prop, b[prop]);
|
|
}
|
|
return a;
|
|
};
|
|
|
|
// src/host.ts
|
|
var db = {
|
|
query(sql, args) {
|
|
return globalThis.__db.query(sql, args || []);
|
|
},
|
|
exec(sql, args) {
|
|
return globalThis.__db.exec(sql, args || []);
|
|
}
|
|
};
|
|
function newId() {
|
|
return globalThis.__newId();
|
|
}
|
|
function now() {
|
|
return globalThis.__now();
|
|
}
|
|
|
|
// src/routes/common.ts
|
|
var okRes = (body) => ({ status: 200, body });
|
|
var created = (body) => ({ status: 201, body });
|
|
var badReq = (errors) => ({
|
|
status: 400,
|
|
body: { success: false, message: "Validation failed", errors }
|
|
});
|
|
var notFound = (msg) => ({
|
|
status: 404,
|
|
body: { success: false, message: msg }
|
|
});
|
|
var conflict = (msg) => ({
|
|
status: 409,
|
|
body: { success: false, message: msg }
|
|
});
|
|
function asObj(body) {
|
|
return body || {};
|
|
}
|
|
function refExists(companyId, table, id) {
|
|
if (!id) return false;
|
|
return db.query(`SELECT 1 FROM ${table} WHERE company_id = ? AND id = ?`, [
|
|
companyId,
|
|
id
|
|
]).length > 0;
|
|
}
|
|
|
|
// src/validate.ts
|
|
var StakeholderType = ["INDIVIDUAL", "INSTITUTION"];
|
|
var StakeholderRelationship = [
|
|
"ADVISOR",
|
|
"BOARD_MEMBER",
|
|
"CONSULTANT",
|
|
"EMPLOYEE",
|
|
"EX_ADVISOR",
|
|
"EX_CONSULTANT",
|
|
"EX_EMPLOYEE",
|
|
"EXECUTIVE",
|
|
"FOUNDER",
|
|
"INVESTOR",
|
|
"NON_US_EMPLOYEE",
|
|
"OFFICER",
|
|
"OTHER"
|
|
];
|
|
var ShareClassType = ["COMMON", "PREFERRED"];
|
|
var ConversionRights = [
|
|
"CONVERTS_TO_FUTURE_ROUND",
|
|
"CONVERTS_TO_SHARE_CLASS_ID"
|
|
];
|
|
var SecuritiesStatus = ["ACTIVE", "DRAFT", "SIGNED", "PENDING"];
|
|
var ShareLegends = [
|
|
"US_SECURITIES_ACT",
|
|
"SALE_AND_ROFR",
|
|
"TRANSFER_RESTRICTIONS"
|
|
];
|
|
var CancellationBehavior = [
|
|
"RETIRE",
|
|
"RETURN_TO_POOL",
|
|
"HOLD_AS_CAPITAL_STOCK",
|
|
"DEFINED_PER_PLAN_SECURITY"
|
|
];
|
|
var OptionType = ["ISO", "NSO", "RSU"];
|
|
var OptionStatus = [
|
|
"DRAFT",
|
|
"ACTIVE",
|
|
"EXERCISED",
|
|
"EXPIRED",
|
|
"CANCELLED"
|
|
];
|
|
var SafeType = ["PRE_MONEY", "POST_MONEY"];
|
|
var SafeStatus = [
|
|
"DRAFT",
|
|
"ACTIVE",
|
|
"PENDING",
|
|
"EXPIRED",
|
|
"CANCELLED"
|
|
];
|
|
var ConvertibleStatus = [
|
|
"DRAFT",
|
|
"ACTIVE",
|
|
"PENDING",
|
|
"EXPIRED",
|
|
"CANCELLED"
|
|
];
|
|
var ConvertibleType = ["CCD", "OCD", "NOTE"];
|
|
var RoundType = ["PRICED", "SAFE", "CONVERTIBLE"];
|
|
function reqString(o, k, errs) {
|
|
const v = o[k];
|
|
if (typeof v !== "string" || v.trim() === "") {
|
|
errs.push(`${k} is required`);
|
|
return "";
|
|
}
|
|
return v;
|
|
}
|
|
function optString(o, k) {
|
|
const v = o[k];
|
|
if (v === void 0 || v === null || v === "") return null;
|
|
return String(v);
|
|
}
|
|
function email(o, k, errs) {
|
|
const v = reqString(o, k, errs);
|
|
if (v && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)) {
|
|
errs.push(`${k} must be a valid email`);
|
|
}
|
|
return v;
|
|
}
|
|
function num(o, k, errs, min) {
|
|
const raw = o[k];
|
|
const n = typeof raw === "number" ? raw : Number(raw);
|
|
if (raw === void 0 || raw === null || Number.isNaN(n)) {
|
|
errs.push(`${k} is required`);
|
|
return 0;
|
|
}
|
|
if (min !== void 0 && n < min) {
|
|
errs.push(`${k} must be >= ${min}`);
|
|
}
|
|
return n;
|
|
}
|
|
function optNum(o, k) {
|
|
const raw = o[k];
|
|
if (raw === void 0 || raw === null || raw === "") return null;
|
|
const n = Number(raw);
|
|
return Number.isNaN(n) ? null : n;
|
|
}
|
|
function intNum(o, k, errs, min) {
|
|
return Math.trunc(num(o, k, errs, min));
|
|
}
|
|
function oneOf(o, k, allowed, errs) {
|
|
const v = o[k];
|
|
if (typeof v !== "string" || !allowed.includes(v)) {
|
|
errs.push(`${k} must be one of ${allowed.join(", ")}`);
|
|
return allowed[0];
|
|
}
|
|
return v;
|
|
}
|
|
function dateString(o, k, errs) {
|
|
const v = o[k];
|
|
if (typeof v !== "string" || v.trim() === "") {
|
|
errs.push(`${k} is required`);
|
|
return "";
|
|
}
|
|
return v;
|
|
}
|
|
function optDateString(o, k) {
|
|
const v = o[k];
|
|
if (v === void 0 || v === null || v === "") return null;
|
|
return String(v);
|
|
}
|
|
function legends(o, k, errs) {
|
|
const v = o[k];
|
|
if (v === void 0 || v === null) return [];
|
|
if (!Array.isArray(v)) {
|
|
errs.push(`${k} must be an array`);
|
|
return [];
|
|
}
|
|
const out = [];
|
|
for (const item of v) {
|
|
if (typeof item !== "string" || !ShareLegends.includes(item)) {
|
|
errs.push(`${k} contains an invalid legend`);
|
|
continue;
|
|
}
|
|
out.push(item);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// src/routes/company.ts
|
|
function getCompany(ctx) {
|
|
const rows = db.query(
|
|
`SELECT id, name, public_id AS publicId, incorporation_type AS incorporationType,
|
|
incorporation_country AS incorporationCountry,
|
|
incorporation_state AS incorporationState,
|
|
created_at AS createdAt, updated_at AS updatedAt
|
|
FROM company WHERE id = ?`,
|
|
[ctx.companyId]
|
|
);
|
|
if (rows.length === 0) return notFound("company not found");
|
|
return okRes(rows[0]);
|
|
}
|
|
function updateCompany(ctx) {
|
|
const o = asObj(ctx.body);
|
|
const errs = [];
|
|
const name = reqString(o, "name", errs);
|
|
if (errs.length) return badReq(errs);
|
|
const r = db.exec(
|
|
`UPDATE company
|
|
SET name = ?, incorporation_type = ?, incorporation_country = ?,
|
|
incorporation_state = ?, updated_at = ?
|
|
WHERE id = ?`,
|
|
[
|
|
name,
|
|
optString(o, "incorporationType") || "",
|
|
optString(o, "incorporationCountry") || "",
|
|
optString(o, "incorporationState") || "",
|
|
now(),
|
|
ctx.companyId
|
|
]
|
|
);
|
|
if (r.changes === 0) return notFound("company not found");
|
|
return okRes({ success: true, message: "Company updated" });
|
|
}
|
|
|
|
// src/routes/stakeholders.ts
|
|
function listStakeholders(ctx) {
|
|
const rows = db.query(
|
|
`SELECT s.id, s.name, s.email, s.institution_name AS institutionName,
|
|
s.stakeholder_type AS stakeholderType,
|
|
s.current_relationship AS currentRelationship,
|
|
s.tax_id AS taxId, s.street_address AS streetAddress, s.city,
|
|
s.state, s.zipcode, s.country, s.created_at AS createdAt,
|
|
c.name AS companyName
|
|
FROM stakeholder s
|
|
JOIN company c ON c.id = s.company_id
|
|
WHERE s.company_id = ?
|
|
ORDER BY s.created_at DESC`,
|
|
[ctx.companyId]
|
|
);
|
|
return okRes(rows);
|
|
}
|
|
function validateStakeholder(o, errs) {
|
|
return {
|
|
name: reqString(o, "name", errs),
|
|
email: email(o, "email", errs),
|
|
institutionName: optString(o, "institutionName"),
|
|
stakeholderType: oneOf(o, "stakeholderType", StakeholderType, errs),
|
|
currentRelationship: oneOf(
|
|
o,
|
|
"currentRelationship",
|
|
StakeholderRelationship,
|
|
errs
|
|
),
|
|
taxId: optString(o, "taxId"),
|
|
streetAddress: optString(o, "streetAddress"),
|
|
city: optString(o, "city"),
|
|
state: optString(o, "state"),
|
|
zipcode: optString(o, "zipcode")
|
|
};
|
|
}
|
|
function addStakeholders(ctx) {
|
|
const raw = ctx.body;
|
|
const list = Array.isArray(raw) ? raw : [raw];
|
|
if (list.length === 0) return badReq(["at least one stakeholder is required"]);
|
|
const errs = [];
|
|
const parsed = list.map(
|
|
(item) => validateStakeholder(asObj(item), errs)
|
|
);
|
|
if (errs.length) return badReq(errs);
|
|
const ts = now();
|
|
let inserted = 0;
|
|
for (const s of parsed) {
|
|
const dup = db.query(
|
|
`SELECT 1 FROM stakeholder WHERE company_id = ? AND email = ?`,
|
|
[ctx.companyId, s.email]
|
|
);
|
|
if (dup.length > 0) continue;
|
|
db.exec(
|
|
`INSERT INTO stakeholder
|
|
(id, company_id, name, email, institution_name, stakeholder_type,
|
|
current_relationship, tax_id, street_address, city, state, zipcode,
|
|
country, created_at, updated_at)
|
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
|
[
|
|
newId(),
|
|
ctx.companyId,
|
|
s.name,
|
|
s.email,
|
|
s.institutionName,
|
|
s.stakeholderType,
|
|
s.currentRelationship,
|
|
s.taxId,
|
|
s.streetAddress,
|
|
s.city,
|
|
s.state,
|
|
s.zipcode,
|
|
"US",
|
|
ts,
|
|
ts
|
|
]
|
|
);
|
|
inserted += 1;
|
|
}
|
|
return created({
|
|
success: true,
|
|
message: "Stakeholders added successfully!",
|
|
inserted
|
|
});
|
|
}
|
|
function updateStakeholder(ctx) {
|
|
const id = ctx.params.id;
|
|
if (!id) return badReq(["stakeholder id is required"]);
|
|
const o = asObj(ctx.body);
|
|
const sets = [];
|
|
const args = [];
|
|
const map = [
|
|
["name", "name"],
|
|
["email", "email"],
|
|
["institutionName", "institution_name"],
|
|
["stakeholderType", "stakeholder_type"],
|
|
["currentRelationship", "current_relationship"],
|
|
["taxId", "tax_id"],
|
|
["streetAddress", "street_address"],
|
|
["city", "city"],
|
|
["state", "state"],
|
|
["zipcode", "zipcode"]
|
|
];
|
|
for (const [jsonKey, col] of map) {
|
|
if (o[jsonKey] !== void 0) {
|
|
sets.push(`${col} = ?`);
|
|
args.push(o[jsonKey]);
|
|
}
|
|
}
|
|
if (sets.length === 0) return badReq(["no fields to update"]);
|
|
sets.push("updated_at = ?");
|
|
args.push(now(), ctx.companyId, id);
|
|
const r = db.exec(
|
|
`UPDATE stakeholder SET ${sets.join(", ")} WHERE company_id = ? AND id = ?`,
|
|
args
|
|
);
|
|
if (r.changes === 0) return notFound("stakeholder not found");
|
|
return okRes({ success: true, message: "Successfully updated the stakeholder" });
|
|
}
|
|
function deleteStakeholder(ctx) {
|
|
const id = ctx.params.id;
|
|
if (!id) return badReq(["stakeholder id is required"]);
|
|
const held = db.query(
|
|
`SELECT (SELECT COUNT(*) FROM share WHERE stakeholder_id = ? AND company_id = ?)
|
|
+ (SELECT COUNT(*) FROM option WHERE stakeholder_id = ? AND company_id = ?) AS n`,
|
|
[id, ctx.companyId, id, ctx.companyId]
|
|
);
|
|
if (Number(held[0].n) > 0) {
|
|
return badReq(["cannot delete a stakeholder that still holds shares or options"]);
|
|
}
|
|
const r = db.exec(`DELETE FROM stakeholder WHERE company_id = ? AND id = ?`, [
|
|
ctx.companyId,
|
|
id
|
|
]);
|
|
if (r.changes === 0) return notFound("stakeholder not found");
|
|
return okRes({ success: true });
|
|
}
|
|
|
|
// src/routes/equity.ts
|
|
function listShareClasses(ctx) {
|
|
const rows = db.query(
|
|
`SELECT sc.id, sc.idx, sc.name, sc.class_type AS classType, sc.prefix,
|
|
sc.initial_shares_authorized AS initialSharesAuthorized,
|
|
sc.votes_per_share AS votesPerShare, sc.par_value AS parValue,
|
|
sc.price_per_share AS pricePerShare, sc.seniority,
|
|
sc.conversion_rights AS conversionRights,
|
|
sc.liquidation_preference_multiple AS liquidationPreferenceMultiple,
|
|
sc.participation_cap_multiple AS participationCapMultiple,
|
|
c.name AS companyName
|
|
FROM share_class sc
|
|
JOIN company c ON c.id = sc.company_id
|
|
WHERE sc.company_id = ?
|
|
ORDER BY sc.idx ASC`,
|
|
[ctx.companyId]
|
|
);
|
|
return okRes(rows);
|
|
}
|
|
function validateShareClass(o, errs) {
|
|
const classType = oneOf(o, "classType", ShareClassType, errs);
|
|
return {
|
|
name: reqString(o, "name", errs),
|
|
classType,
|
|
prefix: classType === "COMMON" ? "CS" : "PS",
|
|
initialSharesAuthorized: intNum(o, "initialSharesAuthorized", errs, 1),
|
|
boardApprovalDate: dateString(o, "boardApprovalDate", errs),
|
|
stockholderApprovalDate: dateString(o, "stockholderApprovalDate", errs),
|
|
votesPerShare: intNum(o, "votesPerShare", errs, 0),
|
|
parValue: num(o, "parValue", errs, 0),
|
|
pricePerShare: num(o, "pricePerShare", errs, 0),
|
|
seniority: intNum(o, "seniority", errs, 0),
|
|
conversionRights: oneOf(o, "conversionRights", ConversionRights, errs),
|
|
convertsToShareClassId: optString(o, "convertsToShareClassId"),
|
|
liquidationPreferenceMultiple: num(o, "liquidationPreferenceMultiple", errs, 0),
|
|
participationCapMultiple: num(o, "participationCapMultiple", errs, 0)
|
|
};
|
|
}
|
|
function createShareClass(ctx) {
|
|
const errs = [];
|
|
const s = validateShareClass(asObj(ctx.body), errs);
|
|
if (errs.length) return badReq(errs);
|
|
const countRows = db.query(
|
|
`SELECT COUNT(*) AS n FROM share_class WHERE company_id = ?`,
|
|
[ctx.companyId]
|
|
);
|
|
const idx = Number(countRows[0].n) + 1;
|
|
const ts = now();
|
|
db.exec(
|
|
`INSERT INTO share_class
|
|
(id, company_id, idx, name, class_type, prefix,
|
|
initial_shares_authorized, board_approval_date, stockholder_approval_date,
|
|
votes_per_share, par_value, price_per_share, seniority, conversion_rights,
|
|
converts_to_share_class_id, liquidation_preference_multiple,
|
|
participation_cap_multiple, created_at, updated_at)
|
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
|
[
|
|
newId(),
|
|
ctx.companyId,
|
|
idx,
|
|
s.name,
|
|
s.classType,
|
|
s.prefix,
|
|
s.initialSharesAuthorized,
|
|
s.boardApprovalDate,
|
|
s.stockholderApprovalDate,
|
|
s.votesPerShare,
|
|
s.parValue,
|
|
s.pricePerShare,
|
|
s.seniority,
|
|
s.conversionRights,
|
|
s.convertsToShareClassId,
|
|
s.liquidationPreferenceMultiple,
|
|
s.participationCapMultiple,
|
|
ts,
|
|
ts
|
|
]
|
|
);
|
|
return created({ success: true, message: "Share class created successfully." });
|
|
}
|
|
function updateShareClass(ctx) {
|
|
const id = ctx.params.id;
|
|
if (!id) return badReq(["share class id is required"]);
|
|
const errs = [];
|
|
const s = validateShareClass(asObj(ctx.body), errs);
|
|
if (errs.length) return badReq(errs);
|
|
const r = db.exec(
|
|
`UPDATE share_class
|
|
SET name = ?, class_type = ?, prefix = ?, initial_shares_authorized = ?,
|
|
board_approval_date = ?, stockholder_approval_date = ?,
|
|
votes_per_share = ?, par_value = ?, price_per_share = ?, seniority = ?,
|
|
conversion_rights = ?, converts_to_share_class_id = ?,
|
|
liquidation_preference_multiple = ?, participation_cap_multiple = ?,
|
|
updated_at = ?
|
|
WHERE company_id = ? AND id = ?`,
|
|
[
|
|
s.name,
|
|
s.classType,
|
|
s.prefix,
|
|
s.initialSharesAuthorized,
|
|
s.boardApprovalDate,
|
|
s.stockholderApprovalDate,
|
|
s.votesPerShare,
|
|
s.parValue,
|
|
s.pricePerShare,
|
|
s.seniority,
|
|
s.conversionRights,
|
|
s.convertsToShareClassId,
|
|
s.liquidationPreferenceMultiple,
|
|
s.participationCapMultiple,
|
|
now(),
|
|
ctx.companyId,
|
|
id
|
|
]
|
|
);
|
|
if (r.changes === 0) return notFound("share class not found");
|
|
return okRes({ success: true, message: "Share class updated successfully." });
|
|
}
|
|
function listEquityPlans(ctx) {
|
|
const rows = db.query(
|
|
`SELECT id, name, board_approval_date AS boardApprovalDate,
|
|
plan_effective_date AS planEffectiveDate,
|
|
initial_shares_reserved AS initialSharesReserved,
|
|
default_cancellation_behavior AS defaultCancellatonBehavior,
|
|
share_class_id AS shareClassId, comments, created_at AS createdAt
|
|
FROM equity_plan WHERE company_id = ?
|
|
ORDER BY created_at DESC`,
|
|
[ctx.companyId]
|
|
);
|
|
return okRes({ data: rows });
|
|
}
|
|
function createEquityPlan(ctx) {
|
|
const o = asObj(ctx.body);
|
|
const errs = [];
|
|
const name = reqString(o, "name", errs);
|
|
const boardApprovalDate = dateString(o, "boardApprovalDate", errs);
|
|
const initialSharesReserved = intNum(o, "initialSharesReserved", errs, 0);
|
|
const shareClassId = reqString(o, "shareClassId", errs);
|
|
const behavior = oneOf(o, "defaultCancellatonBehavior", CancellationBehavior, errs);
|
|
if (errs.length) return badReq(errs);
|
|
if (!refExists(ctx.companyId, "share_class", shareClassId)) {
|
|
return badReq(["shareClassId does not reference a share class in this company"]);
|
|
}
|
|
const ts = now();
|
|
db.exec(
|
|
`INSERT INTO equity_plan
|
|
(id, company_id, name, board_approval_date, plan_effective_date,
|
|
initial_shares_reserved, default_cancellation_behavior, share_class_id,
|
|
comments, created_at, updated_at)
|
|
VALUES (?,?,?,?,?,?,?,?,?,?,?)`,
|
|
[
|
|
newId(),
|
|
ctx.companyId,
|
|
name,
|
|
boardApprovalDate,
|
|
optDateString(o, "planEffectiveDate"),
|
|
initialSharesReserved,
|
|
behavior,
|
|
shareClassId,
|
|
optString(o, "comments"),
|
|
ts,
|
|
ts
|
|
]
|
|
);
|
|
return created({ success: true, message: "Equity plan created successfully." });
|
|
}
|
|
|
|
// src/routes/securities.ts
|
|
function listShares(ctx) {
|
|
const rows = db.query(
|
|
`SELECT sh.id, sh.certificate_id AS certificateId, sh.quantity,
|
|
sh.price_per_share AS pricePerShare,
|
|
sh.capital_contribution AS capitalContribution,
|
|
sh.status, sh.issue_date AS issueDate,
|
|
sh.company_legends AS companyLegends,
|
|
st.name AS stakeholderName, st.id AS stakeholderId,
|
|
sc.class_type AS shareClassType, sc.id AS shareClassId, sc.name AS shareClassName
|
|
FROM share sh
|
|
JOIN stakeholder st ON st.id = sh.stakeholder_id
|
|
JOIN share_class sc ON sc.id = sh.share_class_id
|
|
WHERE sh.company_id = ?
|
|
ORDER BY sh.created_at DESC`,
|
|
[ctx.companyId]
|
|
);
|
|
for (const r of rows) {
|
|
try {
|
|
r.companyLegends = JSON.parse(String(r.companyLegends || "[]"));
|
|
} catch (_e) {
|
|
r.companyLegends = [];
|
|
}
|
|
}
|
|
return okRes({ data: rows });
|
|
}
|
|
function insertShare(companyId, s) {
|
|
const id = newId();
|
|
const ts = now();
|
|
db.exec(
|
|
`INSERT INTO share
|
|
(id, company_id, stakeholder_id, share_class_id, status, certificate_id,
|
|
quantity, price_per_share, capital_contribution, ip_contribution,
|
|
debt_cancelled, other_contributions, cliff_years, vesting_years,
|
|
company_legends, issue_date, rule144_date, vesting_start_date,
|
|
board_approval_date, created_at, updated_at)
|
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
|
[
|
|
id,
|
|
companyId,
|
|
s.stakeholderId,
|
|
s.shareClassId,
|
|
s.status,
|
|
s.certificateId,
|
|
s.quantity,
|
|
s.pricePerShare,
|
|
s.capitalContribution,
|
|
s.ipContribution,
|
|
s.debtCancelled,
|
|
s.otherContributions,
|
|
s.cliffYears,
|
|
s.vestingYears,
|
|
JSON.stringify(s.companyLegends),
|
|
s.issueDate,
|
|
s.rule144Date,
|
|
s.vestingStartDate,
|
|
s.boardApprovalDate,
|
|
ts,
|
|
ts
|
|
]
|
|
);
|
|
return id;
|
|
}
|
|
function addShare(ctx) {
|
|
const o = asObj(ctx.body);
|
|
const errs = [];
|
|
const stakeholderId = reqString(o, "stakeholderId", errs);
|
|
const shareClassId = reqString(o, "shareClassId", errs);
|
|
const certificateId = reqString(o, "certificateId", errs);
|
|
const quantity = intNum(o, "quantity", errs, 0);
|
|
const status = oneOf(o, "status", SecuritiesStatus, errs);
|
|
const rest = {
|
|
pricePerShare: optNum(o, "pricePerShare"),
|
|
capitalContribution: optNum(o, "capitalContribution"),
|
|
ipContribution: optNum(o, "ipContribution"),
|
|
debtCancelled: optNum(o, "debtCancelled"),
|
|
otherContributions: optNum(o, "otherContributions"),
|
|
cliffYears: intNum(o, "cliffYears", errs, 0),
|
|
vestingYears: intNum(o, "vestingYears", errs, 0),
|
|
companyLegends: legends(o, "companyLegends", errs),
|
|
issueDate: dateString(o, "issueDate", errs),
|
|
rule144Date: optDateString(o, "rule144Date"),
|
|
vestingStartDate: optDateString(o, "vestingStartDate"),
|
|
boardApprovalDate: dateString(o, "boardApprovalDate", errs)
|
|
};
|
|
if (errs.length) return badReq(errs);
|
|
if (!refExists(ctx.companyId, "stakeholder", stakeholderId)) {
|
|
return badReq(["stakeholderId does not reference a stakeholder in this company"]);
|
|
}
|
|
if (!refExists(ctx.companyId, "share_class", shareClassId)) {
|
|
return badReq(["shareClassId does not reference a share class in this company"]);
|
|
}
|
|
if (db.query(`SELECT 1 FROM share WHERE company_id = ? AND certificate_id = ?`, [
|
|
ctx.companyId,
|
|
certificateId
|
|
]).length > 0) {
|
|
return conflict("Please use unique Certificate Id.");
|
|
}
|
|
insertShare(ctx.companyId, __spreadValues({
|
|
stakeholderId,
|
|
shareClassId,
|
|
status,
|
|
certificateId,
|
|
quantity
|
|
}, rest));
|
|
return created({ success: true, message: "\u{1F389} Successfully added a share" });
|
|
}
|
|
function deleteShare(ctx) {
|
|
const id = ctx.params.id;
|
|
if (!id) return badReq(["share id is required"]);
|
|
const r = db.exec(`DELETE FROM share WHERE company_id = ? AND id = ?`, [
|
|
ctx.companyId,
|
|
id
|
|
]);
|
|
if (r.changes === 0) return notFound("share not found");
|
|
return okRes({ success: true });
|
|
}
|
|
function transferShare(ctx) {
|
|
const o = asObj(ctx.body);
|
|
const errs = [];
|
|
const shareId = reqString(o, "shareId", errs);
|
|
const toStakeholderId = reqString(o, "toStakeholderId", errs);
|
|
if (errs.length) return badReq(errs);
|
|
const rows = db.query(
|
|
`SELECT id, stakeholder_id AS stakeholderId, share_class_id AS shareClassId,
|
|
quantity, status, certificate_id AS certificateId
|
|
FROM share WHERE company_id = ? AND id = ?`,
|
|
[ctx.companyId, shareId]
|
|
);
|
|
if (rows.length === 0) return notFound("share not found");
|
|
const src = rows[0];
|
|
const held = Number(src.quantity);
|
|
if (!refExists(ctx.companyId, "stakeholder", toStakeholderId)) {
|
|
return badReq(["toStakeholderId does not reference a stakeholder in this company"]);
|
|
}
|
|
const qtyRaw = o.quantity;
|
|
const qty = qtyRaw === void 0 || qtyRaw === null ? held : Math.trunc(Number(qtyRaw));
|
|
if (Number.isNaN(qty) || qty <= 0 || qty > held) {
|
|
return badReq([`quantity must be between 1 and ${held}`]);
|
|
}
|
|
if (qty === held) {
|
|
db.exec(
|
|
`UPDATE share SET stakeholder_id = ?, updated_at = ? WHERE company_id = ? AND id = ?`,
|
|
[toStakeholderId, now(), ctx.companyId, shareId]
|
|
);
|
|
return okRes({
|
|
success: true,
|
|
message: "Share transferred",
|
|
transferred: qty,
|
|
newShareId: null
|
|
});
|
|
}
|
|
const newCert = optString(o, "certificateId");
|
|
if (!newCert) {
|
|
return badReq(["certificateId is required for a partial transfer"]);
|
|
}
|
|
if (db.query(`SELECT 1 FROM share WHERE company_id = ? AND certificate_id = ?`, [
|
|
ctx.companyId,
|
|
newCert
|
|
]).length > 0) {
|
|
return conflict("Please use unique Certificate Id.");
|
|
}
|
|
db.exec(`UPDATE share SET quantity = ?, updated_at = ? WHERE company_id = ? AND id = ?`, [
|
|
held - qty,
|
|
now(),
|
|
ctx.companyId,
|
|
shareId
|
|
]);
|
|
const newId2 = insertShare(ctx.companyId, {
|
|
stakeholderId: toStakeholderId,
|
|
shareClassId: src.shareClassId,
|
|
status: src.status,
|
|
certificateId: newCert,
|
|
quantity: qty,
|
|
pricePerShare: null,
|
|
capitalContribution: null,
|
|
ipContribution: null,
|
|
debtCancelled: null,
|
|
otherContributions: null,
|
|
cliffYears: 0,
|
|
vestingYears: 0,
|
|
companyLegends: [],
|
|
issueDate: new Date(now()).toISOString().slice(0, 10),
|
|
rule144Date: null,
|
|
vestingStartDate: null,
|
|
boardApprovalDate: new Date(now()).toISOString().slice(0, 10)
|
|
});
|
|
return okRes({
|
|
success: true,
|
|
message: "Share partially transferred",
|
|
transferred: qty,
|
|
newShareId: newId2
|
|
});
|
|
}
|
|
function listOptions(ctx) {
|
|
const rows = db.query(
|
|
`SELECT o.id, o.grant_id AS grantId, o.quantity,
|
|
o.exercise_price AS exercisePrice, o.type, o.status,
|
|
o.cliff_years AS cliffYears, o.vesting_years AS vestingYears,
|
|
o.issue_date AS issueDate, o.expiration_date AS expirationDate,
|
|
st.name AS stakeholderName, st.id AS stakeholderId,
|
|
ep.name AS equityPlanName, ep.id AS equityPlanId
|
|
FROM option o
|
|
JOIN stakeholder st ON st.id = o.stakeholder_id
|
|
JOIN equity_plan ep ON ep.id = o.equity_plan_id
|
|
WHERE o.company_id = ?
|
|
ORDER BY o.created_at DESC`,
|
|
[ctx.companyId]
|
|
);
|
|
return okRes({ data: rows });
|
|
}
|
|
function addOption(ctx) {
|
|
const o = asObj(ctx.body);
|
|
const errs = [];
|
|
const grantId = reqString(o, "grantId", errs);
|
|
const stakeholderId = reqString(o, "stakeholderId", errs);
|
|
const equityPlanId = reqString(o, "equityPlanId", errs);
|
|
const quantity = intNum(o, "quantity", errs, 0);
|
|
const exercisePrice = num(o, "exercisePrice", errs, 0);
|
|
const type = oneOf(o, "type", OptionType, errs);
|
|
const status = oneOf(o, "status", OptionStatus, errs);
|
|
const rest = {
|
|
cliffYears: intNum(o, "cliffYears", errs, 0),
|
|
vestingYears: intNum(o, "vestingYears", errs, 0),
|
|
issueDate: dateString(o, "issueDate", errs),
|
|
expirationDate: dateString(o, "expirationDate", errs),
|
|
vestingStartDate: dateString(o, "vestingStartDate", errs),
|
|
boardApprovalDate: dateString(o, "boardApprovalDate", errs),
|
|
rule144Date: dateString(o, "rule144Date", errs),
|
|
notes: optString(o, "notes")
|
|
};
|
|
if (errs.length) return badReq(errs);
|
|
if (!refExists(ctx.companyId, "stakeholder", stakeholderId)) {
|
|
return badReq(["stakeholderId does not reference a stakeholder in this company"]);
|
|
}
|
|
if (!refExists(ctx.companyId, "equity_plan", equityPlanId)) {
|
|
return badReq(["equityPlanId does not reference an equity plan in this company"]);
|
|
}
|
|
if (db.query(`SELECT 1 FROM option WHERE company_id = ? AND grant_id = ?`, [
|
|
ctx.companyId,
|
|
grantId
|
|
]).length > 0) {
|
|
return conflict("Please use unique Grant Id.");
|
|
}
|
|
const ts = now();
|
|
db.exec(
|
|
`INSERT INTO option
|
|
(id, company_id, stakeholder_id, equity_plan_id, grant_id, notes, quantity,
|
|
exercise_price, type, status, cliff_years, vesting_years, issue_date,
|
|
expiration_date, vesting_start_date, board_approval_date, rule144_date,
|
|
created_at, updated_at)
|
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
|
[
|
|
newId(),
|
|
ctx.companyId,
|
|
stakeholderId,
|
|
equityPlanId,
|
|
grantId,
|
|
rest.notes,
|
|
quantity,
|
|
exercisePrice,
|
|
type,
|
|
status,
|
|
rest.cliffYears,
|
|
rest.vestingYears,
|
|
rest.issueDate,
|
|
rest.expirationDate,
|
|
rest.vestingStartDate,
|
|
rest.boardApprovalDate,
|
|
rest.rule144Date,
|
|
ts,
|
|
ts
|
|
]
|
|
);
|
|
return created({ success: true, message: "\u{1F389} Successfully added an option" });
|
|
}
|
|
function deleteOption(ctx) {
|
|
const id = ctx.params.id;
|
|
if (!id) return badReq(["option id is required"]);
|
|
const r = db.exec(`DELETE FROM option WHERE company_id = ? AND id = ?`, [
|
|
ctx.companyId,
|
|
id
|
|
]);
|
|
if (r.changes === 0) return notFound("option not found");
|
|
return okRes({ success: true });
|
|
}
|
|
|
|
// src/routes/convertibles.ts
|
|
function listSafes(ctx) {
|
|
const rows = db.query(
|
|
`SELECT sf.id, sf.public_id AS publicId, sf.type, sf.status, sf.capital,
|
|
sf.valuation_cap AS valuationCap, sf.discount_rate AS discountRate,
|
|
sf.mfn, sf.pro_rata AS proRata, sf.issue_date AS issueDate,
|
|
st.name AS stakeholderName, st.id AS stakeholderId
|
|
FROM safe sf
|
|
JOIN stakeholder st ON st.id = sf.stakeholder_id
|
|
WHERE sf.company_id = ?
|
|
ORDER BY sf.created_at DESC`,
|
|
[ctx.companyId]
|
|
);
|
|
for (const r of rows) {
|
|
r.mfn = Number(r.mfn) !== 0;
|
|
r.proRata = Number(r.proRata) !== 0;
|
|
}
|
|
return okRes({ data: rows });
|
|
}
|
|
function createSafe(ctx) {
|
|
const o = asObj(ctx.body);
|
|
const errs = [];
|
|
const publicId = reqString(o, "publicId", errs);
|
|
const stakeholderId = reqString(o, "stakeholderId", errs);
|
|
const capital = num(o, "capital", errs, 0);
|
|
const type = oneOf(o, "type", SafeType, errs);
|
|
const status = oneOf(o, "status", SafeStatus, errs);
|
|
const issueDate = dateString(o, "issueDate", errs);
|
|
const boardApprovalDate = dateString(o, "boardApprovalDate", errs);
|
|
if (errs.length) return badReq(errs);
|
|
if (!refExists(ctx.companyId, "stakeholder", stakeholderId)) {
|
|
return badReq(["stakeholderId does not reference a stakeholder in this company"]);
|
|
}
|
|
if (db.query(`SELECT 1 FROM safe WHERE company_id = ? AND public_id = ?`, [
|
|
ctx.companyId,
|
|
publicId
|
|
]).length > 0) {
|
|
return conflict("Please use a unique SAFE id.");
|
|
}
|
|
const ts = now();
|
|
db.exec(
|
|
`INSERT INTO safe
|
|
(id, company_id, stakeholder_id, public_id, type, status, capital,
|
|
valuation_cap, discount_rate, mfn, pro_rata, additional_terms,
|
|
issue_date, board_approval_date, created_at, updated_at)
|
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
|
[
|
|
newId(),
|
|
ctx.companyId,
|
|
stakeholderId,
|
|
publicId,
|
|
type,
|
|
status,
|
|
capital,
|
|
optNum(o, "valuationCap"),
|
|
optNum(o, "discountRate"),
|
|
o.mfn ? 1 : 0,
|
|
o.proRata ? 1 : 0,
|
|
optString(o, "additionalTerms"),
|
|
issueDate,
|
|
boardApprovalDate,
|
|
ts,
|
|
ts
|
|
]
|
|
);
|
|
return created({ success: true, message: "SAFE created successfully." });
|
|
}
|
|
function deleteSafe(ctx) {
|
|
const id = ctx.params.id;
|
|
if (!id) return badReq(["safe id is required"]);
|
|
const r = db.exec(`DELETE FROM safe WHERE company_id = ? AND id = ?`, [
|
|
ctx.companyId,
|
|
id
|
|
]);
|
|
if (r.changes === 0) return notFound("safe not found");
|
|
return okRes({ success: true });
|
|
}
|
|
function listConvertibles(ctx) {
|
|
const rows = db.query(
|
|
`SELECT cn.id, cn.public_id AS publicId, cn.type, cn.status, cn.capital,
|
|
cn.conversion_cap AS conversionCap, cn.discount_rate AS discountRate,
|
|
cn.interest_rate AS interestRate, cn.issue_date AS issueDate,
|
|
st.name AS stakeholderName, st.id AS stakeholderId
|
|
FROM convertible_note cn
|
|
JOIN stakeholder st ON st.id = cn.stakeholder_id
|
|
WHERE cn.company_id = ?
|
|
ORDER BY cn.created_at DESC`,
|
|
[ctx.companyId]
|
|
);
|
|
return okRes({ data: rows });
|
|
}
|
|
function createConvertible(ctx) {
|
|
const o = asObj(ctx.body);
|
|
const errs = [];
|
|
const publicId = reqString(o, "publicId", errs);
|
|
const stakeholderId = reqString(o, "stakeholderId", errs);
|
|
const capital = num(o, "capital", errs, 0);
|
|
const type = oneOf(o, "type", ConvertibleType, errs);
|
|
const status = oneOf(o, "status", ConvertibleStatus, errs);
|
|
const issueDate = dateString(o, "issueDate", errs);
|
|
const boardApprovalDate = dateString(o, "boardApprovalDate", errs);
|
|
if (errs.length) return badReq(errs);
|
|
if (!refExists(ctx.companyId, "stakeholder", stakeholderId)) {
|
|
return badReq(["stakeholderId does not reference a stakeholder in this company"]);
|
|
}
|
|
if (db.query(`SELECT 1 FROM convertible_note WHERE company_id = ? AND public_id = ?`, [
|
|
ctx.companyId,
|
|
publicId
|
|
]).length > 0) {
|
|
return conflict("Please use a unique convertible note id.");
|
|
}
|
|
const ts = now();
|
|
db.exec(
|
|
`INSERT INTO convertible_note
|
|
(id, company_id, stakeholder_id, public_id, type, status, capital,
|
|
conversion_cap, discount_rate, mfn, interest_rate, additional_terms,
|
|
issue_date, board_approval_date, created_at, updated_at)
|
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
|
[
|
|
newId(),
|
|
ctx.companyId,
|
|
stakeholderId,
|
|
publicId,
|
|
type,
|
|
status,
|
|
capital,
|
|
optNum(o, "conversionCap"),
|
|
optNum(o, "discountRate"),
|
|
o.mfn ? 1 : 0,
|
|
optNum(o, "interestRate"),
|
|
optString(o, "additionalTerms"),
|
|
issueDate,
|
|
boardApprovalDate,
|
|
ts,
|
|
ts
|
|
]
|
|
);
|
|
return created({ success: true, message: "Convertible note created successfully." });
|
|
}
|
|
function deleteConvertible(ctx) {
|
|
const id = ctx.params.id;
|
|
if (!id) return badReq(["convertible note id is required"]);
|
|
const r = db.exec(`DELETE FROM convertible_note WHERE company_id = ? AND id = ?`, [
|
|
ctx.companyId,
|
|
id
|
|
]);
|
|
if (r.changes === 0) return notFound("convertible note not found");
|
|
return okRes({ success: true });
|
|
}
|
|
|
|
// src/routes/rounds.ts
|
|
function listRounds(ctx) {
|
|
const rows = db.query(
|
|
`SELECT id, name, round_type AS roundType, status,
|
|
target_amount AS targetAmount, raised_amount AS raisedAmount,
|
|
pre_money_valuation AS preMoneyValuation,
|
|
price_per_share AS pricePerShare, share_class_id AS shareClassId,
|
|
close_date AS closeDate, created_at AS createdAt
|
|
FROM round WHERE company_id = ?
|
|
ORDER BY created_at DESC`,
|
|
[ctx.companyId]
|
|
);
|
|
return okRes({ data: rows });
|
|
}
|
|
function createRound(ctx) {
|
|
const o = asObj(ctx.body);
|
|
const errs = [];
|
|
const name = reqString(o, "name", errs);
|
|
const roundType = oneOf(o, "roundType", RoundType, errs);
|
|
const targetAmount = num(o, "targetAmount", errs, 0);
|
|
if (errs.length) return badReq(errs);
|
|
let shareClassId = null;
|
|
let pricePerShare = null;
|
|
let preMoney = null;
|
|
if (roundType === "PRICED") {
|
|
const perrs = [];
|
|
shareClassId = reqString(o, "shareClassId", perrs);
|
|
pricePerShare = num(o, "pricePerShare", perrs, 0);
|
|
preMoney = optNum(o, "preMoneyValuation");
|
|
if (pricePerShare <= 0) perrs.push("pricePerShare must be > 0 for a priced round");
|
|
if (perrs.length) return badReq(perrs);
|
|
if (!refExists(ctx.companyId, "share_class", shareClassId)) {
|
|
return badReq(["shareClassId does not reference a share class in this company"]);
|
|
}
|
|
}
|
|
const ts = now();
|
|
const id = newId();
|
|
db.exec(
|
|
`INSERT INTO round
|
|
(id, company_id, name, round_type, status, target_amount, raised_amount,
|
|
pre_money_valuation, price_per_share, share_class_id, close_date,
|
|
created_at, updated_at)
|
|
VALUES (?,?,?,?, 'OPEN', ?, 0, ?, ?, ?, NULL, ?, ?)`,
|
|
[id, ctx.companyId, name, roundType, targetAmount, preMoney, pricePerShare, shareClassId, ts, ts]
|
|
);
|
|
return created({ success: true, message: "Round created.", id });
|
|
}
|
|
function getRound(ctx) {
|
|
const id = ctx.params.id;
|
|
if (!id) return badReq(["round id is required"]);
|
|
const rows = db.query(
|
|
`SELECT id, name, round_type AS roundType, status,
|
|
target_amount AS targetAmount, raised_amount AS raisedAmount,
|
|
pre_money_valuation AS preMoneyValuation,
|
|
price_per_share AS pricePerShare, share_class_id AS shareClassId,
|
|
close_date AS closeDate, created_at AS createdAt
|
|
FROM round WHERE company_id = ? AND id = ?`,
|
|
[ctx.companyId, id]
|
|
);
|
|
if (rows.length === 0) return notFound("round not found");
|
|
const investments = db.query(
|
|
`SELECT i.id, i.amount, i.shares, i.date, i.comments,
|
|
st.name AS stakeholderName, st.id AS stakeholderId
|
|
FROM investment i
|
|
JOIN stakeholder st ON st.id = i.stakeholder_id
|
|
WHERE i.company_id = ? AND i.round_id = ?
|
|
ORDER BY i.created_at ASC`,
|
|
[ctx.companyId, id]
|
|
);
|
|
for (const r of investments) r.shares = Number(r.shares);
|
|
return okRes({ round: rows[0], investments });
|
|
}
|
|
function closeRound(ctx) {
|
|
const id = ctx.params.id;
|
|
if (!id) return badReq(["round id is required"]);
|
|
const o = asObj(ctx.body);
|
|
const closeDate = optDateString(o, "closeDate") || new Date(now()).toISOString().slice(0, 10);
|
|
const r = db.exec(
|
|
`UPDATE round SET status = 'CLOSED', close_date = ?, updated_at = ?
|
|
WHERE company_id = ? AND id = ? AND status = 'OPEN'`,
|
|
[closeDate, now(), ctx.companyId, id]
|
|
);
|
|
if (r.changes === 0) return notFound("open round not found");
|
|
return okRes({ success: true, message: "Round closed." });
|
|
}
|
|
function listInvestments(ctx) {
|
|
const rows = db.query(
|
|
`SELECT i.id, i.round_id AS roundId, i.amount, i.shares, i.date,
|
|
i.share_class_id AS shareClassId, st.name AS stakeholderName,
|
|
st.id AS stakeholderId
|
|
FROM investment i
|
|
JOIN stakeholder st ON st.id = i.stakeholder_id
|
|
WHERE i.company_id = ?
|
|
ORDER BY i.created_at DESC`,
|
|
[ctx.companyId]
|
|
);
|
|
for (const r of rows) r.shares = Number(r.shares);
|
|
return okRes({ data: rows });
|
|
}
|
|
function addInvestment(ctx) {
|
|
const roundId = ctx.params.id;
|
|
if (!roundId) return badReq(["round id is required"]);
|
|
const o = asObj(ctx.body);
|
|
const errs = [];
|
|
const stakeholderId = reqString(o, "stakeholderId", errs);
|
|
const amount = num(o, "amount", errs, 0);
|
|
if (errs.length) return badReq(errs);
|
|
const roundRows = db.query(
|
|
`SELECT id, round_type AS roundType, status, price_per_share AS pricePerShare,
|
|
share_class_id AS shareClassId
|
|
FROM round WHERE company_id = ? AND id = ?`,
|
|
[ctx.companyId, roundId]
|
|
);
|
|
if (roundRows.length === 0) return notFound("round not found");
|
|
const round = roundRows[0];
|
|
if (round.status !== "OPEN") return badReq(["round is not open for investment"]);
|
|
if (!refExists(ctx.companyId, "stakeholder", stakeholderId)) {
|
|
return badReq(["stakeholderId does not reference a stakeholder in this company"]);
|
|
}
|
|
const date = optDateString(o, "date") || new Date(now()).toISOString().slice(0, 10);
|
|
let shares = 0;
|
|
let newShareId = null;
|
|
if (round.roundType === "PRICED") {
|
|
const pps = Number(round.pricePerShare);
|
|
shares = Math.floor(amount / pps);
|
|
if (shares <= 0) return badReq(["amount is too small to buy a whole share at this round price"]);
|
|
const cert = `INV-${roundId.slice(1, 9)}-${newId().slice(1, 7)}`;
|
|
newShareId = insertShare(ctx.companyId, {
|
|
stakeholderId,
|
|
shareClassId: String(round.shareClassId),
|
|
status: "ACTIVE",
|
|
certificateId: cert,
|
|
quantity: shares,
|
|
pricePerShare: pps,
|
|
capitalContribution: amount,
|
|
ipContribution: null,
|
|
debtCancelled: null,
|
|
otherContributions: null,
|
|
cliffYears: 0,
|
|
vestingYears: 0,
|
|
companyLegends: [],
|
|
issueDate: date,
|
|
rule144Date: null,
|
|
vestingStartDate: null,
|
|
boardApprovalDate: date
|
|
});
|
|
}
|
|
const ts = now();
|
|
const invId = newId();
|
|
db.exec(
|
|
`INSERT INTO investment
|
|
(id, company_id, round_id, stakeholder_id, share_class_id, amount, shares,
|
|
date, comments, created_at, updated_at)
|
|
VALUES (?,?,?,?,?,?,?,?,?,?,?)`,
|
|
[
|
|
invId,
|
|
ctx.companyId,
|
|
roundId,
|
|
stakeholderId,
|
|
round.shareClassId,
|
|
amount,
|
|
shares,
|
|
date,
|
|
optString(o, "comments"),
|
|
ts,
|
|
ts
|
|
]
|
|
);
|
|
db.exec(
|
|
`UPDATE round SET raised_amount = raised_amount + ?, updated_at = ? WHERE company_id = ? AND id = ?`,
|
|
[amount, ts, ctx.companyId, roundId]
|
|
);
|
|
return created({
|
|
success: true,
|
|
message: "Investment recorded.",
|
|
id: invId,
|
|
sharesIssued: shares,
|
|
newShareId
|
|
});
|
|
}
|
|
|
|
// src/routes/captable.ts
|
|
var DILUTIVE_OPTION_STATUS = `status NOT IN ('EXERCISED','EXPIRED','CANCELLED')`;
|
|
function capTable(ctx) {
|
|
const cid = ctx.companyId;
|
|
const company = db.query(`SELECT id, name FROM company WHERE id = ?`, [cid]);
|
|
if (company.length === 0) return notFound("company not found");
|
|
const num2 = (rows, key) => rows.length ? Number(rows[0][key]) : 0;
|
|
const outstanding = num2(
|
|
db.query(`SELECT COALESCE(SUM(quantity),0) AS n FROM share WHERE company_id = ?`, [cid]),
|
|
"n"
|
|
);
|
|
const optionShares = num2(
|
|
db.query(
|
|
`SELECT COALESCE(SUM(quantity),0) AS n FROM option WHERE company_id = ? AND ${DILUTIVE_OPTION_STATUS}`,
|
|
[cid]
|
|
),
|
|
"n"
|
|
);
|
|
const fullyDiluted = outstanding + optionShares;
|
|
const byStakeholder = db.query(
|
|
`SELECT st.id AS stakeholderId, st.name,
|
|
COALESCE((SELECT SUM(sh.quantity) FROM share sh WHERE sh.stakeholder_id = st.id AND sh.company_id = st.company_id), 0) AS shares,
|
|
COALESCE((SELECT SUM(op.quantity) FROM option op WHERE op.stakeholder_id = st.id AND op.company_id = st.company_id AND op.${DILUTIVE_OPTION_STATUS}), 0) AS options
|
|
FROM stakeholder st
|
|
WHERE st.company_id = ?
|
|
ORDER BY shares DESC, options DESC`,
|
|
[cid]
|
|
);
|
|
for (const r of byStakeholder) {
|
|
const shares = Number(r.shares);
|
|
const options = Number(r.options);
|
|
r.shares = shares;
|
|
r.options = options;
|
|
const held = shares + options;
|
|
r.fullyDiluted = held;
|
|
r.ownershipPct = fullyDiluted > 0 ? Math.round(held / fullyDiluted * 1e4) / 100 : 0;
|
|
}
|
|
const byShareClass = db.query(
|
|
`SELECT sc.id AS shareClassId, sc.name, sc.class_type AS classType,
|
|
sc.initial_shares_authorized AS authorized,
|
|
COALESCE((SELECT SUM(sh.quantity) FROM share sh WHERE sh.share_class_id = sc.id AND sh.company_id = sc.company_id), 0) AS issued
|
|
FROM share_class sc
|
|
WHERE sc.company_id = ?
|
|
ORDER BY sc.idx ASC`,
|
|
[cid]
|
|
);
|
|
for (const r of byShareClass) {
|
|
r.authorized = Number(r.authorized);
|
|
r.issued = Number(r.issued);
|
|
}
|
|
const convertibles = {
|
|
safes: {
|
|
count: num2(db.query(`SELECT COUNT(*) AS n FROM safe WHERE company_id = ?`, [cid]), "n"),
|
|
capital: num2(db.query(`SELECT COALESCE(SUM(capital),0) AS n FROM safe WHERE company_id = ?`, [cid]), "n")
|
|
},
|
|
notes: {
|
|
count: num2(db.query(`SELECT COUNT(*) AS n FROM convertible_note WHERE company_id = ?`, [cid]), "n"),
|
|
capital: num2(db.query(`SELECT COALESCE(SUM(capital),0) AS n FROM convertible_note WHERE company_id = ?`, [cid]), "n")
|
|
}
|
|
};
|
|
const rounds = {
|
|
count: num2(db.query(`SELECT COUNT(*) AS n FROM round WHERE company_id = ?`, [cid]), "n"),
|
|
totalRaised: num2(db.query(`SELECT COALESCE(SUM(raised_amount),0) AS n FROM round WHERE company_id = ?`, [cid]), "n")
|
|
};
|
|
return okRes({
|
|
company: company[0],
|
|
totals: {
|
|
outstandingShares: outstanding,
|
|
grantedOptions: optionShares,
|
|
fullyDilutedShares: fullyDiluted,
|
|
stakeholders: num2(db.query(`SELECT COUNT(*) AS n FROM stakeholder WHERE company_id = ?`, [cid]), "n"),
|
|
shareClasses: num2(db.query(`SELECT COUNT(*) AS n FROM share_class WHERE company_id = ?`, [cid]), "n")
|
|
},
|
|
byStakeholder,
|
|
byShareClass,
|
|
convertibles,
|
|
rounds
|
|
});
|
|
}
|
|
|
|
// src/index.ts
|
|
var routes = {
|
|
"company.get": getCompany,
|
|
"company.update": updateCompany,
|
|
"stakeholders.list": listStakeholders,
|
|
"stakeholders.add": addStakeholders,
|
|
"stakeholders.update": updateStakeholder,
|
|
"stakeholders.delete": deleteStakeholder,
|
|
"shareClasses.list": listShareClasses,
|
|
"shareClasses.create": createShareClass,
|
|
"shareClasses.update": updateShareClass,
|
|
"equityPlans.list": listEquityPlans,
|
|
"equityPlans.create": createEquityPlan,
|
|
"shares.list": listShares,
|
|
"shares.add": addShare,
|
|
"shares.delete": deleteShare,
|
|
"shares.transfer": transferShare,
|
|
"options.list": listOptions,
|
|
"options.add": addOption,
|
|
"options.delete": deleteOption,
|
|
"safes.list": listSafes,
|
|
"safes.create": createSafe,
|
|
"safes.delete": deleteSafe,
|
|
"convertibles.list": listConvertibles,
|
|
"convertibles.create": createConvertible,
|
|
"convertibles.delete": deleteConvertible,
|
|
"rounds.list": listRounds,
|
|
"rounds.create": createRound,
|
|
"rounds.get": getRound,
|
|
"rounds.close": closeRound,
|
|
"rounds.investments.list": listInvestments,
|
|
"rounds.investments.add": addInvestment,
|
|
captable: capTable
|
|
};
|
|
globalThis.handle = (req) => {
|
|
req = req || {};
|
|
const route = req.route || "";
|
|
const companyId = req.orgId || "";
|
|
if (!companyId) {
|
|
return { status: 401, body: { success: false, message: "missing tenant" } };
|
|
}
|
|
const fn = routes[route];
|
|
if (!fn) {
|
|
return {
|
|
status: 404,
|
|
body: { success: false, message: `unknown captable route: ${route}` }
|
|
};
|
|
}
|
|
const ctx = { companyId, params: req.params || {}, body: req.body };
|
|
try {
|
|
return fn(ctx);
|
|
} catch (err) {
|
|
return {
|
|
status: 500,
|
|
body: {
|
|
success: false,
|
|
message: String(err && err.message || err)
|
|
}
|
|
};
|
|
}
|
|
};
|
|
})();
|