@fleetbase/fleetops-engine 0.6.55 → 0.6.56

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.
Files changed (109) hide show
  1. package/addon/components/admin/map-settings.hbs +1 -1
  2. package/addon/components/device/details.js +1 -1
  3. package/addon/components/device-event/details.hbs +192 -0
  4. package/addon/components/device-event/details.js +151 -0
  5. package/addon/components/driver/schedule.js +1 -1
  6. package/addon/components/layout/fleet-ops-sidebar.js +15 -9
  7. package/addon/components/telematic/details.hbs +1 -1
  8. package/addon/components/telematic/form.hbs +2 -2
  9. package/addon/components/telematic/settings.hbs +1 -1
  10. package/addon/controllers/connectivity/devices/index/details/events.js +1 -1
  11. package/addon/controllers/connectivity/devices/index/details/vehicle.js +2 -2
  12. package/addon/controllers/connectivity/events/details.js +68 -1
  13. package/addon/controllers/connectivity/sensors/index.js +4 -0
  14. package/addon/controllers/connectivity/telematics/details/events.js +1 -1
  15. package/addon/controllers/connectivity/telematics/details/sensors.js +4 -0
  16. package/addon/controllers/settings/map.js +15 -0
  17. package/addon/instance-initializers/register-vroom-allocation.js +1 -1
  18. package/addon/routes/connectivity/events/details.js +26 -1
  19. package/addon/routes/management/vehicles/index/details.js +3 -1
  20. package/addon/services/map-adapter/google.js +25 -0
  21. package/addon/services/map-adapter/leaflet.js +1 -1
  22. package/addon/services/map-adapter-interface.js +1 -1
  23. package/addon/services/map-manager.js +4 -1
  24. package/addon/services/map-settings.js +15 -0
  25. package/addon/styles/fleetops-engine.css +1 -1
  26. package/addon/templates/connectivity/devices/index/details/vehicle.hbs +2 -1
  27. package/addon/templates/connectivity/events/details.hbs +40 -1
  28. package/addon/templates/settings/map.hbs +38 -9
  29. package/addon/utils/leaflet-plugin-loader.js +5 -5
  30. package/addon/utils/route-colors.js +1 -1
  31. package/app/components/device-event/details.hbs +192 -0
  32. package/app/components/device-event/details.js +1 -0
  33. package/composer.json +1 -1
  34. package/extension.json +1 -1
  35. package/package.json +2 -2
  36. package/server/migrations/2026_06_23_000001_delete_bad_afaqy_telematics_sensors.php +34 -0
  37. package/server/src/Exports/ContactExport.php +12 -5
  38. package/server/src/Exports/DeviceExport.php +92 -0
  39. package/server/src/Exports/DriverExport.php +28 -3
  40. package/server/src/Exports/EquipmentExport.php +94 -0
  41. package/server/src/Exports/FleetExport.php +22 -7
  42. package/server/src/Exports/FuelReportExport.php +16 -1
  43. package/server/src/Exports/IssueExport.php +25 -3
  44. package/server/src/Exports/MaintenanceExport.php +106 -0
  45. package/server/src/Exports/MaintenanceScheduleExport.php +99 -0
  46. package/server/src/Exports/OrderExport.php +24 -5
  47. package/server/src/Exports/PartExport.php +99 -0
  48. package/server/src/Exports/PlaceExport.php +33 -7
  49. package/server/src/Exports/SensorExport.php +94 -0
  50. package/server/src/Exports/ServiceAreaExport.php +31 -7
  51. package/server/src/Exports/ServiceRateExport.php +41 -1
  52. package/server/src/Exports/TelematicExport.php +90 -0
  53. package/server/src/Exports/VehicleExport.php +274 -8
  54. package/server/src/Exports/VendorExport.php +15 -2
  55. package/server/src/Exports/WorkOrderExport.php +92 -0
  56. package/server/src/Http/Controllers/Api/v1/Concerns/ResolvesFleetOpsApiResources.php +176 -0
  57. package/server/src/Http/Controllers/Api/v1/DeviceController.php +225 -0
  58. package/server/src/Http/Controllers/Api/v1/EquipmentController.php +114 -0
  59. package/server/src/Http/Controllers/Api/v1/FuelTransactionController.php +185 -0
  60. package/server/src/Http/Controllers/Api/v1/PartController.php +120 -0
  61. package/server/src/Http/Controllers/Api/v1/SensorController.php +132 -0
  62. package/server/src/Http/Controllers/Api/v1/WorkOrderController.php +161 -0
  63. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +18 -0
  64. package/server/src/Http/Controllers/Internal/v1/EquipmentController.php +17 -0
  65. package/server/src/Http/Controllers/Internal/v1/MaintenanceController.php +17 -0
  66. package/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php +17 -0
  67. package/server/src/Http/Controllers/Internal/v1/PartController.php +17 -0
  68. package/server/src/Http/Controllers/Internal/v1/SensorController.php +18 -0
  69. package/server/src/Http/Controllers/Internal/v1/SettingController.php +29 -5
  70. package/server/src/Http/Controllers/Internal/v1/TelematicController.php +18 -0
  71. package/server/src/Http/Controllers/Internal/v1/WorkOrderController.php +17 -0
  72. package/server/src/Http/Filter/DeviceFilter.php +33 -10
  73. package/server/src/Http/Filter/EquipmentFilter.php +23 -0
  74. package/server/src/Http/Filter/FuelProviderTransactionFilter.php +57 -2
  75. package/server/src/Http/Filter/PartFilter.php +47 -0
  76. package/server/src/Http/Filter/SensorFilter.php +34 -6
  77. package/server/src/Http/Filter/WorkOrderFilter.php +23 -0
  78. package/server/src/Http/Requests/CreateDeviceRequest.php +51 -0
  79. package/server/src/Http/Requests/CreateEquipmentRequest.php +35 -0
  80. package/server/src/Http/Requests/CreateFuelTransactionRequest.php +50 -0
  81. package/server/src/Http/Requests/CreatePartRequest.php +40 -0
  82. package/server/src/Http/Requests/CreateSensorRequest.php +47 -0
  83. package/server/src/Http/Requests/CreateWorkOrderRequest.php +42 -0
  84. package/server/src/Http/Requests/UpdateDeviceRequest.php +7 -0
  85. package/server/src/Http/Requests/UpdateEquipmentRequest.php +7 -0
  86. package/server/src/Http/Requests/UpdateFuelTransactionRequest.php +7 -0
  87. package/server/src/Http/Requests/UpdatePartRequest.php +7 -0
  88. package/server/src/Http/Requests/UpdateSensorRequest.php +7 -0
  89. package/server/src/Http/Requests/UpdateWorkOrderRequest.php +7 -0
  90. package/server/src/Http/Resources/v1/Device.php +72 -0
  91. package/server/src/Http/Resources/v1/Equipment.php +57 -0
  92. package/server/src/Http/Resources/v1/FuelTransaction.php +69 -0
  93. package/server/src/Http/Resources/v1/Part.php +64 -0
  94. package/server/src/Http/Resources/v1/Sensor.php +68 -0
  95. package/server/src/Jobs/SyncTelematicDevicesJob.php +194 -38
  96. package/server/src/Models/Order.php +2 -1
  97. package/server/src/Observers/PurchaseRateObserver.php +2 -1
  98. package/server/src/Support/Analytics/RevenueTrend.php +3 -8
  99. package/server/src/Support/Metrics/ActiveRevenueQuery.php +121 -0
  100. package/server/src/Support/Metrics/AvgOrderValueMetric.php +1 -7
  101. package/server/src/Support/Metrics/EarningsMetric.php +1 -10
  102. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +104 -5
  103. package/server/src/Support/Telematics/Providers/SafeeProvider.php +550 -54
  104. package/server/src/Support/Telematics/TelematicService.php +28 -9
  105. package/server/src/routes.php +67 -14
  106. package/server/tests/ExportCoverageTest.php +79 -0
  107. package/server/tests/MetricsRegistryTest.php +45 -5
  108. package/server/tests/PublicFleetOpsRestEndpointsTest.php +191 -0
  109. package/server/tests/TelematicsHardeningTest.php +636 -1
