@robosystems/client 0.2.48 → 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/LedgerClient.d.ts +180 -6
- package/extensions/LedgerClient.js +251 -10
- package/extensions/LedgerClient.test.ts +528 -11
- package/extensions/LedgerClient.ts +464 -15
- package/index.ts +2 -2
- package/package.json +1 -1
- package/sdk/index.d.ts +2 -2
- package/sdk/index.js +12 -4
- package/sdk/index.ts +2 -2
- package/sdk/sdk.gen.d.ts +107 -1
- package/sdk/sdk.gen.js +172 -2
- package/sdk/sdk.gen.ts +171 -1
- package/sdk/types.gen.d.ts +737 -10
- package/sdk/types.gen.ts +803 -21
- package/sdk-extensions/LedgerClient.d.ts +180 -6
- package/sdk-extensions/LedgerClient.js +251 -10
- package/sdk-extensions/LedgerClient.test.ts +528 -11
- package/sdk-extensions/LedgerClient.ts +464 -15
- package/sdk.gen.d.ts +107 -1
- package/sdk.gen.js +172 -2
- package/sdk.gen.ts +171 -1
- package/types.gen.d.ts +737 -10
- package/types.gen.ts +803 -21
|
@@ -976,9 +976,10 @@ describe('LedgerClient', () => {
|
|
|
976
976
|
})
|
|
977
977
|
|
|
978
978
|
describe('createClosingEntry', () => {
|
|
979
|
-
it('should
|
|
979
|
+
it('should return a created outcome with entry details', async () => {
|
|
980
980
|
mockFetch.mockResolvedValueOnce(
|
|
981
981
|
createMockResponse({
|
|
982
|
+
outcome: 'created',
|
|
982
983
|
entry_id: 'entry_1',
|
|
983
984
|
status: 'draft',
|
|
984
985
|
posting_date: '2025-03-31',
|
|
@@ -986,6 +987,7 @@ describe('LedgerClient', () => {
|
|
|
986
987
|
debit_element_id: 'debit_elem',
|
|
987
988
|
credit_element_id: 'credit_elem',
|
|
988
989
|
amount: 15000,
|
|
990
|
+
reason: null,
|
|
989
991
|
})
|
|
990
992
|
)
|
|
991
993
|
|
|
@@ -999,6 +1001,7 @@ describe('LedgerClient', () => {
|
|
|
999
1001
|
)
|
|
1000
1002
|
|
|
1001
1003
|
expect(result).toEqual({
|
|
1004
|
+
outcome: 'created',
|
|
1002
1005
|
entryId: 'entry_1',
|
|
1003
1006
|
status: 'draft',
|
|
1004
1007
|
postingDate: '2025-03-31',
|
|
@@ -1006,19 +1009,22 @@ describe('LedgerClient', () => {
|
|
|
1006
1009
|
debitElementId: 'debit_elem',
|
|
1007
1010
|
creditElementId: 'credit_elem',
|
|
1008
1011
|
amount: 15000,
|
|
1012
|
+
reason: null,
|
|
1009
1013
|
})
|
|
1010
1014
|
})
|
|
1011
1015
|
|
|
1012
|
-
it('should
|
|
1016
|
+
it('should surface a skipped outcome with null entry fields', async () => {
|
|
1013
1017
|
mockFetch.mockResolvedValueOnce(
|
|
1014
1018
|
createMockResponse({
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
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.',
|
|
1022
1028
|
})
|
|
1023
1029
|
)
|
|
1024
1030
|
|
|
@@ -1030,8 +1036,10 @@ describe('LedgerClient', () => {
|
|
|
1030
1036
|
'2025-03-31'
|
|
1031
1037
|
)
|
|
1032
1038
|
|
|
1033
|
-
expect(result.
|
|
1034
|
-
expect(result.
|
|
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.')
|
|
1035
1043
|
})
|
|
1036
1044
|
|
|
1037
1045
|
it('should throw on error', async () => {
|
|
@@ -1117,6 +1125,515 @@ describe('LedgerClient', () => {
|
|
|
1117
1125
|
})
|
|
1118
1126
|
})
|
|
1119
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
|
+
|
|
1120
1637
|
// ── Constructor ───────────────────────────────────────────────────────
|
|
1121
1638
|
|
|
1122
1639
|
describe('constructor', () => {
|