@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.
- package/addon/components/admin/map-settings.hbs +1 -1
- package/addon/components/device/details.js +1 -1
- package/addon/components/device-event/details.hbs +192 -0
- package/addon/components/device-event/details.js +151 -0
- package/addon/components/driver/schedule.js +1 -1
- package/addon/components/layout/fleet-ops-sidebar.js +15 -9
- package/addon/components/telematic/details.hbs +1 -1
- package/addon/components/telematic/form.hbs +2 -2
- package/addon/components/telematic/settings.hbs +1 -1
- package/addon/controllers/connectivity/devices/index/details/events.js +1 -1
- package/addon/controllers/connectivity/devices/index/details/vehicle.js +2 -2
- package/addon/controllers/connectivity/events/details.js +68 -1
- package/addon/controllers/connectivity/sensors/index.js +4 -0
- package/addon/controllers/connectivity/telematics/details/events.js +1 -1
- package/addon/controllers/connectivity/telematics/details/sensors.js +4 -0
- package/addon/controllers/settings/map.js +15 -0
- package/addon/instance-initializers/register-vroom-allocation.js +1 -1
- package/addon/routes/connectivity/events/details.js +26 -1
- package/addon/routes/management/vehicles/index/details.js +3 -1
- package/addon/services/map-adapter/google.js +25 -0
- package/addon/services/map-adapter/leaflet.js +1 -1
- package/addon/services/map-adapter-interface.js +1 -1
- package/addon/services/map-manager.js +4 -1
- package/addon/services/map-settings.js +15 -0
- package/addon/styles/fleetops-engine.css +1 -1
- package/addon/templates/connectivity/devices/index/details/vehicle.hbs +2 -1
- package/addon/templates/connectivity/events/details.hbs +40 -1
- package/addon/templates/settings/map.hbs +38 -9
- package/addon/utils/leaflet-plugin-loader.js +5 -5
- package/addon/utils/route-colors.js +1 -1
- package/app/components/device-event/details.hbs +192 -0
- package/app/components/device-event/details.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +2 -2
- package/server/migrations/2026_06_23_000001_delete_bad_afaqy_telematics_sensors.php +34 -0
- package/server/src/Exports/ContactExport.php +12 -5
- package/server/src/Exports/DeviceExport.php +92 -0
- package/server/src/Exports/DriverExport.php +28 -3
- package/server/src/Exports/EquipmentExport.php +94 -0
- package/server/src/Exports/FleetExport.php +22 -7
- package/server/src/Exports/FuelReportExport.php +16 -1
- package/server/src/Exports/IssueExport.php +25 -3
- package/server/src/Exports/MaintenanceExport.php +106 -0
- package/server/src/Exports/MaintenanceScheduleExport.php +99 -0
- package/server/src/Exports/OrderExport.php +24 -5
- package/server/src/Exports/PartExport.php +99 -0
- package/server/src/Exports/PlaceExport.php +33 -7
- package/server/src/Exports/SensorExport.php +94 -0
- package/server/src/Exports/ServiceAreaExport.php +31 -7
- package/server/src/Exports/ServiceRateExport.php +41 -1
- package/server/src/Exports/TelematicExport.php +90 -0
- package/server/src/Exports/VehicleExport.php +274 -8
- package/server/src/Exports/VendorExport.php +15 -2
- package/server/src/Exports/WorkOrderExport.php +92 -0
- package/server/src/Http/Controllers/Api/v1/Concerns/ResolvesFleetOpsApiResources.php +176 -0
- package/server/src/Http/Controllers/Api/v1/DeviceController.php +225 -0
- package/server/src/Http/Controllers/Api/v1/EquipmentController.php +114 -0
- package/server/src/Http/Controllers/Api/v1/FuelTransactionController.php +185 -0
- package/server/src/Http/Controllers/Api/v1/PartController.php +120 -0
- package/server/src/Http/Controllers/Api/v1/SensorController.php +132 -0
- package/server/src/Http/Controllers/Api/v1/WorkOrderController.php +161 -0
- package/server/src/Http/Controllers/Internal/v1/DeviceController.php +18 -0
- package/server/src/Http/Controllers/Internal/v1/EquipmentController.php +17 -0
- package/server/src/Http/Controllers/Internal/v1/MaintenanceController.php +17 -0
- package/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php +17 -0
- package/server/src/Http/Controllers/Internal/v1/PartController.php +17 -0
- package/server/src/Http/Controllers/Internal/v1/SensorController.php +18 -0
- package/server/src/Http/Controllers/Internal/v1/SettingController.php +29 -5
- package/server/src/Http/Controllers/Internal/v1/TelematicController.php +18 -0
- package/server/src/Http/Controllers/Internal/v1/WorkOrderController.php +17 -0
- package/server/src/Http/Filter/DeviceFilter.php +33 -10
- package/server/src/Http/Filter/EquipmentFilter.php +23 -0
- package/server/src/Http/Filter/FuelProviderTransactionFilter.php +57 -2
- package/server/src/Http/Filter/PartFilter.php +47 -0
- package/server/src/Http/Filter/SensorFilter.php +34 -6
- package/server/src/Http/Filter/WorkOrderFilter.php +23 -0
- package/server/src/Http/Requests/CreateDeviceRequest.php +51 -0
- package/server/src/Http/Requests/CreateEquipmentRequest.php +35 -0
- package/server/src/Http/Requests/CreateFuelTransactionRequest.php +50 -0
- package/server/src/Http/Requests/CreatePartRequest.php +40 -0
- package/server/src/Http/Requests/CreateSensorRequest.php +47 -0
- package/server/src/Http/Requests/CreateWorkOrderRequest.php +42 -0
- package/server/src/Http/Requests/UpdateDeviceRequest.php +7 -0
- package/server/src/Http/Requests/UpdateEquipmentRequest.php +7 -0
- package/server/src/Http/Requests/UpdateFuelTransactionRequest.php +7 -0
- package/server/src/Http/Requests/UpdatePartRequest.php +7 -0
- package/server/src/Http/Requests/UpdateSensorRequest.php +7 -0
- package/server/src/Http/Requests/UpdateWorkOrderRequest.php +7 -0
- package/server/src/Http/Resources/v1/Device.php +72 -0
- package/server/src/Http/Resources/v1/Equipment.php +57 -0
- package/server/src/Http/Resources/v1/FuelTransaction.php +69 -0
- package/server/src/Http/Resources/v1/Part.php +64 -0
- package/server/src/Http/Resources/v1/Sensor.php +68 -0
- package/server/src/Jobs/SyncTelematicDevicesJob.php +194 -38
- package/server/src/Models/Order.php +2 -1
- package/server/src/Observers/PurchaseRateObserver.php +2 -1
- package/server/src/Support/Analytics/RevenueTrend.php +3 -8
- package/server/src/Support/Metrics/ActiveRevenueQuery.php +121 -0
- package/server/src/Support/Metrics/AvgOrderValueMetric.php +1 -7
- package/server/src/Support/Metrics/EarningsMetric.php +1 -10
- package/server/src/Support/Telematics/Providers/AfaqyProvider.php +104 -5
- package/server/src/Support/Telematics/Providers/SafeeProvider.php +550 -54
- package/server/src/Support/Telematics/TelematicService.php +28 -9
- package/server/src/routes.php +67 -14
- package/server/tests/ExportCoverageTest.php +79 -0
- package/server/tests/MetricsRegistryTest.php +45 -5
- package/server/tests/PublicFleetOpsRestEndpointsTest.php +191 -0
- 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
|
|
197
|
+
$event = null;
|
|
198
|
+
$events = [];
|
|
198
199
|
try {
|
|
199
|
-
$
|
|
200
|
-
if (
|
|
201
|
-
$
|
|
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
|
|
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([
|
|
775
|
+
return array_merge([
|
|
776
|
+
'sensor_key' => $name,
|
|
777
|
+
'name' => $name,
|
|
778
|
+
], $sensor);
|
|
761
779
|
}
|
|
762
780
|
|
|
763
781
|
return [
|
|
764
|
-
'
|
|
765
|
-
'
|
|
766
|
-
'
|
|
782
|
+
'sensor_key' => $name,
|
|
783
|
+
'name' => $name,
|
|
784
|
+
'type' => $name,
|
|
785
|
+
'value' => $sensor,
|
|
767
786
|
];
|
|
768
787
|
})
|
|
769
788
|
->values()
|
package/server/src/routes.php
CHANGED
|
@@ -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
|
|
70
|
-
$fuelCosts
|
|
71
|
-
$aov
|
|
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(
|
|
75
|
+
expect($earnings)->toContain('ActiveRevenueQuery::forCompany');
|
|
74
76
|
expect($fuelCosts)->toContain("->where('currency'");
|
|
75
|
-
expect($aov)->toContain(
|
|
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
|
+
});
|