@trajectonomics/ledger 0.0.0-stage → 0.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":["toAccountId","accountId"],"sources":["../src/account.ts","../src/errors.ts","../src/date.ts","../src/amount.ts","../src/currency.ts","../src/posting.ts","../src/transaction.ts","../src/reverse.ts","../src/amend.ts","../src/compare.ts","../src/ledger-scan.ts","../src/safe-amount.ts","../src/balances.ts","../src/ids.ts","../src/balance.ts","../src/chart-map.ts","../src/hierarchy.ts","../src/chart.ts","../src/legs.ts","../src/compound.ts","../src/money.ts","../src/totals.ts","../src/net-worth.ts","../src/opening-balance.ts","../src/dimensions.ts","../src/period.ts","../src/profit-and-loss-rows.ts","../src/profit-and-loss.ts","../src/simple.ts","../src/trial-balance.ts"],"sourcesContent":["import type { Currency } from \"./currency\"\nimport type { AccountId } from \"./ids\"\n\n/** The five canonical account types. Opening balances use `equity`; there is no sixth type. */\nexport type AccountType = \"asset\" | \"liability\" | \"equity\" | \"income\" | \"expense\"\n\n/** Side on which an account type normally grows. Debit is `+`, credit is `-`. */\nexport type NormalBalance = \"debit\" | \"credit\"\n\n/** An account as supplied by a caller, before validation. */\nexport interface AccountInput {\n\treadonly id: string\n\treadonly type: AccountType\n\treadonly currency: string\n\t/** Legal/accounting entity that owns the account. Defaults to `\"default\"`. */\n\treadonly entityId?: string\n\t/** Header/summary account this account rolls up into. */\n\treadonly parentId?: string\n}\n\n/** A validated account. Only {@link createChartOfAccounts} produces these. */\nexport interface Account {\n\treadonly id: AccountId\n\treadonly type: AccountType\n\treadonly currency: Currency\n\treadonly entityId: string\n\treadonly parentId?: AccountId\n}\n\n/** `asset` and `expense` are debit-normal; `liability`, `equity` and `income` credit-normal. */\nexport const normalBalance = (type: AccountType): NormalBalance =>\n\ttype === \"asset\" || type === \"expense\" ? \"debit\" : \"credit\"\n","/**\n * Every way a ledger input can be rejected. Each code names exactly one rule.\n *\n * TODO(#12): adding a code is source-breaking for a consumer that switches exhaustively over this\n * union. How that growth is versioned and communicated is not decided yet.\n */\nexport type LedgerErrorCode =\n\t| \"SINGLE_POSTING\"\n\t| \"INVALID_AMOUNT\"\n\t| \"ZERO_AMOUNT_POSTING\"\n\t| \"UNKNOWN_ACCOUNT\"\n\t| \"NON_LEAF_ACCOUNT\"\n\t| \"CURRENCY_MISMATCH\"\n\t| \"UNBALANCED_TRANSACTION\"\n\t| \"INVALID_DATE\"\n\t| \"INVALID_CURRENCY\"\n\t| \"INVALID_DIMENSION\"\n\t| \"INVALID_ACCOUNT_HIERARCHY\"\n\t| \"NEGATIVE_AMOUNT\"\n\t| \"DUPLICATE_TRANSACTION_ID\"\n\t| \"REVERSAL_BEFORE_ORIGINAL\"\n\t| \"NON_EQUITY_CONTRA\"\n\t| \"INVALID_PERIOD\"\n\t| \"AMOUNT_OVERFLOW\"\n\n/** Thrown by every smart constructor and validator in this package. */\nexport class LedgerValidationError extends Error {\n\treadonly code: LedgerErrorCode\n\t/** Path of the offending input, e.g. `postings[1].accountId`. */\n\treadonly field: string\n\treadonly value: unknown\n\treadonly reason: string\n\n\tconstructor(code: LedgerErrorCode, field: string, value: unknown, reason: string) {\n\t\tsuper(`${code}: ${field} ${reason}`)\n\t\tthis.name = \"LedgerValidationError\"\n\t\tthis.code = code\n\t\tthis.field = field\n\t\tthis.value = value\n\t\tthis.reason = reason\n\t}\n}\n","import type { Brand } from \"./brand\"\nimport { LedgerValidationError } from \"./errors\"\n\n/** A real calendar date formatted `YYYY-MM-DD`. No time, no timezone. */\nexport type ISODate = Brand<string, \"ISODate\">\n\nconst SHAPE = /^(\\d{4})-(\\d{2})-(\\d{2})$/\n\nconst daysInMonth = (year: number, month: number): number =>\n\tmonth === 2\n\t\t? year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0)\n\t\t\t? 29\n\t\t\t: 28\n\t\t: [4, 6, 9, 11].includes(month)\n\t\t\t? 30\n\t\t\t: 31\n\nexport const parseISODate = (value: unknown, field = \"date\"): ISODate => {\n\tconst match = typeof value === \"string\" ? SHAPE.exec(value) : null\n\tconst [year, month, day] = [Number(match?.[1]), Number(match?.[2]), Number(match?.[3])]\n\tif (!match || month < 1 || month > 12 || day < 1 || day > daysInMonth(year, month)) {\n\t\tthrow new LedgerValidationError(\n\t\t\t\"INVALID_DATE\",\n\t\t\tfield,\n\t\t\tvalue,\n\t\t\t\"must be a real YYYY-MM-DD calendar date\",\n\t\t)\n\t}\n\treturn value as ISODate\n}\n","import { LedgerValidationError } from \"./errors\"\n\n/** Accepts only safe integers; anything else is `INVALID_AMOUNT`. */\nexport const assertSafeInteger = (value: unknown, field: string): number => {\n\tif (typeof value !== \"number\" || !Number.isSafeInteger(value)) {\n\t\tthrow new LedgerValidationError(\n\t\t\t\"INVALID_AMOUNT\",\n\t\t\tfield,\n\t\t\tvalue,\n\t\t\t\"must be a safe integer of minor units\",\n\t\t)\n\t}\n\treturn value\n}\n","import type { Brand } from \"./brand\"\nimport { LedgerValidationError } from \"./errors\"\n\n/** An ISO 4217 alphabetic currency code, e.g. `\"THB\"`. */\nexport type Currency = Brand<string, \"Currency\">\n\nlet known: ReadonlySet<string> | undefined\n\n/**\n * Validate an ISO 4217 code against the runtime's own currency data\n * (`Intl.supportedValuesOf(\"currency\")`), so no code table ships in this package.\n * The accepted set therefore follows the runtime's ICU version.\n */\nexport const parseCurrency = (value: unknown, field = \"currency\"): Currency => {\n\tknown ??= new Set(Intl.supportedValuesOf(\"currency\"))\n\tif (typeof value !== \"string\" || !known.has(value)) {\n\t\tthrow new LedgerValidationError(\n\t\t\t\"INVALID_CURRENCY\",\n\t\t\tfield,\n\t\t\tvalue,\n\t\t\t\"is not an ISO 4217 currency code\",\n\t\t)\n\t}\n\treturn value as Currency\n}\n\n/** Number of minor-unit decimals for a currency: THB 2, JPY 0, KWD 3, CLF 4. */\nexport const minorUnitDigits = (currency: Currency): number =>\n\tnew Intl.NumberFormat(\"en\", { style: \"currency\", currency }).resolvedOptions()\n\t\t.maximumFractionDigits ?? 2\n","import { assertSafeInteger } from \"./amount\"\nimport type { ChartOfAccounts } from \"./chart-map\"\nimport type { Currency } from \"./currency\"\nimport { parseCurrency } from \"./currency\"\nimport { LedgerValidationError } from \"./errors\"\nimport type { AccountId } from \"./ids\"\n\n/** A posting as supplied by a caller. `amount` is signed minor units: debit `+`, credit `-`. */\nexport interface PostingInput {\n\treadonly accountId: string\n\treadonly amount: number\n\treadonly currency: string\n\t/** Cost centre / project / class tags. Keys and values must be non-empty. */\n\treadonly dimensions?: Readonly<Record<string, string>>\n}\n\n/** A validated posting. */\nexport interface Posting {\n\treadonly accountId: AccountId\n\treadonly amount: number\n\treadonly currency: Currency\n\treadonly dimensions?: Readonly<Record<string, string>>\n}\n\nconst fail = (\n\tcode: LedgerValidationError[\"code\"],\n\tfield: string,\n\tvalue: unknown,\n\treason: string,\n): never => {\n\tthrow new LedgerValidationError(code, field, value, reason)\n}\n\nconst checkDimensions = (dimensions: Readonly<Record<string, string>>, field: string): void => {\n\tfor (const [key, value] of Object.entries(dimensions)) {\n\t\tif (key === \"\" || typeof value !== \"string\" || value === \"\") {\n\t\t\tfail(\n\t\t\t\t\"INVALID_DIMENSION\",\n\t\t\t\t`${field}.dimensions`,\n\t\t\t\t{ [key]: value },\n\t\t\t\t\"keys and values must be non-empty strings\",\n\t\t\t)\n\t\t}\n\t}\n}\n\n/** Validate one posting against the chart; `path` is e.g. `postings[0]`. */\nexport const validatePosting = (\n\tinput: PostingInput,\n\tchart: ChartOfAccounts,\n\tpath: string,\n): Posting => {\n\tconst amount = assertSafeInteger(input.amount, `${path}.amount`)\n\tif (amount === 0) fail(\"ZERO_AMOUNT_POSTING\", `${path}.amount`, amount, \"must not be zero\")\n\tconst currency = parseCurrency(input.currency, `${path}.currency`)\n\tconst account = chart.get(input.accountId as AccountId)\n\tif (!account)\n\t\treturn fail(\n\t\t\t\"UNKNOWN_ACCOUNT\",\n\t\t\t`${path}.accountId`,\n\t\t\tinput.accountId,\n\t\t\t\"is not in the chart of accounts\",\n\t\t)\n\tif (!chart.isLeaf(account.id))\n\t\tfail(\"NON_LEAF_ACCOUNT\", `${path}.accountId`, account.id, \"is a header account\")\n\tif (currency !== account.currency)\n\t\tfail(\"CURRENCY_MISMATCH\", `${path}.currency`, currency, `account is ${account.currency}`)\n\tif (input.dimensions) checkDimensions(input.dimensions, path)\n\treturn Object.freeze({\n\t\taccountId: account.id,\n\t\tamount,\n\t\tcurrency,\n\t\t...(input.dimensions ? { dimensions: Object.freeze({ ...input.dimensions }) } : {}),\n\t})\n}\n","import type { Brand } from \"./brand\"\nimport type { ChartOfAccounts } from \"./chart-map\"\nimport type { ISODate } from \"./date\"\nimport { parseISODate } from \"./date\"\nimport { LedgerValidationError } from \"./errors\"\nimport type { TransactionId } from \"./ids\"\nimport type { Posting, PostingInput } from \"./posting\"\nimport { validatePosting } from \"./posting\"\n\n/** A transaction as supplied by a caller. The caller supplies `id`; nothing here generates one. */\nexport interface TransactionInput {\n\treadonly id: string\n\treadonly date: string\n\treadonly postings: readonly PostingInput[]\n\t/** The transaction this one reverses or amends (append-only audit chain). */\n\treadonly linkedTo?: string\n}\n\n/** A balanced transaction. Only {@link validateTransaction} produces one. */\nexport type Transaction = Brand<\n\t{\n\t\treadonly id: TransactionId\n\t\treadonly date: ISODate\n\t\treadonly postings: readonly Posting[]\n\t\treadonly linkedTo?: TransactionId\n\t},\n\t\"Transaction\"\n>\n\nconst assertBalanced = (postings: readonly Posting[], chart: ChartOfAccounts): void => {\n\tconst sums = new Map<string, bigint>()\n\tfor (const { accountId, amount, currency } of postings) {\n\t\tconst key = JSON.stringify([chart.get(accountId)?.entityId, currency])\n\t\tsums.set(key, (sums.get(key) ?? 0n) + BigInt(amount))\n\t}\n\tfor (const [key, sum] of sums) {\n\t\tif (sum !== 0n) {\n\t\t\tconst [entity, currency] = JSON.parse(key) as [string, string]\n\t\t\tthrow new LedgerValidationError(\n\t\t\t\t\"UNBALANCED_TRANSACTION\",\n\t\t\t\t\"postings\",\n\t\t\t\tsum.toString(),\n\t\t\t\t`entity \"${entity}\" ${currency} sums to ${sum} instead of 0`,\n\t\t\t)\n\t\t}\n\t}\n}\n\n/** Validate a transaction against a chart. Throws {@link LedgerValidationError}. */\nexport const validateTransaction = (\n\tinput: TransactionInput,\n\tchart: ChartOfAccounts,\n): Transaction => {\n\tconst date = parseISODate(input.date)\n\tconst count = Array.isArray(input.postings) ? input.postings.length : 0\n\tif (count < 2) {\n\t\tthrow new LedgerValidationError(\n\t\t\t\"SINGLE_POSTING\",\n\t\t\t\"postings\",\n\t\t\tcount,\n\t\t\t\"needs at least 2 postings\",\n\t\t)\n\t}\n\tconst postings = input.postings.map((p, i) => validatePosting(p, chart, `postings[${i}]`))\n\tassertBalanced(postings, chart)\n\treturn Object.freeze({\n\t\tid: input.id as TransactionId,\n\t\tdate,\n\t\tpostings: Object.freeze(postings),\n\t\t...(input.linkedTo === undefined ? {} : { linkedTo: input.linkedTo as TransactionId }),\n\t}) as unknown as Transaction\n}\n","import type { ChartOfAccounts } from \"./chart-map\"\nimport { parseISODate } from \"./date\"\nimport { LedgerValidationError } from \"./errors\"\nimport type { Transaction } from \"./transaction\"\nimport { validateTransaction } from \"./transaction\"\n\n/** Build a Reversal; `path` prefixes error fields so `amend` can mirror its own input shape. */\nexport const reverseAt = (\n\ttx: Transaction,\n\tinput: { readonly id: string; readonly date: string },\n\tchart: ChartOfAccounts,\n\tpath: string,\n): Transaction => {\n\tconst date = parseISODate(input.date, `${path}date`)\n\tif (input.id === tx.id) {\n\t\tthrow new LedgerValidationError(\n\t\t\t\"DUPLICATE_TRANSACTION_ID\",\n\t\t\t`${path}id`,\n\t\t\tinput.id,\n\t\t\t\"reuses the id of the transaction it reverses\",\n\t\t)\n\t}\n\tif (date < tx.date) {\n\t\tthrow new LedgerValidationError(\n\t\t\t\"REVERSAL_BEFORE_ORIGINAL\",\n\t\t\t`${path}date`,\n\t\t\tinput.date,\n\t\t\t`is before the original's date ${tx.date}`,\n\t\t)\n\t}\n\tconst postings = tx.postings.map(({ amount, ...rest }) => ({ ...rest, amount: -amount }))\n\treturn validateTransaction({ id: input.id, date, postings, linkedTo: tx.id }, chart)\n}\n\n/** Build a Reversal: the original's Postings in order, every sign flipped, linked to it. */\nexport const reverse = (\n\ttx: Transaction,\n\tinput: { readonly id: string; readonly date: string },\n\tchart: ChartOfAccounts,\n): Transaction => reverseAt(tx, input, chart, \"\")\n","import type { ChartOfAccounts } from \"./chart-map\"\nimport { LedgerValidationError } from \"./errors\"\nimport { reverseAt } from \"./reverse\"\nimport type { Transaction, TransactionInput } from \"./transaction\"\nimport { validateTransaction } from \"./transaction\"\n\n/**\n * Build an Amendment: `[reversal, replacement]`, both linked to the original. The replacement's\n * own `linkedTo` is overwritten with the original's id.\n */\nexport const amend = (\n\ttx: Transaction,\n\tinput: {\n\t\treadonly reversal: { readonly id: string; readonly date: string }\n\t\treadonly replacement: TransactionInput\n\t},\n\tchart: ChartOfAccounts,\n): readonly [Transaction, Transaction] => {\n\tconst reversal = reverseAt(tx, input.reversal, chart, \"reversal.\")\n\tconst { id } = input.replacement\n\tif (id === tx.id || id === reversal.id) {\n\t\tthrow new LedgerValidationError(\n\t\t\t\"DUPLICATE_TRANSACTION_ID\",\n\t\t\t\"replacement.id\",\n\t\t\tid,\n\t\t\t`reuses the id of the ${id === tx.id ? \"original\" : \"reversal\"} in the same amendment`,\n\t\t)\n\t}\n\tconst replacement = validateTransaction({ ...input.replacement, linkedTo: tx.id }, chart)\n\treturn Object.freeze([reversal, replacement] as const)\n}\n","/** Order two strings by UTF-16 code unit. Never locale-aware: ICU data differs between runtimes. */\nexport const byCodeUnit = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0)\n","import type { Account } from \"./account\"\nimport type { ChartOfAccounts } from \"./chart-map\"\nimport type { ISODate } from \"./date\"\nimport { LedgerValidationError } from \"./errors\"\nimport type { Posting } from \"./posting\"\nimport type { Transaction } from \"./transaction\"\nimport { validateTransaction } from \"./transaction\"\n\n/** A Posting with the Account it targets and the date of its Transaction. */\nexport interface ScannedPosting {\n\treadonly account: Account\n\treadonly posting: Posting\n\treadonly date: ISODate\n}\n\nconst recheck = (tx: Transaction, index: number, chart: ChartOfAccounts): Transaction => {\n\ttry {\n\t\treturn validateTransaction(tx, chart)\n\t} catch (error) {\n\t\tif (!(error instanceof LedgerValidationError)) throw error\n\t\tthrow new LedgerValidationError(\n\t\t\terror.code,\n\t\t\t`transactions[${index}].${error.field}`,\n\t\t\terror.value,\n\t\t\terror.reason,\n\t\t)\n\t}\n}\n\n/**\n * Re-check every Transaction against the supplied chart (a Transaction is not tied to the chart\n * that validated it) and flatten them into Postings. Throws on a duplicate Transaction id.\n */\nexport const scanLedger = (\n\ttransactions: readonly Transaction[],\n\tchart: ChartOfAccounts,\n): readonly ScannedPosting[] => {\n\tconst seen = new Set<string>()\n\treturn transactions.flatMap((tx, index) => {\n\t\tif (seen.has(tx.id)) {\n\t\t\tthrow new LedgerValidationError(\n\t\t\t\t\"DUPLICATE_TRANSACTION_ID\",\n\t\t\t\t`transactions[${index}].id`,\n\t\t\t\ttx.id,\n\t\t\t\t\"is used by an earlier transaction\",\n\t\t\t)\n\t\t}\n\t\tseen.add(tx.id)\n\t\tconst checked = recheck(tx, index, chart)\n\t\treturn checked.postings.map((posting) => ({\n\t\t\taccount: chart.get(posting.accountId)!,\n\t\t\tposting,\n\t\t\tdate: checked.date,\n\t\t}))\n\t})\n}\n","import { LedgerValidationError } from \"./errors\"\n\nconst MAX = BigInt(Number.MAX_SAFE_INTEGER)\n\n/** Narrow a `BigInt` sum to a `number`, or throw `AMOUNT_OVERFLOW` when it is not a safe integer. */\nexport const toSafeAmount = (sum: bigint, field: string): number => {\n\tif (sum > MAX || sum < -MAX) {\n\t\tthrow new LedgerValidationError(\n\t\t\t\"AMOUNT_OVERFLOW\",\n\t\t\tfield,\n\t\t\tsum.toString(),\n\t\t\t\"exceeds the safe integer range\",\n\t\t)\n\t}\n\treturn Number(sum)\n}\n","import type { Account } from \"./account\"\nimport type { ChartOfAccounts } from \"./chart-map\"\nimport { byCodeUnit } from \"./compare\"\nimport type { Currency } from \"./currency\"\nimport { parseISODate } from \"./date\"\nimport type { AccountId } from \"./ids\"\nimport { scanLedger } from \"./ledger-scan\"\nimport { toSafeAmount } from \"./safe-amount\"\nimport type { Transaction } from \"./transaction\"\n\n/** One Leaf Account's raw signed Balance (Debit `+`, Credit `-`) in its own Currency. */\nexport interface BalanceRow {\n\treadonly entityId: string\n\treadonly accountId: AccountId\n\treadonly currency: Currency\n\treadonly amount: number\n}\n\n/** Order rows by `entityId`, `accountId`, then `currency`, by code unit. */\nexport const compareRows = (a: BalanceRow, b: BalanceRow): number =>\n\tbyCodeUnit(a.entityId, b.entityId) ||\n\tbyCodeUnit(a.accountId, b.accountId) ||\n\tbyCodeUnit(a.currency, b.currency)\n\n/**\n * Every Leaf Account's Balance up to and including `asOf`, zeros included, sorted. `fieldOf` gives\n * the output path an `AMOUNT_OVERFLOW` reports for a Leaf, so it names the query the caller called.\n */\nexport const leafBalances = (\n\ttransactions: readonly Transaction[],\n\tasOf: string,\n\tchart: ChartOfAccounts,\n\tfieldOf: (account: Account) => string,\n): readonly BalanceRow[] => {\n\tconst cutoff = parseISODate(asOf, \"asOf\")\n\tconst sums = new Map<AccountId, bigint>()\n\tfor (const { account, posting, date } of scanLedger(transactions, chart)) {\n\t\tif (date <= cutoff) sums.set(account.id, (sums.get(account.id) ?? 0n) + BigInt(posting.amount))\n\t}\n\treturn Object.freeze(\n\t\t[...chart.values()]\n\t\t\t.filter((account) => chart.isLeaf(account.id))\n\t\t\t.map((account) =>\n\t\t\t\tObject.freeze({\n\t\t\t\t\tentityId: account.entityId,\n\t\t\t\t\taccountId: account.id,\n\t\t\t\t\tcurrency: account.currency,\n\t\t\t\t\tamount: toSafeAmount(sums.get(account.id) ?? 0n, fieldOf(account)),\n\t\t\t\t}),\n\t\t\t)\n\t\t\t.sort(compareRows),\n\t)\n}\n\n/**\n * Every Leaf Account's Balance up to and including `asOf`, zeros included, sorted. Header\n * Accounts never appear, so summing the rows never double counts.\n *\n * TODO(#14): Header Account roll-ups live in a separate query, never in these rows.\n */\nexport const balances = (\n\ttransactions: readonly Transaction[],\n\tasOf: string,\n\tchart: ChartOfAccounts,\n): readonly BalanceRow[] =>\n\tleafBalances(transactions, asOf, chart, (account) => `balances[${account.id}]`)\n","import type { Brand } from \"./brand\"\n\n/** Caller-supplied account identifier. This package never generates ids. */\nexport type AccountId = Brand<string, \"AccountId\">\n/** Caller-supplied transaction identifier. This package never generates ids. */\nexport type TransactionId = Brand<string, \"TransactionId\">\n\n/** Tag a string as an {@link AccountId}, e.g. to look an account up in a chart. */\nexport const accountId = (value: string): AccountId => value as AccountId\n/** Tag a string as a {@link TransactionId}. */\nexport const transactionId = (value: string): TransactionId => value as TransactionId\n","import type { BalanceRow } from \"./balances\"\nimport { leafBalances } from \"./balances\"\nimport type { ChartOfAccounts } from \"./chart-map\"\nimport { LedgerValidationError } from \"./errors\"\nimport { accountId as toAccountId } from \"./ids\"\nimport type { Transaction } from \"./transaction\"\n\n/**\n * One Leaf Account's Balance up to and including `asOf`: the row `balances` gives for it.\n * Throws `UNKNOWN_ACCOUNT` for an id outside the chart and `NON_LEAF_ACCOUNT` for a Header.\n *\n * TODO(#14): a Header's roll-up Balance is a separate query, so a Header id is rejected here.\n */\nexport const balance = (\n\ttransactions: readonly Transaction[],\n\taccountId: string,\n\tasOf: string,\n\tchart: ChartOfAccounts,\n): BalanceRow => {\n\tconst id = toAccountId(accountId)\n\tif (!chart.has(id)) {\n\t\tthrow new LedgerValidationError(\n\t\t\t\"UNKNOWN_ACCOUNT\",\n\t\t\t\"accountId\",\n\t\t\taccountId,\n\t\t\t\"is not in the chart of accounts\",\n\t\t)\n\t}\n\tif (!chart.isLeaf(id)) {\n\t\tthrow new LedgerValidationError(\n\t\t\t\"NON_LEAF_ACCOUNT\",\n\t\t\t\"accountId\",\n\t\t\taccountId,\n\t\t\t\"is a header account\",\n\t\t)\n\t}\n\treturn leafBalances(transactions, asOf, chart, (account) =>\n\t\taccount.id === id ? \"balance.amount\" : `balance[${account.id}]`,\n\t).find((row) => row.accountId === id)!\n}\n","import type { Account } from \"./account\"\nimport type { AccountId } from \"./ids\"\n\n/**\n * A validated, hierarchy-checked set of accounts, read-only at runtime. Only\n * {@link createChartOfAccounts} constructs one (this module's constructor is not exported from\n * the package), so holding one proves parents exist, share entity and type, and form no cycle.\n * Its state lives in `#private` fields, so it cannot be mutated even via `Map.prototype`.\n */\nexport class ChartOfAccounts implements ReadonlyMap<AccountId, Account> {\n\treadonly #accounts: ReadonlyMap<AccountId, Account>\n\treadonly #headers: ReadonlySet<AccountId>\n\n\tconstructor(accounts: ReadonlyMap<AccountId, Account>) {\n\t\tthis.#accounts = new Map(accounts)\n\t\tthis.#headers = new Set(\n\t\t\t[...accounts.values()].flatMap((a) => (a.parentId === undefined ? [] : [a.parentId])),\n\t\t)\n\t}\n\n\tget size(): number {\n\t\treturn this.#accounts.size\n\t}\n\n\tget(id: AccountId): Account | undefined {\n\t\treturn this.#accounts.get(id)\n\t}\n\n\thas(id: AccountId): boolean {\n\t\treturn this.#accounts.has(id)\n\t}\n\n\t/** True for an account in this chart that no other account names as its parent. */\n\tisLeaf(id: AccountId): boolean {\n\t\treturn this.#accounts.has(id) && !this.#headers.has(id)\n\t}\n\n\tforEach(\n\t\tcallbackfn: (value: Account, key: AccountId, map: ReadonlyMap<AccountId, Account>) => void,\n\t\tthisArg?: unknown,\n\t): void {\n\t\tfor (const [key, value] of this.#accounts) {\n\t\t\tcallbackfn.call(thisArg, value, key, this)\n\t\t}\n\t}\n\n\tentries(): MapIterator<[AccountId, Account]> {\n\t\treturn this.#accounts.entries()\n\t}\n\n\tkeys(): MapIterator<AccountId> {\n\t\treturn this.#accounts.keys()\n\t}\n\n\tvalues(): MapIterator<Account> {\n\t\treturn this.#accounts.values()\n\t}\n\n\t[Symbol.iterator](): MapIterator<[AccountId, Account]> {\n\t\treturn this.#accounts.entries()\n\t}\n}\n","import type { Account } from \"./account\"\nimport { LedgerValidationError } from \"./errors\"\nimport type { AccountId } from \"./ids\"\n\nconst reject = (index: number, field: string, value: unknown, reason: string): never => {\n\tthrow new LedgerValidationError(\n\t\t\"INVALID_ACCOUNT_HIERARCHY\",\n\t\t`accounts[${index}].${field}`,\n\t\tvalue,\n\t\treason,\n\t)\n}\n\nconst checkParent = (account: Account, parent: Account | undefined, i: number): void => {\n\tif (account.parentId === undefined) return\n\tif (!parent) reject(i, \"parentId\", account.parentId, \"parent account does not exist\")\n\telse if (parent.entityId !== account.entityId)\n\t\treject(i, \"parentId\", parent.id, \"parent belongs to another entity\")\n\telse if (parent.type !== account.type)\n\t\treject(i, \"parentId\", parent.id, \"parent has a different account type\")\n}\n\nconst checkAcyclic = (\n\taccount: Account,\n\taccounts: ReadonlyMap<AccountId, Account>,\n\ti: number,\n): void => {\n\tconst seen = new Set<AccountId>([account.id])\n\tfor (let up = account.parentId; up !== undefined; up = accounts.get(up)?.parentId) {\n\t\tif (seen.has(up)) reject(i, \"parentId\", account.parentId, \"hierarchy contains a cycle\")\n\t\tseen.add(up)\n\t}\n}\n\n/** Reject missing, cross-entity and cross-type parents, and cycles. */\nexport const checkHierarchy = (accounts: ReadonlyMap<AccountId, Account>): void => {\n\tfor (const [i, account] of [...accounts.values()].entries()) {\n\t\tcheckParent(\n\t\t\taccount,\n\t\t\taccount.parentId === undefined ? undefined : accounts.get(account.parentId),\n\t\t\ti,\n\t\t)\n\t\tcheckAcyclic(account, accounts, i)\n\t}\n}\n","import type { Account, AccountInput } from \"./account\"\nimport { ChartOfAccounts } from \"./chart-map\"\nimport { parseCurrency } from \"./currency\"\nimport { LedgerValidationError } from \"./errors\"\nimport { checkHierarchy } from \"./hierarchy\"\nimport type { AccountId } from \"./ids\"\n\nconst toAccount = (input: AccountInput, index: number): Account =>\n\tObject.freeze({\n\t\tid: input.id as AccountId,\n\t\ttype: input.type,\n\t\tcurrency: parseCurrency(input.currency, `accounts[${index}].currency`),\n\t\tentityId: input.entityId ?? \"default\",\n\t\t...(input.parentId === undefined ? {} : { parentId: input.parentId as AccountId }),\n\t})\n\n/** Validate accounts and build the chart. Throws {@link LedgerValidationError}. */\nexport const createChartOfAccounts = (inputs: Iterable<AccountInput>): ChartOfAccounts => {\n\tconst accounts = new Map<AccountId, Account>()\n\tfor (const [index, input] of [...inputs].entries()) {\n\t\tconst account = toAccount(input, index)\n\t\tif (accounts.has(account.id)) {\n\t\t\tthrow new LedgerValidationError(\n\t\t\t\t\"INVALID_ACCOUNT_HIERARCHY\",\n\t\t\t\t`accounts[${index}].id`,\n\t\t\t\tinput.id,\n\t\t\t\t\"duplicate account id\",\n\t\t\t)\n\t\t}\n\t\taccounts.set(account.id, account)\n\t}\n\tcheckHierarchy(accounts)\n\treturn new ChartOfAccounts(accounts)\n}\n","import type { ChartOfAccounts } from \"./chart-map\"\nimport { LedgerValidationError } from \"./errors\"\nimport { accountId } from \"./ids\"\nimport type { PostingInput } from \"./posting\"\n\ntype LegObject = {\n\treadonly accountId: string\n\treadonly dimensions?: Readonly<Record<string, string>>\n}\n\n/** One side of a builder entry: a bare account id, or an account id with Dimensions. */\nexport type LegInput = string | LegObject\n\n/** Resolve a leg's Account in the chart and emit its Posting; Currency comes from the Account. */\nexport const legPosting = (\n\tleg: LegInput,\n\tamount: number,\n\tchart: ChartOfAccounts,\n\tfield: string,\n): PostingInput => {\n\tconst { accountId: id, dimensions }: LegObject =\n\t\ttypeof leg === \"string\" ? { accountId: leg } : leg\n\tconst account = chart.get(accountId(id))\n\tif (!account) {\n\t\tthrow new LedgerValidationError(\n\t\t\t\"UNKNOWN_ACCOUNT\",\n\t\t\ttypeof leg === \"string\" ? field : `${field}.accountId`,\n\t\t\tid,\n\t\t\t\"is not in the chart of accounts\",\n\t\t)\n\t}\n\treturn {\n\t\taccountId: id,\n\t\tamount,\n\t\tcurrency: account.currency,\n\t\t...(dimensions ? { dimensions } : {}),\n\t}\n}\n\n/** A builder amount is a magnitude: the builder derives the sign, so a negative one is a bug. */\nexport const magnitude = (amount: number, field: string): number => {\n\tif (amount < 0) {\n\t\tthrow new LedgerValidationError(\"NEGATIVE_AMOUNT\", field, amount, \"must not be negative\")\n\t}\n\treturn amount\n}\n\n/** The Credit form of a magnitude; never produces negative zero. */\nexport const credited = (amount: number): number => 0 - amount\n\n/** Reject legs whose Accounts hold differing Currencies; the core has no FX. */\nexport const assertSameCurrency = (\n\tpostings: readonly PostingInput[],\n\tfields: readonly string[],\n): void => {\n\tconst first = postings[0]?.currency\n\tconst index = postings.findIndex((p) => p.currency !== first)\n\tif (index >= 0) {\n\t\tthrow new LedgerValidationError(\n\t\t\t\"CURRENCY_MISMATCH\",\n\t\t\tfields[index] ?? \"postings\",\n\t\t\tpostings[index]?.currency,\n\t\t\t`differs from ${first}; legs must share one currency`,\n\t\t)\n\t}\n}\n","import type { ChartOfAccounts } from \"./chart-map\"\nimport { assertSameCurrency, credited, legPosting, magnitude } from \"./legs\"\nimport type { PostingInput } from \"./posting\"\nimport type { Transaction } from \"./transaction\"\nimport { validateTransaction } from \"./transaction\"\n\n/** One leg of a Compound Entry: an Account, its own magnitude, and optional Dimensions. */\nexport interface CompoundLeg {\n\treadonly accountId: string\n\treadonly amount: number\n\treadonly dimensions?: Readonly<Record<string, string>>\n}\n\nconst side = (\n\tlegs: readonly CompoundLeg[],\n\tname: \"debit\" | \"credit\",\n\tchart: ChartOfAccounts,\n): { postings: PostingInput[]; fields: string[] } => ({\n\tpostings: legs.map((leg, i) => {\n\t\tconst amount = magnitude(leg.amount, `${name}[${i}].amount`)\n\t\treturn legPosting(leg, name === \"debit\" ? amount : credited(amount), chart, `${name}[${i}]`)\n\t}),\n\tfields: legs.map((_, i) => `${name}[${i}]`),\n})\n\n/**\n * Build a Compound Entry: many legs on either side, each with its own magnitude.\n * The two sides must total the same amount.\n */\nexport const compound = (\n\tinput: {\n\t\treadonly id: string\n\t\treadonly date: string\n\t\treadonly debit: readonly CompoundLeg[]\n\t\treadonly credit: readonly CompoundLeg[]\n\t},\n\tchart: ChartOfAccounts,\n): Transaction => {\n\tconst debit = side(input.debit, \"debit\", chart)\n\tconst credit = side(input.credit, \"credit\", chart)\n\tconst postings = [...debit.postings, ...credit.postings]\n\tassertSameCurrency(postings, [...debit.fields, ...credit.fields])\n\treturn validateTransaction({ id: input.id, date: input.date, postings }, chart)\n}\n","import { assertSafeInteger } from \"./amount\"\nimport { minorUnitDigits, parseCurrency } from \"./currency\"\nimport { LedgerValidationError } from \"./errors\"\n\n/**\n * Convert a major-unit amount (`12.34`) to signed integer minor units (`1234`).\n * Rounds half away from zero; `toPrecision` strips binary-float noise first.\n */\nexport const toMinorUnits = (majorAmount: number, currency: string): number => {\n\tconst digits = minorUnitDigits(parseCurrency(currency))\n\tif (!Number.isFinite(majorAmount)) {\n\t\tthrow new LedgerValidationError(\"INVALID_AMOUNT\", \"majorAmount\", majorAmount, \"must be finite\")\n\t}\n\tconst raw = Math.abs(majorAmount) * 10 ** digits\n\t// Trimming to 15 significant digits removes float noise (1.005 * 100), but beyond 1e15\n\t// it would erase real digits, and a double's own spacing there already exceeds the noise.\n\tconst scaled = raw < 1e15 ? Number(raw.toPrecision(15)) : raw\n\treturn assertSafeInteger(Math.sign(majorAmount) * Math.round(scaled) + 0, \"majorAmount\")\n}\n\n/** Convert signed integer minor units (`1234`) to a major-unit amount (`12.34`). */\nexport const fromMinorUnits = (minorUnits: number, currency: string): number => {\n\tconst digits = minorUnitDigits(parseCurrency(currency))\n\treturn assertSafeInteger(minorUnits, \"minorUnits\") / 10 ** digits\n}\n","import { byCodeUnit } from \"./compare\"\nimport type { Currency } from \"./currency\"\nimport { toSafeAmount } from \"./safe-amount\"\n\n/** A named total for one `(Entity, Currency)` pair. */\nexport interface Total {\n\treadonly entityId: string\n\treadonly currency: Currency\n\treadonly amount: number\n}\n\ninterface Addend {\n\treadonly entityId: string\n\treadonly currency: Currency\n\treadonly amount: number\n}\n\n/** Order totals by `entityId` then `currency`, by code unit. */\nexport const compareTotals = (a: Total, b: Total): number =>\n\tbyCodeUnit(a.entityId, b.entityId) || byCodeUnit(a.currency, b.currency)\n\n/**\n * Sum `sign * amount` per `(Entity, Currency)` with `BigInt`, checking each total against the safe\n * integer range. One total per pair among the addends, sorted; a zero addend still yields its pair,\n * which is how P&L seeds its totals. `fieldOf` gives the output path an overflow reports for a pair.\n */\nexport const totalsOf = (\n\taddends: readonly Addend[],\n\tsign: 1n | -1n,\n\tfieldOf: (pair: string) => string,\n): readonly Total[] => {\n\tconst slots = new Map<string, { entityId: string; currency: Currency; sum: bigint }>()\n\tfor (const { entityId, currency, amount } of addends) {\n\t\tconst key = JSON.stringify([entityId, currency])\n\t\tconst slot = slots.get(key) ?? { entityId, currency, sum: 0n }\n\t\tslot.sum += sign * BigInt(amount)\n\t\tslots.set(key, slot)\n\t}\n\treturn Object.freeze(\n\t\t[...slots.values()]\n\t\t\t.map(({ entityId, currency, sum }) =>\n\t\t\t\tObject.freeze({\n\t\t\t\t\tentityId,\n\t\t\t\t\tcurrency,\n\t\t\t\t\tamount: toSafeAmount(sum, fieldOf(`${entityId}/${currency}`)),\n\t\t\t\t}),\n\t\t\t)\n\t\t\t.sort(compareTotals),\n\t)\n}\n","import type { Account } from \"./account\"\nimport type { BalanceRow } from \"./balances\"\nimport { leafBalances } from \"./balances\"\nimport type { ChartOfAccounts } from \"./chart-map\"\nimport type { Total } from \"./totals\"\nimport { totalsOf } from \"./totals\"\nimport type { Transaction } from \"./transaction\"\n\n/** Net Worth: the asset and liability Balance rows behind it, and one total per `(Entity, Currency)`. */\nexport interface NetWorth {\n\treadonly rows: readonly BalanceRow[]\n\treadonly totals: readonly Total[]\n}\n\n/**\n * Assets minus Liabilities up to and including `asOf`, per `(Entity, Currency)`, positive when\n * assets exceed liabilities. Rows stay raw signed; Equity is not part of it. Raw liabilities are\n * negative, so the natural-sign total is the plain sum of the rows.\n */\nexport const netWorth = (\n\ttransactions: readonly Transaction[],\n\tasOf: string,\n\tchart: ChartOfAccounts,\n): NetWorth => {\n\tconst counted = (account: Account) => account.type === \"asset\" || account.type === \"liability\"\n\tconst rows = leafBalances(transactions, asOf, chart, (account) =>\n\t\tcounted(account) ? `netWorth.rows[${account.id}]` : `netWorth[${account.id}]`,\n\t).filter((row) => counted(chart.get(row.accountId)!))\n\treturn Object.freeze({\n\t\trows: Object.freeze(rows),\n\t\ttotals: totalsOf(rows, 1n, (pair) => `netWorth.totals[${pair}]`),\n\t})\n}\n","import type { ChartOfAccounts } from \"./chart-map\"\nimport { LedgerValidationError } from \"./errors\"\nimport { accountId } from \"./ids\"\nimport type { LegInput } from \"./legs\"\nimport { assertSameCurrency, credited, legPosting } from \"./legs\"\nimport type { Transaction } from \"./transaction\"\nimport { validateTransaction } from \"./transaction\"\n\nconst assertEquity = (contra: LegInput, chart: ChartOfAccounts): void => {\n\tconst id = typeof contra === \"string\" ? contra : contra.accountId\n\tconst type = chart.get(accountId(id))?.type\n\tif (type !== undefined && type !== \"equity\") {\n\t\tthrow new LedgerValidationError(\n\t\t\t\"NON_EQUITY_CONTRA\",\n\t\t\ttypeof contra === \"string\" ? \"contra\" : \"contra.accountId\",\n\t\t\tid,\n\t\t\t`is ${type}; an opening balance is posted against an equity account`,\n\t\t)\n\t}\n}\n\n/**\n * Build an Opening Balance: `amount` posts on `account`, the opposite Posting on the equity\n * `contra`. `amount` is signed (an overdrawn Asset opens negative); direction is never inferred.\n */\nexport const openingBalance = (\n\tinput: {\n\t\treadonly id: string\n\t\treadonly date: string\n\t\treadonly account: LegInput\n\t\treadonly contra: LegInput\n\t\treadonly amount: number\n\t},\n\tchart: ChartOfAccounts,\n): Transaction => {\n\tconst postings = [\n\t\tlegPosting(input.account, input.amount, chart, \"account\"),\n\t\tlegPosting(input.contra, credited(input.amount), chart, \"contra\"),\n\t]\n\tassertEquity(input.contra, chart)\n\tassertSameCurrency(postings, [\"account\", \"contra\"])\n\treturn validateTransaction({ id: input.id, date: input.date, postings }, chart)\n}\n","import type { Posting } from \"./posting\"\n\n/** A Posting's value for a Dimension key, or null when it lacks the key. Inherited keys never count. */\nexport const dimensionOf = (posting: Posting, key: string): string | null =>\n\tposting.dimensions !== undefined && Object.hasOwn(posting.dimensions, key)\n\t\t? posting.dimensions[key]!\n\t\t: null\n\n/** True when the Posting carries every `where` entry with exactly that value. */\nexport const matchesAll = (posting: Posting, where: Readonly<Record<string, string>>): boolean =>\n\tObject.entries(where).every(([key, value]) => dimensionOf(posting, key) === value)\n","import type { ISODate } from \"./date\"\nimport { parseISODate } from \"./date\"\nimport { LedgerValidationError } from \"./errors\"\n\n/** An inclusive date range; `from` is absent when the period starts with the ledger. */\nexport interface Period {\n\treadonly from?: ISODate\n\treadonly to: ISODate\n}\n\n/** Parse an inclusive period, throwing `INVALID_DATE` for a bad end and `INVALID_PERIOD` if inverted. */\nexport const parsePeriod = (input: { readonly from?: string; readonly to: string }): Period => {\n\tconst from = input.from === undefined ? undefined : parseISODate(input.from, \"options.from\")\n\tconst to = parseISODate(input.to, \"options.to\")\n\tif (from !== undefined && from > to) {\n\t\tthrow new LedgerValidationError(\n\t\t\t\"INVALID_PERIOD\",\n\t\t\t\"options.from\",\n\t\t\tinput.from,\n\t\t\t`is after to (${to})`,\n\t\t)\n\t}\n\treturn from === undefined ? { to } : { from, to }\n}\n\n/** True when `date` falls on or between the period's ends. */\nexport const inPeriod = (period: Period, date: ISODate): boolean =>\n\tdate <= period.to && (period.from === undefined || date >= period.from)\n","import type { Account } from \"./account\"\nimport type { BalanceRow } from \"./balances\"\nimport { compareRows } from \"./balances\"\nimport { byCodeUnit } from \"./compare\"\nimport { dimensionOf } from \"./dimensions\"\nimport type { ScannedPosting } from \"./ledger-scan\"\nimport { toSafeAmount } from \"./safe-amount\"\n\n/** A P&L row for one Dimension value; `null` collects Postings lacking the grouped key. */\nexport interface GroupedRow extends BalanceRow {\n\treadonly dimensionValue: string | null\n}\n\nconst sumByAccount = (entries: readonly ScannedPosting[]): Map<string, bigint> => {\n\tconst sums = new Map<string, bigint>()\n\tfor (const { account, posting } of entries) {\n\t\tsums.set(account.id, (sums.get(account.id) ?? 0n) + BigInt(posting.amount))\n\t}\n\treturn sums\n}\n\n/** One row per Income and Expense Leaf Account, zeros included. */\nexport const plainRows = (\n\taccounts: readonly Account[],\n\tentries: readonly ScannedPosting[],\n): readonly BalanceRow[] => {\n\tconst sums = sumByAccount(entries)\n\treturn accounts\n\t\t.map((a) =>\n\t\t\tObject.freeze({\n\t\t\t\tentityId: a.entityId,\n\t\t\t\taccountId: a.id,\n\t\t\t\tcurrency: a.currency,\n\t\t\t\tamount: toSafeAmount(sums.get(a.id) ?? 0n, `profitAndLoss.rows[${a.id}]`),\n\t\t\t}),\n\t\t)\n\t\t.sort(compareRows)\n}\n\nconst compareGrouped = (a: GroupedRow, b: GroupedRow): number =>\n\tcompareRows(a, b) ||\n\t(a.dimensionValue === b.dimensionValue\n\t\t? 0\n\t\t: a.dimensionValue === null\n\t\t\t? -1\n\t\t\t: b.dimensionValue === null\n\t\t\t\t? 1\n\t\t\t\t: byCodeUnit(a.dimensionValue, b.dimensionValue))\n\n/** One row per `(Account, value of the `groupBy` Dimension)` that has Postings. */\nexport const groupedRows = (\n\tentries: readonly ScannedPosting[],\n\tgroupBy: string,\n): readonly GroupedRow[] => {\n\tconst groups = new Map<string, { entry: ScannedPosting; value: string | null; sum: bigint }>()\n\tfor (const entry of entries) {\n\t\tconst value = dimensionOf(entry.posting, groupBy)\n\t\tconst key = JSON.stringify([entry.account.id, value])\n\t\tconst sum = (groups.get(key)?.sum ?? 0n) + BigInt(entry.posting.amount)\n\t\tgroups.set(key, { entry, value, sum })\n\t}\n\treturn [...groups.values()]\n\t\t.map(({ entry: { account }, value, sum }) =>\n\t\t\tObject.freeze({\n\t\t\t\tentityId: account.entityId,\n\t\t\t\taccountId: account.id,\n\t\t\t\tcurrency: account.currency,\n\t\t\t\tdimensionValue: value,\n\t\t\t\tamount: toSafeAmount(sum, `profitAndLoss.rows[${account.id}]`),\n\t\t\t}),\n\t\t)\n\t\t.sort(compareGrouped)\n}\n","import type { BalanceRow } from \"./balances\"\nimport type { ChartOfAccounts } from \"./chart-map\"\nimport { matchesAll } from \"./dimensions\"\nimport { scanLedger } from \"./ledger-scan\"\nimport { inPeriod, parsePeriod } from \"./period\"\nimport type { GroupedRow } from \"./profit-and-loss-rows\"\nimport { groupedRows, plainRows } from \"./profit-and-loss-rows\"\nimport type { Total } from \"./totals\"\nimport { totalsOf } from \"./totals\"\nimport type { Transaction } from \"./transaction\"\n\n/** P&L: raw rows for Income and Expense Leaf Accounts and income-minus-expenses totals. */\nexport interface ProfitAndLoss<Row extends BalanceRow = BalanceRow> {\n\treadonly rows: readonly Row[]\n\treadonly totals: readonly Total[]\n}\n\n/**\n * The period and optional Dimension handling of a P&L query. An absent `from` means \"from the\n * start of the ledger\". `where` is exact-match, ANDed; a missing key never matches.\n */\nexport interface ProfitAndLossOptions {\n\treadonly from?: string\n\treadonly to: string\n\treadonly groupBy?: string\n\treadonly where?: Readonly<Record<string, string>>\n}\n\n/**\n * Income minus Expenses over an inclusive period, per `(Entity, Currency)`, positive when income\n * exceeds expenses. Rows stay raw signed, so the total is the negated sum of the rows. With\n * `groupBy`, rows split by that Dimension's value and only groups with Postings appear. Totals hold\n * one entry, zero if nothing posted, for every Income and Expense Leaf Account's pair.\n */\nexport function profitAndLoss(\n\ttransactions: readonly Transaction[],\n\toptions: ProfitAndLossOptions & { readonly groupBy: string },\n\tchart: ChartOfAccounts,\n): ProfitAndLoss<GroupedRow>\n/** P&L without grouping: one row per Income and Expense Leaf Account; totals seeded per Leaf pair. */\nexport function profitAndLoss(\n\ttransactions: readonly Transaction[],\n\toptions: ProfitAndLossOptions & { readonly groupBy?: undefined },\n\tchart: ChartOfAccounts,\n): ProfitAndLoss\n/** P&L when `groupBy` is only known at runtime: rows are plain or grouped, totals seeded alike. */\nexport function profitAndLoss(\n\ttransactions: readonly Transaction[],\n\toptions: ProfitAndLossOptions,\n\tchart: ChartOfAccounts,\n): ProfitAndLoss<BalanceRow | GroupedRow>\nexport function profitAndLoss(\n\ttransactions: readonly Transaction[],\n\toptions: ProfitAndLossOptions,\n\tchart: ChartOfAccounts,\n): ProfitAndLoss<BalanceRow | GroupedRow> {\n\tconst period = parsePeriod(options)\n\tconst entries = scanLedger(transactions, chart).filter(\n\t\t({ account, posting, date }) =>\n\t\t\t(account.type === \"income\" || account.type === \"expense\") &&\n\t\t\tinPeriod(period, date) &&\n\t\t\tmatchesAll(posting, options.where ?? {}),\n\t)\n\tconst { groupBy } = options\n\tconst leaves = [...chart.values()].filter(\n\t\t(a) => chart.isLeaf(a.id) && (a.type === \"income\" || a.type === \"expense\"),\n\t)\n\tconst rows = groupBy === undefined ? plainRows(leaves, entries) : groupedRows(entries, groupBy)\n\t// Seed every Income and Expense Leaf's pair so the totals' shape depends only on the chart.\n\tconst pairs = leaves.map(({ entityId, currency }) => ({ entityId, currency, amount: 0 }))\n\treturn Object.freeze({\n\t\trows: Object.freeze(rows),\n\t\ttotals: totalsOf([...pairs, ...rows], -1n, (pair) => `profitAndLoss.totals[${pair}]`),\n\t})\n}\n","import type { ChartOfAccounts } from \"./chart-map\"\nimport type { LegInput } from \"./legs\"\nimport { assertSameCurrency, credited, legPosting, magnitude } from \"./legs\"\nimport type { Transaction } from \"./transaction\"\nimport { validateTransaction } from \"./transaction\"\n\n/** Build a Simple Entry: `+amount` on `debit`, `-amount` on `credit`. `amount` is a magnitude. */\nexport const simple = (\n\tinput: {\n\t\treadonly id: string\n\t\treadonly date: string\n\t\treadonly debit: LegInput\n\t\treadonly credit: LegInput\n\t\treadonly amount: number\n\t},\n\tchart: ChartOfAccounts,\n): Transaction => {\n\tconst amount = magnitude(input.amount, \"amount\")\n\tconst postings = [\n\t\tlegPosting(input.debit, amount, chart, \"debit\"),\n\t\tlegPosting(input.credit, credited(amount), chart, \"credit\"),\n\t]\n\tassertSameCurrency(postings, [\"debit\", \"credit\"])\n\treturn validateTransaction({ id: input.id, date: input.date, postings }, chart)\n}\n","import { leafBalances } from \"./balances\"\nimport type { ChartOfAccounts } from \"./chart-map\"\nimport type { Currency } from \"./currency\"\nimport type { AccountId } from \"./ids\"\nimport { totalsOf } from \"./totals\"\nimport type { Transaction } from \"./transaction\"\n\n/** One Leaf Account's net Balance as a magnitude in its Debit or Credit column. */\nexport interface TrialBalanceRow {\n\treadonly entityId: string\n\treadonly accountId: AccountId\n\treadonly currency: Currency\n\treadonly debit: number\n\treadonly credit: number\n}\n\n/** Column totals for one `(Entity, Currency)` pair. They always match. */\nexport interface TrialBalanceTotal {\n\treadonly entityId: string\n\treadonly currency: Currency\n\treadonly debit: number\n\treadonly credit: number\n}\n\n/** A Trial Balance: one row per Leaf Account and column totals per `(Entity, Currency)`. */\nexport interface TrialBalance {\n\treadonly rows: readonly TrialBalanceRow[]\n\treadonly totals: readonly TrialBalanceTotal[]\n}\n\n/**\n * Every Leaf Account's net Balance at `asOf`, zeros included: positive in `debit`, negative in\n * `credit` as a magnitude (the one exception to raw signed rows).\n */\nexport const trialBalance = (\n\ttransactions: readonly Transaction[],\n\tasOf: string,\n\tchart: ChartOfAccounts,\n): TrialBalance => {\n\tconst rows = leafBalances(transactions, asOf, chart, (a) => `trialBalance.rows[${a.id}]`).map(\n\t\t({ entityId, accountId, currency, amount }) =>\n\t\t\tObject.freeze({\n\t\t\t\tentityId,\n\t\t\t\taccountId,\n\t\t\t\tcurrency,\n\t\t\t\tdebit: Math.max(amount, 0),\n\t\t\t\tcredit: Math.max(-amount, 0),\n\t\t\t}),\n\t)\n\tconst debits = totalsOf(\n\t\trows.map((r) => ({ ...r, amount: r.debit })),\n\t\t1n,\n\t\t(pair) => `trialBalance.totals[${pair}].debit`,\n\t)\n\tconst credits = new Map(\n\t\ttotalsOf(\n\t\t\trows.map((r) => ({ ...r, amount: r.credit })),\n\t\t\t1n,\n\t\t\t(pair) => `trialBalance.totals[${pair}].credit`,\n\t\t).map((t) => [JSON.stringify([t.entityId, t.currency]), t.amount]),\n\t)\n\treturn Object.freeze({\n\t\trows: Object.freeze(rows),\n\t\ttotals: Object.freeze(\n\t\t\tdebits.map(({ entityId, currency, amount }) =>\n\t\t\t\tObject.freeze({\n\t\t\t\t\tentityId,\n\t\t\t\t\tcurrency,\n\t\t\t\t\tdebit: amount,\n\t\t\t\t\tcredit: credits.get(JSON.stringify([entityId, currency]))!,\n\t\t\t\t}),\n\t\t\t),\n\t\t),\n\t})\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,59 @@
1
1
  {
2
- "name": "@trajectonomics/ledger",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@trajectonomics/ledger",
3
+ "version": "0.1.0-alpha.1",
4
+ "description": "Pure double-entry ledger domain: accounts, balanced postings, balances, net worth, P&L. No I/O.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/trajectonomics/ledger-core.git"
9
+ },
10
+ "type": "module",
11
+ "sideEffects": false,
12
+ "packageManager": "bun@1.4.4",
13
+ "engines": {
14
+ "node": ">=20.0.0"
15
+ },
16
+ "main": "./dist/index.cjs",
17
+ "module": "./dist/index.mjs",
18
+ "types": "./dist/index.d.cts",
19
+ "exports": {
20
+ ".": {
21
+ "import": {
22
+ "types": "./dist/index.d.mts",
23
+ "default": "./dist/index.mjs"
24
+ },
25
+ "require": {
26
+ "types": "./dist/index.d.cts",
27
+ "default": "./dist/index.cjs"
28
+ }
29
+ },
30
+ "./package.json": "./package.json"
31
+ },
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "publishConfig": {
36
+ "access": "public",
37
+ "provenance": true,
38
+ "tag": "latest"
39
+ },
40
+ "scripts": {
41
+ "build": "tsdown",
42
+ "test": "bun test",
43
+ "typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.test.json",
44
+ "lint": "biome ci --error-on-warnings .",
45
+ "format": "biome check --write .",
46
+ "check": "bun run lint && bun run typecheck && bun run test && bun run build && bun run lint:package",
47
+ "lint:package": "publint --strict && attw --pack . --profile node16",
48
+ "skills:check": "bunx skills@latest check",
49
+ "skills:update": "bunx skills@latest update"
50
+ },
51
+ "devDependencies": {
52
+ "@arethetypeswrong/cli": "^0.18.5",
53
+ "@biomejs/biome": "2.6.3",
54
+ "@types/bun": "^1.4.4",
55
+ "publint": "^0.3.25",
56
+ "tsdown": "^0.23.2",
57
+ "typescript": "^7.0.2"
58
+ }
59
+ }