@fleetkeep/cli 0.7.0 → 0.8.1

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,15 @@ 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 PDF or CSV attachments of 512 KiB each. PDF charges always wait for approval. Preview and confirm the exact file digest before a CLI import is applied.
197
+
198
+ ### PDF invoices
199
+
200
+ Forward the original PDF invoice to the account’s private forwarding address, or upload it in Toll Control. PDF charges wait for review before saving. CSV statements remain supported. Text-based GBP PDFs from recognised UK toll providers are supported; scanned, encrypted or ambiguous documents return an actionable error rather than guessed charges.
201
+
202
+ `fleetkeep api call listTollPdfReviews` lists the signed-in account’s extracted rows and confirmation digests. Check the rows against the original invoice, then use `fleetkeep api call confirmTollPdfReview --data-file reviewed-pdf.json` with `batchId`, `confirmDigest`, `csv` and `confirmed: true`. The CSV is the reviewed structured data, not a file the customer needs to prepare. Keep the digest from the current review. Repeat confirmation is idempotent; a different account cannot read or confirm the invoice.
203
+
204
+ These operations require a CLI release containing the PDF operation definitions. The backend candidate must be deployed before use.
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,94 @@ 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 PDF invoices or CSV statements to: ${payload.address}`] : []),
456
+ 'PDF charges wait for review: fleetkeep api call listTollPdfReviews',
457
+ `Inbox checked every ${payload.pollIntervalMinutes.toLocaleString('en-GB')} minutes.`,
458
+ ...(payload.enabled || payload.available === false
459
+ ? []
460
+ : ['To create your forwarding address: fleetkeep tolls forwarding enable']),
461
+ ...payload.recentImports.map(
462
+ (item) =>
463
+ `${item.fileName}: ${item.importedCount.toLocaleString('en-GB')} imported, ${item.unmatchedCount.toLocaleString('en-GB')} unmatched, ${item.duplicateCount.toLocaleString('en-GB')} duplicates (${item.status})`
464
+ ),
465
+ ...payload.recentDeliveries.map((item) => `${item.createdAt}: ${item.message}`)
466
+ ];
467
+ printJsonOr(payload, jsonOutput, lines.join('\n'));
468
+ }
469
+
470
+ async function tollImport(action, fileArg, args, jsonOutput) {
471
+ const allowed = new Set(
472
+ action === 'apply' ? ['--json', '--confirm', '--idempotency-key'] : ['--json']
473
+ );
474
+ for (let index = 0; index < args.length; index += 1) {
475
+ const name = args[index];
476
+ if (!allowed.has(name)) throw new Error(`Unknown option: ${name}.`);
477
+ if (name !== '--json') {
478
+ requiredValue(args, name);
479
+ index += 1;
480
+ }
481
+ }
482
+ const confirmDigest = requiredValue(args, '--confirm');
483
+ if (action === 'apply' && !/^[0-9a-f]{64}$/.test(confirmDigest || '')) {
484
+ throw new Error(
485
+ 'Apply is blocked until --confirm contains the exact digest returned by preview.'
486
+ );
487
+ }
488
+ const idempotencyKey = requiredValue(args, '--idempotency-key');
489
+ if (!fileArg || fileArg.startsWith('--') || extname(fileArg).toLowerCase() !== '.csv') {
490
+ throw new Error('A .csv toll statement is required.');
491
+ }
492
+ const body = await spreadsheetPayload(fileArg);
493
+ if (Buffer.byteLength(body.csv, 'utf8') > 256 * 1024) {
494
+ throw new Error(
495
+ 'CLI toll imports accept CSV files up to 256 KiB. Split a larger statement first.'
496
+ );
497
+ }
498
+ if (
499
+ action === 'apply' &&
500
+ createHash('sha256').update(body.csv, 'utf8').digest('hex') !== confirmDigest
501
+ ) {
502
+ throw new Error('The file does not match --confirm. Preview this file again before applying.');
503
+ }
504
+ const payload = await request(`/api/v1/tolls/imports/${action}`, {
505
+ method: 'POST',
506
+ body: action === 'apply' ? { ...body, confirmDigest } : body,
507
+ ...(action === 'apply'
508
+ ? { headers: { 'idempotency-key': idempotencyKey || `cli:tolls:${confirmDigest}` } }
509
+ : {})
510
+ });
511
+ if (jsonOutput) {
512
+ printJsonOr(payload, true, '');
513
+ return;
514
+ }
515
+ const count = (value) => value.toLocaleString('en-GB');
516
+ const lines =
517
+ action === 'preview'
518
+ ? [
519
+ `Source: ${payload.sourceName}`,
520
+ `Digest: ${payload.digest}`,
521
+ `Rows: ${count(payload.rowCount)}`,
522
+ `Importable: ${count(payload.importableCount)}`,
523
+ `Duplicates: ${count(payload.duplicateCount)}`,
524
+ `Unmatched vehicles: ${count(payload.unknownCount)}`,
525
+ '',
526
+ 'No data was changed. To apply this exact file:',
527
+ `fleetkeep tolls import apply ${JSON.stringify(fileArg)} --confirm ${payload.digest}`
528
+ ]
529
+ : [
530
+ payload.replay
531
+ ? 'Toll import result replayed from the original request.'
532
+ : 'Toll import completed.',
533
+ `Imported: ${count(payload.imported)}`,
534
+ `Duplicates: ${count(payload.duplicates)}`,
535
+ `Unmatched vehicles: ${count(payload.unknown)}`
536
+ ];
537
+ process.stdout.write(lines.join('\n') + '\n');
538
+ }
539
+
448
540
  async function main() {
449
541
  const args = process.argv.slice(2);
450
542
  const jsonOutput = args.includes('--json');
@@ -458,6 +550,24 @@ async function main() {
458
550
  return;
459
551
  }
460
552
 
553
+ if (args[0] === 'tolls' && args[1] === 'forwarding') {
554
+ const rest = args.slice(2).filter((arg) => arg !== '--json');
555
+ if (rest.length > 1 || (rest.length === 1 && rest[0] !== 'enable')) {
556
+ throw new Error('Use fleetkeep tolls forwarding [enable] [--json].');
557
+ }
558
+ const payload = await request(
559
+ '/api/v1/tolls/forwarding',
560
+ rest[0] === 'enable' ? { method: 'POST', body: {} } : {}
561
+ );
562
+ printTollForwarding(payload, jsonOutput);
563
+ return;
564
+ }
565
+
566
+ if (args[0] === 'tolls' && args[1] === 'import' && ['preview', 'apply'].includes(args[2])) {
567
+ await tollImport(args[2], args[3], args.slice(4), jsonOutput);
568
+ return;
569
+ }
570
+
461
571
  if (args[0] === 'costs' && args[1] === 'list') {
462
572
  const query = [];
463
573
  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,10 @@ const definitions = [
236
300
  'tolls:write',
237
301
  { idempotency: true }
238
302
  ],
303
+ ['listTollPdfReviews', 'GET', '/api/v1/tolls/pdf-reviews', 'tolls:read'],
304
+ ['confirmTollPdfReview', 'POST', '/api/v1/tolls/pdf-reviews', 'tolls:write'],
305
+ ['getTollForwarding', 'GET', '/api/v1/tolls/forwarding', 'tolls:write'],
306
+ ['enableTollForwarding', 'POST', '/api/v1/tolls/forwarding', 'tolls:write'],
239
307
  ['previewTollImport', 'POST', '/api/v1/tolls/imports/preview', 'tolls:read'],
240
308
  ['applyTollImport', 'POST', '/api/v1/tolls/imports/apply', 'tolls:write', { idempotency: true }],
241
309
  ['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.1",
4
4
  "description": "Operate every published Fleetkeep API workflow with guarded writes and machine-readable output",
5
5
  "type": "module",
6
6
  "bin": {