@fleetkeep/cli 0.7.0 → 0.8.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/README.md +26 -0
- package/fleetkeep.mjs +109 -0
- package/operations.mjs +66 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -105,6 +105,28 @@ maintenance updates cannot edit calculated forecast dates. There are no automati
|
|
|
105
105
|
fuel-card feeds in the CLI. Wex credentials and Xero authorisation are entered in
|
|
106
106
|
the website, never through CLI command arguments.
|
|
107
107
|
|
|
108
|
+
## Toll statements and email forwarding
|
|
109
|
+
|
|
110
|
+
```sh
|
|
111
|
+
fleetkeep tolls forwarding
|
|
112
|
+
fleetkeep tolls forwarding enable
|
|
113
|
+
fleetkeep tolls import preview statement.csv
|
|
114
|
+
fleetkeep tolls import apply statement.csv --confirm <digest-from-preview>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The forwarding command shows your private account address, recent imports and
|
|
118
|
+
email delivery results. `enable` creates the address if needed. Forward CSV
|
|
119
|
+
statement attachments to that address; the inbox is checked every 60 minutes.
|
|
120
|
+
Each email accepts up to five attachments of 512 KiB each. Review unmatched
|
|
121
|
+
vehicles and delivery messages in Fleetkeep. Keep the address private.
|
|
122
|
+
|
|
123
|
+
For a local CSV, preview first and review the importable, duplicate and unmatched
|
|
124
|
+
counts. Apply requires the exact preview digest and rejects a changed file before
|
|
125
|
+
sending it. The CLI sends a stable idempotency key in the request header; use
|
|
126
|
+
`--idempotency-key <key>` to choose your own and reuse it for an identical retry.
|
|
127
|
+
Add `--json` to any command for the full API response. These commands require the
|
|
128
|
+
matching toll API deployment. Statement preview requires `tolls:read`; applying an import and both forwarding commands require `tolls:write`.
|
|
129
|
+
|
|
108
130
|
## Safety model
|
|
109
131
|
|
|
110
132
|
- `api operations` is generated as a fixed allowlist of the published OpenAPI operation IDs.
|
|
@@ -168,3 +190,7 @@ with `--confirmation`.
|
|
|
168
190
|
Service-record creation is a two-step operator-confirmed action. The first call returns a 409
|
|
169
191
|
challenge. Retry the identical body and idempotency key with its `--confirmation` token. A safe
|
|
170
192
|
retry with the same key returns the original record instead of creating a duplicate.
|
|
193
|
+
|
|
194
|
+
The private email-forwarding address requires `tolls:write`, including when reading it: anyone holding that address can submit statements. Read-only integrations do not receive it.
|
|
195
|
+
|
|
196
|
+
CLI toll statement imports accept CSV files up to 256 KiB, the published API limit. Email forwarding accepts up to five CSV attachments of 512 KiB each. Preview and confirm the exact file digest before a CLI import is applied.
|
package/fleetkeep.mjs
CHANGED
|
@@ -40,6 +40,10 @@ Usage:
|
|
|
40
40
|
fleetkeep jobs reopen <job-id> [--json]
|
|
41
41
|
fleetkeep import preview <file.csv|file.xlsx> [--json]
|
|
42
42
|
fleetkeep import apply <file.csv|file.xlsx> --confirm <digest> [--idempotency-key <key>] [--json]
|
|
43
|
+
fleetkeep tolls forwarding [--json]
|
|
44
|
+
fleetkeep tolls forwarding enable [--json]
|
|
45
|
+
fleetkeep tolls import preview <file.csv> [--json]
|
|
46
|
+
fleetkeep tolls import apply <file.csv> --confirm <digest> [--idempotency-key <key>] [--json]
|
|
43
47
|
fleetkeep api operations [--json]
|
|
44
48
|
fleetkeep api call <operation-id> [API options]
|
|
45
49
|
|
|
@@ -445,6 +449,93 @@ function printImport(payload, jsonOutput) {
|
|
|
445
449
|
}
|
|
446
450
|
}
|
|
447
451
|
|
|
452
|
+
function printTollForwarding(payload, jsonOutput) {
|
|
453
|
+
const lines = [
|
|
454
|
+
`Email forwarding: ${payload.available === false ? 'not available' : payload.enabled ? 'enabled' : 'not enabled'}`,
|
|
455
|
+
...(payload.address ? [`Forward CSV statements to: ${payload.address}`] : []),
|
|
456
|
+
`Inbox checked every ${payload.pollIntervalMinutes.toLocaleString('en-GB')} minutes.`,
|
|
457
|
+
...(payload.enabled || payload.available === false
|
|
458
|
+
? []
|
|
459
|
+
: ['To create your forwarding address: fleetkeep tolls forwarding enable']),
|
|
460
|
+
...payload.recentImports.map(
|
|
461
|
+
(item) =>
|
|
462
|
+
`${item.fileName}: ${item.importedCount.toLocaleString('en-GB')} imported, ${item.unmatchedCount.toLocaleString('en-GB')} unmatched, ${item.duplicateCount.toLocaleString('en-GB')} duplicates (${item.status})`
|
|
463
|
+
),
|
|
464
|
+
...payload.recentDeliveries.map((item) => `${item.createdAt}: ${item.message}`)
|
|
465
|
+
];
|
|
466
|
+
printJsonOr(payload, jsonOutput, lines.join('\n'));
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
async function tollImport(action, fileArg, args, jsonOutput) {
|
|
470
|
+
const allowed = new Set(
|
|
471
|
+
action === 'apply' ? ['--json', '--confirm', '--idempotency-key'] : ['--json']
|
|
472
|
+
);
|
|
473
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
474
|
+
const name = args[index];
|
|
475
|
+
if (!allowed.has(name)) throw new Error(`Unknown option: ${name}.`);
|
|
476
|
+
if (name !== '--json') {
|
|
477
|
+
requiredValue(args, name);
|
|
478
|
+
index += 1;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
const confirmDigest = requiredValue(args, '--confirm');
|
|
482
|
+
if (action === 'apply' && !/^[0-9a-f]{64}$/.test(confirmDigest || '')) {
|
|
483
|
+
throw new Error(
|
|
484
|
+
'Apply is blocked until --confirm contains the exact digest returned by preview.'
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
const idempotencyKey = requiredValue(args, '--idempotency-key');
|
|
488
|
+
if (!fileArg || fileArg.startsWith('--') || extname(fileArg).toLowerCase() !== '.csv') {
|
|
489
|
+
throw new Error('A .csv toll statement is required.');
|
|
490
|
+
}
|
|
491
|
+
const body = await spreadsheetPayload(fileArg);
|
|
492
|
+
if (Buffer.byteLength(body.csv, 'utf8') > 256 * 1024) {
|
|
493
|
+
throw new Error(
|
|
494
|
+
'CLI toll imports accept CSV files up to 256 KiB. Split a larger statement first.'
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
if (
|
|
498
|
+
action === 'apply' &&
|
|
499
|
+
createHash('sha256').update(body.csv, 'utf8').digest('hex') !== confirmDigest
|
|
500
|
+
) {
|
|
501
|
+
throw new Error('The file does not match --confirm. Preview this file again before applying.');
|
|
502
|
+
}
|
|
503
|
+
const payload = await request(`/api/v1/tolls/imports/${action}`, {
|
|
504
|
+
method: 'POST',
|
|
505
|
+
body: action === 'apply' ? { ...body, confirmDigest } : body,
|
|
506
|
+
...(action === 'apply'
|
|
507
|
+
? { headers: { 'idempotency-key': idempotencyKey || `cli:tolls:${confirmDigest}` } }
|
|
508
|
+
: {})
|
|
509
|
+
});
|
|
510
|
+
if (jsonOutput) {
|
|
511
|
+
printJsonOr(payload, true, '');
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const count = (value) => value.toLocaleString('en-GB');
|
|
515
|
+
const lines =
|
|
516
|
+
action === 'preview'
|
|
517
|
+
? [
|
|
518
|
+
`Source: ${payload.sourceName}`,
|
|
519
|
+
`Digest: ${payload.digest}`,
|
|
520
|
+
`Rows: ${count(payload.rowCount)}`,
|
|
521
|
+
`Importable: ${count(payload.importableCount)}`,
|
|
522
|
+
`Duplicates: ${count(payload.duplicateCount)}`,
|
|
523
|
+
`Unmatched vehicles: ${count(payload.unknownCount)}`,
|
|
524
|
+
'',
|
|
525
|
+
'No data was changed. To apply this exact file:',
|
|
526
|
+
`fleetkeep tolls import apply ${JSON.stringify(fileArg)} --confirm ${payload.digest}`
|
|
527
|
+
]
|
|
528
|
+
: [
|
|
529
|
+
payload.replay
|
|
530
|
+
? 'Toll import result replayed from the original request.'
|
|
531
|
+
: 'Toll import completed.',
|
|
532
|
+
`Imported: ${count(payload.imported)}`,
|
|
533
|
+
`Duplicates: ${count(payload.duplicates)}`,
|
|
534
|
+
`Unmatched vehicles: ${count(payload.unknown)}`
|
|
535
|
+
];
|
|
536
|
+
process.stdout.write(lines.join('\n') + '\n');
|
|
537
|
+
}
|
|
538
|
+
|
|
448
539
|
async function main() {
|
|
449
540
|
const args = process.argv.slice(2);
|
|
450
541
|
const jsonOutput = args.includes('--json');
|
|
@@ -458,6 +549,24 @@ async function main() {
|
|
|
458
549
|
return;
|
|
459
550
|
}
|
|
460
551
|
|
|
552
|
+
if (args[0] === 'tolls' && args[1] === 'forwarding') {
|
|
553
|
+
const rest = args.slice(2).filter((arg) => arg !== '--json');
|
|
554
|
+
if (rest.length > 1 || (rest.length === 1 && rest[0] !== 'enable')) {
|
|
555
|
+
throw new Error('Use fleetkeep tolls forwarding [enable] [--json].');
|
|
556
|
+
}
|
|
557
|
+
const payload = await request(
|
|
558
|
+
'/api/v1/tolls/forwarding',
|
|
559
|
+
rest[0] === 'enable' ? { method: 'POST', body: {} } : {}
|
|
560
|
+
);
|
|
561
|
+
printTollForwarding(payload, jsonOutput);
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (args[0] === 'tolls' && args[1] === 'import' && ['preview', 'apply'].includes(args[2])) {
|
|
566
|
+
await tollImport(args[2], args[3], args.slice(4), jsonOutput);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
|
|
461
570
|
if (args[0] === 'costs' && args[1] === 'list') {
|
|
462
571
|
const query = [];
|
|
463
572
|
for (const name of ['from', 'to']) {
|
package/operations.mjs
CHANGED
|
@@ -1,5 +1,68 @@
|
|
|
1
1
|
/** @type {Array<[string, string, string, string, Record<string, unknown>?]>} */
|
|
2
2
|
const definitions = [
|
|
3
|
+
['getFuelPrices', 'GET', '/api/fuel-prices', 'operations:read'],
|
|
4
|
+
[
|
|
5
|
+
'getFuelEconomy',
|
|
6
|
+
'GET',
|
|
7
|
+
'/api/fuel-economy',
|
|
8
|
+
'operations:read',
|
|
9
|
+
{ query: ['year', 'make', 'model'] }
|
|
10
|
+
],
|
|
11
|
+
[
|
|
12
|
+
'listFuelEconomyModels',
|
|
13
|
+
'GET',
|
|
14
|
+
'/api/fuel-economy/models',
|
|
15
|
+
'operations:read',
|
|
16
|
+
{ query: ['year', 'make'] }
|
|
17
|
+
],
|
|
18
|
+
[
|
|
19
|
+
'getChargingPrices',
|
|
20
|
+
'GET',
|
|
21
|
+
'/api/charging-prices',
|
|
22
|
+
'operations:read',
|
|
23
|
+
{ query: ['region', 'days'] }
|
|
24
|
+
],
|
|
25
|
+
['getPublicChargingPrices', 'GET', '/api/public-charging-prices', 'operations:read'],
|
|
26
|
+
[
|
|
27
|
+
'replaceServiceSchedule',
|
|
28
|
+
'POST',
|
|
29
|
+
'/api/v1/vehicles/{id}/service-schedules',
|
|
30
|
+
'maintenance:write',
|
|
31
|
+
{ idempotency: true }
|
|
32
|
+
],
|
|
33
|
+
[
|
|
34
|
+
'prepareServiceEvidenceRequest',
|
|
35
|
+
'POST',
|
|
36
|
+
'/api/v1/vehicles/{id}/service-evidence-requests',
|
|
37
|
+
'maintenance:write',
|
|
38
|
+
{ idempotency: true }
|
|
39
|
+
],
|
|
40
|
+
[
|
|
41
|
+
'recordServiceEvidenceContact',
|
|
42
|
+
'POST',
|
|
43
|
+
'/api/v1/vehicles/{id}/service-evidence-requests/{requestId}/contacts',
|
|
44
|
+
'maintenance:write',
|
|
45
|
+
{ idempotency: true }
|
|
46
|
+
],
|
|
47
|
+
[
|
|
48
|
+
'recordServiceEvidenceSubmission',
|
|
49
|
+
'POST',
|
|
50
|
+
'/api/v1/vehicles/{id}/service-evidence-requests/{requestId}/submissions',
|
|
51
|
+
'maintenance:write',
|
|
52
|
+
{ idempotency: true }
|
|
53
|
+
],
|
|
54
|
+
[
|
|
55
|
+
'reviewServiceEvidenceSubmission',
|
|
56
|
+
'POST',
|
|
57
|
+
'/api/v1/vehicles/{id}/service-evidence-requests/{requestId}/submissions/{submissionId}/review',
|
|
58
|
+
'maintenance:write',
|
|
59
|
+
{ idempotency: true }
|
|
60
|
+
],
|
|
61
|
+
['listDrivers', 'GET', '/api/v1/drivers', 'operations:read'],
|
|
62
|
+
['getDriverActivity', 'GET', '/api/v1/drivers/activity', 'operations:read', { query: ['q'] }],
|
|
63
|
+
['listDriverMemberships', 'GET', '/api/v1/driver-workspace', 'vehicles:read'],
|
|
64
|
+
['getDriverWorkspace', 'GET', '/api/v1/driver-workspace/{id}', 'operations:read'],
|
|
65
|
+
['getPushNotificationStatus', 'GET', '/api/v1/settings/push-notifications', 'reminders:read'],
|
|
3
66
|
['importProviderJobs', 'POST', '/api/v1/jobs/import', 'operations:write'],
|
|
4
67
|
['listFleetJobs', 'GET', '/api/v1/jobs', 'operations:read', { query: ['q'] }],
|
|
5
68
|
['saveFleetJob', 'POST', '/api/v1/jobs', 'operations:write'],
|
|
@@ -14,6 +77,7 @@ const definitions = [
|
|
|
14
77
|
],
|
|
15
78
|
['listVehicles', 'GET', '/api/v1/vehicles', 'vehicles:read'],
|
|
16
79
|
['addVehicle', 'POST', '/api/v1/vehicles', 'vehicles:write'],
|
|
80
|
+
['searchVehicles', 'GET', '/api/v1/vehicles/search', 'vehicles:read', { query: ['q', 'limit'] }],
|
|
17
81
|
['getVehicle', 'GET', '/api/v1/vehicles/{id}', 'vehicles:read'],
|
|
18
82
|
['updateVehicle', 'PATCH', '/api/v1/vehicles/{id}', 'vehicles:write'],
|
|
19
83
|
['deleteVehicle', 'DELETE', '/api/v1/vehicles/{id}', 'vehicles:write', { confirmation: true }],
|
|
@@ -236,6 +300,8 @@ const definitions = [
|
|
|
236
300
|
'tolls:write',
|
|
237
301
|
{ idempotency: true }
|
|
238
302
|
],
|
|
303
|
+
['getTollForwarding', 'GET', '/api/v1/tolls/forwarding', 'tolls:write'],
|
|
304
|
+
['enableTollForwarding', 'POST', '/api/v1/tolls/forwarding', 'tolls:write'],
|
|
239
305
|
['previewTollImport', 'POST', '/api/v1/tolls/imports/preview', 'tolls:read'],
|
|
240
306
|
['applyTollImport', 'POST', '/api/v1/tolls/imports/apply', 'tolls:write', { idempotency: true }],
|
|
241
307
|
['listTrackingSnapshots', 'GET', '/api/v1/operations/tracking', 'operations:read'],
|