@slothmoney/agent-cli 0.12.0 → 0.14.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 +16 -0
- package/README.md +84 -4
- package/dist/account-ref.js +4 -0
- package/dist/args.js +21 -1
- package/dist/cli.js +36 -15
- package/dist/contracts.js +109 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.14.0 - 2026-08-20
|
|
4
|
+
|
|
5
|
+
- Add `transactions --account-ref REF` using the opaque reference returned by
|
|
6
|
+
`sloth-agent accounts`, while retaining `--account-id` for compatibility.
|
|
7
|
+
- Require every transaction result to include its matching `accountRef`.
|
|
8
|
+
|
|
9
|
+
## 0.13.0 - 2026-08-19
|
|
10
|
+
|
|
11
|
+
- Let `assign` share or unshare an owned booked personal transaction, update
|
|
12
|
+
its ratio and exclusive amounts in pence, and combine sharing with category
|
|
13
|
+
assignment in one atomic item.
|
|
14
|
+
- Add `transactions --shared[=true|false]` and strict validation for persisted
|
|
15
|
+
sharing results, including the resulting joint-budget contribution.
|
|
16
|
+
- Keep preview local and credential-free while printing the exact payload that
|
|
17
|
+
apply mode will send.
|
|
18
|
+
|
|
3
19
|
## 0.12.0 - 2026-08-16
|
|
4
20
|
|
|
5
21
|
- Add read-only `budget status` for current-period assigned, spent, and
|
package/README.md
CHANGED
|
@@ -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.14.0 -- sloth-agent --help
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
## Authenticate
|
|
@@ -94,8 +94,8 @@ remotely in **Sloth Money Settings > Developer access**.
|
|
|
94
94
|
|
|
95
95
|
## Commands
|
|
96
96
|
|
|
97
|
-
An assignment
|
|
98
|
-
|
|
97
|
+
An assignment can change an owned transaction's sharing, categorisation, or
|
|
98
|
+
both.
|
|
99
99
|
|
|
100
100
|
Every command has built-in reference documentation covering its inputs,
|
|
101
101
|
options, output, and examples:
|
|
@@ -145,7 +145,8 @@ item is already categorised for the joint budget. For example:
|
|
|
145
145
|
|
|
146
146
|
This transaction is uncategorised personally but categorised as Groceries for
|
|
147
147
|
the joint budget. The `--uncategorized` filter applies to the selected
|
|
148
|
-
assignment scope;
|
|
148
|
+
assignment scope; the transaction's native scope is used when
|
|
149
|
+
`--assignment-scope` is omitted.
|
|
149
150
|
|
|
150
151
|
### Categorise a transaction end to end
|
|
151
152
|
|
|
@@ -238,6 +239,71 @@ The transaction should also disappear from the matching `--uncategorized`
|
|
|
238
239
|
query. Confirm that an existing assignment in the other scope was not changed.
|
|
239
240
|
Assignments do not create a separate list.
|
|
240
241
|
|
|
242
|
+
### Share and categorise a transaction
|
|
243
|
+
|
|
244
|
+
Find an owned, unshared booked transaction:
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
sloth-agent transactions --shared=false --q "sainsbury" --limit 20
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Copy its exact `transactionRef` into `assignments.json`. This example shares
|
|
251
|
+
the transaction 60/40, keeps £5 for you personally, and categorises the shared
|
|
252
|
+
remainder as Groceries in Joint:
|
|
253
|
+
|
|
254
|
+
```json
|
|
255
|
+
{
|
|
256
|
+
"assignments": [
|
|
257
|
+
{
|
|
258
|
+
"transactionRef": "PASTE_THE_EXACT_TRANSACTION_REF_HERE",
|
|
259
|
+
"sharing": {
|
|
260
|
+
"isShared": true,
|
|
261
|
+
"shareRatio": 0.6,
|
|
262
|
+
"userExclusiveAmountPence": 500,
|
|
263
|
+
"partnerExclusiveAmountPence": 0
|
|
264
|
+
},
|
|
265
|
+
"assignmentScope": "joint",
|
|
266
|
+
"categoryId": "groceries"
|
|
267
|
+
}
|
|
268
|
+
]
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Preview stays local and does not load credentials or call the API:
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
sloth-agent assign --input assignments.json
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Apply with a token created using **Allow changes**, then read back the same
|
|
279
|
+
state shown in the Web App:
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
sloth-agent assign --input assignments.json --apply
|
|
283
|
+
sloth-agent transactions --shared=true --q "sainsbury" --limit 20
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
When `sharing` contains only `"isShared": true`, a first share uses the
|
|
287
|
+
couple's saved ratio, falling back to `0.5`, and shares the full amount. On an
|
|
288
|
+
already shared transaction, omitted split fields preserve their current values.
|
|
289
|
+
Set both exclusive pence fields to zero to share the full amount again.
|
|
290
|
+
|
|
291
|
+
To unshare, send `"sharing": { "isShared": false }` without ratio or exclusive
|
|
292
|
+
fields. Sloth clears the active split but keeps the Joint category dormant, so
|
|
293
|
+
sharing it again restores that category. Current-period Joint pay income is
|
|
294
|
+
reconciled; interest and completed periods keep their existing behaviour.
|
|
295
|
+
|
|
296
|
+
Sharing is available only for your booked personal-account transactions when
|
|
297
|
+
you have an active partner and Joint budget. Partner-owned rows and native
|
|
298
|
+
joint-account rows cannot be changed this way. Foreign-currency rows can still
|
|
299
|
+
be shared for settlement, but their returned contribution has `eligible: false`
|
|
300
|
+
and `included: false`.
|
|
301
|
+
|
|
302
|
+
If a combined item omits `assignmentScope`, the category uses Joint when you
|
|
303
|
+
have no exclusive amount and Personal when you do. Category-only items retain
|
|
304
|
+
their existing Personal/native default. Each item commits atomically, while a
|
|
305
|
+
bulk request remains best-effort across items.
|
|
306
|
+
|
|
241
307
|
### Other workflows
|
|
242
308
|
|
|
243
309
|
Read a personal or joint budget. Omit `--period` to use Sloth's current budget period:
|
|
@@ -405,6 +471,20 @@ Missing values are JSON
|
|
|
405
471
|
are excluded, while enabled shared joint accounts follow Sloth's existing
|
|
406
472
|
visibility rules.
|
|
407
473
|
|
|
474
|
+
Use an account's opaque reference to read only its transactions. Transaction
|
|
475
|
+
rows return the same `accountRef`, so pagination and follow-up reads keep the
|
|
476
|
+
account boundary explicit:
|
|
477
|
+
|
|
478
|
+
```bash
|
|
479
|
+
sloth-agent transactions \
|
|
480
|
+
--account-ref PASTE_THE_EXACT_ACCOUNT_REF_HERE \
|
|
481
|
+
--limit 50
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
Copy the value from `sloth-agent accounts`. The older `--account-id` filter is
|
|
485
|
+
still accepted for compatibility, but new workflows should use
|
|
486
|
+
`--account-ref`. Do not provide both filters in one command.
|
|
487
|
+
|
|
408
488
|
Account changes are previews unless `--apply` is present. Connected accounts
|
|
409
489
|
support only goal-savings membership. Manual current accounts support their
|
|
410
490
|
institution, name, currency, and ownership. Manual balance accounts also
|
package/dist/args.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { UsageError } from './errors.js';
|
|
2
|
+
import { isAccountRef } from './account-ref.js';
|
|
2
3
|
import { CATEGORY_TYPES, ICON_KEYS, } from './category-metadata.js';
|
|
3
4
|
import { isGoalType, } from './goal-metadata.js';
|
|
4
5
|
const PRODUCTION_BASE_URL = 'https://budget.slothmoney.app';
|
|
@@ -155,6 +156,18 @@ function parseTransactions(args) {
|
|
|
155
156
|
filters.uncategorized = setOnce(filters.uncategorized, value === 'true', '--uncategorized');
|
|
156
157
|
continue;
|
|
157
158
|
}
|
|
159
|
+
if (argument === '--shared') {
|
|
160
|
+
filters.shared = setOnce(filters.shared, true, '--shared');
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (argument.startsWith('--shared=')) {
|
|
164
|
+
const value = argument.slice('--shared='.length);
|
|
165
|
+
if (value !== 'true' && value !== 'false') {
|
|
166
|
+
throw new UsageError('--shared must be true or false');
|
|
167
|
+
}
|
|
168
|
+
filters.shared = setOnce(filters.shared, value === 'true', '--shared');
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
158
171
|
const [name, inlineValue] = argument.includes('=')
|
|
159
172
|
? argument.split(/=(.*)/s, 2)
|
|
160
173
|
: [argument, undefined];
|
|
@@ -163,6 +176,7 @@ function parseTransactions(args) {
|
|
|
163
176
|
'--start-date',
|
|
164
177
|
'--end-date',
|
|
165
178
|
'--q',
|
|
179
|
+
'--account-ref',
|
|
166
180
|
'--account-id',
|
|
167
181
|
'--category-id',
|
|
168
182
|
'--line-item-id',
|
|
@@ -196,6 +210,9 @@ function parseTransactions(args) {
|
|
|
196
210
|
else if (name === '--q') {
|
|
197
211
|
filters.q = setOnce(filters.q, value, name);
|
|
198
212
|
}
|
|
213
|
+
else if (name === '--account-ref') {
|
|
214
|
+
filters.accountRef = setOnce(filters.accountRef, parseAccountRef(value), name);
|
|
215
|
+
}
|
|
199
216
|
else if (name === '--account-id') {
|
|
200
217
|
filters.accountId = setOnce(filters.accountId, value, name);
|
|
201
218
|
}
|
|
@@ -220,6 +237,9 @@ function parseTransactions(args) {
|
|
|
220
237
|
&& filters.endDate < filters.startDate) {
|
|
221
238
|
throw new UsageError('--end-date must not be before --start-date');
|
|
222
239
|
}
|
|
240
|
+
if (filters.accountRef !== undefined && filters.accountId !== undefined) {
|
|
241
|
+
throw new UsageError('Use either --account-ref or --account-id, not both');
|
|
242
|
+
}
|
|
223
243
|
return filters;
|
|
224
244
|
}
|
|
225
245
|
function parseNamedOptions(args, commandLabel, allowed) {
|
|
@@ -255,7 +275,7 @@ function requiredOption(values, option, commandLabel) {
|
|
|
255
275
|
return value;
|
|
256
276
|
}
|
|
257
277
|
function parseAccountRef(value) {
|
|
258
|
-
if (
|
|
278
|
+
if (!isAccountRef(value)) {
|
|
259
279
|
throw new UsageError('--account-ref must be a valid accountRef from sloth-agent accounts');
|
|
260
280
|
}
|
|
261
281
|
return value;
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import { ICON_KEYS } from './category-metadata.js';
|
|
|
4
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.14.0';
|
|
8
8
|
const REQUEST_TIMEOUT_MS = 60_000;
|
|
9
9
|
const API_ORIGIN_HELP_LINES = [
|
|
10
10
|
'',
|
|
@@ -40,9 +40,9 @@ export function usageText() {
|
|
|
40
40
|
' sloth-agent categories rename --category-id ID --name NAME [--apply]',
|
|
41
41
|
' sloth-agent line-items create --scope personal|joint --category-id ID --name NAME [--apply]',
|
|
42
42
|
' sloth-agent line-items rename --scope personal|joint --category-id ID --line-item-id ID --name NAME [--apply]',
|
|
43
|
-
' sloth-agent transactions [--uncategorized[=true|false]] [--limit N]',
|
|
43
|
+
' sloth-agent transactions [--uncategorized[=true|false]] [--shared[=true|false]] [--limit N]',
|
|
44
44
|
' [--start-date YYYY-MM-DD] [--end-date YYYY-MM-DD] [--q TEXT]',
|
|
45
|
-
' [--account-id ID] [--category-id ID] [--line-item-id ID]',
|
|
45
|
+
' [--account-ref REF] [--account-id ID] [--category-id ID] [--line-item-id ID]',
|
|
46
46
|
' [--cursor CURSOR] [--base-url URL]',
|
|
47
47
|
' sloth-agent assign --input assignments.json [--apply] [--base-url URL]',
|
|
48
48
|
' sloth-agent goals [list] [--base-url URL]',
|
|
@@ -542,15 +542,18 @@ export function transactionsHelpText() {
|
|
|
542
542
|
'Options:',
|
|
543
543
|
' --uncategorized[=true|false] Optional. Filter the selected assignment scope by state;',
|
|
544
544
|
' with no value, use true.',
|
|
545
|
+
' --shared[=true|false] Optional. Filter by partner-sharing state; with no value, use true.',
|
|
545
546
|
' --limit N Optional. Integer from 1 to 200; omit for API default.',
|
|
546
547
|
' --start-date YYYY-MM-DD Optional. Include transactions on or after this date.',
|
|
547
548
|
' --end-date YYYY-MM-DD Optional. Include transactions on or before this date.',
|
|
548
549
|
' --q TEXT Optional. Search transactions by text.',
|
|
549
|
-
' --account-
|
|
550
|
+
' --account-ref REF Optional. Filter by the opaque accountRef from sloth-agent accounts.',
|
|
551
|
+
' Copy the exact sloth_account_v1_... value.',
|
|
552
|
+
' --account-id ID Optional legacy filter by provider or stored account ID.',
|
|
550
553
|
' --category-id ID Optional. Filter by category ID.',
|
|
551
554
|
' --line-item-id ID Optional. Filter primary or split assignments by line-item ID.',
|
|
552
555
|
' --assignment-scope SCOPE Optional. Filter assignments by personal or joint.',
|
|
553
|
-
'
|
|
556
|
+
' The transaction\'s native scope is used when omitted.',
|
|
554
557
|
' --cursor CURSOR Optional. Continue from a previous nextCursor.',
|
|
555
558
|
' --base-url URL Optional. Override the API origin.',
|
|
556
559
|
' -h, --help Show this help.',
|
|
@@ -558,6 +561,7 @@ export function transactionsHelpText() {
|
|
|
558
561
|
'',
|
|
559
562
|
'Constraints:',
|
|
560
563
|
' All filters are omitted by default.',
|
|
564
|
+
' Use either --account-ref or --account-id, not both.',
|
|
561
565
|
' --end-date must not be before --start-date.',
|
|
562
566
|
' The first transaction read each UTC day may refresh linked bank data.',
|
|
563
567
|
' Refresh remotely persists booked transactions and account balances.',
|
|
@@ -565,6 +569,7 @@ export function transactionsHelpText() {
|
|
|
565
569
|
'',
|
|
566
570
|
'Output:',
|
|
567
571
|
' JSON containing transactions, nextCursor, and structured refresh status.',
|
|
572
|
+
' Every transaction includes accountRef for its originating account.',
|
|
568
573
|
' Refresh failures do not hide readable cached transactions.',
|
|
569
574
|
' Personal assignments use the top-level categoryId, lineItemId, and categorySplits.',
|
|
570
575
|
' Joint assignments appear under jointBudgetContribution.',
|
|
@@ -583,8 +588,7 @@ export function assignHelpText() {
|
|
|
583
588
|
return [
|
|
584
589
|
'Sloth Agent CLI — assign',
|
|
585
590
|
'',
|
|
586
|
-
'
|
|
587
|
-
'Validate, preview, or apply category assignments from a JSON file.',
|
|
591
|
+
'Validate, preview, or apply transaction sharing and category assignments from a JSON file.',
|
|
588
592
|
'',
|
|
589
593
|
'Usage:',
|
|
590
594
|
' sloth-agent assign --input FILE [--apply] [--base-url URL]',
|
|
@@ -607,7 +611,11 @@ export function assignHelpText() {
|
|
|
607
611
|
'',
|
|
608
612
|
'Input:',
|
|
609
613
|
' The top-level object must contain an assignments array.',
|
|
610
|
-
' Each assignment requires transactionRef and
|
|
614
|
+
' Each assignment requires transactionRef and at least one category operation or sharing object.',
|
|
615
|
+
' sharing.isShared is required. shareRatio is optional from 0 to 1 and is your share.',
|
|
616
|
+
' userExclusiveAmountPence and partnerExclusiveAmountPence are optional nonnegative integers.',
|
|
617
|
+
' Omitted split values use saved defaults for a first share and preserve an existing split.',
|
|
618
|
+
' Set sharing.isShared to false on its own to unshare and retain the dormant joint category.',
|
|
611
619
|
' Copy the exact transactionRef from transactions output and categoryId from',
|
|
612
620
|
' categories output. The example values below are placeholders.',
|
|
613
621
|
' Set categoryId to null to clear an assignment.',
|
|
@@ -619,7 +627,8 @@ export function assignHelpText() {
|
|
|
619
627
|
' a split lineItemId is optional.',
|
|
620
628
|
' incomeSubtype is optional and accepts "pay", "interest", or null.',
|
|
621
629
|
' assignmentScope is optional and accepts "personal" or "joint".',
|
|
622
|
-
'
|
|
630
|
+
' The transaction\'s native scope is used when assignmentScope is omitted for category-only requests.',
|
|
631
|
+
' Combined requests use Joint when you have no exclusive amount and Personal when you do.',
|
|
623
632
|
'',
|
|
624
633
|
'Workflow:',
|
|
625
634
|
' sloth-agent categories',
|
|
@@ -633,7 +642,8 @@ export function assignHelpText() {
|
|
|
633
642
|
' "assignments": [',
|
|
634
643
|
' {',
|
|
635
644
|
' "transactionRef": "PASTE_THE_EXACT_TRANSACTION_REF_HERE",',
|
|
636
|
-
' "
|
|
645
|
+
' "sharing": { "isShared": true, "shareRatio": 0.6 },',
|
|
646
|
+
' "assignmentScope": "joint",',
|
|
637
647
|
' "categoryId": "PASTE_A_CATEGORY_ID_HERE",',
|
|
638
648
|
' "lineItemId": "PASTE_A_LINE_ITEM_ID_HERE"',
|
|
639
649
|
' }',
|
|
@@ -1018,6 +1028,8 @@ function buildTransactionsQuery(filters) {
|
|
|
1018
1028
|
if (filters.uncategorized !== undefined) {
|
|
1019
1029
|
params.set('uncategorized', String(filters.uncategorized));
|
|
1020
1030
|
}
|
|
1031
|
+
if (filters.shared !== undefined)
|
|
1032
|
+
params.set('shared', String(filters.shared));
|
|
1021
1033
|
if (filters.limit !== undefined)
|
|
1022
1034
|
params.set('limit', String(filters.limit));
|
|
1023
1035
|
if (filters.startDate !== undefined)
|
|
@@ -1026,6 +1038,8 @@ function buildTransactionsQuery(filters) {
|
|
|
1026
1038
|
params.set('endDate', filters.endDate);
|
|
1027
1039
|
if (filters.q !== undefined)
|
|
1028
1040
|
params.set('q', filters.q);
|
|
1041
|
+
if (filters.accountRef !== undefined)
|
|
1042
|
+
params.set('accountRef', filters.accountRef);
|
|
1029
1043
|
if (filters.accountId !== undefined)
|
|
1030
1044
|
params.set('accountId', filters.accountId);
|
|
1031
1045
|
if (filters.categoryId !== undefined)
|
|
@@ -1233,6 +1247,17 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
1233
1247
|
});
|
|
1234
1248
|
return 0;
|
|
1235
1249
|
}
|
|
1250
|
+
const assignmentPayload = parsed.command === 'assign'
|
|
1251
|
+
? validateAssignmentPayload(readAssignmentFile(parsed.input))
|
|
1252
|
+
: undefined;
|
|
1253
|
+
if (parsed.command === 'assign' && !parsed.apply) {
|
|
1254
|
+
writeJson(writeStdout, {
|
|
1255
|
+
dryRun: true,
|
|
1256
|
+
endpoint: `${baseUrl}/api/agent/v1/transaction-assignments`,
|
|
1257
|
+
payload: assignmentPayload,
|
|
1258
|
+
});
|
|
1259
|
+
return 0;
|
|
1260
|
+
}
|
|
1236
1261
|
const credential = await resolveCredential(environment, baseUrl, getCredentialStore);
|
|
1237
1262
|
token = credential.token;
|
|
1238
1263
|
const headers = requestHeaders(token);
|
|
@@ -1417,12 +1442,8 @@ export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
|
1417
1442
|
return 0;
|
|
1418
1443
|
}
|
|
1419
1444
|
if (parsed.command === 'assign') {
|
|
1420
|
-
const payload =
|
|
1445
|
+
const payload = assignmentPayload;
|
|
1421
1446
|
const endpoint = `${baseUrl}/api/agent/v1/transaction-assignments`;
|
|
1422
|
-
if (!parsed.apply) {
|
|
1423
|
-
writeJson(writeStdout, { dryRun: true, endpoint, payload });
|
|
1424
|
-
return 0;
|
|
1425
|
-
}
|
|
1426
1447
|
const response = await fetchImplementation(endpoint, {
|
|
1427
1448
|
method: 'POST',
|
|
1428
1449
|
headers: {
|
package/dist/contracts.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ApiError, UsageError, } from './errors.js';
|
|
2
|
+
import { isAccountRef } from './account-ref.js';
|
|
2
3
|
import { CATEGORY_TYPES, ICON_KEYS } from './category-metadata.js';
|
|
3
4
|
import { isGoalType } from './goal-metadata.js';
|
|
4
5
|
function isObject(value) {
|
|
@@ -46,6 +47,7 @@ function validateAssignment(value, index) {
|
|
|
46
47
|
const assignment = requireObject(value, label);
|
|
47
48
|
rejectUnknownFields(assignment, new Set([
|
|
48
49
|
'transactionRef',
|
|
50
|
+
'sharing',
|
|
49
51
|
'assignmentScope',
|
|
50
52
|
'categoryId',
|
|
51
53
|
'lineItemId',
|
|
@@ -53,6 +55,48 @@ function validateAssignment(value, index) {
|
|
|
53
55
|
'incomeSubtype',
|
|
54
56
|
]), label);
|
|
55
57
|
requireString(assignment.transactionRef, `${label}.transactionRef`);
|
|
58
|
+
let sharing;
|
|
59
|
+
if (assignment.sharing !== undefined) {
|
|
60
|
+
const sharingValue = requireObject(assignment.sharing, `${label}.sharing`);
|
|
61
|
+
rejectUnknownFields(sharingValue, new Set([
|
|
62
|
+
'isShared',
|
|
63
|
+
'shareRatio',
|
|
64
|
+
'userExclusiveAmountPence',
|
|
65
|
+
'partnerExclusiveAmountPence',
|
|
66
|
+
]), `${label}.sharing`);
|
|
67
|
+
if (typeof sharingValue.isShared !== 'boolean') {
|
|
68
|
+
throw new UsageError(`${label}.sharing.isShared must be true or false`);
|
|
69
|
+
}
|
|
70
|
+
if (sharingValue.shareRatio !== undefined
|
|
71
|
+
&& (typeof sharingValue.shareRatio !== 'number'
|
|
72
|
+
|| !Number.isFinite(sharingValue.shareRatio)
|
|
73
|
+
|| sharingValue.shareRatio < 0
|
|
74
|
+
|| sharingValue.shareRatio > 1)) {
|
|
75
|
+
throw new UsageError(`${label}.sharing.shareRatio must be a number from 0 to 1`);
|
|
76
|
+
}
|
|
77
|
+
for (const field of ['userExclusiveAmountPence', 'partnerExclusiveAmountPence']) {
|
|
78
|
+
if (sharingValue[field] !== undefined
|
|
79
|
+
&& (!Number.isSafeInteger(sharingValue[field]) || Number(sharingValue[field]) < 0)) {
|
|
80
|
+
throw new UsageError(`${label}.sharing.${field} must be a nonnegative safe integer`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (sharingValue.isShared === false
|
|
84
|
+
&& (sharingValue.shareRatio !== undefined
|
|
85
|
+
|| sharingValue.userExclusiveAmountPence !== undefined
|
|
86
|
+
|| sharingValue.partnerExclusiveAmountPence !== undefined)) {
|
|
87
|
+
throw new UsageError(`${label}.sharing cannot include split fields when isShared is false`);
|
|
88
|
+
}
|
|
89
|
+
sharing = {
|
|
90
|
+
isShared: sharingValue.isShared,
|
|
91
|
+
...(sharingValue.shareRatio === undefined ? {} : { shareRatio: sharingValue.shareRatio }),
|
|
92
|
+
...(sharingValue.userExclusiveAmountPence === undefined
|
|
93
|
+
? {}
|
|
94
|
+
: { userExclusiveAmountPence: Number(sharingValue.userExclusiveAmountPence) }),
|
|
95
|
+
...(sharingValue.partnerExclusiveAmountPence === undefined
|
|
96
|
+
? {}
|
|
97
|
+
: { partnerExclusiveAmountPence: Number(sharingValue.partnerExclusiveAmountPence) }),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
56
100
|
if (assignment.assignmentScope !== undefined
|
|
57
101
|
&& assignment.assignmentScope !== 'personal'
|
|
58
102
|
&& assignment.assignmentScope !== 'joint') {
|
|
@@ -83,11 +127,20 @@ function validateAssignment(value, index) {
|
|
|
83
127
|
const hasCategory = typeof assignment.categoryId === 'string' && assignment.categoryId.trim().length > 0;
|
|
84
128
|
const isClear = assignment.categoryId === null && (!categorySplits || categorySplits.length === 0);
|
|
85
129
|
const hasSplits = Array.isArray(categorySplits) && categorySplits.length > 0;
|
|
86
|
-
|
|
130
|
+
const hasCategoryOperation = hasCategory || isClear || hasSplits;
|
|
131
|
+
if (!hasCategoryOperation && sharing === undefined) {
|
|
87
132
|
throw new UsageError(`${label}.categoryId or categorySplits is required`);
|
|
88
133
|
}
|
|
134
|
+
if (!hasCategoryOperation
|
|
135
|
+
&& (assignment.assignmentScope !== undefined
|
|
136
|
+
|| assignment.lineItemId !== undefined
|
|
137
|
+
|| assignment.incomeSubtype !== undefined
|
|
138
|
+
|| assignment.categorySplits !== undefined)) {
|
|
139
|
+
throw new UsageError(`${label} category options require categoryId or categorySplits`);
|
|
140
|
+
}
|
|
89
141
|
return {
|
|
90
142
|
transactionRef: assignment.transactionRef,
|
|
143
|
+
...(sharing === undefined ? {} : { sharing }),
|
|
91
144
|
...(assignment.assignmentScope !== undefined
|
|
92
145
|
? { assignmentScope: assignment.assignmentScope }
|
|
93
146
|
: {}),
|
|
@@ -200,6 +253,7 @@ function isTransaction(value) {
|
|
|
200
253
|
&& typeof value.currency === 'string'
|
|
201
254
|
&& typeof value.date === 'string'
|
|
202
255
|
&& value.status === 'booked'
|
|
256
|
+
&& isAccountRef(value.accountRef)
|
|
203
257
|
&& typeof value.accountId === 'string'
|
|
204
258
|
&& typeof value.accountDocId === 'string'
|
|
205
259
|
&& typeof value.requisitionId === 'string'
|
|
@@ -255,13 +309,64 @@ function isTransactionsResponse(value) {
|
|
|
255
309
|
&& isRefreshStatus(value.refresh));
|
|
256
310
|
}
|
|
257
311
|
function isAssignmentResponse(value) {
|
|
312
|
+
const isResponseSplit = (split) => (isObject(split)
|
|
313
|
+
&& hasOnlyFields(split, ['categoryId', 'amountPence', 'lineItemId'])
|
|
314
|
+
&& typeof split.categoryId === 'string'
|
|
315
|
+
&& isNonnegativeSafeInteger(split.amountPence)
|
|
316
|
+
&& split.amountPence > 0
|
|
317
|
+
&& (split.lineItemId === undefined || typeof split.lineItemId === 'string'));
|
|
318
|
+
const isContribution = (contribution) => (isObject(contribution)
|
|
319
|
+
&& hasOnlyFields(contribution, [
|
|
320
|
+
'eligible', 'included', 'amountPence', 'categoryId', 'lineItemId',
|
|
321
|
+
'categorySplits', 'incomeSubtype',
|
|
322
|
+
])
|
|
323
|
+
&& typeof contribution.eligible === 'boolean'
|
|
324
|
+
&& typeof contribution.included === 'boolean'
|
|
325
|
+
&& isNonnegativeSafeInteger(contribution.amountPence)
|
|
326
|
+
&& contribution.amountPence > 0
|
|
327
|
+
&& (contribution.categoryId === null || typeof contribution.categoryId === 'string')
|
|
328
|
+
&& (contribution.lineItemId === null || typeof contribution.lineItemId === 'string')
|
|
329
|
+
&& Array.isArray(contribution.categorySplits)
|
|
330
|
+
&& contribution.categorySplits.every(isResponseSplit)
|
|
331
|
+
&& (contribution.incomeSubtype === null
|
|
332
|
+
|| contribution.incomeSubtype === 'pay'
|
|
333
|
+
|| contribution.incomeSubtype === 'interest'));
|
|
334
|
+
const isSharing = (sharing) => (isObject(sharing)
|
|
335
|
+
&& hasOnlyFields(sharing, [
|
|
336
|
+
'isShared', 'shareRatio', 'sharedAmountPence', 'userExclusiveAmountPence',
|
|
337
|
+
'partnerExclusiveAmountPence', 'jointBudgetContribution',
|
|
338
|
+
])
|
|
339
|
+
&& typeof sharing.isShared === 'boolean'
|
|
340
|
+
&& typeof sharing.shareRatio === 'number'
|
|
341
|
+
&& Number.isFinite(sharing.shareRatio)
|
|
342
|
+
&& sharing.shareRatio >= 0
|
|
343
|
+
&& sharing.shareRatio <= 1
|
|
344
|
+
&& isNonnegativeSafeInteger(sharing.sharedAmountPence)
|
|
345
|
+
&& isNonnegativeSafeInteger(sharing.userExclusiveAmountPence)
|
|
346
|
+
&& isNonnegativeSafeInteger(sharing.partnerExclusiveAmountPence)
|
|
347
|
+
&& (sharing.jointBudgetContribution === null
|
|
348
|
+
|| isContribution(sharing.jointBudgetContribution)));
|
|
349
|
+
const isCategoryResult = (item) => ((item.assignmentScope === 'personal' || item.assignmentScope === 'joint')
|
|
350
|
+
&& (item.categoryId === null || typeof item.categoryId === 'string')
|
|
351
|
+
&& (item.lineItemId === null || typeof item.lineItemId === 'string')
|
|
352
|
+
&& Array.isArray(item.categorySplits)
|
|
353
|
+
&& item.categorySplits.every(isResponseSplit)
|
|
354
|
+
&& (item.incomeSubtype === null
|
|
355
|
+
|| item.incomeSubtype === 'pay'
|
|
356
|
+
|| item.incomeSubtype === 'interest'));
|
|
258
357
|
return (isObject(value)
|
|
259
358
|
&& Array.isArray(value.succeeded)
|
|
260
359
|
&& value.succeeded.every((item) => (isObject(item)
|
|
360
|
+
&& hasOnlyFields(item, [
|
|
361
|
+
'transactionRef', 'categoryId', 'lineItemId', 'categorySplits',
|
|
362
|
+
'incomeSubtype', 'assignmentScope', 'sharing',
|
|
363
|
+
])
|
|
261
364
|
&& typeof item.transactionRef === 'string'
|
|
262
|
-
&& (item
|
|
365
|
+
&& (isCategoryResult(item) || isSharing(item.sharing))
|
|
366
|
+
&& (item.sharing === undefined || isSharing(item.sharing))))
|
|
263
367
|
&& Array.isArray(value.failed)
|
|
264
368
|
&& value.failed.every((item) => (isObject(item)
|
|
369
|
+
&& hasOnlyFields(item, ['transactionRef', 'error'])
|
|
265
370
|
&& typeof item.error === 'string'
|
|
266
371
|
&& (item.transactionRef === undefined || typeof item.transactionRef === 'string'))));
|
|
267
372
|
}
|
|
@@ -518,8 +623,7 @@ function isAccount(value) {
|
|
|
518
623
|
'connectionState',
|
|
519
624
|
'isGoalSavingsSource',
|
|
520
625
|
])
|
|
521
|
-
&&
|
|
522
|
-
&& /^sloth_account_v1_[A-Za-z0-9_-]{43}$/.test(value.accountRef)
|
|
626
|
+
&& isAccountRef(value.accountRef)
|
|
523
627
|
&& isNullableNonEmptyString(value.accountName)
|
|
524
628
|
&& isNullableNonEmptyString(value.institutionName)
|
|
525
629
|
&& (value.accountType === 'current'
|
|
@@ -556,8 +660,7 @@ function isAccountRemovalResponse(value) {
|
|
|
556
660
|
&& hasOnlyFields(value, ['removed', 'changed', 'accountRef'])
|
|
557
661
|
&& value.removed === true
|
|
558
662
|
&& typeof value.changed === 'boolean'
|
|
559
|
-
&&
|
|
560
|
-
&& /^sloth_account_v1_[A-Za-z0-9_-]{43}$/.test(value.accountRef));
|
|
663
|
+
&& isAccountRef(value.accountRef));
|
|
561
664
|
}
|
|
562
665
|
function isInvestmentHolding(value) {
|
|
563
666
|
return (isObject(value)
|