@lacspace/ledger 1.0.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 ADDED
@@ -0,0 +1,51 @@
1
+ Lacspace Free Licence
2
+ Version 1.0, August 2026
3
+
4
+ Copyright (c) 2026 Lacspace
5
+
6
+ PREAMBLE
7
+
8
+ This software is published by Lacspace under the Lacspace Free Licence — a free,
9
+ permissive licence that lets you use this software for any purpose, including in
10
+ commercial products and services, at no cost. It grants the same freedoms as
11
+ common permissive open-source licences; the only condition is that this notice
12
+ travels with the software. The canonical, always-current text of this licence is
13
+ maintained at https://lacspace.com/licenses/lacspace-free-1.0
14
+
15
+ GRANT OF RIGHTS
16
+
17
+ Permission is hereby granted, free of charge, to any person or organisation
18
+ obtaining a copy of this software and its associated documentation and data files
19
+ (the "Software"), to deal in the Software without restriction, including without
20
+ limitation the rights to use, copy, modify, merge, publish, distribute,
21
+ sublicense, and/or sell copies of the Software, and to permit persons to whom the
22
+ Software is furnished to do so, subject to the conditions below. These rights are
23
+ granted for any purpose, personal or commercial, and are perpetual, worldwide,
24
+ non-exclusive, and royalty-free.
25
+
26
+ CONDITIONS
27
+
28
+ The above copyright notice, this permission notice, and the name of this licence
29
+ ("Lacspace Free Licence") shall be included in all copies or substantial portions
30
+ of the Software.
31
+
32
+ TRADEMARKS
33
+
34
+ This licence does not grant permission to use the trade names, trademarks, service
35
+ marks, logos, or product names of Lacspace, except as required to reproduce the
36
+ notice above or to describe the origin of the Software in a truthful manner.
37
+
38
+ DISCLAIMER OF WARRANTY AND LIMITATION OF LIABILITY
39
+
40
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
41
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
42
+ FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
43
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN
44
+ AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION
45
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
46
+
47
+ ---
48
+
49
+ The Lacspace Free Licence is a source-available, permissive licence and is not (as
50
+ of this version) an OSI-approved licence. In substance it grants the same freedoms
51
+ as the MIT Licence. Learn more at https://lacspace.com/licenses
package/README.md ADDED
@@ -0,0 +1,86 @@
1
+ <div align="center">
2
+
3
+ # @lacspace/ledger
4
+
5
+ **A tiny double-entry ledger / wallet — balanced transactions, per-account balances and a trial balance that always sums to zero.**
6
+
7
+ [![npm version](https://img.shields.io/npm/v/@lacspace/ledger?color=%2316a34a&label=npm)](https://www.npmjs.com/package/@lacspace/ledger)
8
+ [![install size](https://packagephobia.com/badge?p=@lacspace/ledger)](https://packagephobia.com/result?p=@lacspace/ledger)
9
+ [![minzipped](https://img.shields.io/bundlephobia/minzip/@lacspace/ledger?label=minzip)](https://bundlephobia.com/package/@lacspace/ledger)
10
+ [![types](https://img.shields.io/badge/types-included-blue)](https://www.npmjs.com/package/@lacspace/ledger)
11
+ [![license](https://img.shields.io/npm/l/@lacspace/ledger?color=green)](https://github.com/lacspace/npm-packages/blob/main/LICENSE)
12
+
13
+ </div>
14
+
15
+ > Wallets and internal balances are usually a single mutable number that silently drifts. This is **double-entry** in a few bytes: every transaction is a set of signed lines that sum to **zero**, so the books can't go out of balance. Integer minor units, immutable operations, crypto-random ids.
16
+
17
+ - ⚖️ **Always balanced** — a transaction's lines must sum to `0`, or it throws
18
+ - 💯 **Integer minor units** — cents / paisa, never floats
19
+ - 🧊 **Immutable** — every op returns a new ledger
20
+ - 📒 `balance`, `statement` and a `trialBalance` that totals zero
21
+ - ⚡ Isomorphic (Web Crypto ids) · zero dependencies · fully typed
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ npm install @lacspace/ledger # or pnpm add / yarn add / bun add
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ ```ts
32
+ import { createLedger, post, postMany, balance, statement, trialBalance } from "@lacspace/ledger";
33
+
34
+ let book = createLedger();
35
+
36
+ // Simple two-line posting: +amount to `debit`, -amount to `credit`
37
+ book = post(book, { debit: "cash", credit: "sales", amount: 10000, ref: "INV-1" });
38
+
39
+ // Multi-line posting — throws unless the signed amounts sum to 0
40
+ book = postMany(book, [
41
+ { account: "cash", amount: 9700 },
42
+ { account: "fees", amount: 300 },
43
+ { account: "sales", amount: -10000 },
44
+ ], { memo: "sale less processor fee" });
45
+
46
+ balance(book, "cash"); // 19700
47
+ statement(book, "cash"); // [{ at, amount, ref?, memo? }, …]
48
+ trialBalance(book); // [{ account, balance }, …] — always sums to 0
49
+ ```
50
+
51
+ ## Sign convention
52
+
53
+ An account's balance is the **sum of its signed line amounts**. A **debit is positive**, a **credit is negative**. So `post({ debit, credit, amount })` books `+amount` to `debit` and `-amount` to `credit`. Under this rule an asset/expense account rises when debited; a liability/income/equity account rises when credited (its balance goes further negative).
54
+
55
+ ## API
56
+
57
+ | Function | Description |
58
+ | --- | --- |
59
+ | `createLedger()` | a new empty ledger |
60
+ | `post(ledger, { debit, credit, amount, ref?, memo? })` | two-line balanced entry (`amount` positive) |
61
+ | `postMany(ledger, lines[], meta?)` | arbitrary entry — **throws** if lines don't sum to 0 |
62
+ | `balance(ledger, account)` | sum of that account's signed lines |
63
+ | `statement(ledger, account)` | rows `{ at, amount, ref?, memo? }` for entries touching the account |
64
+ | `trialBalance(ledger)` | `{ account, balance }[]`, sorted, always sums to 0 |
65
+
66
+ ## Licensing
67
+
68
+ This package is **free** under the **[Lacspace Free Licence](https://lacspace.com/licenses/lacspace-free-1.0)** — permissive freedoms. Use it in personal and commercial projects at no cost; just keep the notice.
69
+
70
+ Not every Lacspace package is free. We also offer **Commercial** (paid), **Client-specific**, and **Private** (proprietary) packages under separate terms. See the full **[Lacspace Licence Centre](https://lacspace.com/licenses)**.
71
+
72
+ <!-- LACSPACE-DEV-PLATFORM -->
73
+
74
+ ---
75
+
76
+ ## The Lacspace Developer Platform
77
+
78
+ `@lacspace/ledger` is part of **63+ zero-dependency, isomorphic TypeScript packages**. Explore the ecosystem:
79
+
80
+ - 🗂️ **All packages** — https://developer.lacspace.com/packages
81
+ - 🧭 **Developer handbook** — https://developer.lacspace.com/handbook
82
+ - 🧪 **Live playground** — https://developer.lacspace.com/playground
83
+ - 🖥️ **Finished app templates** — https://templates.lacspace.com
84
+ - 🚀 **Scaffold a full app** — `npm create lacspace-app@latest`
85
+
86
+ Free under the **[Lacspace Free Licence](https://lacspace.com/licenses/lacspace-free-1.0)** — a permissive, free-to-use licence.
package/dist/index.cjs ADDED
@@ -0,0 +1,108 @@
1
+ 'use strict';
2
+
3
+ // src/index.ts
4
+ var HEX = [];
5
+ for (let i = 0; i < 256; i++) HEX.push((i + 256).toString(16).slice(1));
6
+ function genId() {
7
+ const c = globalThis.crypto;
8
+ if (!c || typeof c.getRandomValues !== "function") {
9
+ throw new Error("Web Crypto getRandomValues is unavailable in this environment.");
10
+ }
11
+ const b = new Uint8Array(16);
12
+ c.getRandomValues(b);
13
+ let out = "";
14
+ for (let i = 0; i < 16; i++) out += HEX[b[i]];
15
+ return out;
16
+ }
17
+ function assertInteger(amount, label) {
18
+ if (!Number.isInteger(amount)) {
19
+ throw new TypeError(`${label} must be an integer number of minor units, got ${amount}`);
20
+ }
21
+ }
22
+ function createLedger() {
23
+ return { entries: [] };
24
+ }
25
+ function append(ledger, lines, meta) {
26
+ const entry = {
27
+ id: genId(),
28
+ at: (/* @__PURE__ */ new Date()).toISOString(),
29
+ lines,
30
+ ...meta?.ref !== void 0 ? { ref: meta.ref } : {},
31
+ ...meta?.memo !== void 0 ? { memo: meta.memo } : {}
32
+ };
33
+ return { entries: [...ledger.entries, entry] };
34
+ }
35
+ function post(ledger, tx) {
36
+ assertInteger(tx.amount, "amount");
37
+ if (tx.amount <= 0) {
38
+ throw new RangeError(`post() amount must be positive, got ${tx.amount}`);
39
+ }
40
+ const lines = [
41
+ { account: tx.debit, amount: tx.amount },
42
+ { account: tx.credit, amount: -tx.amount }
43
+ ];
44
+ return append(ledger, lines, { ref: tx.ref, memo: tx.memo });
45
+ }
46
+ function postMany(ledger, lines, meta) {
47
+ if (!Array.isArray(lines) || lines.length === 0) {
48
+ throw new RangeError("postMany() requires at least one line");
49
+ }
50
+ let sum = 0;
51
+ for (const line of lines) {
52
+ assertInteger(line.amount, "line amount");
53
+ sum += line.amount;
54
+ }
55
+ if (sum !== 0) {
56
+ throw new RangeError(`unbalanced transaction: lines sum to ${sum}, expected 0`);
57
+ }
58
+ return append(ledger, lines.map((l) => ({ account: l.account, amount: l.amount })), meta);
59
+ }
60
+ function balance(ledger, account) {
61
+ let total = 0;
62
+ for (const entry of ledger.entries) {
63
+ for (const line of entry.lines) {
64
+ if (line.account === account) total += line.amount;
65
+ }
66
+ }
67
+ return total;
68
+ }
69
+ function statement(ledger, account) {
70
+ const rows = [];
71
+ for (const entry of ledger.entries) {
72
+ let net = 0;
73
+ let touched = false;
74
+ for (const line of entry.lines) {
75
+ if (line.account === account) {
76
+ net += line.amount;
77
+ touched = true;
78
+ }
79
+ }
80
+ if (touched) {
81
+ rows.push({
82
+ at: entry.at,
83
+ amount: net,
84
+ ...entry.ref !== void 0 ? { ref: entry.ref } : {},
85
+ ...entry.memo !== void 0 ? { memo: entry.memo } : {}
86
+ });
87
+ }
88
+ }
89
+ return rows;
90
+ }
91
+ function trialBalance(ledger) {
92
+ const totals = /* @__PURE__ */ new Map();
93
+ for (const entry of ledger.entries) {
94
+ for (const line of entry.lines) {
95
+ totals.set(line.account, (totals.get(line.account) ?? 0) + line.amount);
96
+ }
97
+ }
98
+ return [...totals.entries()].map(([account, bal]) => ({ account, balance: bal })).sort((a, b) => a.account < b.account ? -1 : a.account > b.account ? 1 : 0);
99
+ }
100
+
101
+ exports.balance = balance;
102
+ exports.createLedger = createLedger;
103
+ exports.post = post;
104
+ exports.postMany = postMany;
105
+ exports.statement = statement;
106
+ exports.trialBalance = trialBalance;
107
+ //# sourceMappingURL=index.cjs.map
108
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * @lacspace/ledger\n *\n * A tiny **double-entry** ledger / wallet. Every transaction is a set of lines\n * whose signed amounts sum to **zero**, so the books can never drift: balances\n * are just sums, and the trial balance always totals zero.\n *\n * Sign convention: an account's balance is the sum of its signed line amounts.\n * A debit is a **positive** amount, a credit is a **negative** one — so\n * `post({ debit, credit, amount })` books `+amount` to `debit` and `-amount` to\n * `credit`. Under this convention an asset/expense account rises when debited\n * and a liability/income/equity account rises when credited (its balance goes\n * more negative). Amounts are **integer minor units** (cents, paisa, …).\n *\n * ```ts\n * import { createLedger, post, balance, trialBalance } from \"@lacspace/ledger\";\n *\n * let book = createLedger();\n * book = post(book, { debit: \"cash\", credit: \"sales\", amount: 10000 });\n * balance(book, \"cash\"); // 10000\n * balance(book, \"sales\"); // -10000\n * trialBalance(book); // sums to 0\n * ```\n *\n * Immutable — every operation returns a new {@link Ledger}. Zero dependencies,\n * isomorphic (ids use Web Crypto `getRandomValues`).\n */\n\n/** A single posting line: a signed amount against an account (minor units). */\nexport interface LedgerLine {\n account: string;\n /** Signed amount in minor units. A transaction's lines MUST sum to 0. */\n amount: number;\n}\n\n/** One posted transaction. Its `lines` always sum to zero. */\nexport interface LedgerEntry {\n /** Unique id (crypto-random). */\n id: string;\n /** ISO-8601 timestamp of when it was posted. */\n at: string;\n lines: LedgerLine[];\n /** Optional external reference (invoice #, txn id, …). */\n ref?: string;\n /** Optional human-readable note. */\n memo?: string;\n}\n\n/** An immutable ledger — an ordered list of balanced entries. */\nexport interface Ledger {\n entries: LedgerEntry[];\n}\n\n/** A single row of a {@link statement}. */\nexport interface StatementRow {\n at: string;\n /** Net signed amount this entry moved on the account (minor units). */\n amount: number;\n ref?: string;\n memo?: string;\n}\n\n/** A single row of a {@link trialBalance}. */\nexport interface TrialBalanceRow {\n account: string;\n balance: number;\n}\n\nconst HEX: string[] = [];\nfor (let i = 0; i < 256; i++) HEX.push((i + 0x100).toString(16).slice(1));\n\n/** Crypto-random 16-byte hex id. Isomorphic via Web Crypto. */\nfunction genId(): string {\n const c = (globalThis as { crypto?: Crypto }).crypto;\n if (!c || typeof c.getRandomValues !== \"function\") {\n throw new Error(\"Web Crypto getRandomValues is unavailable in this environment.\");\n }\n const b = new Uint8Array(16);\n c.getRandomValues(b);\n let out = \"\";\n for (let i = 0; i < 16; i++) out += HEX[b[i]!]!;\n return out;\n}\n\nfunction assertInteger(amount: number, label: string): void {\n if (!Number.isInteger(amount)) {\n throw new TypeError(`${label} must be an integer number of minor units, got ${amount}`);\n }\n}\n\n/** Create a new, empty ledger. */\nexport function createLedger(): Ledger {\n return { entries: [] };\n}\n\n/** Append a pre-built, already-balanced entry (immutable). */\nfunction append(\n ledger: Ledger,\n lines: LedgerLine[],\n meta?: { ref?: string; memo?: string }\n): Ledger {\n const entry: LedgerEntry = {\n id: genId(),\n at: new Date().toISOString(),\n lines,\n ...(meta?.ref !== undefined ? { ref: meta.ref } : {}),\n ...(meta?.memo !== undefined ? { memo: meta.memo } : {}),\n };\n return { entries: [...ledger.entries, entry] };\n}\n\n/**\n * Post a simple two-line transaction: `+amount` to `debit`, `-amount` to\n * `credit`. `amount` must be a positive integer (minor units). Returns a new\n * ledger — the input is never mutated.\n */\nexport function post(\n ledger: Ledger,\n tx: { debit: string; credit: string; amount: number; ref?: string; memo?: string }\n): Ledger {\n assertInteger(tx.amount, \"amount\");\n if (tx.amount <= 0) {\n throw new RangeError(`post() amount must be positive, got ${tx.amount}`);\n }\n const lines: LedgerLine[] = [\n { account: tx.debit, amount: tx.amount },\n { account: tx.credit, amount: -tx.amount },\n ];\n return append(ledger, lines, { ref: tx.ref, memo: tx.memo });\n}\n\n/**\n * Post an arbitrary multi-line transaction. **Throws** if the signed line\n * amounts don't sum to exactly zero (or any amount isn't an integer).\n */\nexport function postMany(\n ledger: Ledger,\n lines: LedgerLine[],\n meta?: { ref?: string; memo?: string }\n): Ledger {\n if (!Array.isArray(lines) || lines.length === 0) {\n throw new RangeError(\"postMany() requires at least one line\");\n }\n let sum = 0;\n for (const line of lines) {\n assertInteger(line.amount, \"line amount\");\n sum += line.amount;\n }\n if (sum !== 0) {\n throw new RangeError(`unbalanced transaction: lines sum to ${sum}, expected 0`);\n }\n return append(ledger, lines.map((l) => ({ account: l.account, amount: l.amount })), meta);\n}\n\n/** Balance of an account: the sum of all its signed line amounts. */\nexport function balance(ledger: Ledger, account: string): number {\n let total = 0;\n for (const entry of ledger.entries) {\n for (const line of entry.lines) {\n if (line.account === account) total += line.amount;\n }\n }\n return total;\n}\n\n/**\n * Statement for one account: one row per entry that touches it, in order, with\n * the net signed amount that entry moved on the account.\n */\nexport function statement(ledger: Ledger, account: string): StatementRow[] {\n const rows: StatementRow[] = [];\n for (const entry of ledger.entries) {\n let net = 0;\n let touched = false;\n for (const line of entry.lines) {\n if (line.account === account) {\n net += line.amount;\n touched = true;\n }\n }\n if (touched) {\n rows.push({\n at: entry.at,\n amount: net,\n ...(entry.ref !== undefined ? { ref: entry.ref } : {}),\n ...(entry.memo !== undefined ? { memo: entry.memo } : {}),\n });\n }\n }\n return rows;\n}\n\n/**\n * Trial balance: every account with its balance, sorted by account name. Since\n * every entry is balanced, the balances always sum to exactly zero.\n */\nexport function trialBalance(ledger: Ledger): TrialBalanceRow[] {\n const totals = new Map<string, number>();\n for (const entry of ledger.entries) {\n for (const line of entry.lines) {\n totals.set(line.account, (totals.get(line.account) ?? 0) + line.amount);\n }\n }\n return [...totals.entries()]\n .map(([account, bal]) => ({ account, balance: bal }))\n .sort((a, b) => (a.account < b.account ? -1 : a.account > b.account ? 1 : 0));\n}\n"]}
@@ -0,0 +1,98 @@
1
+ /**
2
+ * @lacspace/ledger
3
+ *
4
+ * A tiny **double-entry** ledger / wallet. Every transaction is a set of lines
5
+ * whose signed amounts sum to **zero**, so the books can never drift: balances
6
+ * are just sums, and the trial balance always totals zero.
7
+ *
8
+ * Sign convention: an account's balance is the sum of its signed line amounts.
9
+ * A debit is a **positive** amount, a credit is a **negative** one — so
10
+ * `post({ debit, credit, amount })` books `+amount` to `debit` and `-amount` to
11
+ * `credit`. Under this convention an asset/expense account rises when debited
12
+ * and a liability/income/equity account rises when credited (its balance goes
13
+ * more negative). Amounts are **integer minor units** (cents, paisa, …).
14
+ *
15
+ * ```ts
16
+ * import { createLedger, post, balance, trialBalance } from "@lacspace/ledger";
17
+ *
18
+ * let book = createLedger();
19
+ * book = post(book, { debit: "cash", credit: "sales", amount: 10000 });
20
+ * balance(book, "cash"); // 10000
21
+ * balance(book, "sales"); // -10000
22
+ * trialBalance(book); // sums to 0
23
+ * ```
24
+ *
25
+ * Immutable — every operation returns a new {@link Ledger}. Zero dependencies,
26
+ * isomorphic (ids use Web Crypto `getRandomValues`).
27
+ */
28
+ /** A single posting line: a signed amount against an account (minor units). */
29
+ interface LedgerLine {
30
+ account: string;
31
+ /** Signed amount in minor units. A transaction's lines MUST sum to 0. */
32
+ amount: number;
33
+ }
34
+ /** One posted transaction. Its `lines` always sum to zero. */
35
+ interface LedgerEntry {
36
+ /** Unique id (crypto-random). */
37
+ id: string;
38
+ /** ISO-8601 timestamp of when it was posted. */
39
+ at: string;
40
+ lines: LedgerLine[];
41
+ /** Optional external reference (invoice #, txn id, …). */
42
+ ref?: string;
43
+ /** Optional human-readable note. */
44
+ memo?: string;
45
+ }
46
+ /** An immutable ledger — an ordered list of balanced entries. */
47
+ interface Ledger {
48
+ entries: LedgerEntry[];
49
+ }
50
+ /** A single row of a {@link statement}. */
51
+ interface StatementRow {
52
+ at: string;
53
+ /** Net signed amount this entry moved on the account (minor units). */
54
+ amount: number;
55
+ ref?: string;
56
+ memo?: string;
57
+ }
58
+ /** A single row of a {@link trialBalance}. */
59
+ interface TrialBalanceRow {
60
+ account: string;
61
+ balance: number;
62
+ }
63
+ /** Create a new, empty ledger. */
64
+ declare function createLedger(): Ledger;
65
+ /**
66
+ * Post a simple two-line transaction: `+amount` to `debit`, `-amount` to
67
+ * `credit`. `amount` must be a positive integer (minor units). Returns a new
68
+ * ledger — the input is never mutated.
69
+ */
70
+ declare function post(ledger: Ledger, tx: {
71
+ debit: string;
72
+ credit: string;
73
+ amount: number;
74
+ ref?: string;
75
+ memo?: string;
76
+ }): Ledger;
77
+ /**
78
+ * Post an arbitrary multi-line transaction. **Throws** if the signed line
79
+ * amounts don't sum to exactly zero (or any amount isn't an integer).
80
+ */
81
+ declare function postMany(ledger: Ledger, lines: LedgerLine[], meta?: {
82
+ ref?: string;
83
+ memo?: string;
84
+ }): Ledger;
85
+ /** Balance of an account: the sum of all its signed line amounts. */
86
+ declare function balance(ledger: Ledger, account: string): number;
87
+ /**
88
+ * Statement for one account: one row per entry that touches it, in order, with
89
+ * the net signed amount that entry moved on the account.
90
+ */
91
+ declare function statement(ledger: Ledger, account: string): StatementRow[];
92
+ /**
93
+ * Trial balance: every account with its balance, sorted by account name. Since
94
+ * every entry is balanced, the balances always sum to exactly zero.
95
+ */
96
+ declare function trialBalance(ledger: Ledger): TrialBalanceRow[];
97
+
98
+ export { type Ledger, type LedgerEntry, type LedgerLine, type StatementRow, type TrialBalanceRow, balance, createLedger, post, postMany, statement, trialBalance };
@@ -0,0 +1,98 @@
1
+ /**
2
+ * @lacspace/ledger
3
+ *
4
+ * A tiny **double-entry** ledger / wallet. Every transaction is a set of lines
5
+ * whose signed amounts sum to **zero**, so the books can never drift: balances
6
+ * are just sums, and the trial balance always totals zero.
7
+ *
8
+ * Sign convention: an account's balance is the sum of its signed line amounts.
9
+ * A debit is a **positive** amount, a credit is a **negative** one — so
10
+ * `post({ debit, credit, amount })` books `+amount` to `debit` and `-amount` to
11
+ * `credit`. Under this convention an asset/expense account rises when debited
12
+ * and a liability/income/equity account rises when credited (its balance goes
13
+ * more negative). Amounts are **integer minor units** (cents, paisa, …).
14
+ *
15
+ * ```ts
16
+ * import { createLedger, post, balance, trialBalance } from "@lacspace/ledger";
17
+ *
18
+ * let book = createLedger();
19
+ * book = post(book, { debit: "cash", credit: "sales", amount: 10000 });
20
+ * balance(book, "cash"); // 10000
21
+ * balance(book, "sales"); // -10000
22
+ * trialBalance(book); // sums to 0
23
+ * ```
24
+ *
25
+ * Immutable — every operation returns a new {@link Ledger}. Zero dependencies,
26
+ * isomorphic (ids use Web Crypto `getRandomValues`).
27
+ */
28
+ /** A single posting line: a signed amount against an account (minor units). */
29
+ interface LedgerLine {
30
+ account: string;
31
+ /** Signed amount in minor units. A transaction's lines MUST sum to 0. */
32
+ amount: number;
33
+ }
34
+ /** One posted transaction. Its `lines` always sum to zero. */
35
+ interface LedgerEntry {
36
+ /** Unique id (crypto-random). */
37
+ id: string;
38
+ /** ISO-8601 timestamp of when it was posted. */
39
+ at: string;
40
+ lines: LedgerLine[];
41
+ /** Optional external reference (invoice #, txn id, …). */
42
+ ref?: string;
43
+ /** Optional human-readable note. */
44
+ memo?: string;
45
+ }
46
+ /** An immutable ledger — an ordered list of balanced entries. */
47
+ interface Ledger {
48
+ entries: LedgerEntry[];
49
+ }
50
+ /** A single row of a {@link statement}. */
51
+ interface StatementRow {
52
+ at: string;
53
+ /** Net signed amount this entry moved on the account (minor units). */
54
+ amount: number;
55
+ ref?: string;
56
+ memo?: string;
57
+ }
58
+ /** A single row of a {@link trialBalance}. */
59
+ interface TrialBalanceRow {
60
+ account: string;
61
+ balance: number;
62
+ }
63
+ /** Create a new, empty ledger. */
64
+ declare function createLedger(): Ledger;
65
+ /**
66
+ * Post a simple two-line transaction: `+amount` to `debit`, `-amount` to
67
+ * `credit`. `amount` must be a positive integer (minor units). Returns a new
68
+ * ledger — the input is never mutated.
69
+ */
70
+ declare function post(ledger: Ledger, tx: {
71
+ debit: string;
72
+ credit: string;
73
+ amount: number;
74
+ ref?: string;
75
+ memo?: string;
76
+ }): Ledger;
77
+ /**
78
+ * Post an arbitrary multi-line transaction. **Throws** if the signed line
79
+ * amounts don't sum to exactly zero (or any amount isn't an integer).
80
+ */
81
+ declare function postMany(ledger: Ledger, lines: LedgerLine[], meta?: {
82
+ ref?: string;
83
+ memo?: string;
84
+ }): Ledger;
85
+ /** Balance of an account: the sum of all its signed line amounts. */
86
+ declare function balance(ledger: Ledger, account: string): number;
87
+ /**
88
+ * Statement for one account: one row per entry that touches it, in order, with
89
+ * the net signed amount that entry moved on the account.
90
+ */
91
+ declare function statement(ledger: Ledger, account: string): StatementRow[];
92
+ /**
93
+ * Trial balance: every account with its balance, sorted by account name. Since
94
+ * every entry is balanced, the balances always sum to exactly zero.
95
+ */
96
+ declare function trialBalance(ledger: Ledger): TrialBalanceRow[];
97
+
98
+ export { type Ledger, type LedgerEntry, type LedgerLine, type StatementRow, type TrialBalanceRow, balance, createLedger, post, postMany, statement, trialBalance };
package/dist/index.js ADDED
@@ -0,0 +1,101 @@
1
+ // src/index.ts
2
+ var HEX = [];
3
+ for (let i = 0; i < 256; i++) HEX.push((i + 256).toString(16).slice(1));
4
+ function genId() {
5
+ const c = globalThis.crypto;
6
+ if (!c || typeof c.getRandomValues !== "function") {
7
+ throw new Error("Web Crypto getRandomValues is unavailable in this environment.");
8
+ }
9
+ const b = new Uint8Array(16);
10
+ c.getRandomValues(b);
11
+ let out = "";
12
+ for (let i = 0; i < 16; i++) out += HEX[b[i]];
13
+ return out;
14
+ }
15
+ function assertInteger(amount, label) {
16
+ if (!Number.isInteger(amount)) {
17
+ throw new TypeError(`${label} must be an integer number of minor units, got ${amount}`);
18
+ }
19
+ }
20
+ function createLedger() {
21
+ return { entries: [] };
22
+ }
23
+ function append(ledger, lines, meta) {
24
+ const entry = {
25
+ id: genId(),
26
+ at: (/* @__PURE__ */ new Date()).toISOString(),
27
+ lines,
28
+ ...meta?.ref !== void 0 ? { ref: meta.ref } : {},
29
+ ...meta?.memo !== void 0 ? { memo: meta.memo } : {}
30
+ };
31
+ return { entries: [...ledger.entries, entry] };
32
+ }
33
+ function post(ledger, tx) {
34
+ assertInteger(tx.amount, "amount");
35
+ if (tx.amount <= 0) {
36
+ throw new RangeError(`post() amount must be positive, got ${tx.amount}`);
37
+ }
38
+ const lines = [
39
+ { account: tx.debit, amount: tx.amount },
40
+ { account: tx.credit, amount: -tx.amount }
41
+ ];
42
+ return append(ledger, lines, { ref: tx.ref, memo: tx.memo });
43
+ }
44
+ function postMany(ledger, lines, meta) {
45
+ if (!Array.isArray(lines) || lines.length === 0) {
46
+ throw new RangeError("postMany() requires at least one line");
47
+ }
48
+ let sum = 0;
49
+ for (const line of lines) {
50
+ assertInteger(line.amount, "line amount");
51
+ sum += line.amount;
52
+ }
53
+ if (sum !== 0) {
54
+ throw new RangeError(`unbalanced transaction: lines sum to ${sum}, expected 0`);
55
+ }
56
+ return append(ledger, lines.map((l) => ({ account: l.account, amount: l.amount })), meta);
57
+ }
58
+ function balance(ledger, account) {
59
+ let total = 0;
60
+ for (const entry of ledger.entries) {
61
+ for (const line of entry.lines) {
62
+ if (line.account === account) total += line.amount;
63
+ }
64
+ }
65
+ return total;
66
+ }
67
+ function statement(ledger, account) {
68
+ const rows = [];
69
+ for (const entry of ledger.entries) {
70
+ let net = 0;
71
+ let touched = false;
72
+ for (const line of entry.lines) {
73
+ if (line.account === account) {
74
+ net += line.amount;
75
+ touched = true;
76
+ }
77
+ }
78
+ if (touched) {
79
+ rows.push({
80
+ at: entry.at,
81
+ amount: net,
82
+ ...entry.ref !== void 0 ? { ref: entry.ref } : {},
83
+ ...entry.memo !== void 0 ? { memo: entry.memo } : {}
84
+ });
85
+ }
86
+ }
87
+ return rows;
88
+ }
89
+ function trialBalance(ledger) {
90
+ const totals = /* @__PURE__ */ new Map();
91
+ for (const entry of ledger.entries) {
92
+ for (const line of entry.lines) {
93
+ totals.set(line.account, (totals.get(line.account) ?? 0) + line.amount);
94
+ }
95
+ }
96
+ return [...totals.entries()].map(([account, bal]) => ({ account, balance: bal })).sort((a, b) => a.account < b.account ? -1 : a.account > b.account ? 1 : 0);
97
+ }
98
+
99
+ export { balance, createLedger, post, postMany, statement, trialBalance };
100
+ //# sourceMappingURL=index.js.map
101
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";AAoEA,IAAM,MAAgB,EAAC;AACvB,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,GAAA,EAAK,KAAK,GAAA,CAAI,IAAA,CAAA,CAAM,CAAA,GAAI,GAAA,EAAO,QAAA,CAAS,EAAE,CAAA,CAAE,KAAA,CAAM,CAAC,CAAC,CAAA;AAGxE,SAAS,KAAA,GAAgB;AACvB,EAAA,MAAM,IAAK,UAAA,CAAmC,MAAA;AAC9C,EAAA,IAAI,CAAC,CAAA,IAAK,OAAO,CAAA,CAAE,oBAAoB,UAAA,EAAY;AACjD,IAAA,MAAM,IAAI,MAAM,gEAAgE,CAAA;AAAA,EAClF;AACA,EAAA,MAAM,CAAA,GAAI,IAAI,UAAA,CAAW,EAAE,CAAA;AAC3B,EAAA,CAAA,CAAE,gBAAgB,CAAC,CAAA;AACnB,EAAA,IAAI,GAAA,GAAM,EAAA;AACV,EAAA,KAAA,IAAS,CAAA,GAAI,GAAG,CAAA,GAAI,EAAA,EAAI,KAAK,GAAA,IAAO,GAAA,CAAI,CAAA,CAAE,CAAC,CAAE,CAAA;AAC7C,EAAA,OAAO,GAAA;AACT;AAEA,SAAS,aAAA,CAAc,QAAgB,KAAA,EAAqB;AAC1D,EAAA,IAAI,CAAC,MAAA,CAAO,SAAA,CAAU,MAAM,CAAA,EAAG;AAC7B,IAAA,MAAM,IAAI,SAAA,CAAU,CAAA,EAAG,KAAK,CAAA,+CAAA,EAAkD,MAAM,CAAA,CAAE,CAAA;AAAA,EACxF;AACF;AAGO,SAAS,YAAA,GAAuB;AACrC,EAAA,OAAO,EAAE,OAAA,EAAS,EAAC,EAAE;AACvB;AAGA,SAAS,MAAA,CACP,MAAA,EACA,KAAA,EACA,IAAA,EACQ;AACR,EAAA,MAAM,KAAA,GAAqB;AAAA,IACzB,IAAI,KAAA,EAAM;AAAA,IACV,EAAA,EAAA,iBAAI,IAAI,IAAA,EAAK,EAAE,WAAA,EAAY;AAAA,IAC3B,KAAA;AAAA,IACA,GAAI,MAAM,GAAA,KAAQ,MAAA,GAAY,EAAE,GAAA,EAAK,IAAA,CAAK,GAAA,EAAI,GAAI,EAAC;AAAA,IACnD,GAAI,MAAM,IAAA,KAAS,MAAA,GAAY,EAAE,IAAA,EAAM,IAAA,CAAK,IAAA,EAAK,GAAI;AAAC,GACxD;AACA,EAAA,OAAO,EAAE,OAAA,EAAS,CAAC,GAAG,MAAA,CAAO,OAAA,EAAS,KAAK,CAAA,EAAE;AAC/C;AAOO,SAAS,IAAA,CACd,QACA,EAAA,EACQ;AACR,EAAA,aAAA,CAAc,EAAA,CAAG,QAAQ,QAAQ,CAAA;AACjC,EAAA,IAAI,EAAA,CAAG,UAAU,CAAA,EAAG;AAClB,IAAA,MAAM,IAAI,UAAA,CAAW,CAAA,oCAAA,EAAuC,EAAA,CAAG,MAAM,CAAA,CAAE,CAAA;AAAA,EACzE;AACA,EAAA,MAAM,KAAA,GAAsB;AAAA,IAC1B,EAAE,OAAA,EAAS,EAAA,CAAG,KAAA,EAAO,MAAA,EAAQ,GAAG,MAAA,EAAO;AAAA,IACvC,EAAE,OAAA,EAAS,EAAA,CAAG,QAAQ,MAAA,EAAQ,CAAC,GAAG,MAAA;AAAO,GAC3C;AACA,EAAA,OAAO,MAAA,CAAO,MAAA,EAAQ,KAAA,EAAO,EAAE,GAAA,EAAK,GAAG,GAAA,EAAK,IAAA,EAAM,EAAA,CAAG,IAAA,EAAM,CAAA;AAC7D;AAMO,SAAS,QAAA,CACd,MAAA,EACA,KAAA,EACA,IAAA,EACQ;AACR,EAAA,IAAI,CAAC,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,IAAK,KAAA,CAAM,WAAW,CAAA,EAAG;AAC/C,IAAA,MAAM,IAAI,WAAW,uCAAuC,CAAA;AAAA,EAC9D;AACA,EAAA,IAAI,GAAA,GAAM,CAAA;AACV,EAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,IAAA,aAAA,CAAc,IAAA,CAAK,QAAQ,aAAa,CAAA;AACxC,IAAA,GAAA,IAAO,IAAA,CAAK,MAAA;AAAA,EACd;AACA,EAAA,IAAI,QAAQ,CAAA,EAAG;AACb,IAAA,MAAM,IAAI,UAAA,CAAW,CAAA,qCAAA,EAAwC,GAAG,CAAA,YAAA,CAAc,CAAA;AAAA,EAChF;AACA,EAAA,OAAO,MAAA,CAAO,MAAA,EAAQ,KAAA,CAAM,GAAA,CAAI,CAAC,CAAA,MAAO,EAAE,OAAA,EAAS,CAAA,CAAE,SAAS,MAAA,EAAQ,CAAA,CAAE,MAAA,EAAO,CAAE,GAAG,IAAI,CAAA;AAC1F;AAGO,SAAS,OAAA,CAAQ,QAAgB,OAAA,EAAyB;AAC/D,EAAA,IAAI,KAAA,GAAQ,CAAA;AACZ,EAAA,KAAA,MAAW,KAAA,IAAS,OAAO,OAAA,EAAS;AAClC,IAAA,KAAA,MAAW,IAAA,IAAQ,MAAM,KAAA,EAAO;AAC9B,MAAA,IAAI,IAAA,CAAK,OAAA,KAAY,OAAA,EAAS,KAAA,IAAS,IAAA,CAAK,MAAA;AAAA,IAC9C;AAAA,EACF;AACA,EAAA,OAAO,KAAA;AACT;AAMO,SAAS,SAAA,CAAU,QAAgB,OAAA,EAAiC;AACzE,EAAA,MAAM,OAAuB,EAAC;AAC9B,EAAA,KAAA,MAAW,KAAA,IAAS,OAAO,OAAA,EAAS;AAClC,IAAA,IAAI,GAAA,GAAM,CAAA;AACV,IAAA,IAAI,OAAA,GAAU,KAAA;AACd,IAAA,KAAA,MAAW,IAAA,IAAQ,MAAM,KAAA,EAAO;AAC9B,MAAA,IAAI,IAAA,CAAK,YAAY,OAAA,EAAS;AAC5B,QAAA,GAAA,IAAO,IAAA,CAAK,MAAA;AACZ,QAAA,OAAA,GAAU,IAAA;AAAA,MACZ;AAAA,IACF;AACA,IAAA,IAAI,OAAA,EAAS;AACX,MAAA,IAAA,CAAK,IAAA,CAAK;AAAA,QACR,IAAI,KAAA,CAAM,EAAA;AAAA,QACV,MAAA,EAAQ,GAAA;AAAA,QACR,GAAI,MAAM,GAAA,KAAQ,MAAA,GAAY,EAAE,GAAA,EAAK,KAAA,CAAM,GAAA,EAAI,GAAI,EAAC;AAAA,QACpD,GAAI,MAAM,IAAA,KAAS,MAAA,GAAY,EAAE,IAAA,EAAM,KAAA,CAAM,IAAA,EAAK,GAAI;AAAC,OACxD,CAAA;AAAA,IACH;AAAA,EACF;AACA,EAAA,OAAO,IAAA;AACT;AAMO,SAAS,aAAa,MAAA,EAAmC;AAC9D,EAAA,MAAM,MAAA,uBAAa,GAAA,EAAoB;AACvC,EAAA,KAAA,MAAW,KAAA,IAAS,OAAO,OAAA,EAAS;AAClC,IAAA,KAAA,MAAW,IAAA,IAAQ,MAAM,KAAA,EAAO;AAC9B,MAAA,MAAA,CAAO,GAAA,CAAI,IAAA,CAAK,OAAA,EAAA,CAAU,MAAA,CAAO,GAAA,CAAI,KAAK,OAAO,CAAA,IAAK,CAAA,IAAK,IAAA,CAAK,MAAM,CAAA;AAAA,IACxE;AAAA,EACF;AACA,EAAA,OAAO,CAAC,GAAG,MAAA,CAAO,OAAA,EAAS,CAAA,CACxB,GAAA,CAAI,CAAC,CAAC,OAAA,EAAS,GAAG,CAAA,MAAO,EAAE,SAAS,OAAA,EAAS,GAAA,EAAI,CAAE,CAAA,CACnD,IAAA,CAAK,CAAC,CAAA,EAAG,CAAA,KAAO,EAAE,OAAA,GAAU,CAAA,CAAE,OAAA,GAAU,EAAA,GAAK,CAAA,CAAE,OAAA,GAAU,CAAA,CAAE,OAAA,GAAU,IAAI,CAAE,CAAA;AAChF","file":"index.js","sourcesContent":["/**\n * @lacspace/ledger\n *\n * A tiny **double-entry** ledger / wallet. Every transaction is a set of lines\n * whose signed amounts sum to **zero**, so the books can never drift: balances\n * are just sums, and the trial balance always totals zero.\n *\n * Sign convention: an account's balance is the sum of its signed line amounts.\n * A debit is a **positive** amount, a credit is a **negative** one — so\n * `post({ debit, credit, amount })` books `+amount` to `debit` and `-amount` to\n * `credit`. Under this convention an asset/expense account rises when debited\n * and a liability/income/equity account rises when credited (its balance goes\n * more negative). Amounts are **integer minor units** (cents, paisa, …).\n *\n * ```ts\n * import { createLedger, post, balance, trialBalance } from \"@lacspace/ledger\";\n *\n * let book = createLedger();\n * book = post(book, { debit: \"cash\", credit: \"sales\", amount: 10000 });\n * balance(book, \"cash\"); // 10000\n * balance(book, \"sales\"); // -10000\n * trialBalance(book); // sums to 0\n * ```\n *\n * Immutable — every operation returns a new {@link Ledger}. Zero dependencies,\n * isomorphic (ids use Web Crypto `getRandomValues`).\n */\n\n/** A single posting line: a signed amount against an account (minor units). */\nexport interface LedgerLine {\n account: string;\n /** Signed amount in minor units. A transaction's lines MUST sum to 0. */\n amount: number;\n}\n\n/** One posted transaction. Its `lines` always sum to zero. */\nexport interface LedgerEntry {\n /** Unique id (crypto-random). */\n id: string;\n /** ISO-8601 timestamp of when it was posted. */\n at: string;\n lines: LedgerLine[];\n /** Optional external reference (invoice #, txn id, …). */\n ref?: string;\n /** Optional human-readable note. */\n memo?: string;\n}\n\n/** An immutable ledger — an ordered list of balanced entries. */\nexport interface Ledger {\n entries: LedgerEntry[];\n}\n\n/** A single row of a {@link statement}. */\nexport interface StatementRow {\n at: string;\n /** Net signed amount this entry moved on the account (minor units). */\n amount: number;\n ref?: string;\n memo?: string;\n}\n\n/** A single row of a {@link trialBalance}. */\nexport interface TrialBalanceRow {\n account: string;\n balance: number;\n}\n\nconst HEX: string[] = [];\nfor (let i = 0; i < 256; i++) HEX.push((i + 0x100).toString(16).slice(1));\n\n/** Crypto-random 16-byte hex id. Isomorphic via Web Crypto. */\nfunction genId(): string {\n const c = (globalThis as { crypto?: Crypto }).crypto;\n if (!c || typeof c.getRandomValues !== \"function\") {\n throw new Error(\"Web Crypto getRandomValues is unavailable in this environment.\");\n }\n const b = new Uint8Array(16);\n c.getRandomValues(b);\n let out = \"\";\n for (let i = 0; i < 16; i++) out += HEX[b[i]!]!;\n return out;\n}\n\nfunction assertInteger(amount: number, label: string): void {\n if (!Number.isInteger(amount)) {\n throw new TypeError(`${label} must be an integer number of minor units, got ${amount}`);\n }\n}\n\n/** Create a new, empty ledger. */\nexport function createLedger(): Ledger {\n return { entries: [] };\n}\n\n/** Append a pre-built, already-balanced entry (immutable). */\nfunction append(\n ledger: Ledger,\n lines: LedgerLine[],\n meta?: { ref?: string; memo?: string }\n): Ledger {\n const entry: LedgerEntry = {\n id: genId(),\n at: new Date().toISOString(),\n lines,\n ...(meta?.ref !== undefined ? { ref: meta.ref } : {}),\n ...(meta?.memo !== undefined ? { memo: meta.memo } : {}),\n };\n return { entries: [...ledger.entries, entry] };\n}\n\n/**\n * Post a simple two-line transaction: `+amount` to `debit`, `-amount` to\n * `credit`. `amount` must be a positive integer (minor units). Returns a new\n * ledger — the input is never mutated.\n */\nexport function post(\n ledger: Ledger,\n tx: { debit: string; credit: string; amount: number; ref?: string; memo?: string }\n): Ledger {\n assertInteger(tx.amount, \"amount\");\n if (tx.amount <= 0) {\n throw new RangeError(`post() amount must be positive, got ${tx.amount}`);\n }\n const lines: LedgerLine[] = [\n { account: tx.debit, amount: tx.amount },\n { account: tx.credit, amount: -tx.amount },\n ];\n return append(ledger, lines, { ref: tx.ref, memo: tx.memo });\n}\n\n/**\n * Post an arbitrary multi-line transaction. **Throws** if the signed line\n * amounts don't sum to exactly zero (or any amount isn't an integer).\n */\nexport function postMany(\n ledger: Ledger,\n lines: LedgerLine[],\n meta?: { ref?: string; memo?: string }\n): Ledger {\n if (!Array.isArray(lines) || lines.length === 0) {\n throw new RangeError(\"postMany() requires at least one line\");\n }\n let sum = 0;\n for (const line of lines) {\n assertInteger(line.amount, \"line amount\");\n sum += line.amount;\n }\n if (sum !== 0) {\n throw new RangeError(`unbalanced transaction: lines sum to ${sum}, expected 0`);\n }\n return append(ledger, lines.map((l) => ({ account: l.account, amount: l.amount })), meta);\n}\n\n/** Balance of an account: the sum of all its signed line amounts. */\nexport function balance(ledger: Ledger, account: string): number {\n let total = 0;\n for (const entry of ledger.entries) {\n for (const line of entry.lines) {\n if (line.account === account) total += line.amount;\n }\n }\n return total;\n}\n\n/**\n * Statement for one account: one row per entry that touches it, in order, with\n * the net signed amount that entry moved on the account.\n */\nexport function statement(ledger: Ledger, account: string): StatementRow[] {\n const rows: StatementRow[] = [];\n for (const entry of ledger.entries) {\n let net = 0;\n let touched = false;\n for (const line of entry.lines) {\n if (line.account === account) {\n net += line.amount;\n touched = true;\n }\n }\n if (touched) {\n rows.push({\n at: entry.at,\n amount: net,\n ...(entry.ref !== undefined ? { ref: entry.ref } : {}),\n ...(entry.memo !== undefined ? { memo: entry.memo } : {}),\n });\n }\n }\n return rows;\n}\n\n/**\n * Trial balance: every account with its balance, sorted by account name. Since\n * every entry is balanced, the balances always sum to exactly zero.\n */\nexport function trialBalance(ledger: Ledger): TrialBalanceRow[] {\n const totals = new Map<string, number>();\n for (const entry of ledger.entries) {\n for (const line of entry.lines) {\n totals.set(line.account, (totals.get(line.account) ?? 0) + line.amount);\n }\n }\n return [...totals.entries()]\n .map(([account, bal]) => ({ account, balance: bal }))\n .sort((a, b) => (a.account < b.account ? -1 : a.account > b.account ? 1 : 0));\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@lacspace/ledger",
3
+ "version": "1.0.0",
4
+ "description": "A tiny double-entry ledger & wallet — balanced transactions, per-account balances and a trial balance that always sums to zero. Integer minor units, immutable ops, crypto-random ids. Isomorphic.",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "sideEffects": false,
25
+ "scripts": {
26
+ "build": "tsup",
27
+ "prepublishOnly": "npm run build"
28
+ },
29
+ "keywords": [
30
+ "ledger",
31
+ "double-entry",
32
+ "accounting",
33
+ "wallet",
34
+ "bookkeeping",
35
+ "trial-balance",
36
+ "transactions",
37
+ "balances",
38
+ "minor-units",
39
+ "immutable",
40
+ "isomorphic",
41
+ "typescript"
42
+ ],
43
+ "author": "Lacspace <contact@lacspace.com>",
44
+ "license": "SEE LICENSE IN LICENSE",
45
+ "homepage": "https://developer.lacspace.com/packages/ledger",
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/lacspace/npm-packages.git",
49
+ "directory": "ledger"
50
+ },
51
+ "bugs": {
52
+ "url": "https://github.com/lacspace/npm-packages/issues"
53
+ },
54
+ "engines": {
55
+ "node": ">=18"
56
+ },
57
+ "dependencies": {},
58
+ "publishConfig": {
59
+ "access": "public"
60
+ }
61
+ }