@slothmoney/agent-cli 0.10.0 → 0.11.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/CHANGELOG.md +7 -0
- package/README.md +35 -3
- package/dist/args.js +53 -12
- package/dist/cli.js +75 -2
- package/dist/contracts.js +76 -17
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.11.0 - 2026-08-16
|
|
4
|
+
|
|
5
|
+
- Add preview-by-default `budget move` for atomically moving current assigned
|
|
6
|
+
money between categories or To Assign without changing planned budgets.
|
|
7
|
+
- Accept human-readable currency amounts at the CLI boundary, send integer
|
|
8
|
+
pence to the Agent API, and validate the returned affected balances.
|
|
9
|
+
|
|
3
10
|
## 0.10.0 - 2026-08-15
|
|
4
11
|
|
|
5
12
|
- Require every goal create to specify a positive target amount and Keep or
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Sloth Agent CLI
|
|
2
2
|
|
|
3
|
-
Use your own agent to inspect accounts, investments, and budgets, manage goals, update planned amounts, and categorise transactions through the
|
|
3
|
+
Use your own agent to inspect accounts, investments, and budgets, manage goals, move assigned budget money, update planned amounts, and categorise transactions through the
|
|
4
4
|
[Sloth Money Agent API](https://slothmoney.app/developers/).
|
|
5
5
|
|
|
6
6
|
## Install
|
|
@@ -15,7 +15,7 @@ sloth-agent --version
|
|
|
15
15
|
For a one-off pinned run:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
npm exec --yes --package=@slothmoney/agent-cli@0.
|
|
18
|
+
npm exec --yes --package=@slothmoney/agent-cli@0.11.0 -- sloth-agent --help
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
## Authenticate
|
|
@@ -27,7 +27,7 @@ where the CLI runs.
|
|
|
27
27
|
New tokens are view-only. That is enough for `auth status`, `accounts`, `investments`,
|
|
28
28
|
`budget`, `categories`, `transactions`, and `goals` list. Enable **Allow changes** when
|
|
29
29
|
creating the token only if the CLI must apply assignments, manage categories
|
|
30
|
-
or line items, update planned budgets, manage accounts, ask a partner for an explanation, or manage goals. Token
|
|
30
|
+
or line items, move assigned budget money, update planned budgets, manage accounts, ask a partner for an explanation, or manage goals. Token
|
|
31
31
|
permissions cannot be changed later - revoke and reissue the token instead.
|
|
32
32
|
|
|
33
33
|
### Local computer
|
|
@@ -105,6 +105,7 @@ sloth-agent auth login --help
|
|
|
105
105
|
sloth-agent accounts --help
|
|
106
106
|
sloth-agent budget --help
|
|
107
107
|
sloth-agent budget update --help
|
|
108
|
+
sloth-agent budget move --help
|
|
108
109
|
sloth-agent categories --help
|
|
109
110
|
sloth-agent categories create --help
|
|
110
111
|
sloth-agent line-items create --help
|
|
@@ -285,6 +286,37 @@ period and everything after it. Earlier and historical periods remain unchanged.
|
|
|
285
286
|
Without `--apply`, the CLI validates the file locally and does not load a token
|
|
286
287
|
or contact Sloth Money. Applying requires a write-enabled token.
|
|
287
288
|
|
|
289
|
+
Move current assigned money between two categories, or use the reserved
|
|
290
|
+
`to-assign` ID to move money to or from To Assign:
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
sloth-agent budget move \
|
|
294
|
+
--scope personal \
|
|
295
|
+
--from-category-id activities \
|
|
296
|
+
--to-category-id groceries \
|
|
297
|
+
--amount 52.95
|
|
298
|
+
|
|
299
|
+
sloth-agent budget move \
|
|
300
|
+
--scope personal \
|
|
301
|
+
--from-category-id activities \
|
|
302
|
+
--to-category-id groceries \
|
|
303
|
+
--amount 52.95 \
|
|
304
|
+
--apply
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Copy category IDs from `sloth-agent budget` output. `--amount` is expressed in
|
|
308
|
+
the budget currency and accepts up to two decimal places; the CLI converts the
|
|
309
|
+
decimal digits exactly and sends a positive safe-integer number of pence to the
|
|
310
|
+
API. Without `--apply`, the command validates and prints the exact request
|
|
311
|
+
without loading credentials or contacting Sloth Money.
|
|
312
|
+
|
|
313
|
+
Applying subtracts and adds the amount atomically, records the movement in the
|
|
314
|
+
budget history, and returns the affected assigned balances. It does not change
|
|
315
|
+
planned line-item amounts or future budget plans. Like the UI, it permits a
|
|
316
|
+
source category or To Assign to become negative; an automated workflow should
|
|
317
|
+
choose donors from its own available-balance policy. Historical periods cannot
|
|
318
|
+
be changed, and applying requires a write-enabled token.
|
|
319
|
+
|
|
288
320
|
Create or rename a custom category. Writes are previews until `--apply` is
|
|
289
321
|
present:
|
|
290
322
|
|
package/dist/args.js
CHANGED
|
@@ -49,16 +49,33 @@ function requireNonEmpty(value, name) {
|
|
|
49
49
|
throw new UsageError(`${name} requires a value`);
|
|
50
50
|
return value;
|
|
51
51
|
}
|
|
52
|
-
function
|
|
52
|
+
function validatePositiveDecimalAmount(value, name) {
|
|
53
53
|
if (!/^\d+(?:\.\d{1,2})?$/.test(value)) {
|
|
54
54
|
throw new UsageError(`${name} must be a positive amount with at most two decimal places`);
|
|
55
55
|
}
|
|
56
|
+
const digits = value.replace('.', '');
|
|
57
|
+
if (!/[1-9]/.test(digits)) {
|
|
58
|
+
throw new UsageError(`${name} must be a positive amount with at most two decimal places`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function parsePositiveDecimalAmount(value, name) {
|
|
62
|
+
validatePositiveDecimalAmount(value, name);
|
|
56
63
|
const amount = Number(value);
|
|
57
64
|
if (!Number.isFinite(amount) || amount <= 0) {
|
|
58
65
|
throw new UsageError(`${name} must be a positive amount with at most two decimal places`);
|
|
59
66
|
}
|
|
60
67
|
return amount;
|
|
61
68
|
}
|
|
69
|
+
function parsePositiveAmountPence(value, name) {
|
|
70
|
+
validatePositiveDecimalAmount(value, name);
|
|
71
|
+
const [wholePounds, fractionalPounds = ''] = value.split('.');
|
|
72
|
+
const amountPence = BigInt(wholePounds) * 100n
|
|
73
|
+
+ BigInt(fractionalPounds.padEnd(2, '0'));
|
|
74
|
+
if (amountPence > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
75
|
+
throw new UsageError(`${name} must be a positive amount with at most two decimal places`);
|
|
76
|
+
}
|
|
77
|
+
return Number(amountPence);
|
|
78
|
+
}
|
|
62
79
|
function parseGoalMonthKey(value, name) {
|
|
63
80
|
if (!/^\d{4}-(0[1-9]|1[0-2])$/.test(value)) {
|
|
64
81
|
throw new UsageError(`${name} must be a valid YYYY-MM month`);
|
|
@@ -109,7 +126,7 @@ function parseResourceId(value, option) {
|
|
|
109
126
|
const codePoint = character.codePointAt(0);
|
|
110
127
|
return codePoint !== undefined && (codePoint < 32 || codePoint === 127);
|
|
111
128
|
})) {
|
|
112
|
-
const resource = option === '--
|
|
129
|
+
const resource = option === '--line-item-id' ? 'line-item' : 'category';
|
|
113
130
|
throw new UsageError(`${option} must be a valid ${resource} document ID`);
|
|
114
131
|
}
|
|
115
132
|
return id;
|
|
@@ -355,13 +372,18 @@ function parseInvestments(args, baseUrl) {
|
|
|
355
372
|
return withBaseUrl({ command: 'investments', ...(accountRef ? { accountRef } : {}) }, baseUrl);
|
|
356
373
|
}
|
|
357
374
|
function parseBudget(args, baseUrl) {
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const
|
|
362
|
-
|
|
375
|
+
const subcommand = args[0] === 'update' || args[0] === 'move' ? args.shift() : undefined;
|
|
376
|
+
const update = subcommand === 'update';
|
|
377
|
+
const move = subcommand === 'move';
|
|
378
|
+
const commandLabel = update ? 'budget update' : move ? 'budget move' : 'budget';
|
|
379
|
+
const { values, apply } = parseNamedOptions(args, commandLabel, new Set(update
|
|
380
|
+
? ['--scope', '--period', '--input']
|
|
381
|
+
: move
|
|
382
|
+
? ['--scope', '--period', '--from-category-id', '--to-category-id', '--amount']
|
|
383
|
+
: ['--scope', '--period']));
|
|
384
|
+
if (!update && !move && apply)
|
|
363
385
|
throw new UsageError('Unknown budget option: --apply');
|
|
364
|
-
const scope = requiredOption(values, '--scope',
|
|
386
|
+
const scope = requiredOption(values, '--scope', commandLabel);
|
|
365
387
|
if (scope !== 'personal' && scope !== 'joint') {
|
|
366
388
|
throw new UsageError('--scope must be personal or joint');
|
|
367
389
|
}
|
|
@@ -370,8 +392,23 @@ function parseBudget(args, baseUrl) {
|
|
|
370
392
|
scope: scope,
|
|
371
393
|
...(period === undefined ? {} : { periodKey: parseGoalMonthKey(period, '--period') }),
|
|
372
394
|
};
|
|
373
|
-
if (!update)
|
|
395
|
+
if (!update && !move)
|
|
374
396
|
return withBaseUrl({ command: 'budget', ...common }, baseUrl);
|
|
397
|
+
if (move) {
|
|
398
|
+
const fromCategoryId = parseResourceId(requiredOption(values, '--from-category-id', commandLabel), '--from-category-id');
|
|
399
|
+
const toCategoryId = parseResourceId(requiredOption(values, '--to-category-id', commandLabel), '--to-category-id');
|
|
400
|
+
if (fromCategoryId === toCategoryId) {
|
|
401
|
+
throw new UsageError('--from-category-id and --to-category-id must differ');
|
|
402
|
+
}
|
|
403
|
+
return withBaseUrl({
|
|
404
|
+
command: 'budget-move',
|
|
405
|
+
...common,
|
|
406
|
+
fromCategoryId,
|
|
407
|
+
toCategoryId,
|
|
408
|
+
amountPence: parsePositiveAmountPence(requiredOption(values, '--amount', commandLabel), '--amount'),
|
|
409
|
+
apply,
|
|
410
|
+
}, baseUrl);
|
|
411
|
+
}
|
|
375
412
|
return withBaseUrl({
|
|
376
413
|
command: 'budget-update',
|
|
377
414
|
...common,
|
|
@@ -527,7 +564,7 @@ function parseGoals(args, baseUrl) {
|
|
|
527
564
|
name = setOnce(name, parseGoalName(value), option);
|
|
528
565
|
}
|
|
529
566
|
else if (option === '--target-amount') {
|
|
530
|
-
targetAmount = setOnce(targetAmount,
|
|
567
|
+
targetAmount = setOnce(targetAmount, parsePositiveDecimalAmount(value, option), option);
|
|
531
568
|
}
|
|
532
569
|
else if (option === '--target-month') {
|
|
533
570
|
targetMonthKey = setOnce(targetMonthKey, parseGoalMonthKey(value, option), option);
|
|
@@ -597,7 +634,7 @@ function parseGoals(args, baseUrl) {
|
|
|
597
634
|
name = setOnce(name, parseGoalName(value), option);
|
|
598
635
|
}
|
|
599
636
|
else if (option === '--target-amount') {
|
|
600
|
-
targetAmount = setOnce(targetAmount,
|
|
637
|
+
targetAmount = setOnce(targetAmount, parsePositiveDecimalAmount(value, option), option);
|
|
601
638
|
}
|
|
602
639
|
else if (option === '--target-month') {
|
|
603
640
|
if (targetMonthKey !== undefined) {
|
|
@@ -732,7 +769,11 @@ function helpTopic(argv) {
|
|
|
732
769
|
return undefined;
|
|
733
770
|
}
|
|
734
771
|
if (command === 'budget') {
|
|
735
|
-
|
|
772
|
+
if (subcommand === 'update')
|
|
773
|
+
return 'budget-update';
|
|
774
|
+
if (subcommand === 'move')
|
|
775
|
+
return 'budget-move';
|
|
776
|
+
return 'budget';
|
|
736
777
|
}
|
|
737
778
|
if (command === 'accounts'
|
|
738
779
|
|| command === 'transactions'
|
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { parseArgs, resolveBaseUrl, } from './args.js';
|
|
3
3
|
import { ICON_KEYS } from './category-metadata.js';
|
|
4
|
-
import { parseApiResponse, validateAssignmentPayload, validateBudgetUpdatePayload, } from './contracts.js';
|
|
4
|
+
import { parseApiResponse, validateAssignmentPayload, validateBudgetMovementResponse, validateBudgetUpdatePayload, } from './contracts.js';
|
|
5
5
|
import { createSystemCredentialStore, secureStorageUnavailableError, } from './credential-store.js';
|
|
6
6
|
import { ApiError, CliError, ConfigError, UsageError, } from './errors.js';
|
|
7
|
-
export const CLI_VERSION = '0.
|
|
7
|
+
export const CLI_VERSION = '0.11.0';
|
|
8
8
|
const REQUEST_TIMEOUT_MS = 60_000;
|
|
9
9
|
const API_ORIGIN_HELP_LINES = [
|
|
10
10
|
'',
|
|
@@ -32,6 +32,8 @@ export function usageText() {
|
|
|
32
32
|
' sloth-agent budget --scope personal|joint [--period YYYY-MM] [--base-url URL]',
|
|
33
33
|
' sloth-agent budget update --scope personal|joint [--period YYYY-MM]',
|
|
34
34
|
' --input budget.json [--apply] [--base-url URL]',
|
|
35
|
+
' sloth-agent budget move --scope personal|joint [--period YYYY-MM]',
|
|
36
|
+
' --from-category-id ID --to-category-id ID --amount AMOUNT [--apply]',
|
|
35
37
|
' sloth-agent categories [list] [--base-url URL]',
|
|
36
38
|
' sloth-agent categories create --name NAME --icon-key KEY --type TYPE [--apply]',
|
|
37
39
|
' sloth-agent categories rename --category-id ID --name NAME [--apply]',
|
|
@@ -457,6 +459,47 @@ export function budgetUpdateHelpText() {
|
|
|
457
459
|
' Apply mode returns the complete persisted budget response.',
|
|
458
460
|
].join('\n');
|
|
459
461
|
}
|
|
462
|
+
export function budgetMoveHelpText() {
|
|
463
|
+
return [
|
|
464
|
+
'Sloth Agent CLI — budget move',
|
|
465
|
+
'',
|
|
466
|
+
'Preview or move assigned money between categories or To Assign.',
|
|
467
|
+
'',
|
|
468
|
+
'Usage:',
|
|
469
|
+
' sloth-agent budget move --scope personal|joint [--period YYYY-MM] --from-category-id ID --to-category-id ID --amount AMOUNT [--apply] [--base-url URL]',
|
|
470
|
+
'',
|
|
471
|
+
'Required inputs:',
|
|
472
|
+
' --scope personal|joint Budget ownership scope.',
|
|
473
|
+
' --from-category-id ID Source category ID, or to-assign.',
|
|
474
|
+
' --to-category-id ID Destination category ID, or to-assign.',
|
|
475
|
+
' --amount AMOUNT Positive amount in the budget currency, with up to two decimals.',
|
|
476
|
+
' The integer-pence value must be at most 9,007,199,254,740,991.',
|
|
477
|
+
'',
|
|
478
|
+
'Optional inputs:',
|
|
479
|
+
' --period YYYY-MM Defaults to the current Sloth budget period.',
|
|
480
|
+
' --apply Send the movement. Without it, only validate and preview.',
|
|
481
|
+
' --base-url URL Override the API origin.',
|
|
482
|
+
' -h, --help Show this help.',
|
|
483
|
+
...API_ORIGIN_HELP_LINES,
|
|
484
|
+
'',
|
|
485
|
+
'Write behavior:',
|
|
486
|
+
' Without --apply, returns JSON locally without loading credentials or contacting Sloth Money.',
|
|
487
|
+
' With --apply, atomically subtracts from the source and adds to the destination.',
|
|
488
|
+
' Use the reserved ID to-assign to move money to or from To Assign.',
|
|
489
|
+
' The move changes current assigned balances and records budget movement history.',
|
|
490
|
+
' The source category or To Assign may become negative, so choose the source deliberately.',
|
|
491
|
+
' It does not change planned amounts or future budget plans.',
|
|
492
|
+
' Historical periods cannot be changed. Applying requires agent:write.',
|
|
493
|
+
'',
|
|
494
|
+
'Output:',
|
|
495
|
+
' Preview mode returns dryRun, endpoint, method, and the amountPence payload.',
|
|
496
|
+
' Apply mode returns the period, currency, movement, To Assign balance, and affected category balances.',
|
|
497
|
+
'',
|
|
498
|
+
'Examples:',
|
|
499
|
+
' sloth-agent budget move --scope personal --from-category-id activities --to-category-id groceries --amount 52.95',
|
|
500
|
+
' sloth-agent budget move --scope personal --from-category-id activities --to-category-id groceries --amount 52.95 --apply',
|
|
501
|
+
].join('\n');
|
|
502
|
+
}
|
|
460
503
|
export function transactionsHelpText() {
|
|
461
504
|
return [
|
|
462
505
|
'Sloth Agent CLI — transactions',
|
|
@@ -824,6 +867,7 @@ export function commandHelpText(topic) {
|
|
|
824
867
|
'accounts-remove': accountsRemoveHelpText,
|
|
825
868
|
investments: investmentsHelpText,
|
|
826
869
|
budget: budgetHelpText,
|
|
870
|
+
'budget-move': budgetMoveHelpText,
|
|
827
871
|
'budget-update': budgetUpdateHelpText,
|
|
828
872
|
categories: categoriesHelpText,
|
|
829
873
|
'categories-create': categoriesCreateHelpText,
|
|
@@ -1140,6 +1184,24 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
1140
1184
|
});
|
|
1141
1185
|
return 0;
|
|
1142
1186
|
}
|
|
1187
|
+
const budgetMovementPayload = parsed.command === 'budget-move'
|
|
1188
|
+
? {
|
|
1189
|
+
scope: parsed.scope,
|
|
1190
|
+
...(parsed.periodKey === undefined ? {} : { periodKey: parsed.periodKey }),
|
|
1191
|
+
fromCategoryId: parsed.fromCategoryId,
|
|
1192
|
+
toCategoryId: parsed.toCategoryId,
|
|
1193
|
+
amountPence: parsed.amountPence,
|
|
1194
|
+
}
|
|
1195
|
+
: undefined;
|
|
1196
|
+
if (parsed.command === 'budget-move' && !parsed.apply) {
|
|
1197
|
+
writeJson(writeStdout, {
|
|
1198
|
+
dryRun: true,
|
|
1199
|
+
endpoint: `${baseUrl}/api/agent/v1/budget-movements`,
|
|
1200
|
+
method: 'POST',
|
|
1201
|
+
payload: budgetMovementPayload,
|
|
1202
|
+
});
|
|
1203
|
+
return 0;
|
|
1204
|
+
}
|
|
1143
1205
|
const credential = await resolveCredential(environment, baseUrl, getCredentialStore);
|
|
1144
1206
|
token = credential.token;
|
|
1145
1207
|
const headers = requestHeaders(token);
|
|
@@ -1182,6 +1244,17 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
1182
1244
|
writeJson(writeStdout, data);
|
|
1183
1245
|
return 0;
|
|
1184
1246
|
}
|
|
1247
|
+
if (parsed.command === 'budget-move') {
|
|
1248
|
+
const response = await fetchImplementation(`${baseUrl}/api/agent/v1/budget-movements`, {
|
|
1249
|
+
method: 'POST',
|
|
1250
|
+
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
1251
|
+
body: JSON.stringify(budgetMovementPayload),
|
|
1252
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1253
|
+
});
|
|
1254
|
+
const data = validateBudgetMovementResponse(await parseHttpResponse(response, token), budgetMovementPayload);
|
|
1255
|
+
writeJson(writeStdout, data);
|
|
1256
|
+
return 0;
|
|
1257
|
+
}
|
|
1185
1258
|
if (parsed.command === 'categories-create'
|
|
1186
1259
|
|| parsed.command === 'categories-rename'
|
|
1187
1260
|
|| parsed.command === 'line-items-create'
|
package/dist/contracts.js
CHANGED
|
@@ -375,6 +375,63 @@ function isBudgetResponse(value) {
|
|
|
375
375
|
&& Array.isArray(value.categories)
|
|
376
376
|
&& value.categories.every(isBudgetCategory));
|
|
377
377
|
}
|
|
378
|
+
function isBudgetMovementResponse(value) {
|
|
379
|
+
return (isObject(value)
|
|
380
|
+
&& hasOnlyFields(value, [
|
|
381
|
+
'moved',
|
|
382
|
+
'scope',
|
|
383
|
+
'periodKey',
|
|
384
|
+
'currency',
|
|
385
|
+
'fromCategoryId',
|
|
386
|
+
'toCategoryId',
|
|
387
|
+
'amountPence',
|
|
388
|
+
'toAssignPence',
|
|
389
|
+
'categoryBalances',
|
|
390
|
+
])
|
|
391
|
+
&& value.moved === true
|
|
392
|
+
&& (value.scope === 'personal' || value.scope === 'joint')
|
|
393
|
+
&& typeof value.periodKey === 'string'
|
|
394
|
+
&& /^\d{4}-(0[1-9]|1[0-2])$/.test(value.periodKey)
|
|
395
|
+
&& isCurrency(value.currency)
|
|
396
|
+
&& typeof value.fromCategoryId === 'string'
|
|
397
|
+
&& value.fromCategoryId.trim().length > 0
|
|
398
|
+
&& typeof value.toCategoryId === 'string'
|
|
399
|
+
&& value.toCategoryId.trim().length > 0
|
|
400
|
+
&& value.fromCategoryId !== value.toCategoryId
|
|
401
|
+
&& isNonnegativeSafeInteger(value.amountPence)
|
|
402
|
+
&& value.amountPence > 0
|
|
403
|
+
&& isSafeInteger(value.toAssignPence)
|
|
404
|
+
&& Array.isArray(value.categoryBalances)
|
|
405
|
+
&& value.categoryBalances.length >= 1
|
|
406
|
+
&& value.categoryBalances.length <= 2
|
|
407
|
+
&& value.categoryBalances.every((balance) => (isObject(balance)
|
|
408
|
+
&& hasOnlyFields(balance, ['categoryId', 'assignedPence'])
|
|
409
|
+
&& typeof balance.categoryId === 'string'
|
|
410
|
+
&& balance.categoryId.trim().length > 0
|
|
411
|
+
&& isSafeInteger(balance.assignedPence))));
|
|
412
|
+
}
|
|
413
|
+
export function validateBudgetMovementResponse(value, expected) {
|
|
414
|
+
if (!isBudgetMovementResponse(value)) {
|
|
415
|
+
throw new ApiError('Invalid budget-move response from the Agent API');
|
|
416
|
+
}
|
|
417
|
+
const expectedCategoryIds = [expected.fromCategoryId, expected.toCategoryId]
|
|
418
|
+
.filter(categoryId => categoryId !== 'to-assign')
|
|
419
|
+
.sort();
|
|
420
|
+
const actualCategoryIds = value.categoryBalances
|
|
421
|
+
.map(balance => balance.categoryId)
|
|
422
|
+
.sort();
|
|
423
|
+
const matchesRequest = value.scope === expected.scope
|
|
424
|
+
&& (expected.periodKey === undefined || value.periodKey === expected.periodKey)
|
|
425
|
+
&& value.fromCategoryId === expected.fromCategoryId
|
|
426
|
+
&& value.toCategoryId === expected.toCategoryId
|
|
427
|
+
&& value.amountPence === expected.amountPence;
|
|
428
|
+
const matchesAffectedCategories = actualCategoryIds.length === expectedCategoryIds.length
|
|
429
|
+
&& actualCategoryIds.every((categoryId, index) => categoryId === expectedCategoryIds[index]);
|
|
430
|
+
if (!matchesRequest || !matchesAffectedCategories) {
|
|
431
|
+
throw new ApiError('Invalid budget-move response from the Agent API');
|
|
432
|
+
}
|
|
433
|
+
return value;
|
|
434
|
+
}
|
|
378
435
|
function isNullableNonEmptyString(value) {
|
|
379
436
|
return value === null || (typeof value === 'string'
|
|
380
437
|
&& value.length > 0
|
|
@@ -511,23 +568,25 @@ export function parseApiResponse(command, value) {
|
|
|
511
568
|
? isInvestmentsResponse(value)
|
|
512
569
|
: command === 'budget' || command === 'budget-update'
|
|
513
570
|
? isBudgetResponse(value)
|
|
514
|
-
: command === '
|
|
515
|
-
?
|
|
516
|
-
: command === 'categories
|
|
517
|
-
?
|
|
518
|
-
: command === '
|
|
519
|
-
?
|
|
520
|
-
: command === '
|
|
521
|
-
?
|
|
522
|
-
: command === '
|
|
523
|
-
?
|
|
524
|
-
: command === '
|
|
525
|
-
?
|
|
526
|
-
: command === '
|
|
527
|
-
?
|
|
528
|
-
: command === 'goals-
|
|
529
|
-
?
|
|
530
|
-
:
|
|
571
|
+
: command === 'budget-move'
|
|
572
|
+
? isBudgetMovementResponse(value)
|
|
573
|
+
: command === 'categories'
|
|
574
|
+
? isCategoryResponse(value)
|
|
575
|
+
: command === 'categories-create' || command === 'categories-rename'
|
|
576
|
+
? isCategoryMutationResponse(value)
|
|
577
|
+
: command === 'line-items-create' || command === 'line-items-rename'
|
|
578
|
+
? isLineItemMutationResponse(value)
|
|
579
|
+
: command === 'transactions'
|
|
580
|
+
? isTransactionsResponse(value)
|
|
581
|
+
: command === 'assign'
|
|
582
|
+
? isAssignmentResponse(value)
|
|
583
|
+
: command === 'ask-partner'
|
|
584
|
+
? isPartnerResponse(value)
|
|
585
|
+
: command === 'goals-list'
|
|
586
|
+
? isGoalsResponse(value)
|
|
587
|
+
: command === 'goals-delete'
|
|
588
|
+
? isGoalDeleteResponse(value)
|
|
589
|
+
: isGoalMutationResponse(value);
|
|
531
590
|
if (!valid) {
|
|
532
591
|
const label = command === 'assign' ? 'assignment' : command;
|
|
533
592
|
throw new ApiError(`Invalid ${label} response from the Agent API`);
|