@@ -194,19 +194,30 @@ class TelematicService
194
194
  {
195
195
  $device = $this->linkDevice($telematic, $provider->normalizeDevice($payload));
196
196
 
197
- $event = null;
197
+ $event = null;
198
+ $events = [];
198
199
  try {
199
- $eventData = $provider->normalizeEvent($payload);
200
- if ($this->hasEventSignal($eventData)) {
201
- $event = $this->storeDeviceEvent($telematic, $eventData, $device);
200
+ $eventPayloads = method_exists($provider, 'normalizeEvents') ? $provider->normalizeEvents($payload) : [];
201
+ if (empty($eventPayloads)) {
202
+ $eventPayloads = [$provider->normalizeEvent($payload)];
203
+ }
204
+
205
+ foreach ($eventPayloads as $eventData) {
206
+ if ($this->hasEventSignal($eventData)) {
207
+ $storedEvent = $this->storeDeviceEvent($telematic, $eventData, $device);
208
+ $events[] = $storedEvent;
209
+ $event ??= $storedEvent;
210
+ }
202
211
  }
203
212
  } catch (\Throwable) {
204
- $event = null;
213
+ $event = null;
214
+ $events = [];
205
215
  }
206
216
 
207
217
  return [
208
218
  'device' => $device,
209
219
  'event' => $event,
220
+ 'events' => $events,
210
221
  'sensors' => $this->storeSnapshotSensors($telematic, $provider, $payload, $device),
211
222
  ];
212
223
  }
@@ -663,6 +674,10 @@ class TelematicService
663
674
  $vehicle->altitude = $eventData['altitude'];
664
675
  }
665
676
 
