@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 +21 -0
- package/README.md +64 -0
- package/dist/index.cjs +559 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +590 -0
- package/dist/index.d.ts +590 -0
- package/dist/index.js +530 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -0
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
|