@trajectonomics/ledger 0.0.0-stage → 0.1.0-alpha.0
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.
- package/LICENSE +21 -0
- package/README.md +40 -2
- package/dist/index.cjs +575 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +285 -0
- package/dist/index.d.mts +285 -0
- package/dist/index.mjs +555 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +58 -5
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nominal tag. The private member makes the type unforgeable: TypeScript omits private members
|
|
3
|
+
* from object spreads and rejects object literals, so only an explicit cast can fake a brand.
|
|
4
|
+
*/
|
|
5
|
+
declare class Nominal<Tag extends string> {
|
|
6
|
+
private readonly tag;
|
|
7
|
+
}
|
|
8
|
+
/** A `T` carrying a nominal tag so unrelated values cannot be mixed up. */
|
|
9
|
+
type Brand<T, Tag extends string> = T & Nominal<Tag>;
|
|
10
|
+
/** An ISO 4217 alphabetic currency code, e.g. `"THB"`. */
|
|
11
|
+
type Currency = Brand<string, "Currency">;
|
|
12
|
+
/**
|
|
13
|
+
* Validate an ISO 4217 code against the runtime's own currency data
|
|
14
|
+
* (`Intl.supportedValuesOf("currency")`), so no code table ships in this package.
|
|
15
|
+
* The accepted set therefore follows the runtime's ICU version.
|
|
16
|
+
*/
|
|
17
|
+
export declare const parseCurrency: (value: unknown, field?: string) => Currency;
|
|
18
|
+
/** Caller-supplied account identifier. This package never generates ids. */
|
|
19
|
+
type AccountId = Brand<string, "AccountId">;
|
|
20
|
+
/** Caller-supplied transaction identifier. This package never generates ids. */
|
|
21
|
+
type TransactionId = Brand<string, "TransactionId">;
|
|
22
|
+
/** Tag a string as an {@link AccountId}, e.g. to look an account up in a chart. */
|
|
23
|
+
export declare const accountId: (value: string) => AccountId;
|
|
24
|
+
/** Tag a string as a {@link TransactionId}. */
|
|
25
|
+
export declare const transactionId: (value: string) => TransactionId;
|
|
26
|
+
/** The five canonical account types. Opening balances use `equity`; there is no sixth type. */
|
|
27
|
+
type AccountType = "asset" | "liability" | "equity" | "income" | "expense";
|
|
28
|
+
/** Side on which an account type normally grows. Debit is `+`, credit is `-`. */
|
|
29
|
+
type NormalBalance = "debit" | "credit";
|
|
30
|
+
/** An account as supplied by a caller, before validation. */
|
|
31
|
+
interface AccountInput {
|
|
32
|
+
readonly id: string;
|
|
33
|
+
readonly type: AccountType;
|
|
34
|
+
readonly currency: string;
|
|
35
|
+
/** Legal/accounting entity that owns the account. Defaults to `"default"`. */
|
|
36
|
+
readonly entityId?: string;
|
|
37
|
+
/** Header/summary account this account rolls up into. */
|
|
38
|
+
readonly parentId?: string;
|
|
39
|
+
}
|
|
40
|
+
/** A validated account. Only {@link createChartOfAccounts} produces these. */
|
|
41
|
+
interface Account {
|
|
42
|
+
readonly id: AccountId;
|
|
43
|
+
readonly type: AccountType;
|
|
44
|
+
readonly currency: Currency;
|
|
45
|
+
readonly entityId: string;
|
|
46
|
+
readonly parentId?: AccountId;
|
|
47
|
+
}
|
|
48
|
+
/** `asset` and `expense` are debit-normal; `liability`, `equity` and `income` credit-normal. */
|
|
49
|
+
export declare const normalBalance: (type: AccountType) => NormalBalance;
|
|
50
|
+
/**
|
|
51
|
+
* A validated, hierarchy-checked set of accounts, read-only at runtime. Only
|
|
52
|
+
* {@link createChartOfAccounts} constructs one (this module's constructor is not exported from
|
|
53
|
+
* the package), so holding one proves parents exist, share entity and type, and form no cycle.
|
|
54
|
+
* Its state lives in `#private` fields, so it cannot be mutated even via `Map.prototype`.
|
|
55
|
+
*/
|
|
56
|
+
declare class ChartOfAccounts implements ReadonlyMap<AccountId, Account> {
|
|
57
|
+
#private;
|
|
58
|
+
constructor(accounts: ReadonlyMap<AccountId, Account>);
|
|
59
|
+
get size(): number;
|
|
60
|
+
get(id: AccountId): Account | undefined;
|
|
61
|
+
has(id: AccountId): boolean;
|
|
62
|
+
/** True for an account in this chart that no other account names as its parent. */
|
|
63
|
+
isLeaf(id: AccountId): boolean;
|
|
64
|
+
forEach(callbackfn: (value: Account, key: AccountId, map: ReadonlyMap<AccountId, Account>) => void, thisArg?: unknown): void;
|
|
65
|
+
entries(): MapIterator<[AccountId, Account]>;
|
|
66
|
+
keys(): MapIterator<AccountId>;
|
|
67
|
+
values(): MapIterator<Account>;
|
|
68
|
+
[Symbol.iterator](): MapIterator<[AccountId, Account]>;
|
|
69
|
+
}
|
|
70
|
+
/** A real calendar date formatted `YYYY-MM-DD`. No time, no timezone. */
|
|
71
|
+
type ISODate = Brand<string, "ISODate">;
|
|
72
|
+
export declare const parseISODate: (value: unknown, field?: string) => ISODate;
|
|
73
|
+
/** A posting as supplied by a caller. `amount` is signed minor units: debit `+`, credit `-`. */
|
|
74
|
+
interface PostingInput {
|
|
75
|
+
readonly accountId: string;
|
|
76
|
+
readonly amount: number;
|
|
77
|
+
readonly currency: string;
|
|
78
|
+
/** Cost centre / project / class tags. Keys and values must be non-empty. */
|
|
79
|
+
readonly dimensions?: Readonly<Record<string, string>>;
|
|
80
|
+
}
|
|
81
|
+
/** A validated posting. */
|
|
82
|
+
interface Posting {
|
|
83
|
+
readonly accountId: AccountId;
|
|
84
|
+
readonly amount: number;
|
|
85
|
+
readonly currency: Currency;
|
|
86
|
+
readonly dimensions?: Readonly<Record<string, string>>;
|
|
87
|
+
}
|
|
88
|
+
/** A transaction as supplied by a caller. The caller supplies `id`; nothing here generates one. */
|
|
89
|
+
interface TransactionInput {
|
|
90
|
+
readonly id: string;
|
|
91
|
+
readonly date: string;
|
|
92
|
+
readonly postings: readonly PostingInput[];
|
|
93
|
+
/** The transaction this one reverses or amends (append-only audit chain). */
|
|
94
|
+
readonly linkedTo?: string;
|
|
95
|
+
}
|
|
96
|
+
/** A balanced transaction. Only {@link validateTransaction} produces one. */
|
|
97
|
+
type Transaction = Brand<{
|
|
98
|
+
readonly id: TransactionId;
|
|
99
|
+
readonly date: ISODate;
|
|
100
|
+
readonly postings: readonly Posting[];
|
|
101
|
+
readonly linkedTo?: TransactionId;
|
|
102
|
+
}, "Transaction">;
|
|
103
|
+
/** Validate a transaction against a chart. Throws {@link LedgerValidationError}. */
|
|
104
|
+
export declare const validateTransaction: (input: TransactionInput, chart: ChartOfAccounts) => Transaction;
|
|
105
|
+
/**
|
|
106
|
+
* Build an Amendment: `[reversal, replacement]`, both linked to the original. The replacement's
|
|
107
|
+
* own `linkedTo` is overwritten with the original's id.
|
|
108
|
+
*/
|
|
109
|
+
export declare const amend: (tx: Transaction, input: {
|
|
110
|
+
readonly reversal: {
|
|
111
|
+
readonly id: string;
|
|
112
|
+
readonly date: string;
|
|
113
|
+
};
|
|
114
|
+
readonly replacement: TransactionInput;
|
|
115
|
+
}, chart: ChartOfAccounts) => readonly [Transaction, Transaction];
|
|
116
|
+
/** One Leaf Account's raw signed Balance (Debit `+`, Credit `-`) in its own Currency. */
|
|
117
|
+
interface BalanceRow {
|
|
118
|
+
readonly entityId: string;
|
|
119
|
+
readonly accountId: AccountId;
|
|
120
|
+
readonly currency: Currency;
|
|
121
|
+
readonly amount: number;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Every Leaf Account's Balance up to and including `asOf`, zeros included, sorted. Header
|
|
125
|
+
* Accounts never appear, so summing the rows never double counts.
|
|
126
|
+
*
|
|
127
|
+
* TODO(#14): Header Account roll-ups live in a separate query, never in these rows.
|
|
128
|
+
*/
|
|
129
|
+
export declare const balances: (transactions: readonly Transaction[], asOf: string, chart: ChartOfAccounts) => readonly BalanceRow[];
|
|
130
|
+
/**
|
|
131
|
+
* One Leaf Account's Balance up to and including `asOf`: the row `balances` gives for it.
|
|
132
|
+
* Throws `UNKNOWN_ACCOUNT` for an id outside the chart and `NON_LEAF_ACCOUNT` for a Header.
|
|
133
|
+
*
|
|
134
|
+
* TODO(#14): a Header's roll-up Balance is a separate query, so a Header id is rejected here.
|
|
135
|
+
*/
|
|
136
|
+
export declare const balance: (transactions: readonly Transaction[], accountId: string, asOf: string, chart: ChartOfAccounts) => BalanceRow;
|
|
137
|
+
/** Validate accounts and build the chart. Throws {@link LedgerValidationError}. */
|
|
138
|
+
export declare const createChartOfAccounts: (inputs: Iterable<AccountInput>) => ChartOfAccounts;
|
|
139
|
+
/** One leg of a Compound Entry: an Account, its own magnitude, and optional Dimensions. */
|
|
140
|
+
interface CompoundLeg {
|
|
141
|
+
readonly accountId: string;
|
|
142
|
+
readonly amount: number;
|
|
143
|
+
readonly dimensions?: Readonly<Record<string, string>>;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Build a Compound Entry: many legs on either side, each with its own magnitude.
|
|
147
|
+
* The two sides must total the same amount.
|
|
148
|
+
*/
|
|
149
|
+
export declare const compound: (input: {
|
|
150
|
+
readonly id: string;
|
|
151
|
+
readonly date: string;
|
|
152
|
+
readonly debit: readonly CompoundLeg[];
|
|
153
|
+
readonly credit: readonly CompoundLeg[];
|
|
154
|
+
}, chart: ChartOfAccounts) => Transaction;
|
|
155
|
+
/**
|
|
156
|
+
* Every way a ledger input can be rejected. Each code names exactly one rule.
|
|
157
|
+
*
|
|
158
|
+
* TODO(#12): adding a code is source-breaking for a consumer that switches exhaustively over this
|
|
159
|
+
* union. How that growth is versioned and communicated is not decided yet.
|
|
160
|
+
*/
|
|
161
|
+
type LedgerErrorCode = "SINGLE_POSTING" | "INVALID_AMOUNT" | "ZERO_AMOUNT_POSTING" | "UNKNOWN_ACCOUNT" | "NON_LEAF_ACCOUNT" | "CURRENCY_MISMATCH" | "UNBALANCED_TRANSACTION" | "INVALID_DATE" | "INVALID_CURRENCY" | "INVALID_DIMENSION" | "INVALID_ACCOUNT_HIERARCHY" | "NEGATIVE_AMOUNT" | "DUPLICATE_TRANSACTION_ID" | "REVERSAL_BEFORE_ORIGINAL" | "NON_EQUITY_CONTRA" | "INVALID_PERIOD" | "AMOUNT_OVERFLOW";
|
|
162
|
+
/** Thrown by every smart constructor and validator in this package. */
|
|
163
|
+
export declare class LedgerValidationError extends Error {
|
|
164
|
+
readonly code: LedgerErrorCode;
|
|
165
|
+
/** Path of the offending input, e.g. `postings[1].accountId`. */
|
|
166
|
+
readonly field: string;
|
|
167
|
+
readonly value: unknown;
|
|
168
|
+
readonly reason: string;
|
|
169
|
+
constructor(code: LedgerErrorCode, field: string, value: unknown, reason: string);
|
|
170
|
+
}
|
|
171
|
+
type LegObject = {
|
|
172
|
+
readonly accountId: string;
|
|
173
|
+
readonly dimensions?: Readonly<Record<string, string>>;
|
|
174
|
+
};
|
|
175
|
+
/** One side of a builder entry: a bare account id, or an account id with Dimensions. */
|
|
176
|
+
type LegInput = string | LegObject;
|
|
177
|
+
/**
|
|
178
|
+
* Convert a major-unit amount (`12.34`) to signed integer minor units (`1234`).
|
|
179
|
+
* Rounds half away from zero; `toPrecision` strips binary-float noise first.
|
|
180
|
+
*/
|
|
181
|
+
export declare const toMinorUnits: (majorAmount: number, currency: string) => number;
|
|
182
|
+
/** Convert signed integer minor units (`1234`) to a major-unit amount (`12.34`). */
|
|
183
|
+
export declare const fromMinorUnits: (minorUnits: number, currency: string) => number;
|
|
184
|
+
/** A named total for one `(Entity, Currency)` pair. */
|
|
185
|
+
interface Total {
|
|
186
|
+
readonly entityId: string;
|
|
187
|
+
readonly currency: Currency;
|
|
188
|
+
readonly amount: number;
|
|
189
|
+
}
|
|
190
|
+
/** Net Worth: the asset and liability Balance rows behind it, and one total per `(Entity, Currency)`. */
|
|
191
|
+
interface NetWorth {
|
|
192
|
+
readonly rows: readonly BalanceRow[];
|
|
193
|
+
readonly totals: readonly Total[];
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Assets minus Liabilities up to and including `asOf`, per `(Entity, Currency)`, positive when
|
|
197
|
+
* assets exceed liabilities. Rows stay raw signed; Equity is not part of it. Raw liabilities are
|
|
198
|
+
* negative, so the natural-sign total is the plain sum of the rows.
|
|
199
|
+
*/
|
|
200
|
+
export declare const netWorth: (transactions: readonly Transaction[], asOf: string, chart: ChartOfAccounts) => NetWorth;
|
|
201
|
+
/**
|
|
202
|
+
* Build an Opening Balance: `amount` posts on `account`, the opposite Posting on the equity
|
|
203
|
+
* `contra`. `amount` is signed (an overdrawn Asset opens negative); direction is never inferred.
|
|
204
|
+
*/
|
|
205
|
+
export declare const openingBalance: (input: {
|
|
206
|
+
readonly id: string;
|
|
207
|
+
readonly date: string;
|
|
208
|
+
readonly account: LegInput;
|
|
209
|
+
readonly contra: LegInput;
|
|
210
|
+
readonly amount: number;
|
|
211
|
+
}, chart: ChartOfAccounts) => Transaction;
|
|
212
|
+
/** A P&L row for one Dimension value; `null` collects Postings lacking the grouped key. */
|
|
213
|
+
interface GroupedRow extends BalanceRow {
|
|
214
|
+
readonly dimensionValue: string | null;
|
|
215
|
+
}
|
|
216
|
+
/** P&L: raw rows for Income and Expense Leaf Accounts and income-minus-expenses totals. */
|
|
217
|
+
interface ProfitAndLoss<Row extends BalanceRow = BalanceRow> {
|
|
218
|
+
readonly rows: readonly Row[];
|
|
219
|
+
readonly totals: readonly Total[];
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* The period and optional Dimension handling of a P&L query. An absent `from` means "from the
|
|
223
|
+
* start of the ledger". `where` is exact-match, ANDed; a missing key never matches.
|
|
224
|
+
*/
|
|
225
|
+
interface ProfitAndLossOptions {
|
|
226
|
+
readonly from?: string;
|
|
227
|
+
readonly to: string;
|
|
228
|
+
readonly groupBy?: string;
|
|
229
|
+
readonly where?: Readonly<Record<string, string>>;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Income minus Expenses over an inclusive period, per `(Entity, Currency)`, positive when income
|
|
233
|
+
* exceeds expenses. Rows stay raw signed, so the total is the negated sum of the rows. With
|
|
234
|
+
* `groupBy`, rows split by that Dimension's value and only groups with Postings appear. Totals hold
|
|
235
|
+
* one entry, zero if nothing posted, for every Income and Expense Leaf Account's pair.
|
|
236
|
+
*/
|
|
237
|
+
export declare function profitAndLoss(transactions: readonly Transaction[], options: ProfitAndLossOptions & {
|
|
238
|
+
readonly groupBy: string;
|
|
239
|
+
}, chart: ChartOfAccounts): ProfitAndLoss<GroupedRow>;
|
|
240
|
+
/** P&L without grouping: one row per Income and Expense Leaf Account; totals seeded per Leaf pair. */
|
|
241
|
+
export declare function profitAndLoss(transactions: readonly Transaction[], options: ProfitAndLossOptions & {
|
|
242
|
+
readonly groupBy?: undefined;
|
|
243
|
+
}, chart: ChartOfAccounts): ProfitAndLoss;
|
|
244
|
+
/** P&L when `groupBy` is only known at runtime: rows are plain or grouped, totals seeded alike. */
|
|
245
|
+
export declare function profitAndLoss(transactions: readonly Transaction[], options: ProfitAndLossOptions, chart: ChartOfAccounts): ProfitAndLoss<BalanceRow | GroupedRow>;
|
|
246
|
+
/** Build a Reversal: the original's Postings in order, every sign flipped, linked to it. */
|
|
247
|
+
export declare const reverse: (tx: Transaction, input: {
|
|
248
|
+
readonly id: string;
|
|
249
|
+
readonly date: string;
|
|
250
|
+
}, chart: ChartOfAccounts) => Transaction;
|
|
251
|
+
/** Build a Simple Entry: `+amount` on `debit`, `-amount` on `credit`. `amount` is a magnitude. */
|
|
252
|
+
export declare const simple: (input: {
|
|
253
|
+
readonly id: string;
|
|
254
|
+
readonly date: string;
|
|
255
|
+
readonly debit: LegInput;
|
|
256
|
+
readonly credit: LegInput;
|
|
257
|
+
readonly amount: number;
|
|
258
|
+
}, chart: ChartOfAccounts) => Transaction;
|
|
259
|
+
/** One Leaf Account's net Balance as a magnitude in its Debit or Credit column. */
|
|
260
|
+
interface TrialBalanceRow {
|
|
261
|
+
readonly entityId: string;
|
|
262
|
+
readonly accountId: AccountId;
|
|
263
|
+
readonly currency: Currency;
|
|
264
|
+
readonly debit: number;
|
|
265
|
+
readonly credit: number;
|
|
266
|
+
}
|
|
267
|
+
/** Column totals for one `(Entity, Currency)` pair. They always match. */
|
|
268
|
+
interface TrialBalanceTotal {
|
|
269
|
+
readonly entityId: string;
|
|
270
|
+
readonly currency: Currency;
|
|
271
|
+
readonly debit: number;
|
|
272
|
+
readonly credit: number;
|
|
273
|
+
}
|
|
274
|
+
/** A Trial Balance: one row per Leaf Account and column totals per `(Entity, Currency)`. */
|
|
275
|
+
interface TrialBalance {
|
|
276
|
+
readonly rows: readonly TrialBalanceRow[];
|
|
277
|
+
readonly totals: readonly TrialBalanceTotal[];
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Every Leaf Account's net Balance at `asOf`, zeros included: positive in `debit`, negative in
|
|
281
|
+
* `credit` as a magnitude (the one exception to raw signed rows).
|
|
282
|
+
*/
|
|
283
|
+
export declare const trialBalance: (transactions: readonly Transaction[], asOf: string, chart: ChartOfAccounts) => TrialBalance;
|
|
284
|
+
export type { Account, AccountId, AccountInput, AccountType, BalanceRow, ChartOfAccounts, CompoundLeg, Currency, GroupedRow, ISODate, LedgerErrorCode, LegInput, NetWorth, NormalBalance, Posting, PostingInput, ProfitAndLoss, ProfitAndLossOptions, Total, Transaction, TransactionId, TransactionInput, TrialBalance, TrialBalanceRow, TrialBalanceTotal };
|
|
285
|
+
//# sourceMappingURL=index.d.mts.map
|