677
+ if (array_key_exists('odometer', $eventData) && $eventData['odometer'] !== null) {
678
+ $vehicle->odometer = $eventData['odometer'];
679
+ }
680
+
666
681
  $vehicle->telematics = array_merge($vehicle->telematics ?? [], [
667
682
  'last_event_uuid' => $event->uuid,
668
683
  'last_event_id' => $event->public_id,
@@ -757,13 +772,17 @@ class TelematicService
757
772
  return collect($rawSensors)
758
773
  ->map(function ($sensor, $name) {
759
774
  if (is_array($sensor)) {
760
- return array_merge(['name' => $name], $sensor);
775
+ return array_merge([
776
+ 'sensor_key' => $name,
777
+ 'name' => $name,
778
+ ], $sensor);
761
779
  }
762
780
 
763
781
  return [
764
- 'name' => $name,
765
- 'type' => $name,
766
- 'value' => $sensor,
782
+ 'sensor_key' => $name,
783
+ 'name' => $name,
784
+ 'type' => $name,
785
+ 'value' => $sensor,
767
786
  ];
768
787
  })
769
788
  ->values()
@@ -105,6 +105,61 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
105
105
  $router->put('{id}', 'FuelReportController@update');
106
106
  $router->delete('{id}', 'FuelReportController@delete');
107
107
  });
108
+ // fuel-transactions routes
109
+ $router->group(['prefix' => 'fuel-transactions'], function () use ($router) {
110
+ $router->post('/', 'FuelTransactionController@create');
111
+ $router->get('/', 'FuelTransactionController@query');
112
+ $router->get('{id}', 'FuelTransactionController@find');
113
+ $router->put('{id}', 'FuelTransactionController@update');
114
+ $router->delete('{id}', 'FuelTransactionController@delete');
115
+ $router->post('{id}/match-vehicle', 'FuelTransactionController@matchVehicle');
116
+ $router->post('{id}/match-order', 'FuelTransactionController@matchOrder');
117
+ $router->post('{id}/reprocess', 'FuelTransactionController@reprocess');
118
+ $router->post('{id}/review', 'FuelTransactionController@review');
119
+ });
120
+ // equipment routes
121
+ $router->group(['prefix' => 'equipment'], function () use ($router) {
122
+ $router->post('/', 'EquipmentController@create');
123
+ $router->get('/', 'EquipmentController@query');
124
+ $router->get('{id}', 'EquipmentController@find');
125
+ $router->put('{id}', 'EquipmentController@update');
126
+ $router->delete('{id}', 'EquipmentController@delete');
127
+ });
128
+ // parts routes
129
+ $router->group(['prefix' => 'parts'], function () use ($router) {
130
+ $router->post('/', 'PartController@create');
131
+ $router->get('/', 'PartController@query');
132
+ $router->get('{id}', 'PartController@find');
133
+ $router->put('{id}', 'PartController@update');
134
+ $router->delete('{id}', 'PartController@delete');
135
+ });
136
+ // work-orders routes
137
+ $router->group(['prefix' => 'work-orders'], function () use ($router) {
138
+ $router->post('/', 'WorkOrderController@create');
139
+ $router->get('/', 'WorkOrderController@query');
140
+ $router->get('{id}', 'WorkOrderController@find');
141
+ $router->put('{id}', 'WorkOrderController@update');
142
+ $router->delete('{id}', 'WorkOrderController@delete');
143
+ $router->post('{id}/send', 'WorkOrderController@send');
144
+ });
145
+ // devices routes
146
+ $router->group(['prefix' => 'devices'], function () use ($router) {
147
+ $router->post('/', 'DeviceController@create');
148
+ $router->get('/', 'DeviceController@query');
149
+ $router->get('{id}', 'DeviceController@find');
150
+ $router->put('{id}', 'DeviceController@update');
151
+ $router->delete('{id}', 'DeviceController@delete');
152
+ $router->post('{id}/attach', 'DeviceController@attach');
153
+ $router->post('{id}/detach', 'DeviceController@detach');
154
+ });
155
+ // sensors routes
156
+ $router->group(['prefix' => 'sensors'], function () use ($router) {
157
+ $router->post('/', 'SensorController@create');
158
+ $router->get('/', 'SensorController@query');
159
+ $router->get('{id}', 'SensorController@find');
160
+ $router->put('{id}', 'SensorController@update');
161
+ $router->delete('{id}', 'SensorController@delete');
162
+ });
108
163
  // orders routes
109
164
  $router->group(['prefix' => 'orders', 'middleware' => []], function () use ($router) {
110
165
  $router->post('/', 'OrderController@create');
@@ -315,7 +370,6 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
315
370
  $router->get('facilitators/{id}', $controller('getAsFacilitator'));
316
371
  $router->get('customers/{id}', $controller('getAsCustomer'));
317
372
  $router->post('{id}/convert-to-vendor', $controller('convertToVendor'));
318
- $router->delete('bulk-delete', $controller('bulkDelete'));
319
373
  }
320
374
  );
321
375
  $router->fleetbaseRoutes(
@@ -330,7 +384,6 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
330
384
  $router->post('{id}/assign-vehicle', $controller('assignVehicle'));
331
385
  $router->post('{id}/unassign-vehicle', $controller('unassignVehicle'));
332
386
  $router->match(['get', 'post'], 'export', $controller('export'));
333
- $router->delete('bulk-delete', $controller('bulkDelete'));
334
387
  $router->post('import', $controller('import'));
335
388
  // Driver scheduling endpoints
336
389
  $router->get('{id}/schedule-items', $controller('scheduleItems'));
@@ -349,7 +402,6 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
349
402
  $router->post('remove-vehicle', $controller('removeVehicle'));
350
403
  $router->match(['get', 'post'], 'export', $controller('export'));
351
404
  $router->post('import', $controller('import'));
352
- $router->delete('bulk-delete', $controller('bulkDelete'));
353
405
  }
