@whocomply/ledger 0.1.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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 WhoComply
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # @whocomply/ledger
2
+
3
+ TypeScript SDK for the WhoComply Ledger API.
4
+
5
+ ```bash
6
+ npm install @whocomply/ledger
7
+ ```
8
+
9
+ Zero runtime dependencies, built on global `fetch` (Node 18+ and browsers),
10
+ dual ESM/CJS output with full type declarations. MIT licensed.
11
+
12
+ ```ts
13
+ import { LedgerClient, LedgerApiError } from '@whocomply/ledger';
14
+
15
+ const ledger = new LedgerClient({
16
+ baseUrl: 'https://ledger.internal.example.com',
17
+ tenant: 'demo-fintech',
18
+ apiKey: process.env.LEDGER_API_KEY,
19
+ });
20
+
21
+ // Post a balanced double-entry transaction
22
+ const tx = await ledger.transactions.post({
23
+ description: 'Customer deposit via bank transfer',
24
+ reference: 'DEP-2026-001',
25
+ idempotencyKey: 'deposit-evt-8842', // optional; generated when omitted
26
+ entries: [
27
+ { account_code: '1000', amount: '250000.00', side: 'debit', currency: 'NGN' },
28
+ { account_code: '2100', amount: '250000.00', side: 'credit', currency: 'NGN' },
29
+ ],
30
+ });
31
+
32
+ // Balances, current and historical
33
+ const now = await ledger.accounts.balance(accountId);
34
+ const mayClose = await ledger.accounts.balanceAsOf(accountId, '2026-05-31');
35
+
36
+ // Reverse (at most once; second attempt raises a 409)
37
+ await ledger.transactions.reverse(tx.id);
38
+
39
+ // Treasury surface
40
+ await ledger.currencies.register({ code: 'BTC', exponent: 8, name: 'Bitcoin' });
41
+ await ledger.periods.create({ name: 'June 2026', starts_on: '2026-06-01', ends_on: '2026-06-30' });
42
+ const tb = await ledger.reports.trialBalance({ asOf: '2026-06-30' });
43
+ const csv = await ledger.reports.xeroJournalsCsv({ startDate: '2026-06-01', endDate: '2026-06-30' });
44
+ ```
45
+
46
+ ## Rules the types enforce
47
+
48
+ - **Amounts are strings, always.** The ledger stores NUMERIC(38,18); JavaScript
49
+ numbers cannot represent one wei or one satoshi exactly. Send strings,
50
+ receive strings.
51
+ - **Errors are `LedgerApiError`** with `status`, the server's message, and an
52
+ `isConflict` helper for the 409 family (double reversal, closed period,
53
+ duplicate currency or period).
54
+ - The wire format (snake_case) is preserved verbatim; the SDK adds no mapping
55
+ layer that could drift from the API.
56
+
57
+ ## Development
58
+
59
+ ```bash
60
+ npm install
61
+ npm test # unit tests, mocked fetch
62
+ npm run build # dual ESM/CJS + d.ts via tsup
63
+ npm run smoke # live end-to-end against localhost:8080 (see docs/development.md)
64
+ ```
package/dist/index.cjs ADDED
@@ -0,0 +1,559 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ LedgerApiError: () => LedgerApiError,
24
+ LedgerClient: () => LedgerClient,
25
+ MissingIdempotencyKeyError: () => MissingIdempotencyKeyError
26
+ });
27
+ module.exports = __toCommonJS(index_exports);
28
+
29
+ // src/errors.ts
30
+ var LedgerApiError = class extends Error {
31
+ /** HTTP status code */
32
+ status;
33
+ /** Alias for status, per the Alphaex integration contract. */
34
+ httpStatus;
35
+ /** Stable machine-readable code (absent on transport-level failures). */
36
+ code;
37
+ /** Structured error context (e.g. InsufficientBalanceDetails). */
38
+ details;
39
+ /** Raw response body, when one could be read */
40
+ body;
41
+ constructor(status, message, options = {}) {
42
+ super(message);
43
+ this.name = "LedgerApiError";
44
+ this.status = status;
45
+ this.httpStatus = status;
46
+ this.code = options.code;
47
+ this.details = options.details;
48
+ this.body = options.body;
49
+ }
50
+ /** True for 409s: double reversals, closed periods, existing resources. */
51
+ get isConflict() {
52
+ return this.status === 409;
53
+ }
54
+ /** Typed accessor for INSUFFICIENT_BALANCE detail. */
55
+ get insufficientBalance() {
56
+ if (this.code !== "INSUFFICIENT_BALANCE") {
57
+ return void 0;
58
+ }
59
+ return this.details;
60
+ }
61
+ };
62
+ var MissingIdempotencyKeyError = class extends Error {
63
+ constructor(method) {
64
+ super(`${method} requires an explicit idempotencyKey (client has requireIdempotencyKey: true)`);
65
+ this.name = "MissingIdempotencyKeyError";
66
+ }
67
+ };
68
+
69
+ // src/http.ts
70
+ var HttpClient = class {
71
+ baseUrl;
72
+ headers;
73
+ fetchImpl;
74
+ timeoutMs;
75
+ tenant;
76
+ requireIdempotencyKey;
77
+ constructor(options) {
78
+ if (!options.baseUrl) {
79
+ throw new Error("baseUrl is required");
80
+ }
81
+ if (!options.tenant) {
82
+ throw new Error("tenant is required");
83
+ }
84
+ if (!options.apiKey && !options.token) {
85
+ throw new Error("either apiKey or token is required");
86
+ }
87
+ this.baseUrl = options.baseUrl.replace(/\/+$/, "");
88
+ this.tenant = options.tenant;
89
+ this.fetchImpl = options.fetch ?? globalThis.fetch;
90
+ this.timeoutMs = options.timeoutMs;
91
+ this.requireIdempotencyKey = options.requireIdempotencyKey ?? false;
92
+ this.headers = { "Content-Type": "application/json" };
93
+ if (options.apiKey) {
94
+ this.headers["X-API-Key"] = options.apiKey;
95
+ } else if (options.token) {
96
+ this.headers["Authorization"] = `Bearer ${options.token}`;
97
+ }
98
+ }
99
+ /** Path inside the tenant scope, e.g. tenantPath('/accounts') */
100
+ tenantPath(path) {
101
+ return `/tenants/${encodeURIComponent(this.tenant)}${path}`;
102
+ }
103
+ effectiveSignal(callerSignal) {
104
+ const timeoutSignal = this.timeoutMs ? AbortSignal.timeout(this.timeoutMs) : void 0;
105
+ if (callerSignal && timeoutSignal) {
106
+ return typeof AbortSignal.any === "function" ? AbortSignal.any([callerSignal, timeoutSignal]) : callerSignal;
107
+ }
108
+ return callerSignal ?? timeoutSignal;
109
+ }
110
+ async request(method, path, options = {}) {
111
+ let url = `${this.baseUrl}/api/v1${path}`;
112
+ if (options.query) {
113
+ const params = new URLSearchParams();
114
+ for (const [key, value] of Object.entries(options.query)) {
115
+ if (value !== void 0 && value !== "") {
116
+ params.set(key, String(value));
117
+ }
118
+ }
119
+ const qs = params.toString();
120
+ if (qs) {
121
+ url += `?${qs}`;
122
+ }
123
+ }
124
+ const response = await this.fetchImpl(url, {
125
+ method,
126
+ headers: this.headers,
127
+ body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
128
+ signal: this.effectiveSignal(options.signal)
129
+ });
130
+ if (options.raw) {
131
+ const text = await response.text();
132
+ if (!response.ok) {
133
+ throw new LedgerApiError(response.status, text || response.statusText, { body: text });
134
+ }
135
+ return text;
136
+ }
137
+ let envelope;
138
+ try {
139
+ envelope = await response.json();
140
+ } catch {
141
+ envelope = void 0;
142
+ }
143
+ if (!response.ok || !envelope || envelope.success !== true) {
144
+ const message = envelope?.error ?? `request failed with status ${response.status}`;
145
+ throw new LedgerApiError(response.status, message, {
146
+ code: envelope?.code,
147
+ details: envelope?.details,
148
+ body: envelope
149
+ });
150
+ }
151
+ return envelope.data;
152
+ }
153
+ };
154
+ var generateIdempotencyKey = () => {
155
+ const cryptoApi = globalThis.crypto;
156
+ if (cryptoApi && "randomUUID" in cryptoApi) {
157
+ return cryptoApi.randomUUID();
158
+ }
159
+ return `idem-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 12)}`;
160
+ };
161
+
162
+ // src/resources/accounts.ts
163
+ var AccountsResource = class {
164
+ constructor(http) {
165
+ this.http = http;
166
+ }
167
+ http;
168
+ create(input, options = {}) {
169
+ return this.http.request("POST", this.http.tenantPath("/accounts"), { body: input, signal: options.signal });
170
+ }
171
+ list(params = {}, options = {}) {
172
+ return this.http.request("GET", this.http.tenantPath("/accounts"), {
173
+ signal: options.signal,
174
+ query: {
175
+ account_type: params.accountType,
176
+ parent_code: params.parentCode,
177
+ currency: params.currency,
178
+ search: params.search,
179
+ page: params.page,
180
+ page_size: params.pageSize
181
+ }
182
+ });
183
+ }
184
+ get(accountId, options = {}) {
185
+ return this.http.request("GET", this.http.tenantPath(`/accounts/${accountId}`), { signal: options.signal });
186
+ }
187
+ getByCode(code, options = {}) {
188
+ return this.http.request("GET", this.http.tenantPath(`/accounts/code/${encodeURIComponent(code)}`), { signal: options.signal });
189
+ }
190
+ update(accountId, input, options = {}) {
191
+ return this.http.request("PUT", this.http.tenantPath(`/accounts/${accountId}`), { body: input, signal: options.signal });
192
+ }
193
+ deactivate(accountId, options = {}) {
194
+ return this.http.request("DELETE", this.http.tenantPath(`/accounts/${accountId}`), { signal: options.signal });
195
+ }
196
+ /** Current materialized balance. Currency defaults to the tenant base currency. */
197
+ balance(accountId, options = {}) {
198
+ return this.http.request("GET", this.http.tenantPath(`/accounts/${accountId}/balance`), {
199
+ signal: options.signal,
200
+ query: { currency: options.currency }
201
+ });
202
+ }
203
+ /** Journal-derived balance through the end of a past day (YYYY-MM-DD, UTC). */
204
+ balanceAsOf(accountId, asOf, options = {}) {
205
+ return this.http.request("GET", this.http.tenantPath(`/accounts/${accountId}/balance`), {
206
+ signal: options.signal,
207
+ query: { as_of: asOf, currency: options.currency }
208
+ });
209
+ }
210
+ hierarchy(options = {}) {
211
+ return this.http.request("GET", this.http.tenantPath("/accounts/hierarchy"), { signal: options.signal });
212
+ }
213
+ stats(options = {}) {
214
+ return this.http.request("GET", this.http.tenantPath("/accounts/stats"), { signal: options.signal });
215
+ }
216
+ };
217
+
218
+ // src/resources/balances.ts
219
+ var BalancesResource = class {
220
+ constructor(http) {
221
+ this.http = http;
222
+ }
223
+ http;
224
+ /**
225
+ * Bulk balances: every (account, currency) row with its monotonic
226
+ * version. Filter by codePrefix/codeSuffix, currency, or account
227
+ * metadata containment (e.g. { org_id: '...' }).
228
+ */
229
+ list(params = {}, options = {}) {
230
+ const metadata = params.metadata && Object.keys(params.metadata).length > 0 ? Object.entries(params.metadata).map(([key, value]) => `${key}:${value}`).join(",") : void 0;
231
+ return this.http.request("GET", this.http.tenantPath("/balances"), {
232
+ query: {
233
+ limit: params.limit,
234
+ offset: params.offset,
235
+ code_prefix: params.codePrefix,
236
+ code_suffix: params.codeSuffix,
237
+ currency: params.currency,
238
+ metadata
239
+ },
240
+ signal: options.signal
241
+ });
242
+ }
243
+ };
244
+
245
+ // src/resources/events.ts
246
+ var EventsResource = class {
247
+ constructor(http) {
248
+ this.http = http;
249
+ }
250
+ http;
251
+ /**
252
+ * Durable event feed, strictly ordered by sequence. Poll with
253
+ * afterId = the last sequence you processed; store next_cursor.
254
+ */
255
+ list(params = {}, options = {}) {
256
+ return this.http.request("GET", this.http.tenantPath("/events"), {
257
+ query: {
258
+ after_id: params.afterId,
259
+ limit: params.limit
260
+ },
261
+ signal: options.signal
262
+ });
263
+ }
264
+ };
265
+
266
+ // src/resources/currencies.ts
267
+ var CurrenciesResource = class {
268
+ constructor(http) {
269
+ this.http = http;
270
+ }
271
+ http;
272
+ /**
273
+ * Register a currency with its precision policy. Postings beyond the
274
+ * exponent's decimal places are rejected. Unregistered currencies fall
275
+ * back to the storage maximum of 18 decimal places.
276
+ */
277
+ register(input, options = {}) {
278
+ return this.http.request("POST", this.http.tenantPath("/currencies"), { body: input, signal: options.signal });
279
+ }
280
+ list(options = {}) {
281
+ return this.http.request("GET", this.http.tenantPath("/currencies"), { signal: options.signal });
282
+ }
283
+ update(code, input, options = {}) {
284
+ return this.http.request("PUT", this.http.tenantPath(`/currencies/${encodeURIComponent(code)}`), {
285
+ signal: options.signal,
286
+ body: input
287
+ });
288
+ }
289
+ };
290
+
291
+ // src/resources/periods.ts
292
+ var PeriodsResource = class {
293
+ constructor(http) {
294
+ this.http = http;
295
+ }
296
+ http;
297
+ /** Periods of one tenant may not overlap; violations raise a 409. */
298
+ create(input, options = {}) {
299
+ return this.http.request("POST", this.http.tenantPath("/periods"), { body: input, signal: options.signal });
300
+ }
301
+ list(options = {}) {
302
+ return this.http.request("GET", this.http.tenantPath("/periods"), { signal: options.signal });
303
+ }
304
+ /**
305
+ * Close a period. Enforced by the database: nothing can post into a
306
+ * closed period, and posting attempts raise a 409.
307
+ */
308
+ close(periodId, options = {}) {
309
+ return this.http.request("POST", this.http.tenantPath(`/periods/${periodId}/close`), { signal: options.signal });
310
+ }
311
+ reopen(periodId, options = {}) {
312
+ return this.http.request("POST", this.http.tenantPath(`/periods/${periodId}/reopen`), { signal: options.signal });
313
+ }
314
+ };
315
+
316
+ // src/resources/recurring.ts
317
+ var RecurringJournalsResource = class {
318
+ constructor(http) {
319
+ this.http = http;
320
+ }
321
+ http;
322
+ /**
323
+ * Register a repeating journal. Each occurrence posts exactly once: the
324
+ * worker derives the idempotency key from (schedule id, run date).
325
+ */
326
+ create(input, options = {}) {
327
+ return this.http.request("POST", this.http.tenantPath("/recurring-journals"), { body: input, signal: options.signal });
328
+ }
329
+ list(options = {}) {
330
+ return this.http.request("GET", this.http.tenantPath("/recurring-journals"), { signal: options.signal });
331
+ }
332
+ pause(scheduleId, options = {}) {
333
+ return this.http.request("POST", this.http.tenantPath(`/recurring-journals/${scheduleId}/pause`), { signal: options.signal });
334
+ }
335
+ resume(scheduleId, options = {}) {
336
+ return this.http.request("POST", this.http.tenantPath(`/recurring-journals/${scheduleId}/resume`), { signal: options.signal });
337
+ }
338
+ };
339
+
340
+ // src/resources/reports.ts
341
+ var dimensionsParam = (dimensions) => {
342
+ if (!dimensions || Object.keys(dimensions).length === 0) {
343
+ return void 0;
344
+ }
345
+ return Object.entries(dimensions).map(([key, value]) => `${key}:${value}`).join(",");
346
+ };
347
+ var ReportsResource = class {
348
+ constructor(http) {
349
+ this.http = http;
350
+ }
351
+ http;
352
+ /**
353
+ * Trial balance derived from posted journal lines, grouped by currency.
354
+ * asOf (YYYY-MM-DD) means "through the end of that day, UTC"; defaults
355
+ * to now.
356
+ */
357
+ trialBalance(options = {}) {
358
+ return this.http.request("GET", this.http.tenantPath("/reports/trial-balance"), {
359
+ signal: options.signal,
360
+ query: { as_of: options.asOf }
361
+ });
362
+ }
363
+ /** Revenue and expenses over an inclusive date range, optionally filtered by dimensions. */
364
+ profitAndLoss(options) {
365
+ return this.http.request("GET", this.http.tenantPath("/reports/profit-and-loss"), {
366
+ signal: options.signal,
367
+ query: {
368
+ start_date: options.startDate,
369
+ end_date: options.endDate,
370
+ dimensions: dimensionsParam(options.dimensions)
371
+ }
372
+ });
373
+ }
374
+ /** Assets, liabilities, and equity (with computed retained earnings) as of a date. */
375
+ balanceSheet(options = {}) {
376
+ return this.http.request("GET", this.http.tenantPath("/reports/balance-sheet"), {
377
+ signal: options.signal,
378
+ query: { as_of: options.asOf }
379
+ });
380
+ }
381
+ /** One account's activity with opening, running, and closing balances. */
382
+ generalLedger(options) {
383
+ return this.http.request("GET", this.http.tenantPath("/reports/general-ledger"), {
384
+ signal: options.signal,
385
+ query: {
386
+ account_code: options.accountCode,
387
+ start_date: options.startDate,
388
+ end_date: options.endDate,
389
+ currency: options.currency
390
+ }
391
+ });
392
+ }
393
+ /** Per-currency net positions with base equivalents at the provided rates. */
394
+ fxExposure(options = {}) {
395
+ const rates = options.rates ? Object.entries(options.rates).map(([code, rate]) => `${code}:${rate}`).join(",") : void 0;
396
+ return this.http.request("GET", this.http.tenantPath("/reports/fx-exposure"), {
397
+ signal: options.signal,
398
+ query: { rates, as_of: options.asOf }
399
+ });
400
+ }
401
+ /**
402
+ * Posted journals as a Xero manual-journal import CSV (raw string).
403
+ * Dates are inclusive. Currency defaults to the tenant base currency.
404
+ */
405
+ xeroJournalsCsv(options) {
406
+ return this.http.request("GET", this.http.tenantPath("/reports/xero-journals"), {
407
+ signal: options.signal,
408
+ query: {
409
+ start_date: options.startDate,
410
+ end_date: options.endDate,
411
+ currency: options.currency
412
+ },
413
+ raw: true
414
+ });
415
+ }
416
+ };
417
+
418
+ // src/resources/transactions.ts
419
+ var TransactionsResource = class {
420
+ constructor(http) {
421
+ this.http = http;
422
+ }
423
+ http;
424
+ idempotencyKey(method, provided) {
425
+ if (provided) {
426
+ return provided;
427
+ }
428
+ if (this.http.requireIdempotencyKey) {
429
+ throw new MissingIdempotencyKeyError(method);
430
+ }
431
+ return generateIdempotencyKey();
432
+ }
433
+ /**
434
+ * Post a balanced double-entry transaction. Debits must equal credits per
435
+ * currency or the API rejects with UNBALANCED_TRANSACTION. posted_at
436
+ * backdates the posting and requires the transactions:backdate scope.
437
+ */
438
+ post(input, options = {}) {
439
+ const { idempotencyKey, postedAt, ...rest } = input;
440
+ return this.http.request("POST", this.http.tenantPath("/transactions/double-entry"), {
441
+ body: {
442
+ ...rest,
443
+ idempotency_key: this.idempotencyKey("transactions.post", idempotencyKey),
444
+ ...postedAt ? { posted_at: postedAt } : {}
445
+ },
446
+ signal: options.signal
447
+ });
448
+ }
449
+ get(transactionId, options = {}) {
450
+ return this.http.request("GET", this.http.tenantPath(`/transactions/${transactionId}`), {
451
+ query: { include: options.includeLines ? "lines" : void 0 },
452
+ signal: options.signal
453
+ });
454
+ }
455
+ lines(transactionId, options = {}) {
456
+ return this.http.request("GET", this.http.tenantPath(`/transactions/${transactionId}/lines`), {
457
+ signal: options.signal
458
+ });
459
+ }
460
+ list(params = {}, options = {}) {
461
+ return this.http.request("GET", this.http.tenantPath("/transactions"), {
462
+ query: {
463
+ limit: params.limit,
464
+ offset: params.offset,
465
+ account_code: params.accountCode,
466
+ reference: params.reference,
467
+ status: params.status,
468
+ start_date: params.startDate,
469
+ end_date: params.endDate,
470
+ include: params.includeLines ? "lines" : void 0
471
+ },
472
+ signal: options.signal
473
+ });
474
+ }
475
+ /**
476
+ * Maker-checker: store a fully validated entry with zero balance impact.
477
+ * Balances apply only when a DIFFERENT actor approves.
478
+ */
479
+ draft(input, options = {}) {
480
+ const { idempotencyKey, postedAt, ...rest } = input;
481
+ void postedAt;
482
+ return this.http.request("POST", this.http.tenantPath("/transactions/draft"), {
483
+ body: {
484
+ ...rest,
485
+ idempotency_key: this.idempotencyKey("transactions.draft", idempotencyKey)
486
+ },
487
+ signal: options.signal
488
+ });
489
+ }
490
+ /** Approve a draft (requires the transactions:approve scope and a different actor). */
491
+ approve(transactionId, options = {}) {
492
+ return this.http.request("POST", this.http.tenantPath(`/transactions/${transactionId}/approve`), {
493
+ signal: options.signal
494
+ });
495
+ }
496
+ /** Reject a draft; it never had balance impact. */
497
+ reject(transactionId, options = {}) {
498
+ return this.http.request("POST", this.http.tenantPath(`/transactions/${transactionId}/reject`), {
499
+ signal: options.signal
500
+ });
501
+ }
502
+ /** Append an immutable note (audit support). */
503
+ addNote(transactionId, note, options = {}) {
504
+ return this.http.request("POST", this.http.tenantPath(`/transactions/${transactionId}/notes`), {
505
+ body: { note },
506
+ signal: options.signal
507
+ });
508
+ }
509
+ listNotes(transactionId, options = {}) {
510
+ return this.http.request("GET", this.http.tenantPath(`/transactions/${transactionId}/notes`), {
511
+ signal: options.signal
512
+ });
513
+ }
514
+ /**
515
+ * Post a reversal: a new transaction mirroring the original with debits
516
+ * and credits swapped, linked via reverses_transaction_id. A transaction
517
+ * can be reversed at most once; a second attempt raises ALREADY_REVERSED.
518
+ */
519
+ reverse(transactionId, reverseOptions = {}, options = {}) {
520
+ return this.http.request("POST", this.http.tenantPath(`/transactions/${transactionId}/reverse`), {
521
+ body: {
522
+ idempotency_key: this.idempotencyKey("transactions.reverse", reverseOptions.idempotencyKey),
523
+ description: reverseOptions.description,
524
+ metadata: reverseOptions.metadata
525
+ },
526
+ signal: options.signal
527
+ });
528
+ }
529
+ };
530
+
531
+ // src/index.ts
532
+ var LedgerClient = class {
533
+ accounts;
534
+ balances;
535
+ events;
536
+ transactions;
537
+ currencies;
538
+ periods;
539
+ recurringJournals;
540
+ reports;
541
+ constructor(options) {
542
+ const http = new HttpClient(options);
543
+ this.accounts = new AccountsResource(http);
544
+ this.balances = new BalancesResource(http);
545
+ this.events = new EventsResource(http);
546
+ this.transactions = new TransactionsResource(http);
547
+ this.currencies = new CurrenciesResource(http);
548
+ this.periods = new PeriodsResource(http);
549
+ this.recurringJournals = new RecurringJournalsResource(http);
550
+ this.reports = new ReportsResource(http);
551
+ }
552
+ };
553
+ // Annotate the CommonJS export names for ESM import in node:
554
+ 0 && (module.exports = {
555
+ LedgerApiError,
556
+ LedgerClient,
557
+ MissingIdempotencyKeyError
558
+ });
559
+ //# sourceMappingURL=index.cjs.map