@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 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'],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fleetkeep/cli",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Operate every published Fleetkeep API workflow with guarded writes and machine-readable output",
5
5
  "type": "module",
6
6
  "bin": {