354
406
  );
355
407
  $router->fleetbaseRoutes(
@@ -357,7 +409,6 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
357
409
  function ($router, $controller) {
358
410
  $router->match(['get', 'post'], 'export', $controller('export'));
359
411
  $router->post('import', $controller('import'));
360
- $router->delete('bulk-delete', $controller('bulkDelete'));
361
412
  }
362
413
  );
363
414
  $router->fleetbaseRoutes(
@@ -385,14 +436,12 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
385
436
  function ($router, $controller) {
386
437
  $router->match(['get', 'post'], 'export', $controller('export'));
387
438
  $router->post('import', $controller('import'));
388
- $router->delete('bulk-delete', $controller('bulkDelete'));
389
439
  }
390
440
  );
391
441
  $router->fleetbaseRoutes(
392
442
  'integrated-vendors',
393
443
  function ($router, $controller) {
394
444
  $router->get('supported', $controller('getSupported'));
395
- $router->delete('bulk-delete', $controller('bulkDelete'));
396
445
  }
397
446
  );
398
447
  $router->fleetbaseRoutes(
@@ -421,7 +470,6 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
421
470
  $router->patch('start', $controller('start'));
422
471
  // Scheduler: set scheduled_at + driver without triggering dispatch
423
472
  $router->patch('schedule', $controller('scheduleOrder'));
424
- $router->delete('bulk-delete', $controller('bulkDelete'));
425
473
  $router->match(['get', 'post'], 'export', $controller('export'));
426
474
  }
427
475
  );
@@ -434,7 +482,6 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
434
482
  $router->get('lookup', $controller('geocode'))->middleware(['cache.headers:private;max_age=3600']);
435
483
  $router->get('avatars', $controller('avatars'));
436
484
  $router->match(['get', 'post'], 'export', $controller('export'));
437
- $router->delete('bulk-delete', $controller('bulkDelete'));
438
485
  $router->post('import', $controller('import'));
439
486
  }
440
487
  );
@@ -449,7 +496,6 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
449
496
  'service-areas',
450
497
  function ($router, $controller) {
451
498
  $router->match(['get', 'post'], 'export', $controller('export'));
452
- $router->delete('bulk-delete', $controller('bulkDelete'));
453
499
  }
454
500
  );
455
501
  $router->group(
@@ -473,7 +519,6 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
473
519
  $router->fleetbaseRoutes(
474
520
  'service-rates',
475
521
  function ($router, $controller) {
476
- $router->delete('bulk-delete', $controller('bulkDelete'));
477
522
  $router->get('for-route', $controller('getServicesForRoute'));
478
523
  $router->match(['get', 'post'], 'export', $controller('export'));
479
524
  }
@@ -493,7 +538,6 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
493
538
  $router->post('{id}/detach-device', $controller('detachDevice'));
494
539
  $router->match(['get', 'post'], 'export', $controller('export'));
495
540
  $router->post('import', $controller('import'));
496
- $router->delete('bulk-delete', $controller('bulkDelete'));
497
541
  }
498
542
  );
499
543
  $router->fleetbaseRoutes('vehicle-devices');
@@ -509,18 +553,22 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
509
553
  $router->delete('{id}/personnels/{contact}', $controller('removeVendorPersonnel'));
510
554
  $router->post('{id}/assign-driver', $controller('assignDriver'));
511
555
  $router->post('{id}/remove-driver', $controller('removeDriver'));
512
- $router->delete('bulk-delete', $controller('bulkDelete'));
513
556
  $router->post('import', $controller('import'));
514
557
  }
515
558
  );
516
- $router->fleetbaseRoutes('devices');
559
+ $router->fleetbaseRoutes('devices', function ($router, $controller) {
560
+ $router->match(['get', 'post'], 'export', $controller('export'));
561
+ });
517
562
  $router->post('devices/{id}/attach', 'DeviceController@attach');
518
563
  $router->post('devices/{id}/detach', 'DeviceController@detach');
519
564
  $router->fleetbaseRoutes('device-events', function ($router, $controller) {
520
565
  $router->post('{id}/mark-processed', $controller('markProcessed'));
521
566
  });
522
- $router->fleetbaseRoutes('sensors');
567
+ $router->fleetbaseRoutes('sensors', function ($router, $controller) {
568
+ $router->match(['get', 'post'], 'export', $controller('export'));
569
+ });
523
570
  $router->fleetbaseRoutes('telematics', function ($router, $controller) {
571
+ $router->match(['get', 'post'], 'export', $controller('export'));
524
572
  $router->get('providers', $controller('providers'));
525
573
  $router->get('{id}/logs', $controller('logs'));
526
574
  $router->get('{id}/devices', $controller('devices'));
@@ -530,6 +578,7 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
530
578
  $router->post('{key}/test-credentials', $controller('testCredentials'));
531
579
  });
532
580
  $router->fleetbaseRoutes('maintenance-schedules', function ($router, $controller) {
581
+ $router->match(['get', 'post'], 'export', $controller('export'));
533
582
  $router->post('import', $controller('import'));
534
583
  $router->post('{id}/pause', $controller('pause'));
535
584
  $router->post('{id}/resume', $controller('resume'));
@@ -538,19 +587,23 @@ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\Fleet
538
587
  $router->get('{id}/ical', $controller('ical'));
539
588
  });
