@robosystems/client 0.2.47 → 0.2.49
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/extensions/AgentClient.test.ts +403 -0
- package/extensions/LedgerClient.d.ts +196 -7
- package/extensions/LedgerClient.js +282 -8
- package/extensions/LedgerClient.test.ts +1655 -0
- package/extensions/LedgerClient.ts +509 -13
- package/extensions/OperationClient.test.ts +199 -1
- package/extensions/QueryClient.test.ts +169 -0
- package/extensions/ReportClient.test.ts +828 -0
- package/extensions/hooks.test.ts +373 -0
- package/extensions/index.test.ts +279 -60
- package/index.ts +2 -2
- package/package.json +1 -1
- package/sdk/index.d.ts +2 -2
- package/sdk/index.js +14 -4
- package/sdk/index.ts +2 -2
- package/sdk/sdk.gen.d.ts +126 -1
- package/sdk/sdk.gen.js +201 -2
- package/sdk/sdk.gen.ts +200 -1
- package/sdk/types.gen.d.ts +976 -13
- package/sdk/types.gen.ts +1045 -11
- package/sdk-extensions/AgentClient.test.ts +403 -0
- package/sdk-extensions/LedgerClient.d.ts +196 -7
- package/sdk-extensions/LedgerClient.js +282 -8
- package/sdk-extensions/LedgerClient.test.ts +1655 -0
- package/sdk-extensions/LedgerClient.ts +509 -13
- package/sdk-extensions/OperationClient.test.ts +199 -1
- package/sdk-extensions/QueryClient.test.ts +169 -0
- package/sdk-extensions/ReportClient.test.ts +828 -0
- package/sdk-extensions/hooks.test.ts +373 -0
- package/sdk-extensions/index.test.ts +279 -60
- package/sdk.gen.d.ts +126 -1
- package/sdk.gen.js +201 -2
- package/sdk.gen.ts +200 -1
- package/types.gen.d.ts +976 -13
- package/types.gen.ts +1045 -11
|
@@ -0,0 +1,1655 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { LedgerClient } from './LedgerClient'
|
|
3
|
+
|
|
4
|
+
// Helper to create proper mock Response objects
|
|
5
|
+
function createMockResponse(data: any, options: { ok?: boolean; status?: number } = {}) {
|
|
6
|
+
return {
|
|
7
|
+
ok: options.ok ?? true,
|
|
8
|
+
status: options.status ?? 200,
|
|
9
|
+
statusText: options.status === 200 ? 'OK' : 'Error',
|
|
10
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
11
|
+
json: async () => data,
|
|
12
|
+
text: async () => JSON.stringify(data),
|
|
13
|
+
blob: async () => new Blob([JSON.stringify(data)]),
|
|
14
|
+
arrayBuffer: async () => new TextEncoder().encode(JSON.stringify(data)).buffer,
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('LedgerClient', () => {
|
|
19
|
+
let client: LedgerClient
|
|
20
|
+
let mockFetch: any
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
client = new LedgerClient({
|
|
24
|
+
baseUrl: 'http://localhost:8000',
|
|
25
|
+
token: 'test-token',
|
|
26
|
+
headers: { 'X-API-Key': 'test-key' },
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
mockFetch = vi.fn()
|
|
30
|
+
global.fetch = mockFetch
|
|
31
|
+
globalThis.fetch = mockFetch
|
|
32
|
+
vi.clearAllMocks()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
vi.restoreAllMocks()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// ── Entity ────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
describe('getEntity', () => {
|
|
42
|
+
it('should return entity data', async () => {
|
|
43
|
+
mockFetch.mockResolvedValueOnce(
|
|
44
|
+
createMockResponse({
|
|
45
|
+
id: 'ent_123',
|
|
46
|
+
name: 'ACME Corp',
|
|
47
|
+
legal_name: 'ACME Corporation Inc.',
|
|
48
|
+
entity_type: 'corporation',
|
|
49
|
+
industry: 'Technology',
|
|
50
|
+
status: 'active',
|
|
51
|
+
})
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
const entity = await client.getEntity('graph_1')
|
|
55
|
+
|
|
56
|
+
expect(entity).not.toBeNull()
|
|
57
|
+
expect(entity!.id).toBe('ent_123')
|
|
58
|
+
expect(entity!.name).toBe('ACME Corp')
|
|
59
|
+
expect(entity!.legalName).toBe('ACME Corporation Inc.')
|
|
60
|
+
expect(entity!.entityType).toBe('corporation')
|
|
61
|
+
expect(entity!.industry).toBe('Technology')
|
|
62
|
+
expect(entity!.status).toBe('active')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('should return null for 404', async () => {
|
|
66
|
+
mockFetch.mockResolvedValueOnce(
|
|
67
|
+
createMockResponse({ detail: 'Not found' }, { ok: false, status: 404 })
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
const entity = await client.getEntity('graph_nonexistent')
|
|
71
|
+
|
|
72
|
+
expect(entity).toBeNull()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('should handle missing optional fields', async () => {
|
|
76
|
+
mockFetch.mockResolvedValueOnce(
|
|
77
|
+
createMockResponse({
|
|
78
|
+
id: 'ent_456',
|
|
79
|
+
name: 'Minimal Corp',
|
|
80
|
+
})
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
const entity = await client.getEntity('graph_1')
|
|
84
|
+
|
|
85
|
+
expect(entity!.legalName).toBeNull()
|
|
86
|
+
expect(entity!.entityType).toBeNull()
|
|
87
|
+
expect(entity!.industry).toBeNull()
|
|
88
|
+
expect(entity!.status).toBeNull()
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('should throw on non-404 errors', async () => {
|
|
92
|
+
mockFetch.mockResolvedValueOnce(
|
|
93
|
+
createMockResponse({ detail: 'Server error' }, { ok: false, status: 500 })
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
await expect(client.getEntity('graph_1')).rejects.toThrow('Get entity failed')
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
// ── Accounts ──────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
describe('listAccounts', () => {
|
|
103
|
+
it('should return account list', async () => {
|
|
104
|
+
const mockAccounts = {
|
|
105
|
+
accounts: [
|
|
106
|
+
{ account_id: 'acc_1', name: 'Cash', account_number: '1000' },
|
|
107
|
+
{ account_id: 'acc_2', name: 'Revenue', account_number: '4000' },
|
|
108
|
+
],
|
|
109
|
+
total_count: 2,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
mockFetch.mockResolvedValueOnce(createMockResponse(mockAccounts))
|
|
113
|
+
|
|
114
|
+
const result = await client.listAccounts('graph_1')
|
|
115
|
+
|
|
116
|
+
expect(result).toEqual(mockAccounts)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('should throw on error', async () => {
|
|
120
|
+
mockFetch.mockResolvedValueOnce(
|
|
121
|
+
createMockResponse({ detail: 'Forbidden' }, { ok: false, status: 403 })
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
await expect(client.listAccounts('graph_1')).rejects.toThrow('List accounts failed')
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
describe('getAccountTree', () => {
|
|
129
|
+
it('should return hierarchical account tree', async () => {
|
|
130
|
+
const mockTree = {
|
|
131
|
+
root: {
|
|
132
|
+
account_id: 'root',
|
|
133
|
+
name: 'All Accounts',
|
|
134
|
+
children: [
|
|
135
|
+
{ account_id: 'acc_1', name: 'Assets', children: [] },
|
|
136
|
+
{ account_id: 'acc_2', name: 'Liabilities', children: [] },
|
|
137
|
+
],
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
mockFetch.mockResolvedValueOnce(createMockResponse(mockTree))
|
|
142
|
+
|
|
143
|
+
const result = await client.getAccountTree('graph_1')
|
|
144
|
+
|
|
145
|
+
expect(result).toEqual(mockTree)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('should throw on error', async () => {
|
|
149
|
+
mockFetch.mockResolvedValueOnce(
|
|
150
|
+
createMockResponse({ detail: 'Error' }, { ok: false, status: 500 })
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
await expect(client.getAccountTree('graph_1')).rejects.toThrow('Get account tree failed')
|
|
154
|
+
})
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
// ── Transactions ──────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
describe('listTransactions', () => {
|
|
160
|
+
it('should return transactions', async () => {
|
|
161
|
+
const mockTxns = {
|
|
162
|
+
transactions: [
|
|
163
|
+
{ transaction_id: 'txn_1', date: '2025-01-15', description: 'Payment received' },
|
|
164
|
+
],
|
|
165
|
+
total_count: 1,
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
mockFetch.mockResolvedValueOnce(createMockResponse(mockTxns))
|
|
169
|
+
|
|
170
|
+
const result = await client.listTransactions('graph_1')
|
|
171
|
+
|
|
172
|
+
expect(result).toEqual(mockTxns)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('should accept date filters and pagination options', async () => {
|
|
176
|
+
mockFetch.mockResolvedValueOnce(createMockResponse({ transactions: [], total_count: 0 }))
|
|
177
|
+
|
|
178
|
+
const result = await client.listTransactions('graph_1', {
|
|
179
|
+
startDate: '2025-01-01',
|
|
180
|
+
endDate: '2025-03-31',
|
|
181
|
+
limit: 50,
|
|
182
|
+
offset: 10,
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
expect(mockFetch).toHaveBeenCalledTimes(1)
|
|
186
|
+
expect(result).toEqual({ transactions: [], total_count: 0 })
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('should throw on error', async () => {
|
|
190
|
+
mockFetch.mockResolvedValueOnce(
|
|
191
|
+
createMockResponse({ detail: 'Error' }, { ok: false, status: 500 })
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
await expect(client.listTransactions('graph_1')).rejects.toThrow('List transactions failed')
|
|
195
|
+
})
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
describe('getTransaction', () => {
|
|
199
|
+
it('should return transaction detail', async () => {
|
|
200
|
+
const mockDetail = {
|
|
201
|
+
transaction_id: 'txn_1',
|
|
202
|
+
date: '2025-01-15',
|
|
203
|
+
entries: [{ debit: 1000, credit: 0, account_id: 'acc_1' }],
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
mockFetch.mockResolvedValueOnce(createMockResponse(mockDetail))
|
|
207
|
+
|
|
208
|
+
const result = await client.getTransaction('graph_1', 'txn_1')
|
|
209
|
+
|
|
210
|
+
expect(result).toEqual(mockDetail)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('should throw on error', async () => {
|
|
214
|
+
mockFetch.mockResolvedValueOnce(
|
|
215
|
+
createMockResponse({ detail: 'Not found' }, { ok: false, status: 404 })
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
await expect(client.getTransaction('graph_1', 'txn_bad')).rejects.toThrow(
|
|
219
|
+
'Get transaction failed'
|
|
220
|
+
)
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
// ── Trial Balance ─────────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
describe('getTrialBalance', () => {
|
|
227
|
+
it('should return trial balance', async () => {
|
|
228
|
+
const mockTB = {
|
|
229
|
+
rows: [
|
|
230
|
+
{ account_id: 'acc_1', debit: 50000, credit: 0 },
|
|
231
|
+
{ account_id: 'acc_2', debit: 0, credit: 50000 },
|
|
232
|
+
],
|
|
233
|
+
total_debit: 50000,
|
|
234
|
+
total_credit: 50000,
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
mockFetch.mockResolvedValueOnce(createMockResponse(mockTB))
|
|
238
|
+
|
|
239
|
+
const result = await client.getTrialBalance('graph_1')
|
|
240
|
+
|
|
241
|
+
expect(result).toEqual(mockTB)
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('should accept date range options', async () => {
|
|
245
|
+
mockFetch.mockResolvedValueOnce(createMockResponse({ rows: [] }))
|
|
246
|
+
|
|
247
|
+
const result = await client.getTrialBalance('graph_1', {
|
|
248
|
+
startDate: '2025-01-01',
|
|
249
|
+
endDate: '2025-03-31',
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
expect(mockFetch).toHaveBeenCalledTimes(1)
|
|
253
|
+
expect(result).toEqual({ rows: [] })
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('should throw on error', async () => {
|
|
257
|
+
mockFetch.mockResolvedValueOnce(
|
|
258
|
+
createMockResponse({ detail: 'Error' }, { ok: false, status: 500 })
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
await expect(client.getTrialBalance('graph_1')).rejects.toThrow('Get trial balance failed')
|
|
262
|
+
})
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
describe('getMappedTrialBalance', () => {
|
|
266
|
+
it('should return mapped trial balance', async () => {
|
|
267
|
+
const mockMapped = {
|
|
268
|
+
rows: [{ concept: 'Revenue', amount: 100000 }],
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
mockFetch.mockResolvedValueOnce(createMockResponse(mockMapped))
|
|
272
|
+
|
|
273
|
+
const result = await client.getMappedTrialBalance('graph_1', {
|
|
274
|
+
mappingId: 'map_1',
|
|
275
|
+
startDate: '2025-01-01',
|
|
276
|
+
endDate: '2025-03-31',
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
expect(result).toEqual(mockMapped)
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it('should throw on error', async () => {
|
|
283
|
+
mockFetch.mockResolvedValueOnce(
|
|
284
|
+
createMockResponse({ detail: 'Error' }, { ok: false, status: 500 })
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
await expect(client.getMappedTrialBalance('graph_1')).rejects.toThrow(
|
|
288
|
+
'Get mapped trial balance failed'
|
|
289
|
+
)
|
|
290
|
+
})
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
// ── Summary ───────────────────────────────────────────────────────────
|
|
294
|
+
|
|
295
|
+
describe('getSummary', () => {
|
|
296
|
+
it('should return ledger summary', async () => {
|
|
297
|
+
const mockSummary = {
|
|
298
|
+
account_count: 150,
|
|
299
|
+
transaction_count: 5000,
|
|
300
|
+
earliest_date: '2024-01-01',
|
|
301
|
+
latest_date: '2025-03-31',
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
mockFetch.mockResolvedValueOnce(createMockResponse(mockSummary))
|
|
305
|
+
|
|
306
|
+
const result = await client.getSummary('graph_1')
|
|
307
|
+
|
|
308
|
+
expect(result).toEqual(mockSummary)
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
it('should throw on error', async () => {
|
|
312
|
+
mockFetch.mockResolvedValueOnce(
|
|
313
|
+
createMockResponse({ detail: 'Error' }, { ok: false, status: 500 })
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
await expect(client.getSummary('graph_1')).rejects.toThrow('Get summary failed')
|
|
317
|
+
})
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
// ── Taxonomy ──────────────────────────────────────────────────────────
|
|
321
|
+
|
|
322
|
+
describe('getReportingTaxonomy', () => {
|
|
323
|
+
it('should return taxonomy data', async () => {
|
|
324
|
+
const mockTax = {
|
|
325
|
+
taxonomy_id: 'tax_usgaap',
|
|
326
|
+
name: 'US GAAP Reporting',
|
|
327
|
+
elements: [{ id: 'elem_1', name: 'Revenue' }],
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
mockFetch.mockResolvedValueOnce(createMockResponse(mockTax))
|
|
331
|
+
|
|
332
|
+
const result = await client.getReportingTaxonomy('graph_1')
|
|
333
|
+
|
|
334
|
+
expect(result).toEqual(mockTax)
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
it('should throw on error', async () => {
|
|
338
|
+
mockFetch.mockResolvedValueOnce(
|
|
339
|
+
createMockResponse({ detail: 'Error' }, { ok: false, status: 500 })
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
await expect(client.getReportingTaxonomy('graph_1')).rejects.toThrow(
|
|
343
|
+
'Get reporting taxonomy failed'
|
|
344
|
+
)
|
|
345
|
+
})
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
describe('listStructures', () => {
|
|
349
|
+
it('should return transformed structures', async () => {
|
|
350
|
+
mockFetch.mockResolvedValueOnce(
|
|
351
|
+
createMockResponse({
|
|
352
|
+
structures: [
|
|
353
|
+
{ id: 'str_1', name: 'Income Statement', structure_type: 'income_statement' },
|
|
354
|
+
{ id: 'str_2', name: 'Balance Sheet', structure_type: 'balance_sheet' },
|
|
355
|
+
],
|
|
356
|
+
})
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
const result = await client.listStructures('graph_1')
|
|
360
|
+
|
|
361
|
+
expect(result).toHaveLength(2)
|
|
362
|
+
expect(result[0]).toEqual({
|
|
363
|
+
id: 'str_1',
|
|
364
|
+
name: 'Income Statement',
|
|
365
|
+
structureType: 'income_statement',
|
|
366
|
+
})
|
|
367
|
+
expect(result[1]).toEqual({
|
|
368
|
+
id: 'str_2',
|
|
369
|
+
name: 'Balance Sheet',
|
|
370
|
+
structureType: 'balance_sheet',
|
|
371
|
+
})
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
it('should return empty array when no structures', async () => {
|
|
375
|
+
mockFetch.mockResolvedValueOnce(createMockResponse({ structures: [] }))
|
|
376
|
+
|
|
377
|
+
const result = await client.listStructures('graph_1')
|
|
378
|
+
|
|
379
|
+
expect(result).toEqual([])
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
it('should handle missing structures key', async () => {
|
|
383
|
+
mockFetch.mockResolvedValueOnce(createMockResponse({}))
|
|
384
|
+
|
|
385
|
+
const result = await client.listStructures('graph_1')
|
|
386
|
+
|
|
387
|
+
expect(result).toEqual([])
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
it('should accept taxonomyId filter', async () => {
|
|
391
|
+
mockFetch.mockResolvedValueOnce(createMockResponse({ structures: [] }))
|
|
392
|
+
|
|
393
|
+
const result = await client.listStructures('graph_1', 'tax_usgaap')
|
|
394
|
+
|
|
395
|
+
expect(mockFetch).toHaveBeenCalledTimes(1)
|
|
396
|
+
expect(result).toEqual([])
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
it('should throw on error', async () => {
|
|
400
|
+
mockFetch.mockResolvedValueOnce(
|
|
401
|
+
createMockResponse({ detail: 'Error' }, { ok: false, status: 500 })
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
await expect(client.listStructures('graph_1')).rejects.toThrow('List structures failed')
|
|
405
|
+
})
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
describe('listElements', () => {
|
|
409
|
+
it('should return elements data', async () => {
|
|
410
|
+
const mockElements = {
|
|
411
|
+
elements: [{ id: 'elem_1', qname: 'us-gaap:Revenue', name: 'Revenue' }],
|
|
412
|
+
total_count: 1,
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
mockFetch.mockResolvedValueOnce(createMockResponse(mockElements))
|
|
416
|
+
|
|
417
|
+
const result = await client.listElements('graph_1', {
|
|
418
|
+
taxonomyId: 'tax_usgaap',
|
|
419
|
+
source: 'gaap',
|
|
420
|
+
classification: 'revenue',
|
|
421
|
+
isAbstract: false,
|
|
422
|
+
limit: 100,
|
|
423
|
+
offset: 0,
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
expect(result).toEqual(mockElements)
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
it('should throw on error', async () => {
|
|
430
|
+
mockFetch.mockResolvedValueOnce(
|
|
431
|
+
createMockResponse({ detail: 'Error' }, { ok: false, status: 500 })
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
await expect(client.listElements('graph_1')).rejects.toThrow('List elements failed')
|
|
435
|
+
})
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
// ── Mappings ──────────────────────────────────────────────────────────
|
|
439
|
+
|
|
440
|
+
describe('createMappingStructure', () => {
|
|
441
|
+
it('should create and return mapping info', async () => {
|
|
442
|
+
mockFetch.mockResolvedValueOnce(
|
|
443
|
+
createMockResponse({
|
|
444
|
+
id: 'map_1',
|
|
445
|
+
name: 'My Mapping',
|
|
446
|
+
description: 'Custom mapping',
|
|
447
|
+
structure_type: 'coa_mapping',
|
|
448
|
+
taxonomy_id: 'tax_usgaap_reporting',
|
|
449
|
+
is_active: true,
|
|
450
|
+
})
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
const result = await client.createMappingStructure('graph_1', {
|
|
454
|
+
name: 'My Mapping',
|
|
455
|
+
description: 'Custom mapping',
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
expect(result).toEqual({
|
|
459
|
+
id: 'map_1',
|
|
460
|
+
name: 'My Mapping',
|
|
461
|
+
description: 'Custom mapping',
|
|
462
|
+
structureType: 'coa_mapping',
|
|
463
|
+
taxonomyId: 'tax_usgaap_reporting',
|
|
464
|
+
isActive: true,
|
|
465
|
+
})
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
it('should use defaults when no options provided', async () => {
|
|
469
|
+
mockFetch.mockResolvedValueOnce(
|
|
470
|
+
createMockResponse({
|
|
471
|
+
id: 'map_2',
|
|
472
|
+
name: 'CoA to Reporting',
|
|
473
|
+
description: 'Map Chart of Accounts to US GAAP reporting concepts',
|
|
474
|
+
structure_type: 'coa_mapping',
|
|
475
|
+
taxonomy_id: 'tax_usgaap_reporting',
|
|
476
|
+
is_active: true,
|
|
477
|
+
})
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
const result = await client.createMappingStructure('graph_1')
|
|
481
|
+
|
|
482
|
+
expect(result.name).toBe('CoA to Reporting')
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
it('should handle null description', async () => {
|
|
486
|
+
mockFetch.mockResolvedValueOnce(
|
|
487
|
+
createMockResponse({
|
|
488
|
+
id: 'map_3',
|
|
489
|
+
name: 'Mapping',
|
|
490
|
+
structure_type: 'coa_mapping',
|
|
491
|
+
taxonomy_id: 'tax_usgaap_reporting',
|
|
492
|
+
})
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
const result = await client.createMappingStructure('graph_1')
|
|
496
|
+
|
|
497
|
+
expect(result.description).toBeNull()
|
|
498
|
+
expect(result.isActive).toBe(true)
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
it('should throw on error', async () => {
|
|
502
|
+
mockFetch.mockResolvedValueOnce(
|
|
503
|
+
createMockResponse({ detail: 'Error' }, { ok: false, status: 500 })
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
await expect(client.createMappingStructure('graph_1')).rejects.toThrow(
|
|
507
|
+
'Create mapping structure failed'
|
|
508
|
+
)
|
|
509
|
+
})
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
describe('listMappings', () => {
|
|
513
|
+
it('should return transformed mapping list', async () => {
|
|
514
|
+
mockFetch.mockResolvedValueOnce(
|
|
515
|
+
createMockResponse({
|
|
516
|
+
structures: [
|
|
517
|
+
{
|
|
518
|
+
id: 'map_1',
|
|
519
|
+
name: 'Primary Mapping',
|
|
520
|
+
description: 'Main mapping',
|
|
521
|
+
structure_type: 'coa_mapping',
|
|
522
|
+
taxonomy_id: 'tax_usgaap',
|
|
523
|
+
is_active: true,
|
|
524
|
+
},
|
|
525
|
+
],
|
|
526
|
+
})
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
const result = await client.listMappings('graph_1')
|
|
530
|
+
|
|
531
|
+
expect(result).toHaveLength(1)
|
|
532
|
+
expect(result[0]).toEqual({
|
|
533
|
+
id: 'map_1',
|
|
534
|
+
name: 'Primary Mapping',
|
|
535
|
+
description: 'Main mapping',
|
|
536
|
+
structureType: 'coa_mapping',
|
|
537
|
+
taxonomyId: 'tax_usgaap',
|
|
538
|
+
isActive: true,
|
|
539
|
+
})
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
it('should return empty array when no mappings', async () => {
|
|
543
|
+
mockFetch.mockResolvedValueOnce(createMockResponse({ structures: [] }))
|
|
544
|
+
|
|
545
|
+
const result = await client.listMappings('graph_1')
|
|
546
|
+
|
|
547
|
+
expect(result).toEqual([])
|
|
548
|
+
})
|
|
549
|
+
|
|
550
|
+
it('should throw on error', async () => {
|
|
551
|
+
mockFetch.mockResolvedValueOnce(
|
|
552
|
+
createMockResponse({ detail: 'Error' }, { ok: false, status: 500 })
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
await expect(client.listMappings('graph_1')).rejects.toThrow('List mappings failed')
|
|
556
|
+
})
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
describe('getMappingDetail', () => {
|
|
560
|
+
it('should return mapping detail', async () => {
|
|
561
|
+
const mockDetail = {
|
|
562
|
+
mapping_id: 'map_1',
|
|
563
|
+
associations: [{ from_element_id: 'coa_1', to_element_id: 'gaap_1', confidence: 0.95 }],
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
mockFetch.mockResolvedValueOnce(createMockResponse(mockDetail))
|
|
567
|
+
|
|
568
|
+
const result = await client.getMappingDetail('graph_1', 'map_1')
|
|
569
|
+
|
|
570
|
+
expect(result).toEqual(mockDetail)
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
it('should throw on error', async () => {
|
|
574
|
+
mockFetch.mockResolvedValueOnce(
|
|
575
|
+
createMockResponse({ detail: 'Error' }, { ok: false, status: 500 })
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
await expect(client.getMappingDetail('graph_1', 'map_1')).rejects.toThrow(
|
|
579
|
+
'Get mapping detail failed'
|
|
580
|
+
)
|
|
581
|
+
})
|
|
582
|
+
})
|
|
583
|
+
|
|
584
|
+
describe('getMappingCoverage', () => {
|
|
585
|
+
it('should return transformed coverage data', async () => {
|
|
586
|
+
mockFetch.mockResolvedValueOnce(
|
|
587
|
+
createMockResponse({
|
|
588
|
+
total_coa_elements: 150,
|
|
589
|
+
mapped_count: 120,
|
|
590
|
+
unmapped_count: 30,
|
|
591
|
+
coverage_percent: 80.0,
|
|
592
|
+
high_confidence: 100,
|
|
593
|
+
medium_confidence: 15,
|
|
594
|
+
low_confidence: 5,
|
|
595
|
+
})
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
const result = await client.getMappingCoverage('graph_1', 'map_1')
|
|
599
|
+
|
|
600
|
+
expect(result).toEqual({
|
|
601
|
+
totalCoaElements: 150,
|
|
602
|
+
mappedCount: 120,
|
|
603
|
+
unmappedCount: 30,
|
|
604
|
+
coveragePercent: 80.0,
|
|
605
|
+
highConfidence: 100,
|
|
606
|
+
mediumConfidence: 15,
|
|
607
|
+
lowConfidence: 5,
|
|
608
|
+
})
|
|
609
|
+
})
|
|
610
|
+
|
|
611
|
+
it('should default missing fields to 0', async () => {
|
|
612
|
+
mockFetch.mockResolvedValueOnce(createMockResponse({}))
|
|
613
|
+
|
|
614
|
+
const result = await client.getMappingCoverage('graph_1', 'map_1')
|
|
615
|
+
|
|
616
|
+
expect(result.totalCoaElements).toBe(0)
|
|
617
|
+
expect(result.mappedCount).toBe(0)
|
|
618
|
+
expect(result.unmappedCount).toBe(0)
|
|
619
|
+
expect(result.coveragePercent).toBe(0)
|
|
620
|
+
expect(result.highConfidence).toBe(0)
|
|
621
|
+
expect(result.mediumConfidence).toBe(0)
|
|
622
|
+
expect(result.lowConfidence).toBe(0)
|
|
623
|
+
})
|
|
624
|
+
|
|
625
|
+
it('should throw on error', async () => {
|
|
626
|
+
mockFetch.mockResolvedValueOnce(
|
|
627
|
+
createMockResponse({ detail: 'Error' }, { ok: false, status: 500 })
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
await expect(client.getMappingCoverage('graph_1', 'map_1')).rejects.toThrow(
|
|
631
|
+
'Get mapping coverage failed'
|
|
632
|
+
)
|
|
633
|
+
})
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
describe('createMapping', () => {
|
|
637
|
+
it('should create a mapping association', async () => {
|
|
638
|
+
mockFetch.mockResolvedValueOnce(createMockResponse({ success: true }))
|
|
639
|
+
|
|
640
|
+
await expect(
|
|
641
|
+
client.createMapping('graph_1', 'map_1', 'from_elem', 'to_elem', 0.9)
|
|
642
|
+
).resolves.toBeUndefined()
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
it('should accept call without explicit confidence', async () => {
|
|
646
|
+
mockFetch.mockResolvedValueOnce(createMockResponse({ success: true }))
|
|
647
|
+
|
|
648
|
+
await expect(
|
|
649
|
+
client.createMapping('graph_1', 'map_1', 'from_elem', 'to_elem')
|
|
650
|
+
).resolves.toBeUndefined()
|
|
651
|
+
})
|
|
652
|
+
|
|
653
|
+
it('should throw on error', async () => {
|
|
654
|
+
mockFetch.mockResolvedValueOnce(
|
|
655
|
+
createMockResponse({ detail: 'Error' }, { ok: false, status: 500 })
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
await expect(client.createMapping('graph_1', 'map_1', 'from', 'to')).rejects.toThrow(
|
|
659
|
+
'Create mapping failed'
|
|
660
|
+
)
|
|
661
|
+
})
|
|
662
|
+
})
|
|
663
|
+
|
|
664
|
+
describe('deleteMapping', () => {
|
|
665
|
+
it('should delete a mapping association', async () => {
|
|
666
|
+
mockFetch.mockResolvedValueOnce(createMockResponse({ success: true }))
|
|
667
|
+
|
|
668
|
+
await expect(client.deleteMapping('graph_1', 'map_1', 'assoc_1')).resolves.toBeUndefined()
|
|
669
|
+
})
|
|
670
|
+
|
|
671
|
+
it('should throw on error', async () => {
|
|
672
|
+
mockFetch.mockResolvedValueOnce(
|
|
673
|
+
createMockResponse({ detail: 'Error' }, { ok: false, status: 500 })
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
await expect(client.deleteMapping('graph_1', 'map_1', 'assoc_1')).rejects.toThrow(
|
|
677
|
+
'Delete mapping failed'
|
|
678
|
+
)
|
|
679
|
+
})
|
|
680
|
+
})
|
|
681
|
+
|
|
682
|
+
describe('autoMap', () => {
|
|
683
|
+
it('should return operation ID', async () => {
|
|
684
|
+
mockFetch.mockResolvedValueOnce(createMockResponse({ operation_id: 'op_automap_1' }))
|
|
685
|
+
|
|
686
|
+
const result = await client.autoMap('graph_1', 'map_1')
|
|
687
|
+
|
|
688
|
+
expect(result.operationId).toBe('op_automap_1')
|
|
689
|
+
})
|
|
690
|
+
|
|
691
|
+
it('should handle response with no operation_id', async () => {
|
|
692
|
+
mockFetch.mockResolvedValueOnce(createMockResponse({}))
|
|
693
|
+
|
|
694
|
+
const result = await client.autoMap('graph_1', 'map_1')
|
|
695
|
+
|
|
696
|
+
expect(result.operationId).toBeUndefined()
|
|
697
|
+
})
|
|
698
|
+
|
|
699
|
+
it('should throw on error', async () => {
|
|
700
|
+
mockFetch.mockResolvedValueOnce(
|
|
701
|
+
createMockResponse({ detail: 'Error' }, { ok: false, status: 500 })
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
await expect(client.autoMap('graph_1', 'map_1')).rejects.toThrow('Auto-map failed')
|
|
705
|
+
})
|
|
706
|
+
})
|
|
707
|
+
|
|
708
|
+
// ── Schedules ─────────────────────────────────────────────────────────
|
|
709
|
+
|
|
710
|
+
describe('createSchedule', () => {
|
|
711
|
+
const scheduleOptions = {
|
|
712
|
+
name: 'Depreciation - Equipment',
|
|
713
|
+
elementIds: ['elem_1', 'elem_2'],
|
|
714
|
+
periodStart: '2025-01-01',
|
|
715
|
+
periodEnd: '2025-12-31',
|
|
716
|
+
monthlyAmount: 5000,
|
|
717
|
+
entryTemplate: {
|
|
718
|
+
debitElementId: 'debit_elem',
|
|
719
|
+
creditElementId: 'credit_elem',
|
|
720
|
+
entryType: 'adjusting',
|
|
721
|
+
memoTemplate: 'Monthly depreciation',
|
|
722
|
+
},
|
|
723
|
+
taxonomyId: 'tax_usgaap',
|
|
724
|
+
scheduleMetadata: {
|
|
725
|
+
method: 'straight_line',
|
|
726
|
+
originalAmount: 60000,
|
|
727
|
+
residualValue: 0,
|
|
728
|
+
usefulLifeMonths: 12,
|
|
729
|
+
assetElementId: 'asset_elem',
|
|
730
|
+
},
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
it('should create schedule and return transformed result', async () => {
|
|
734
|
+
mockFetch.mockResolvedValueOnce(
|
|
735
|
+
createMockResponse({
|
|
736
|
+
structure_id: 'sched_1',
|
|
737
|
+
name: 'Depreciation - Equipment',
|
|
738
|
+
taxonomy_id: 'tax_usgaap',
|
|
739
|
+
total_periods: 12,
|
|
740
|
+
total_facts: 24,
|
|
741
|
+
})
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
const result = await client.createSchedule('graph_1', scheduleOptions)
|
|
745
|
+
|
|
746
|
+
expect(result).toEqual({
|
|
747
|
+
structureId: 'sched_1',
|
|
748
|
+
name: 'Depreciation - Equipment',
|
|
749
|
+
taxonomyId: 'tax_usgaap',
|
|
750
|
+
totalPeriods: 12,
|
|
751
|
+
totalFacts: 24,
|
|
752
|
+
})
|
|
753
|
+
})
|
|
754
|
+
|
|
755
|
+
it('should handle schedule without metadata', async () => {
|
|
756
|
+
mockFetch.mockResolvedValueOnce(
|
|
757
|
+
createMockResponse({
|
|
758
|
+
structure_id: 'sched_2',
|
|
759
|
+
name: 'Simple',
|
|
760
|
+
taxonomy_id: 'tax',
|
|
761
|
+
total_periods: 6,
|
|
762
|
+
total_facts: 6,
|
|
763
|
+
})
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
const simpleOptions = {
|
|
767
|
+
name: 'Simple',
|
|
768
|
+
elementIds: ['elem_1'],
|
|
769
|
+
periodStart: '2025-01-01',
|
|
770
|
+
periodEnd: '2025-06-30',
|
|
771
|
+
monthlyAmount: 1000,
|
|
772
|
+
entryTemplate: {
|
|
773
|
+
debitElementId: 'debit',
|
|
774
|
+
creditElementId: 'credit',
|
|
775
|
+
},
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const result = await client.createSchedule('graph_1', simpleOptions)
|
|
779
|
+
|
|
780
|
+
expect(result.structureId).toBe('sched_2')
|
|
781
|
+
expect(result.totalPeriods).toBe(6)
|
|
782
|
+
})
|
|
783
|
+
|
|
784
|
+
it('should throw on error', async () => {
|
|
785
|
+
mockFetch.mockResolvedValueOnce(
|
|
786
|
+
createMockResponse({ detail: 'Error' }, { ok: false, status: 500 })
|
|
787
|
+
)
|
|
788
|
+
|
|
789
|
+
await expect(client.createSchedule('graph_1', scheduleOptions)).rejects.toThrow(
|
|
790
|
+
'Create schedule failed'
|
|
791
|
+
)
|
|
792
|
+
})
|
|
793
|
+
})
|
|
794
|
+
|
|
795
|
+
describe('listSchedules', () => {
|
|
796
|
+
it('should return transformed schedule list', async () => {
|
|
797
|
+
mockFetch.mockResolvedValueOnce(
|
|
798
|
+
createMockResponse({
|
|
799
|
+
schedules: [
|
|
800
|
+
{
|
|
801
|
+
structure_id: 'sched_1',
|
|
802
|
+
name: 'Depreciation',
|
|
803
|
+
taxonomy_name: 'US GAAP',
|
|
804
|
+
entry_template: { debit_element_id: 'd', credit_element_id: 'c' },
|
|
805
|
+
schedule_metadata: { method: 'straight_line' },
|
|
806
|
+
total_periods: 12,
|
|
807
|
+
periods_with_entries: 6,
|
|
808
|
+
},
|
|
809
|
+
],
|
|
810
|
+
})
|
|
811
|
+
)
|
|
812
|
+
|
|
813
|
+
const result = await client.listSchedules('graph_1')
|
|
814
|
+
|
|
815
|
+
expect(result).toHaveLength(1)
|
|
816
|
+
expect(result[0]).toEqual({
|
|
817
|
+
structureId: 'sched_1',
|
|
818
|
+
name: 'Depreciation',
|
|
819
|
+
taxonomyName: 'US GAAP',
|
|
820
|
+
entryTemplate: { debit_element_id: 'd', credit_element_id: 'c' },
|
|
821
|
+
scheduleMetadata: { method: 'straight_line' },
|
|
822
|
+
totalPeriods: 12,
|
|
823
|
+
periodsWithEntries: 6,
|
|
824
|
+
})
|
|
825
|
+
})
|
|
826
|
+
|
|
827
|
+
it('should handle null entry_template and schedule_metadata', async () => {
|
|
828
|
+
mockFetch.mockResolvedValueOnce(
|
|
829
|
+
createMockResponse({
|
|
830
|
+
schedules: [
|
|
831
|
+
{
|
|
832
|
+
structure_id: 'sched_1',
|
|
833
|
+
name: 'Bare',
|
|
834
|
+
taxonomy_name: 'GAAP',
|
|
835
|
+
total_periods: 1,
|
|
836
|
+
periods_with_entries: 0,
|
|
837
|
+
},
|
|
838
|
+
],
|
|
839
|
+
})
|
|
840
|
+
)
|
|
841
|
+
|
|
842
|
+
const result = await client.listSchedules('graph_1')
|
|
843
|
+
|
|
844
|
+
expect(result[0].entryTemplate).toBeNull()
|
|
845
|
+
expect(result[0].scheduleMetadata).toBeNull()
|
|
846
|
+
})
|
|
847
|
+
|
|
848
|
+
it('should return empty array when no schedules', async () => {
|
|
849
|
+
mockFetch.mockResolvedValueOnce(createMockResponse({ schedules: [] }))
|
|
850
|
+
|
|
851
|
+
const result = await client.listSchedules('graph_1')
|
|
852
|
+
|
|
853
|
+
expect(result).toEqual([])
|
|
854
|
+
})
|
|
855
|
+
|
|
856
|
+
it('should throw on error', async () => {
|
|
857
|
+
mockFetch.mockResolvedValueOnce(
|
|
858
|
+
createMockResponse({ detail: 'Error' }, { ok: false, status: 500 })
|
|
859
|
+
)
|
|
860
|
+
|
|
861
|
+
await expect(client.listSchedules('graph_1')).rejects.toThrow('List schedules failed')
|
|
862
|
+
})
|
|
863
|
+
})
|
|
864
|
+
|
|
865
|
+
describe('getScheduleFacts', () => {
|
|
866
|
+
it('should return transformed facts', async () => {
|
|
867
|
+
mockFetch.mockResolvedValueOnce(
|
|
868
|
+
createMockResponse({
|
|
869
|
+
facts: [
|
|
870
|
+
{
|
|
871
|
+
element_id: 'elem_1',
|
|
872
|
+
element_name: 'Depreciation',
|
|
873
|
+
value: 5000,
|
|
874
|
+
period_start: '2025-01-01',
|
|
875
|
+
period_end: '2025-01-31',
|
|
876
|
+
},
|
|
877
|
+
{
|
|
878
|
+
element_id: 'elem_1',
|
|
879
|
+
element_name: 'Depreciation',
|
|
880
|
+
value: 5000,
|
|
881
|
+
period_start: '2025-02-01',
|
|
882
|
+
period_end: '2025-02-28',
|
|
883
|
+
},
|
|
884
|
+
],
|
|
885
|
+
})
|
|
886
|
+
)
|
|
887
|
+
|
|
888
|
+
const result = await client.getScheduleFacts('graph_1', 'sched_1')
|
|
889
|
+
|
|
890
|
+
expect(result).toHaveLength(2)
|
|
891
|
+
expect(result[0]).toEqual({
|
|
892
|
+
elementId: 'elem_1',
|
|
893
|
+
elementName: 'Depreciation',
|
|
894
|
+
value: 5000,
|
|
895
|
+
periodStart: '2025-01-01',
|
|
896
|
+
periodEnd: '2025-01-31',
|
|
897
|
+
})
|
|
898
|
+
})
|
|
899
|
+
|
|
900
|
+
it('should accept period filters', async () => {
|
|
901
|
+
mockFetch.mockResolvedValueOnce(createMockResponse({ facts: [] }))
|
|
902
|
+
|
|
903
|
+
const result = await client.getScheduleFacts('graph_1', 'sched_1', '2025-01-01', '2025-06-30')
|
|
904
|
+
|
|
905
|
+
expect(mockFetch).toHaveBeenCalledTimes(1)
|
|
906
|
+
expect(result).toEqual([])
|
|
907
|
+
})
|
|
908
|
+
|
|
909
|
+
it('should throw on error', async () => {
|
|
910
|
+
mockFetch.mockResolvedValueOnce(
|
|
911
|
+
createMockResponse({ detail: 'Error' }, { ok: false, status: 500 })
|
|
912
|
+
)
|
|
913
|
+
|
|
914
|
+
await expect(client.getScheduleFacts('graph_1', 'sched_1')).rejects.toThrow(
|
|
915
|
+
'Get schedule facts failed'
|
|
916
|
+
)
|
|
917
|
+
})
|
|
918
|
+
})
|
|
919
|
+
|
|
920
|
+
// ── Period Close ──────────────────────────────────────────────────────
|
|
921
|
+
|
|
922
|
+
describe('getPeriodCloseStatus', () => {
|
|
923
|
+
it('should return transformed close status', async () => {
|
|
924
|
+
mockFetch.mockResolvedValueOnce(
|
|
925
|
+
createMockResponse({
|
|
926
|
+
fiscal_period_start: '2025-01-01',
|
|
927
|
+
fiscal_period_end: '2025-03-31',
|
|
928
|
+
period_status: 'open',
|
|
929
|
+
schedules: [
|
|
930
|
+
{
|
|
931
|
+
structure_id: 'sched_1',
|
|
932
|
+
structure_name: 'Depreciation',
|
|
933
|
+
amount: 15000,
|
|
934
|
+
status: 'draft',
|
|
935
|
+
entry_id: 'entry_1',
|
|
936
|
+
},
|
|
937
|
+
{
|
|
938
|
+
structure_id: 'sched_2',
|
|
939
|
+
structure_name: 'Amortization',
|
|
940
|
+
amount: 3000,
|
|
941
|
+
status: 'posted',
|
|
942
|
+
},
|
|
943
|
+
],
|
|
944
|
+
total_draft: 1,
|
|
945
|
+
total_posted: 1,
|
|
946
|
+
})
|
|
947
|
+
)
|
|
948
|
+
|
|
949
|
+
const result = await client.getPeriodCloseStatus('graph_1', '2025-01-01', '2025-03-31')
|
|
950
|
+
|
|
951
|
+
expect(result.fiscalPeriodStart).toBe('2025-01-01')
|
|
952
|
+
expect(result.fiscalPeriodEnd).toBe('2025-03-31')
|
|
953
|
+
expect(result.periodStatus).toBe('open')
|
|
954
|
+
expect(result.totalDraft).toBe(1)
|
|
955
|
+
expect(result.totalPosted).toBe(1)
|
|
956
|
+
expect(result.schedules).toHaveLength(2)
|
|
957
|
+
expect(result.schedules[0]).toEqual({
|
|
958
|
+
structureId: 'sched_1',
|
|
959
|
+
structureName: 'Depreciation',
|
|
960
|
+
amount: 15000,
|
|
961
|
+
status: 'draft',
|
|
962
|
+
entryId: 'entry_1',
|
|
963
|
+
})
|
|
964
|
+
expect(result.schedules[1].entryId).toBeNull()
|
|
965
|
+
})
|
|
966
|
+
|
|
967
|
+
it('should throw on error', async () => {
|
|
968
|
+
mockFetch.mockResolvedValueOnce(
|
|
969
|
+
createMockResponse({ detail: 'Error' }, { ok: false, status: 500 })
|
|
970
|
+
)
|
|
971
|
+
|
|
972
|
+
await expect(
|
|
973
|
+
client.getPeriodCloseStatus('graph_1', '2025-01-01', '2025-03-31')
|
|
974
|
+
).rejects.toThrow('Get period close status failed')
|
|
975
|
+
})
|
|
976
|
+
})
|
|
977
|
+
|
|
978
|
+
describe('createClosingEntry', () => {
|
|
979
|
+
it('should return a created outcome with entry details', async () => {
|
|
980
|
+
mockFetch.mockResolvedValueOnce(
|
|
981
|
+
createMockResponse({
|
|
982
|
+
outcome: 'created',
|
|
983
|
+
entry_id: 'entry_1',
|
|
984
|
+
status: 'draft',
|
|
985
|
+
posting_date: '2025-03-31',
|
|
986
|
+
memo: 'Q1 depreciation',
|
|
987
|
+
debit_element_id: 'debit_elem',
|
|
988
|
+
credit_element_id: 'credit_elem',
|
|
989
|
+
amount: 15000,
|
|
990
|
+
reason: null,
|
|
991
|
+
})
|
|
992
|
+
)
|
|
993
|
+
|
|
994
|
+
const result = await client.createClosingEntry(
|
|
995
|
+
'graph_1',
|
|
996
|
+
'sched_1',
|
|
997
|
+
'2025-03-31',
|
|
998
|
+
'2025-01-01',
|
|
999
|
+
'2025-03-31',
|
|
1000
|
+
'Q1 depreciation'
|
|
1001
|
+
)
|
|
1002
|
+
|
|
1003
|
+
expect(result).toEqual({
|
|
1004
|
+
outcome: 'created',
|
|
1005
|
+
entryId: 'entry_1',
|
|
1006
|
+
status: 'draft',
|
|
1007
|
+
postingDate: '2025-03-31',
|
|
1008
|
+
memo: 'Q1 depreciation',
|
|
1009
|
+
debitElementId: 'debit_elem',
|
|
1010
|
+
creditElementId: 'credit_elem',
|
|
1011
|
+
amount: 15000,
|
|
1012
|
+
reason: null,
|
|
1013
|
+
})
|
|
1014
|
+
})
|
|
1015
|
+
|
|
1016
|
+
it('should surface a skipped outcome with null entry fields', async () => {
|
|
1017
|
+
mockFetch.mockResolvedValueOnce(
|
|
1018
|
+
createMockResponse({
|
|
1019
|
+
outcome: 'skipped',
|
|
1020
|
+
entry_id: null,
|
|
1021
|
+
status: null,
|
|
1022
|
+
posting_date: null,
|
|
1023
|
+
memo: null,
|
|
1024
|
+
debit_element_id: null,
|
|
1025
|
+
credit_element_id: null,
|
|
1026
|
+
amount: null,
|
|
1027
|
+
reason: 'No in-scope fact for this period.',
|
|
1028
|
+
})
|
|
1029
|
+
)
|
|
1030
|
+
|
|
1031
|
+
const result = await client.createClosingEntry(
|
|
1032
|
+
'graph_1',
|
|
1033
|
+
'sched_1',
|
|
1034
|
+
'2025-03-31',
|
|
1035
|
+
'2025-01-01',
|
|
1036
|
+
'2025-03-31'
|
|
1037
|
+
)
|
|
1038
|
+
|
|
1039
|
+
expect(result.outcome).toBe('skipped')
|
|
1040
|
+
expect(result.entryId).toBeNull()
|
|
1041
|
+
expect(result.amount).toBeNull()
|
|
1042
|
+
expect(result.reason).toBe('No in-scope fact for this period.')
|
|
1043
|
+
})
|
|
1044
|
+
|
|
1045
|
+
it('should throw on error', async () => {
|
|
1046
|
+
mockFetch.mockResolvedValueOnce(
|
|
1047
|
+
createMockResponse({ detail: 'Error' }, { ok: false, status: 500 })
|
|
1048
|
+
)
|
|
1049
|
+
|
|
1050
|
+
await expect(
|
|
1051
|
+
client.createClosingEntry('graph_1', 'sched_1', '2025-03-31', '2025-01-01', '2025-03-31')
|
|
1052
|
+
).rejects.toThrow('Create closing entry failed')
|
|
1053
|
+
})
|
|
1054
|
+
})
|
|
1055
|
+
|
|
1056
|
+
// ── Closing Book ──────────────────────────────────────────────────────
|
|
1057
|
+
|
|
1058
|
+
describe('getClosingBookStructures', () => {
|
|
1059
|
+
it('should return closing book structures', async () => {
|
|
1060
|
+
const mockData = {
|
|
1061
|
+
categories: [
|
|
1062
|
+
{ name: 'Statements', structures: [{ id: 'str_1', name: 'Income Statement' }] },
|
|
1063
|
+
{ name: 'Schedules', structures: [{ id: 'sched_1', name: 'Depreciation' }] },
|
|
1064
|
+
],
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
mockFetch.mockResolvedValueOnce(createMockResponse(mockData))
|
|
1068
|
+
|
|
1069
|
+
const result = await client.getClosingBookStructures('graph_1')
|
|
1070
|
+
|
|
1071
|
+
expect(result).toEqual(mockData)
|
|
1072
|
+
})
|
|
1073
|
+
|
|
1074
|
+
it('should throw on error', async () => {
|
|
1075
|
+
mockFetch.mockResolvedValueOnce(
|
|
1076
|
+
createMockResponse({ detail: 'Error' }, { ok: false, status: 500 })
|
|
1077
|
+
)
|
|
1078
|
+
|
|
1079
|
+
await expect(client.getClosingBookStructures('graph_1')).rejects.toThrow(
|
|
1080
|
+
'Get closing book structures failed'
|
|
1081
|
+
)
|
|
1082
|
+
})
|
|
1083
|
+
})
|
|
1084
|
+
|
|
1085
|
+
describe('getAccountRollups', () => {
|
|
1086
|
+
it('should return account rollups data', async () => {
|
|
1087
|
+
const mockRollups = {
|
|
1088
|
+
rollups: [
|
|
1089
|
+
{
|
|
1090
|
+
element_id: 'gaap_revenue',
|
|
1091
|
+
element_name: 'Revenue',
|
|
1092
|
+
accounts: [{ account_id: 'acc_1', name: 'Sales Revenue', balance: 100000 }],
|
|
1093
|
+
total: 100000,
|
|
1094
|
+
},
|
|
1095
|
+
],
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
mockFetch.mockResolvedValueOnce(createMockResponse(mockRollups))
|
|
1099
|
+
|
|
1100
|
+
const result = await client.getAccountRollups('graph_1', {
|
|
1101
|
+
mappingId: 'map_1',
|
|
1102
|
+
startDate: '2025-01-01',
|
|
1103
|
+
endDate: '2025-03-31',
|
|
1104
|
+
})
|
|
1105
|
+
|
|
1106
|
+
expect(result).toEqual(mockRollups)
|
|
1107
|
+
})
|
|
1108
|
+
|
|
1109
|
+
it('should work without options', async () => {
|
|
1110
|
+
mockFetch.mockResolvedValueOnce(createMockResponse({ rollups: [] }))
|
|
1111
|
+
|
|
1112
|
+
const result = await client.getAccountRollups('graph_1')
|
|
1113
|
+
|
|
1114
|
+
expect(result).toEqual({ rollups: [] })
|
|
1115
|
+
})
|
|
1116
|
+
|
|
1117
|
+
it('should throw on error', async () => {
|
|
1118
|
+
mockFetch.mockResolvedValueOnce(
|
|
1119
|
+
createMockResponse({ detail: 'Error' }, { ok: false, status: 500 })
|
|
1120
|
+
)
|
|
1121
|
+
|
|
1122
|
+
await expect(client.getAccountRollups('graph_1')).rejects.toThrow(
|
|
1123
|
+
'Get account rollups failed'
|
|
1124
|
+
)
|
|
1125
|
+
})
|
|
1126
|
+
})
|
|
1127
|
+
|
|
1128
|
+
// ── Fiscal Calendar ───────────────────────────────────────────────────
|
|
1129
|
+
|
|
1130
|
+
const mockFiscalCalendar = {
|
|
1131
|
+
graph_id: 'graph_1',
|
|
1132
|
+
fiscal_year_start_month: 1,
|
|
1133
|
+
closed_through: '2026-02',
|
|
1134
|
+
close_target: '2026-03',
|
|
1135
|
+
gap_periods: 1,
|
|
1136
|
+
catch_up_sequence: ['2026-03'],
|
|
1137
|
+
closeable_now: true,
|
|
1138
|
+
blockers: [],
|
|
1139
|
+
last_close_at: '2026-03-01T00:00:00Z',
|
|
1140
|
+
initialized_at: '2025-01-01T00:00:00Z',
|
|
1141
|
+
last_sync_at: '2026-03-15T00:00:00Z',
|
|
1142
|
+
periods: [
|
|
1143
|
+
{
|
|
1144
|
+
name: '2026-03',
|
|
1145
|
+
start_date: '2026-03-01',
|
|
1146
|
+
end_date: '2026-03-31',
|
|
1147
|
+
status: 'open',
|
|
1148
|
+
closed_at: null,
|
|
1149
|
+
},
|
|
1150
|
+
],
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
describe('initializeLedger', () => {
|
|
1154
|
+
it('should initialize and return calendar state', async () => {
|
|
1155
|
+
mockFetch.mockResolvedValueOnce(
|
|
1156
|
+
createMockResponse({
|
|
1157
|
+
fiscal_calendar: mockFiscalCalendar,
|
|
1158
|
+
periods_created: 24,
|
|
1159
|
+
warnings: [],
|
|
1160
|
+
})
|
|
1161
|
+
)
|
|
1162
|
+
|
|
1163
|
+
const result = await client.initializeLedger('graph_1', {
|
|
1164
|
+
closedThrough: '2026-02',
|
|
1165
|
+
fiscalYearStartMonth: 1,
|
|
1166
|
+
})
|
|
1167
|
+
|
|
1168
|
+
expect(result.periodsCreated).toBe(24)
|
|
1169
|
+
expect(result.warnings).toEqual([])
|
|
1170
|
+
expect(result.fiscalCalendar.graphId).toBe('graph_1')
|
|
1171
|
+
expect(result.fiscalCalendar.closedThrough).toBe('2026-02')
|
|
1172
|
+
expect(result.fiscalCalendar.closeTarget).toBe('2026-03')
|
|
1173
|
+
expect(result.fiscalCalendar.catchUpSequence).toEqual(['2026-03'])
|
|
1174
|
+
expect(result.fiscalCalendar.closeableNow).toBe(true)
|
|
1175
|
+
expect(result.fiscalCalendar.periods).toHaveLength(1)
|
|
1176
|
+
})
|
|
1177
|
+
|
|
1178
|
+
it('should propagate warnings for unimplemented features', async () => {
|
|
1179
|
+
mockFetch.mockResolvedValueOnce(
|
|
1180
|
+
createMockResponse({
|
|
1181
|
+
fiscal_calendar: mockFiscalCalendar,
|
|
1182
|
+
periods_created: 0,
|
|
1183
|
+
warnings: ['auto_seed_schedules is not yet implemented.'],
|
|
1184
|
+
})
|
|
1185
|
+
)
|
|
1186
|
+
|
|
1187
|
+
const result = await client.initializeLedger('graph_1', {
|
|
1188
|
+
autoSeedSchedules: true,
|
|
1189
|
+
})
|
|
1190
|
+
|
|
1191
|
+
expect(result.warnings).toHaveLength(1)
|
|
1192
|
+
expect(result.warnings[0]).toContain('auto_seed_schedules')
|
|
1193
|
+
})
|
|
1194
|
+
|
|
1195
|
+
it('should map camelCase options to snake_case body fields', async () => {
|
|
1196
|
+
mockFetch.mockResolvedValueOnce(
|
|
1197
|
+
createMockResponse({
|
|
1198
|
+
fiscal_calendar: mockFiscalCalendar,
|
|
1199
|
+
periods_created: 12,
|
|
1200
|
+
warnings: [],
|
|
1201
|
+
})
|
|
1202
|
+
)
|
|
1203
|
+
|
|
1204
|
+
await client.initializeLedger('graph_1', {
|
|
1205
|
+
closedThrough: '2026-02',
|
|
1206
|
+
fiscalYearStartMonth: 7,
|
|
1207
|
+
earliestDataPeriod: '2024-01',
|
|
1208
|
+
autoSeedSchedules: true,
|
|
1209
|
+
note: 'kickoff',
|
|
1210
|
+
})
|
|
1211
|
+
|
|
1212
|
+
const request = mockFetch.mock.calls[0][0] as Request
|
|
1213
|
+
const body = JSON.parse(await request.text())
|
|
1214
|
+
expect(body).toEqual({
|
|
1215
|
+
closed_through: '2026-02',
|
|
1216
|
+
fiscal_year_start_month: 7,
|
|
1217
|
+
earliest_data_period: '2024-01',
|
|
1218
|
+
auto_seed_schedules: true,
|
|
1219
|
+
note: 'kickoff',
|
|
1220
|
+
})
|
|
1221
|
+
})
|
|
1222
|
+
|
|
1223
|
+
it('should throw on 409 already-initialized', async () => {
|
|
1224
|
+
mockFetch.mockResolvedValueOnce(
|
|
1225
|
+
createMockResponse({ detail: 'already initialized' }, { ok: false, status: 409 })
|
|
1226
|
+
)
|
|
1227
|
+
|
|
1228
|
+
await expect(client.initializeLedger('graph_1')).rejects.toThrow('Initialize ledger failed')
|
|
1229
|
+
})
|
|
1230
|
+
})
|
|
1231
|
+
|
|
1232
|
+
describe('getFiscalCalendar', () => {
|
|
1233
|
+
it('should return current calendar state', async () => {
|
|
1234
|
+
mockFetch.mockResolvedValueOnce(createMockResponse(mockFiscalCalendar))
|
|
1235
|
+
|
|
1236
|
+
const result = await client.getFiscalCalendar('graph_1')
|
|
1237
|
+
|
|
1238
|
+
expect(result.graphId).toBe('graph_1')
|
|
1239
|
+
expect(result.closedThrough).toBe('2026-02')
|
|
1240
|
+
expect(result.gapPeriods).toBe(1)
|
|
1241
|
+
expect(result.blockers).toEqual([])
|
|
1242
|
+
})
|
|
1243
|
+
|
|
1244
|
+
it('should throw on 404 calendar missing', async () => {
|
|
1245
|
+
mockFetch.mockResolvedValueOnce(
|
|
1246
|
+
createMockResponse({ detail: 'Not initialized' }, { ok: false, status: 404 })
|
|
1247
|
+
)
|
|
1248
|
+
|
|
1249
|
+
await expect(client.getFiscalCalendar('graph_1')).rejects.toThrow(
|
|
1250
|
+
'Get fiscal calendar failed'
|
|
1251
|
+
)
|
|
1252
|
+
})
|
|
1253
|
+
})
|
|
1254
|
+
|
|
1255
|
+
describe('setCloseTarget', () => {
|
|
1256
|
+
it('should set target and return updated calendar', async () => {
|
|
1257
|
+
mockFetch.mockResolvedValueOnce(
|
|
1258
|
+
createMockResponse({
|
|
1259
|
+
...mockFiscalCalendar,
|
|
1260
|
+
close_target: '2026-06',
|
|
1261
|
+
})
|
|
1262
|
+
)
|
|
1263
|
+
|
|
1264
|
+
const result = await client.setCloseTarget('graph_1', '2026-06', 'catch up Q2')
|
|
1265
|
+
|
|
1266
|
+
expect(result.closeTarget).toBe('2026-06')
|
|
1267
|
+
})
|
|
1268
|
+
|
|
1269
|
+
it('should throw on 422 invalid target', async () => {
|
|
1270
|
+
mockFetch.mockResolvedValueOnce(
|
|
1271
|
+
createMockResponse(
|
|
1272
|
+
{ detail: 'target cannot be before closed_through' },
|
|
1273
|
+
{ ok: false, status: 422 }
|
|
1274
|
+
)
|
|
1275
|
+
)
|
|
1276
|
+
|
|
1277
|
+
await expect(client.setCloseTarget('graph_1', '2025-01')).rejects.toThrow(
|
|
1278
|
+
'Set close target failed'
|
|
1279
|
+
)
|
|
1280
|
+
})
|
|
1281
|
+
})
|
|
1282
|
+
|
|
1283
|
+
describe('closePeriod', () => {
|
|
1284
|
+
it('should close the period and return transition results', async () => {
|
|
1285
|
+
mockFetch.mockResolvedValueOnce(
|
|
1286
|
+
createMockResponse({
|
|
1287
|
+
fiscal_calendar: {
|
|
1288
|
+
...mockFiscalCalendar,
|
|
1289
|
+
closed_through: '2026-03',
|
|
1290
|
+
close_target: '2026-04',
|
|
1291
|
+
},
|
|
1292
|
+
period: '2026-03',
|
|
1293
|
+
entries_posted: 3,
|
|
1294
|
+
target_auto_advanced: true,
|
|
1295
|
+
})
|
|
1296
|
+
)
|
|
1297
|
+
|
|
1298
|
+
const result = await client.closePeriod('graph_1', '2026-03')
|
|
1299
|
+
|
|
1300
|
+
expect(result.period).toBe('2026-03')
|
|
1301
|
+
expect(result.entriesPosted).toBe(3)
|
|
1302
|
+
expect(result.targetAutoAdvanced).toBe(true)
|
|
1303
|
+
expect(result.fiscalCalendar.closedThrough).toBe('2026-03')
|
|
1304
|
+
expect(result.fiscalCalendar.closeTarget).toBe('2026-04')
|
|
1305
|
+
})
|
|
1306
|
+
|
|
1307
|
+
it('should propagate allow_stale_sync flag', async () => {
|
|
1308
|
+
mockFetch.mockResolvedValueOnce(
|
|
1309
|
+
createMockResponse({
|
|
1310
|
+
fiscal_calendar: mockFiscalCalendar,
|
|
1311
|
+
period: '2026-03',
|
|
1312
|
+
entries_posted: 0,
|
|
1313
|
+
target_auto_advanced: false,
|
|
1314
|
+
})
|
|
1315
|
+
)
|
|
1316
|
+
|
|
1317
|
+
await client.closePeriod('graph_1', '2026-03', { allowStaleSync: true })
|
|
1318
|
+
|
|
1319
|
+
// hey-api's client-fetch passes a Request object to fetch. Read the
|
|
1320
|
+
// body back off it to verify the flag was serialized.
|
|
1321
|
+
const request = mockFetch.mock.calls[0][0] as Request
|
|
1322
|
+
const body = await request.text()
|
|
1323
|
+
expect(body).toContain('"allow_stale_sync":true')
|
|
1324
|
+
})
|
|
1325
|
+
|
|
1326
|
+
it('should throw on blocked close with structured detail', async () => {
|
|
1327
|
+
mockFetch.mockResolvedValueOnce(
|
|
1328
|
+
createMockResponse(
|
|
1329
|
+
{
|
|
1330
|
+
detail: {
|
|
1331
|
+
message: 'Cannot close period',
|
|
1332
|
+
blockers: ['sync_stale'],
|
|
1333
|
+
},
|
|
1334
|
+
},
|
|
1335
|
+
{ ok: false, status: 422 }
|
|
1336
|
+
)
|
|
1337
|
+
)
|
|
1338
|
+
|
|
1339
|
+
await expect(client.closePeriod('graph_1', '2026-03')).rejects.toThrow('Close period failed')
|
|
1340
|
+
})
|
|
1341
|
+
})
|
|
1342
|
+
|
|
1343
|
+
describe('reopenPeriod', () => {
|
|
1344
|
+
it('should reopen the period and return updated calendar', async () => {
|
|
1345
|
+
mockFetch.mockResolvedValueOnce(
|
|
1346
|
+
createMockResponse({
|
|
1347
|
+
...mockFiscalCalendar,
|
|
1348
|
+
closed_through: '2026-01',
|
|
1349
|
+
})
|
|
1350
|
+
)
|
|
1351
|
+
|
|
1352
|
+
const result = await client.reopenPeriod('graph_1', '2026-02', 'missed expense')
|
|
1353
|
+
|
|
1354
|
+
expect(result.closedThrough).toBe('2026-01')
|
|
1355
|
+
})
|
|
1356
|
+
|
|
1357
|
+
it('should require reason propagation', async () => {
|
|
1358
|
+
mockFetch.mockResolvedValueOnce(createMockResponse(mockFiscalCalendar))
|
|
1359
|
+
|
|
1360
|
+
await client.reopenPeriod('graph_1', '2026-02', 'audit correction', 'see ticket #123')
|
|
1361
|
+
|
|
1362
|
+
const request = mockFetch.mock.calls[0][0] as Request
|
|
1363
|
+
const body = await request.text()
|
|
1364
|
+
expect(body).toContain('"reason":"audit correction"')
|
|
1365
|
+
expect(body).toContain('"note":"see ticket #123"')
|
|
1366
|
+
})
|
|
1367
|
+
|
|
1368
|
+
it('should throw on 422 when period not closed', async () => {
|
|
1369
|
+
mockFetch.mockResolvedValueOnce(
|
|
1370
|
+
createMockResponse({ detail: 'not closed' }, { ok: false, status: 422 })
|
|
1371
|
+
)
|
|
1372
|
+
|
|
1373
|
+
await expect(client.reopenPeriod('graph_1', '2026-02', 'reason')).rejects.toThrow(
|
|
1374
|
+
'Reopen period failed'
|
|
1375
|
+
)
|
|
1376
|
+
})
|
|
1377
|
+
})
|
|
1378
|
+
|
|
1379
|
+
describe('listPeriodDrafts', () => {
|
|
1380
|
+
it('should return grouped draft entries with line items', async () => {
|
|
1381
|
+
mockFetch.mockResolvedValueOnce(
|
|
1382
|
+
createMockResponse({
|
|
1383
|
+
period: '2026-03',
|
|
1384
|
+
period_start: '2026-03-01',
|
|
1385
|
+
period_end: '2026-03-31',
|
|
1386
|
+
draft_count: 1,
|
|
1387
|
+
total_debit: 15000,
|
|
1388
|
+
total_credit: 15000,
|
|
1389
|
+
all_balanced: true,
|
|
1390
|
+
drafts: [
|
|
1391
|
+
{
|
|
1392
|
+
entry_id: 'entry_1',
|
|
1393
|
+
posting_date: '2026-03-31',
|
|
1394
|
+
type: 'closing',
|
|
1395
|
+
memo: 'Monthly depreciation',
|
|
1396
|
+
provenance: 'schedule_derived',
|
|
1397
|
+
source_structure_id: 'str_sched',
|
|
1398
|
+
source_structure_name: 'Computer Depreciation',
|
|
1399
|
+
line_items: [
|
|
1400
|
+
{
|
|
1401
|
+
line_item_id: 'li_1',
|
|
1402
|
+
element_id: 'el_dep_exp',
|
|
1403
|
+
element_code: '6100',
|
|
1404
|
+
element_name: 'Depreciation Expense',
|
|
1405
|
+
debit_amount: 15000,
|
|
1406
|
+
credit_amount: 0,
|
|
1407
|
+
description: null,
|
|
1408
|
+
},
|
|
1409
|
+
{
|
|
1410
|
+
line_item_id: 'li_2',
|
|
1411
|
+
element_id: 'el_accum_dep',
|
|
1412
|
+
element_code: '1510',
|
|
1413
|
+
element_name: 'Accumulated Depreciation',
|
|
1414
|
+
debit_amount: 0,
|
|
1415
|
+
credit_amount: 15000,
|
|
1416
|
+
description: null,
|
|
1417
|
+
},
|
|
1418
|
+
],
|
|
1419
|
+
total_debit: 15000,
|
|
1420
|
+
total_credit: 15000,
|
|
1421
|
+
balanced: true,
|
|
1422
|
+
},
|
|
1423
|
+
],
|
|
1424
|
+
})
|
|
1425
|
+
)
|
|
1426
|
+
|
|
1427
|
+
const result = await client.listPeriodDrafts('graph_1', '2026-03')
|
|
1428
|
+
|
|
1429
|
+
expect(result.period).toBe('2026-03')
|
|
1430
|
+
expect(result.draftCount).toBe(1)
|
|
1431
|
+
expect(result.allBalanced).toBe(true)
|
|
1432
|
+
expect(result.drafts[0].entryId).toBe('entry_1')
|
|
1433
|
+
expect(result.drafts[0].sourceStructureName).toBe('Computer Depreciation')
|
|
1434
|
+
expect(result.drafts[0].lineItems).toHaveLength(2)
|
|
1435
|
+
expect(result.drafts[0].lineItems[0].elementCode).toBe('6100')
|
|
1436
|
+
expect(result.drafts[0].lineItems[0].debitAmount).toBe(15000)
|
|
1437
|
+
expect(result.drafts[0].balanced).toBe(true)
|
|
1438
|
+
})
|
|
1439
|
+
|
|
1440
|
+
it('should surface unbalanced drafts', async () => {
|
|
1441
|
+
mockFetch.mockResolvedValueOnce(
|
|
1442
|
+
createMockResponse({
|
|
1443
|
+
period: '2026-03',
|
|
1444
|
+
period_start: '2026-03-01',
|
|
1445
|
+
period_end: '2026-03-31',
|
|
1446
|
+
draft_count: 1,
|
|
1447
|
+
total_debit: 10000,
|
|
1448
|
+
total_credit: 9000,
|
|
1449
|
+
all_balanced: false,
|
|
1450
|
+
drafts: [
|
|
1451
|
+
{
|
|
1452
|
+
entry_id: 'entry_bad',
|
|
1453
|
+
posting_date: '2026-03-31',
|
|
1454
|
+
type: 'closing',
|
|
1455
|
+
memo: null,
|
|
1456
|
+
provenance: null,
|
|
1457
|
+
source_structure_id: null,
|
|
1458
|
+
source_structure_name: null,
|
|
1459
|
+
line_items: [
|
|
1460
|
+
{
|
|
1461
|
+
line_item_id: 'li_1',
|
|
1462
|
+
element_id: 'el_1',
|
|
1463
|
+
element_code: null,
|
|
1464
|
+
element_name: 'A',
|
|
1465
|
+
debit_amount: 10000,
|
|
1466
|
+
credit_amount: 0,
|
|
1467
|
+
description: null,
|
|
1468
|
+
},
|
|
1469
|
+
{
|
|
1470
|
+
line_item_id: 'li_2',
|
|
1471
|
+
element_id: 'el_2',
|
|
1472
|
+
element_code: null,
|
|
1473
|
+
element_name: 'B',
|
|
1474
|
+
debit_amount: 0,
|
|
1475
|
+
credit_amount: 9000,
|
|
1476
|
+
description: null,
|
|
1477
|
+
},
|
|
1478
|
+
],
|
|
1479
|
+
total_debit: 10000,
|
|
1480
|
+
total_credit: 9000,
|
|
1481
|
+
balanced: false,
|
|
1482
|
+
},
|
|
1483
|
+
],
|
|
1484
|
+
})
|
|
1485
|
+
)
|
|
1486
|
+
|
|
1487
|
+
const result = await client.listPeriodDrafts('graph_1', '2026-03')
|
|
1488
|
+
|
|
1489
|
+
expect(result.allBalanced).toBe(false)
|
|
1490
|
+
expect(result.drafts[0].balanced).toBe(false)
|
|
1491
|
+
})
|
|
1492
|
+
})
|
|
1493
|
+
|
|
1494
|
+
// ── Schedule mutations (truncate + manual entry) ────────────────────
|
|
1495
|
+
|
|
1496
|
+
describe('truncateSchedule', () => {
|
|
1497
|
+
it('should truncate and return counts', async () => {
|
|
1498
|
+
mockFetch.mockResolvedValueOnce(
|
|
1499
|
+
createMockResponse({
|
|
1500
|
+
structure_id: 'sched_1',
|
|
1501
|
+
new_end_date: '2026-06-30',
|
|
1502
|
+
facts_deleted: 18,
|
|
1503
|
+
reason: 'Asset sold 2026-06-15',
|
|
1504
|
+
})
|
|
1505
|
+
)
|
|
1506
|
+
|
|
1507
|
+
const result = await client.truncateSchedule('graph_1', 'sched_1', {
|
|
1508
|
+
newEndDate: '2026-06-30',
|
|
1509
|
+
reason: 'Asset sold 2026-06-15',
|
|
1510
|
+
})
|
|
1511
|
+
|
|
1512
|
+
expect(result.structureId).toBe('sched_1')
|
|
1513
|
+
expect(result.newEndDate).toBe('2026-06-30')
|
|
1514
|
+
expect(result.factsDeleted).toBe(18)
|
|
1515
|
+
expect(result.reason).toBe('Asset sold 2026-06-15')
|
|
1516
|
+
})
|
|
1517
|
+
|
|
1518
|
+
it('should throw on 422 mid-month date', async () => {
|
|
1519
|
+
mockFetch.mockResolvedValueOnce(
|
|
1520
|
+
createMockResponse(
|
|
1521
|
+
{ detail: 'new_end_date must be a month-end' },
|
|
1522
|
+
{ ok: false, status: 422 }
|
|
1523
|
+
)
|
|
1524
|
+
)
|
|
1525
|
+
|
|
1526
|
+
await expect(
|
|
1527
|
+
client.truncateSchedule('graph_1', 'sched_1', {
|
|
1528
|
+
newEndDate: '2026-06-15',
|
|
1529
|
+
reason: 'test',
|
|
1530
|
+
})
|
|
1531
|
+
).rejects.toThrow('Truncate schedule failed')
|
|
1532
|
+
})
|
|
1533
|
+
})
|
|
1534
|
+
|
|
1535
|
+
describe('createManualClosingEntry', () => {
|
|
1536
|
+
it('should create entry with arbitrary balanced line items', async () => {
|
|
1537
|
+
mockFetch.mockResolvedValueOnce(
|
|
1538
|
+
createMockResponse({
|
|
1539
|
+
outcome: 'created',
|
|
1540
|
+
entry_id: 'entry_manual',
|
|
1541
|
+
status: 'draft',
|
|
1542
|
+
posting_date: '2026-03-15',
|
|
1543
|
+
memo: 'Sale of computer to Vendor X',
|
|
1544
|
+
debit_element_id: null,
|
|
1545
|
+
credit_element_id: null,
|
|
1546
|
+
amount: 500000,
|
|
1547
|
+
reason: null,
|
|
1548
|
+
})
|
|
1549
|
+
)
|
|
1550
|
+
|
|
1551
|
+
const result = await client.createManualClosingEntry('graph_1', {
|
|
1552
|
+
postingDate: '2026-03-15',
|
|
1553
|
+
memo: 'Sale of computer to Vendor X',
|
|
1554
|
+
entryType: 'closing',
|
|
1555
|
+
lineItems: [
|
|
1556
|
+
{ elementId: 'el_cash', debitAmount: 500000 },
|
|
1557
|
+
{ elementId: 'el_asset', creditAmount: 300000 },
|
|
1558
|
+
{ elementId: 'el_gain', creditAmount: 200000 },
|
|
1559
|
+
],
|
|
1560
|
+
})
|
|
1561
|
+
|
|
1562
|
+
expect(result.outcome).toBe('created')
|
|
1563
|
+
expect(result.entryId).toBe('entry_manual')
|
|
1564
|
+
expect(result.memo).toBe('Sale of computer to Vendor X')
|
|
1565
|
+
})
|
|
1566
|
+
|
|
1567
|
+
it('should serialize line items to snake_case and default missing amounts to 0', async () => {
|
|
1568
|
+
mockFetch.mockResolvedValueOnce(
|
|
1569
|
+
createMockResponse({
|
|
1570
|
+
outcome: 'created',
|
|
1571
|
+
entry_id: 'entry_manual',
|
|
1572
|
+
status: 'draft',
|
|
1573
|
+
posting_date: '2026-03-15',
|
|
1574
|
+
memo: 'Sale',
|
|
1575
|
+
debit_element_id: null,
|
|
1576
|
+
credit_element_id: null,
|
|
1577
|
+
amount: 500000,
|
|
1578
|
+
reason: null,
|
|
1579
|
+
})
|
|
1580
|
+
)
|
|
1581
|
+
|
|
1582
|
+
await client.createManualClosingEntry('graph_1', {
|
|
1583
|
+
postingDate: '2026-03-15',
|
|
1584
|
+
memo: 'Sale',
|
|
1585
|
+
entryType: 'closing',
|
|
1586
|
+
lineItems: [
|
|
1587
|
+
{ elementId: 'el_cash', debitAmount: 500000, description: 'cash in' },
|
|
1588
|
+
{ elementId: 'el_asset', creditAmount: 300000 },
|
|
1589
|
+
{ elementId: 'el_gain', creditAmount: 200000 },
|
|
1590
|
+
],
|
|
1591
|
+
})
|
|
1592
|
+
|
|
1593
|
+
const request = mockFetch.mock.calls[0][0] as Request
|
|
1594
|
+
const body = JSON.parse(await request.text())
|
|
1595
|
+
expect(body).toEqual({
|
|
1596
|
+
posting_date: '2026-03-15',
|
|
1597
|
+
memo: 'Sale',
|
|
1598
|
+
entry_type: 'closing',
|
|
1599
|
+
line_items: [
|
|
1600
|
+
{
|
|
1601
|
+
element_id: 'el_cash',
|
|
1602
|
+
debit_amount: 500000,
|
|
1603
|
+
credit_amount: 0,
|
|
1604
|
+
description: 'cash in',
|
|
1605
|
+
},
|
|
1606
|
+
{
|
|
1607
|
+
element_id: 'el_asset',
|
|
1608
|
+
debit_amount: 0,
|
|
1609
|
+
credit_amount: 300000,
|
|
1610
|
+
description: null,
|
|
1611
|
+
},
|
|
1612
|
+
{
|
|
1613
|
+
element_id: 'el_gain',
|
|
1614
|
+
debit_amount: 0,
|
|
1615
|
+
credit_amount: 200000,
|
|
1616
|
+
description: null,
|
|
1617
|
+
},
|
|
1618
|
+
],
|
|
1619
|
+
})
|
|
1620
|
+
})
|
|
1621
|
+
|
|
1622
|
+
it('should throw on 422 unbalanced line items', async () => {
|
|
1623
|
+
mockFetch.mockResolvedValueOnce(
|
|
1624
|
+
createMockResponse({ detail: 'line items must balance' }, { ok: false, status: 422 })
|
|
1625
|
+
)
|
|
1626
|
+
|
|
1627
|
+
await expect(
|
|
1628
|
+
client.createManualClosingEntry('graph_1', {
|
|
1629
|
+
postingDate: '2026-03-15',
|
|
1630
|
+
memo: 'Unbalanced',
|
|
1631
|
+
lineItems: [{ elementId: 'el_a', debitAmount: 1000 }],
|
|
1632
|
+
})
|
|
1633
|
+
).rejects.toThrow('Create manual closing entry failed')
|
|
1634
|
+
})
|
|
1635
|
+
})
|
|
1636
|
+
|
|
1637
|
+
// ── Constructor ───────────────────────────────────────────────────────
|
|
1638
|
+
|
|
1639
|
+
describe('constructor', () => {
|
|
1640
|
+
it('should create with minimal config', () => {
|
|
1641
|
+
const c = new LedgerClient({ baseUrl: 'http://localhost:8000' })
|
|
1642
|
+
expect(c).toBeInstanceOf(LedgerClient)
|
|
1643
|
+
})
|
|
1644
|
+
|
|
1645
|
+
it('should accept all config options', () => {
|
|
1646
|
+
const c = new LedgerClient({
|
|
1647
|
+
baseUrl: 'http://localhost:8000',
|
|
1648
|
+
credentials: 'include',
|
|
1649
|
+
headers: { Authorization: 'Bearer token' },
|
|
1650
|
+
token: 'jwt-token',
|
|
1651
|
+
})
|
|
1652
|
+
expect(c).toBeInstanceOf(LedgerClient)
|
|
1653
|
+
})
|
|
1654
|
+
})
|
|
1655
|
+
})
|