540
589
  $router->fleetbaseRoutes('work-orders', function ($router, $controller) {
590
+ $router->match(['get', 'post'], 'export', $controller('export'));
541
591
  $router->post('import', $controller('import'));
542
592
  $router->post('{id}/send', $controller('sendEmail'));
543
593
  });
544
594
  $router->fleetbaseRoutes('maintenances', function ($router, $controller) {
595
+ $router->match(['get', 'post'], 'export', $controller('export'));
545
596
  $router->post('import', $controller('import'));
546
597
  $router->post('{id}/line-items', $controller('addLineItem'));
547
598
  $router->put('{id}/line-items/{index}', $controller('updateLineItem'));
548
599
  $router->delete('{id}/line-items/{index}', $controller('removeLineItem'));
549
600
  });
550
601
  $router->fleetbaseRoutes('equipment', function ($router, $controller) {
602
+ $router->match(['get', 'post'], 'export', $controller('export'));
551
603
  $router->post('import', $controller('import'));
552
604
  });
553
605
  $router->fleetbaseRoutes('parts', function ($router, $controller) {
606
+ $router->match(['get', 'post'], 'export', $controller('export'));
554
607
  $router->post('import', $controller('import'));
555
608
  });
556
609
  $router->fleetbaseRoutes('warranties');
@@ -0,0 +1,79 @@
1
+ <?php
2
+
3
+ use Fleetbase\FleetOps\Exports\DeviceExport;
4
+ use Fleetbase\FleetOps\Exports\MaintenanceExport;
5
+ use Fleetbase\FleetOps\Exports\MaintenanceScheduleExport;
6
+ use Fleetbase\FleetOps\Exports\VehicleExport;
7
+ use Fleetbase\FleetOps\Exports\WorkOrderExport;
8
+
9
+ test('expanded export headings include important operational fields', function () {
10
+ expect((new VehicleExport())->headings())
11
+ ->toContain('Plate Number')
12
+ ->toContain('VIN')
13
+ ->toContain('Fuel Card Number')
14
+ ->toContain('Status')
15
+ ->toContain('Body Type')
16
+ ->toContain('Usage Type')
17
+ ->toContain('Transmission')
18
+ ->toContain('Measurement System')
19
+ ->toContain('Engine Number')
20
+ ->toContain('Engine Size (L)')
21
+ ->toContain('Payload Capacity (kg)')
22
+ ->toContain('Payload Volume (m3)')
23
+ ->toContain('DPF Equipped')
24
+ ->toContain('Insurance Value')
25
+ ->toContain('Loan Amount')
26
+ ->toContain('Vehicle Skills')
27
+ ->toContain('Updated At');
28
+
29
+ expect((new WorkOrderExport())->headings())
30
+ ->toContain('Assignee')
31
+ ->toContain('Target')
32
+ ->toContain('Due At')
33
+ ->toContain('Completion Percentage');
34
+
35
+ expect((new DeviceExport())->headings())
36
+ ->toContain('Connection Status')
37
+ ->toContain('Attached To')
38
+ ->toContain('Telematic Provider')
39
+ ->toContain('Last Seen');
40
+ });
41
+
42
+ test('relationship exports use readable name columns', function () {
43
+ expect((new VehicleExport())->headings())
44
+ ->toContain('Driver')
45
+ ->toContain('Vendor');
46
+
47
+ expect((new WorkOrderExport())->headings())
48
+ ->toContain('Assignee')
49
+ ->toContain('Target');
50
+
51
+ expect((new MaintenanceExport())->headings())
52
+ ->toContain('Asset')
53
+ ->toContain('Performed By');
54
+
55
+ expect((new MaintenanceScheduleExport())->headings())
56
+ ->toContain('Subject')
57
+ ->toContain('Default Assignee');
58
+ });
59
+
60
+ test('vehicle export helpers format spreadsheet friendly values', function () {
61
+ $export = new VehicleExport();
62
+ $helper = fn (string $method, ...$arguments) => (new ReflectionMethod($export, $method))->invoke($export, ...$arguments);
63
+
64
+ expect($helper('yesNo', true))->toBe('Yes')
65
+ ->and($helper('yesNo', false))->toBe('No')
66
+ ->and($helper('yesNo', null))->toBeNull()
67
+ ->and($helper('joinSkills', ['hazmat', 'refrigerated']))->toBe('hazmat, refrigerated')
68
+ ->and($helper('joinSkills', []))->toBeNull();
69
+ });
70
+
71
+ test('resources with visible export actions have backend export routes', function () {
72
+ $routes = file_get_contents(dirname(__DIR__) . '/src/routes.php');
73
+
74
+ foreach (['devices', 'sensors', 'telematics', 'maintenance-schedules', 'work-orders', 'maintenances', 'equipment', 'parts'] as $resource) {
75
+ expect($routes)->toContain("\$router->fleetbaseRoutes('{$resource}'");
76
+ }
77
+
78
+ expect(substr_count($routes, "\$router->match(['get', 'post'], 'export', \$controller('export'));"))->toBeGreaterThanOrEqual(19);
79
+ });
@@ -4,6 +4,7 @@ use Fleetbase\FleetOps\Support\Metrics\AbstractMetric;
4
4
  use Fleetbase\FleetOps\Support\Metrics\OrdersInProgressMetric;
5
5
  use Fleetbase\FleetOps\Support\Metrics\Registry;
6
6
  use Fleetbase\FleetOps\Support\Metrics\TotalTimeTraveledMetric;
7
+ use Fleetbase\Models\Transaction;
7
8
 
8
9
  test('registry exposes every known metric slug', function () {
9
10
  $slugs = Registry::slugs();
@@ -66,13 +67,52 @@ test('money metrics return float values to preserve cents', function () {
66
67
  });
67
68
 
68
69
  test('money metrics filter by currency to avoid mixed-currency sums', function () {
69
- $earnings = file_get_contents(dirname(__DIR__) . '/src/Support/Metrics/EarningsMetric.php');
70
- $fuelCosts = file_get_contents(dirname(__DIR__) . '/src/Support/Metrics/FuelCostsMetric.php');
71
- $aov = file_get_contents(dirname(__DIR__) . '/src/Support/Metrics/AvgOrderValueMetric.php');
70
+ $earnings = file_get_contents(dirname(__DIR__) . '/src/Support/Metrics/EarningsMetric.php');
71
+ $fuelCosts = file_get_contents(dirname(__DIR__) . '/src/Support/Metrics/FuelCostsMetric.php');
72
+ $aov = file_get_contents(dirname(__DIR__) . '/src/Support/Metrics/AvgOrderValueMetric.php');
73
+ $activeRevenue = file_get_contents(dirname(__DIR__) . '/src/Support/Metrics/ActiveRevenueQuery.php');
72
74
 
73
- expect($earnings)->toContain("->where('currency'");
75
+ expect($earnings)->toContain('ActiveRevenueQuery::forCompany');
74
76
  expect($fuelCosts)->toContain("->where('currency'");
75
- expect($aov)->toContain("->where('currency'");
77
+ expect($aov)->toContain('ActiveRevenueQuery::forCompany');
78
+ expect($activeRevenue)->toContain("->where('currency'");
79
+ });
80
+
81
+ test('earnings use the shared active revenue query', function () {
82
+ $earnings = file_get_contents(dirname(__DIR__) . '/src/Support/Metrics/EarningsMetric.php');
83
+ $aov = file_get_contents(dirname(__DIR__) . '/src/Support/Metrics/AvgOrderValueMetric.php');
84
+ $revenueTrend = file_get_contents(dirname(__DIR__) . '/src/Support/Analytics/RevenueTrend.php');
85
+
86
+ expect($earnings)->toContain('ActiveRevenueQuery::forCompany');
87
+ expect($aov)->toContain('ActiveRevenueQuery::forCompany');
88
+ expect($revenueTrend)->toContain('ActiveRevenueQuery::forCompany');
89
+ });
90
+
91
+ test('active revenue query excludes inactive financial and operational lifecycle records', function () {
92
+ $source = file_get_contents(dirname(__DIR__) . '/src/Support/Metrics/ActiveRevenueQuery.php');
93
+
94
+ expect($source)->toContain("where('direction', Transaction::DIRECTION_CREDIT)");
95
+ expect($source)->toContain("whereIn('status', self::ACTIVE_STATUSES)");
96
+ expect($source)->toContain("whereNull('voided_at')");
97
+ expect($source)->toContain("whereNull('reversed_at')");
98
+ expect($source)->toContain("whereNull('parent_transaction_uuid')");
99
+ expect($source)->toContain('excludeInactiveOrders');
100
+ expect($source)->toContain('excludeInactiveInvoices');
101
+ expect($source)->toContain('ledger_invoices.deleted_at');
102
+ expect($source)->toContain('orders.deleted_at');
103
+ expect(Fleetbase\FleetOps\Support\Metrics\ActiveRevenueQuery::ACTIVE_STATUSES)->toBe([Transaction::STATUS_SUCCESS]);
104
+ expect(Fleetbase\FleetOps\Support\Metrics\ActiveRevenueQuery::ACTIVE_STATUSES)->not->toContain('completed');
105
+ expect(Fleetbase\FleetOps\Support\Metrics\ActiveRevenueQuery::ACTIVE_STATUSES)->not->toContain('paid');
106
+ });
107
+
108
+ test('active revenue query treats invoice status as invalidation only', function () {
109
+ $inactiveInvoiceStatuses = Fleetbase\FleetOps\Support\Metrics\ActiveRevenueQuery::INACTIVE_INVOICE_STATUSES;
110
+
111
+ expect($inactiveInvoiceStatuses)->not->toContain('draft');
112
+ expect($inactiveInvoiceStatuses)->toContain('void');
113
+ expect($inactiveInvoiceStatuses)->toContain('voided');
114
+ expect($inactiveInvoiceStatuses)->toContain('cancelled');
115
+ expect($inactiveInvoiceStatuses)->toContain('canceled');
76
116
  });
77
117
 
78
118
  test('ordersInProgress uses an explicit allowlist rather than an exclusion list', function () {
@@ -0,0 +1,191 @@
1
+ <?php
2
+
3
+ use Fleetbase\FleetOps\Http\Controllers\Api\v1\DeviceController;
4
+ use Fleetbase\FleetOps\Http\Controllers\Api\v1\EquipmentController;
5
+ use Fleetbase\FleetOps\Http\Controllers\Api\v1\FuelTransactionController;
6
+ use Fleetbase\FleetOps\Http\Controllers\Api\v1\PartController;
7
+ use Fleetbase\FleetOps\Http\Controllers\Api\v1\SensorController;
8
+ use Fleetbase\FleetOps\Http\Controllers\Api\v1\WorkOrderController;
9
+ use Fleetbase\FleetOps\Http\Resources\v1\Device;
10
+ use Fleetbase\FleetOps\Http\Resources\v1\Equipment;
11
+ use Fleetbase\FleetOps\Http\Resources\v1\FuelTransaction;
12
+ use Fleetbase\FleetOps\Http\Resources\v1\Part;
13
+ use Fleetbase\FleetOps\Http\Resources\v1\Sensor;
14
+ use Fleetbase\FleetOps\Http\Resources\v1\WorkOrder;
15
+
16
+ test('new first-class fleetops rest routes are registered in the consumable v1 namespace', function () {
17
+ $routes = file_get_contents(dirname(__DIR__) . '/src/routes.php');
18
+
19
+ foreach (['equipment', 'parts', 'work-orders', 'devices', 'sensors', 'fuel-transactions'] as $prefix) {
20
+ expect($routes)->toContain("\$router->group(['prefix' => '{$prefix}']");
21
+ }
22
+
23
+ expect($routes)
24
+ ->toContain('EquipmentController@create')
25
+ ->toContain('PartController@create')
26
+ ->toContain('WorkOrderController@send')
27
+ ->toContain('DeviceController@attach')
28
+ ->toContain('DeviceController@detach')
29
+ ->toContain('SensorController@create')
30
+ ->toContain('FuelTransactionController@matchVehicle')
31
+ ->toContain('FuelTransactionController@matchOrder')
32
+ ->toContain('FuelTransactionController@reprocess')
33
+ ->toContain('FuelTransactionController@review');
34
+ });
35
+
36
+ test('public fuel transaction api keeps provider naming internal only', function () {
37
+ $publicRoutes = file_get_contents(dirname(__DIR__) . '/src/routes.php');
38
+ $publicGroup = substr($publicRoutes, 0, strpos($publicRoutes, 'Internal FleetOps API Routes'));
39
+ $controller = file_get_contents(dirname(__DIR__) . '/src/Http/Controllers/Api/v1/FuelTransactionController.php');
40
+
41
+ expect($publicGroup)
42
+ ->toContain("['prefix' => 'fuel-transactions']")
43
+ ->not->toContain("['prefix' => 'fuel-provider-transactions']")
44
+ ->and($controller)
45
+ ->toContain('class FuelTransactionController')
46
+ ->toContain('FuelProviderTransaction')
47
+ ->toContain("'FuelTransaction resource not found.'");
48
+ });
49
+
50
+ test('new public controllers expose the expected rest methods', function () {
51
+ $controllers = [
52
+ EquipmentController::class => ['create', 'query', 'find', 'update', 'delete'],
53
+ PartController::class => ['create', 'query', 'find', 'update', 'delete'],
54
+ WorkOrderController::class => ['create', 'query', 'find', 'update', 'delete', 'send'],
55
+ DeviceController::class => ['create', 'query', 'find', 'update', 'delete', 'attach', 'detach'],
56
+ SensorController::class => ['create', 'query', 'find', 'update', 'delete'],
57
+ FuelTransactionController::class => ['create', 'query', 'find', 'update', 'delete', 'matchVehicle', 'matchOrder', 'reprocess', 'review'],
58
+ ];
59
+
60
+ foreach ($controllers as $controller => $methods) {
61
+ foreach ($methods as $method) {
62
+ expect(method_exists($controller, $method))->toBeTrue("{$controller}::{$method} missing");
63
+ }
64
+ }
65
+ });
66
+
67
+ test('new public resources preserve internal uuid fields behind internal request checks', function () {
68
+ foreach ([Equipment::class, Part::class, WorkOrder::class, Device::class, Sensor::class, FuelTransaction::class] as $resource) {
69
+ expect(class_exists($resource))->toBeTrue();
70
+ }
71
+
72
+ $internalFields = [
73
+ 'Equipment.php' => ['uuid', 'company_uuid', 'warranty_uuid', 'photo_uuid', 'equipable_uuid'],
74
+ 'Part.php' => ['uuid', 'company_uuid', 'vendor_uuid', 'warranty_uuid', 'photo_uuid', 'asset_uuid'],
75
+ 'Device.php' => ['uuid', 'company_uuid', 'telematic_uuid', 'attachable_uuid', 'warranty_uuid', 'photo_uuid'],
76
+ 'Sensor.php' => ['uuid', 'company_uuid', 'device_uuid', 'warranty_uuid', 'telematic_uuid', 'photo_uuid', 'sensorable_uuid'],
77
+ 'FuelTransaction.php' => ['uuid', 'fuel_provider_connection_uuid', 'fuel_report_uuid', 'vehicle_uuid', 'driver_uuid', 'order_uuid'],
78
+ 'WorkOrder.php' => ['uuid', 'company_uuid', 'schedule_uuid', 'created_by_uuid', 'updated_by_uuid', 'target_uuid', 'assignee_uuid'],
79
+ ];
80
+
81
+ foreach ($internalFields as $resourceFile => $fields) {
82
+ $source = file_get_contents(dirname(__DIR__) . '/src/Http/Resources/v1/' . $resourceFile);
83
+
84
+ expect($source)->toContain("'id'");
85
+
86
+ foreach ($fields as $field) {
87
+ expect($source)
88
+ ->toContain("'{$field}'")
89
+ ->toMatch("/'{$field}'\\s*=>\\s*\\\$this->when\\(Http::isInternalRequest\\(\\)/");
90
+ }
91
+ }
92
+ });
93
+
94
+ test('new shared resources do not expose unguarded uuid fields', function () {
95
+ foreach ([
96
+ 'Equipment.php',
97
+ 'Part.php',
98
+ 'Device.php',
99
+ 'Sensor.php',
100
+ 'FuelTransaction.php',
101
+ 'WorkOrder.php',
102
+ ] as $resourceFile) {
103
+ $source = file_get_contents(dirname(__DIR__) . '/src/Http/Resources/v1/' . $resourceFile);
104
+
105
+ preg_match_all("/'([^']*uuid)'\\s*=>\\s*([^,\\n]+)/i", $source, $matches, PREG_SET_ORDER);
106
+
107
+ foreach ($matches as $match) {
108
+ expect($match[2])->toContain('Http::isInternalRequest()');
109
+ }
110
+ }
111
+ });
112
+
113
+ test('new shared resources preserve public and internal relationship shapes', function () {
114
+ foreach ([
115
+ 'Equipment.php',
116
+ 'Part.php',
117
+ 'Device.php',
118
+ 'Sensor.php',
119
+ 'FuelTransaction.php',
120
+ ] as $resourceFile) {
121
+ $source = file_get_contents(dirname(__DIR__) . '/src/Http/Resources/v1/' . $resourceFile);
122
+
123
+ expect($source)
124
+ ->toContain('resolveLoadedRelation')
125
+ ->toContain('Resolve::httpResourceForModel($model)')
126
+ ->toContain('$model->public_id');
127
+ }
128
+ });
129
+
130
+ test('new public resolver rejects uuid lookup while preserving public and internal identifiers', function () {
131
+ $resolver = file_get_contents(dirname(__DIR__) . '/src/Http/Controllers/Api/v1/Concerns/ResolvesFleetOpsApiResources.php');
132
+
133
+ expect($resolver)
134
+ ->not->toContain("where('uuid', \$id)")
135
+ ->not->toContain("orWhere('uuid', \$id)")
136
+ ->toContain("orWhere('public_id', \$id)")
137
+ ->toContain("orWhere('internal_id', \$id)")
138
+ ->toContain('rejectUuidIdentifiers')
139
+ ->toContain('Use public_id or internal_id values instead');
140
+ });
141
+
142
+ test('public controllers reject uuid shaped request keys', function () {
143
+ foreach ([
144
+ 'EquipmentController.php',
145
+ 'PartController.php',
146
+ 'WorkOrderController.php',
147
+ 'DeviceController.php',
148
+ 'SensorController.php',
149
+ 'FuelTransactionController.php',
150
+ ] as $controllerFile) {
151
+ $source = file_get_contents(dirname(__DIR__) . '/src/Http/Controllers/Api/v1/' . $controllerFile);
152
+
153
+ expect($source)->toContain('rejectUuidIdentifiers($request)');
154
+ }
155
+ });
156
+
157
+ test('new public list endpoints have company scoped filters', function () {
158
+ foreach ([
159
+ 'EquipmentFilter.php',
160
+ 'PartFilter.php',
161
+ 'WorkOrderFilter.php',
162
+ 'DeviceFilter.php',
163
+ 'SensorFilter.php',
164
+ 'FuelProviderTransactionFilter.php',
165
+ ] as $filterFile) {
166
+ $source = file_get_contents(dirname(__DIR__) . '/src/Http/Filter/' . $filterFile);
167
+
168
+ expect($source)
169
+ ->toContain('public function queryForPublic()')
170
+ ->toContain("where('company_uuid', \$this->session->get('company'))");
171
+ }
172
+ });
173
+
174
+ test('fuel transaction filter applies public company scoping and public id relation filters without raw uuid matching', function () {
175
+ $filter = file_get_contents(dirname(__DIR__) . '/src/Http/Filter/FuelProviderTransactionFilter.php');
176
+
177
+ expect($filter)
178
+ ->toContain('public function queryForPublic()')
179
+ ->toContain('$this->queryForInternal();')
180
+ ->toContain('public function vehicle(?string $vehicle)')
181
+ ->toContain('public function driver(?string $driver)')
182
+ ->toContain('public function order(?string $order)')
183
+ ->toContain('public function fuelReport(?string $fuelReport)')
184
+ ->toContain("where('public_id', \$identifier)")
185
+ ->toContain("orWhere('internal_id', \$identifier)")
186
+ ->toContain('if ($allowUuid)')
187
+ ->not->toContain("where('vehicle_uuid', \$vehicle)")
188
+ ->not->toContain("where('driver_uuid', \$driver)")
189
+ ->not->toContain("where('order_uuid', \$order)")
190
+ ->not->toContain("where('fuel_report_uuid', \$fuelReport)");
191
+ });