@fleetbase/fleetops-engine 0.6.20 → 0.6.21
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/custom-entity/form.hbs +14 -14
- package/addon/components/device/details.hbs +92 -43
- package/addon/components/device/form.hbs +108 -60
- package/addon/components/device/form.js +36 -8
- package/addon/components/device/panel-header.hbs +32 -0
- package/addon/components/device/panel-header.js +3 -0
- package/addon/components/driver/form.hbs +1 -1
- package/addon/components/driver/form.js +49 -47
- package/addon/components/entity/form.hbs +7 -5
- package/addon/components/layout/fleet-ops-sidebar.js +12 -12
- package/addon/components/map/drawer/device-event-listing.hbs +58 -0
- package/addon/components/map/drawer/device-event-listing.js +181 -0
- package/addon/components/map/drawer/position-listing.hbs +84 -0
- package/addon/components/map/drawer/position-listing.js +289 -0
- package/addon/components/map/drawer.js +2 -0
- package/addon/components/map/leaflet-live-map.hbs +7 -2
- package/addon/components/order/details/payload.hbs +6 -4
- package/addon/components/order/details/payload.js +2 -0
- package/addon/components/order-config-manager/custom-fields.js +1 -1
- package/addon/components/positions-replay.hbs +333 -0
- package/addon/components/positions-replay.js +372 -0
- package/addon/components/sensor/details.hbs +64 -38
- package/addon/components/sensor/form.hbs +112 -63
- package/addon/components/sensor/form.js +36 -24
- package/addon/components/sensor/panel-header.hbs +32 -0
- package/addon/components/sensor/panel-header.js +3 -0
- package/addon/components/telematic/details.hbs +40 -16
- package/addon/components/telematic/form.hbs +63 -64
- package/addon/components/telematic/form.js +73 -4
- package/addon/components/vehicle/card.hbs +1 -1
- package/addon/controllers/analytics/reports/index/edit.js +1 -1
- package/addon/controllers/connectivity/devices/index/details.js +22 -1
- package/addon/controllers/connectivity/devices/index/edit.js +66 -1
- package/addon/controllers/connectivity/devices/index.js +51 -9
- package/addon/controllers/connectivity/events/index.js +65 -16
- package/addon/controllers/connectivity/sensors/index/details.js +22 -1
- package/addon/controllers/connectivity/sensors/index/edit.js +66 -1
- package/addon/controllers/connectivity/sensors/index.js +66 -6
- package/addon/controllers/connectivity/telematics/index/details.js +22 -1
- package/addon/controllers/connectivity/telematics/index/edit.js +66 -1
- package/addon/controllers/connectivity/telematics/index.js +20 -11
- package/addon/controllers/management/fleets/index/details.js +26 -21
- package/addon/controllers/management/fleets/index/edit.js +9 -6
- package/addon/controllers/management/vehicles/index/details.js +21 -13
- package/addon/controllers/settings/custom-fields.js +6 -0
- package/addon/helpers/get-fleet-ops-option-label.js +11 -0
- package/addon/routes/connectivity/devices/index/details.js +27 -1
- package/addon/routes/connectivity/devices/index/edit.js +27 -1
- package/addon/routes/connectivity/sensors/index/details.js +27 -1
- package/addon/routes/connectivity/sensors/index/edit.js +27 -1
- package/addon/routes/connectivity/telematics/index/details.js +27 -1
- package/addon/routes/connectivity/telematics/index/edit.js +27 -1
- package/addon/routes/management/vehicles/index/details/positions.js +3 -0
- package/addon/routes.js +1 -0
- package/addon/services/movement-tracker.js +81 -30
- package/addon/styles/fleetops-engine.css +157 -0
- package/addon/templates/connectivity/devices/index/details/index.hbs +2 -2
- package/addon/templates/connectivity/devices/index/details.hbs +15 -2
- package/addon/templates/connectivity/devices/index/edit.hbs +1 -1
- package/addon/templates/connectivity/events/index.hbs +1 -1
- package/addon/templates/connectivity/sensors/index/details/index.hbs +2 -2
- package/addon/templates/connectivity/sensors/index/details.hbs +15 -2
- package/addon/templates/connectivity/sensors/index/edit.hbs +1 -1
- package/addon/templates/connectivity/telematics/index/details/index.hbs +2 -2
- package/addon/templates/connectivity/telematics/index/details.hbs +14 -2
- package/addon/templates/connectivity/telematics/index/edit.hbs +1 -1
- package/addon/templates/management/vehicles/index/details/positions.hbs +1 -0
- package/addon/utils/fleet-ops-options.js +95 -0
- package/app/components/device/panel-header.js +1 -0
- package/app/components/map/drawer/device-event-listing.js +1 -0
- package/app/components/map/drawer/position-listing.js +1 -0
- package/app/components/positions-replay.js +1 -0
- package/app/components/sensor/panel-header.js +1 -0
- package/app/helpers/get-fleet-ops-option-label.js +1 -0
- package/app/routes/management/vehicles/index/details/positions.js +1 -0
- package/app/templates/management/vehicles/index/details/positions.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +4 -4
- package/server/config/telematics.php +111 -0
- package/server/migrations/2025_10_27_000001_add_telematics_integration_fields.php +70 -0
- package/server/migrations/2025_10_27_171322_fix_device_column_names.php +107 -0
- package/server/migrations/2025_10_27_203023_add_company_uuid_to_device_events_table.php +28 -0
- package/server/src/Console/Commands/ReplayVehicleLocations.php +225 -0
- package/server/src/Contracts/TelematicProviderDescriptor.php +72 -0
- package/server/src/Contracts/TelematicProviderInterface.php +119 -0
- package/server/src/Exceptions/TelematicProviderException.php +14 -0
- package/server/src/Exceptions/TelematicRateLimitExceededException.php +12 -0
- package/server/src/Http/Controllers/Api/v1/DriverController.php +24 -14
- package/server/src/Http/Controllers/Api/v1/VehicleController.php +27 -7
- package/server/src/Http/Controllers/Internal/v1/DeviceController.php +22 -0
- package/server/src/Http/Controllers/Internal/v1/PositionController.php +240 -0
- package/server/src/Http/Controllers/Internal/v1/SensorController.php +11 -0
- package/server/src/Http/Controllers/Internal/v1/TelematicController.php +141 -0
- package/server/src/Http/Controllers/Internal/v1/TelematicWebhookController.php +170 -0
- package/server/src/Http/Filter/DeviceEventFilter.php +68 -0
- package/server/src/Http/Filter/PositionFilter.php +35 -0
- package/server/src/Http/Resources/v1/Position.php +44 -0
- package/server/src/Jobs/ReplayPositions.php +64 -0
- package/server/src/Jobs/SendPositionReplay.php +65 -0
- package/server/src/Jobs/SyncTelematicDevicesJob.php +106 -0
- package/server/src/Jobs/TestTelematicConnectionJob.php +102 -0
- package/server/src/Models/Device.php +72 -10
- package/server/src/Models/DeviceEvent.php +7 -0
- package/server/src/Models/Driver.php +28 -1
- package/server/src/Models/Payload.php +0 -1
- package/server/src/Models/Place.php +4 -1
- package/server/src/Models/Position.php +17 -17
- package/server/src/Models/Sensor.php +78 -13
- package/server/src/Models/Telematic.php +116 -6
- package/server/src/Models/Vehicle.php +8 -11
- package/server/src/Providers/FleetOpsServiceProvider.php +2 -0
- package/server/src/Support/Telematics/Providers/AbstractProvider.php +151 -0
- package/server/src/Support/Telematics/Providers/FlespiProvider.php +182 -0
- package/server/src/Support/Telematics/Providers/GeotabProvider.php +181 -0
- package/server/src/Support/Telematics/Providers/SamsaraProvider.php +177 -0
- package/server/src/Support/Telematics/TelematicProviderRegistry.php +147 -0
- package/server/src/Support/Telematics/TelematicService.php +223 -0
- package/server/src/Support/Utils.php +1 -1
- package/server/src/routes.php +12 -1
|
@@ -12,4 +12,26 @@ class DeviceController extends FleetOpsController
|
|
|
12
12
|
* @var string
|
|
13
13
|
*/
|
|
14
14
|
public $resource = 'device';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Query callback when querying record.
|
|
18
|
+
*
|
|
19
|
+
* @param \Illuminate\Database\Query\Builder $query
|
|
20
|
+
* @param Request $request
|
|
21
|
+
*/
|
|
22
|
+
public static function onQueryRecord($query, $request): void
|
|
23
|
+
{
|
|
24
|
+
$query->with(['telematic', 'warranty']);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Query callback when finding record.
|
|
29
|
+
*
|
|
30
|
+
* @param \Illuminate\Database\Query\Builder $query
|
|
31
|
+
* @param Request $request
|
|
32
|
+
*/
|
|
33
|
+
public static function onFindRecord($query, $request): void
|
|
34
|
+
{
|
|
35
|
+
$query->with(['telematic', 'warranty']);
|
|
36
|
+
}
|
|
15
37
|
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Http\Controllers\FleetOpsController;
|
|
6
|
+
use Fleetbase\FleetOps\Jobs\ReplayPositions;
|
|
7
|
+
use Fleetbase\FleetOps\Models\Position;
|
|
8
|
+
use Fleetbase\FleetOps\Support\Utils;
|
|
9
|
+
use Illuminate\Http\Request;
|
|
10
|
+
|
|
11
|
+
class PositionController extends FleetOpsController
|
|
12
|
+
{
|
|
13
|
+
/**
|
|
14
|
+
* The resource to query.
|
|
15
|
+
*
|
|
16
|
+
* @var string
|
|
17
|
+
*/
|
|
18
|
+
public $resource = 'position';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Replay positions on a custom channel.
|
|
22
|
+
*
|
|
23
|
+
* @return \Illuminate\Http\Response
|
|
24
|
+
*/
|
|
25
|
+
public function replay(Request $request)
|
|
26
|
+
{
|
|
27
|
+
$positionIds = $request->input('position_ids', []);
|
|
28
|
+
$channelId = $request->input('channel_id');
|
|
29
|
+
$speed = max((float) $request->input('speed', 1), 0.1); // avoid division by zero
|
|
30
|
+
$subjectUuid = $request->input('subject_uuid');
|
|
31
|
+
|
|
32
|
+
if (!$channelId) {
|
|
33
|
+
return response()->error('Channel ID is required');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (empty($positionIds)) {
|
|
37
|
+
return response()->error('Position IDs are required');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
$positions = Position::whereIn('uuid', $positionIds)
|
|
41
|
+
->where('company_uuid', session('company'))
|
|
42
|
+
->orderBy('created_at')
|
|
43
|
+
->get();
|
|
44
|
+
|
|
45
|
+
if ($positions->isEmpty()) {
|
|
46
|
+
return response()->error('No positions found');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Dispatch async job (will handle replay logic)
|
|
50
|
+
ReplayPositions::dispatch($positions, $channelId, $speed, $subjectUuid);
|
|
51
|
+
|
|
52
|
+
return response()->json([
|
|
53
|
+
'status' => 'ok',
|
|
54
|
+
'message' => 'Replay started',
|
|
55
|
+
'channel_id' => $channelId,
|
|
56
|
+
'total_positions' => $positions->count(),
|
|
57
|
+
]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get position statistics/metrics.
|
|
62
|
+
*
|
|
63
|
+
* @return \Illuminate\Http\Response
|
|
64
|
+
*/
|
|
65
|
+
public function metrics(Request $request)
|
|
66
|
+
{
|
|
67
|
+
$positionIds = $request->input('position_ids', []);
|
|
68
|
+
|
|
69
|
+
if (empty($positionIds)) {
|
|
70
|
+
return response()->error('Position IDs are required');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
$positions = Position::whereIn('uuid', $positionIds)
|
|
74
|
+
->where('company_uuid', session('company'))
|
|
75
|
+
->orderBy('created_at', 'asc')
|
|
76
|
+
->get();
|
|
77
|
+
|
|
78
|
+
if ($positions->isEmpty()) {
|
|
79
|
+
return response()->json([
|
|
80
|
+
'metrics' => [],
|
|
81
|
+
]);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Calculate metrics
|
|
85
|
+
$metrics = $this->calculateMetrics($positions);
|
|
86
|
+
|
|
87
|
+
return response()->json([
|
|
88
|
+
'metrics' => $metrics,
|
|
89
|
+
]);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Calculate metrics from positions.
|
|
94
|
+
*
|
|
95
|
+
* @param \Illuminate\Support\Collection $positions
|
|
96
|
+
*
|
|
97
|
+
* @return array
|
|
98
|
+
*/
|
|
99
|
+
private function calculateMetrics($positions)
|
|
100
|
+
{
|
|
101
|
+
$totalDistance = 0;
|
|
102
|
+
$maxSpeed = 0;
|
|
103
|
+
$avgSpeed = 0;
|
|
104
|
+
$speedingEvents = [];
|
|
105
|
+
$dwellTimes = [];
|
|
106
|
+
$accelerationEvents = [];
|
|
107
|
+
$speedLimit = 100; // km/h - configurable
|
|
108
|
+
$dwellThreshold = 300; // 5 minutes in seconds
|
|
109
|
+
$accelerationThreshold = 2.5; // m/s²
|
|
110
|
+
|
|
111
|
+
$previousPosition = null;
|
|
112
|
+
$previousSpeed = null;
|
|
113
|
+
$dwellStart = null;
|
|
114
|
+
|
|
115
|
+
foreach ($positions as $index => $position) {
|
|
116
|
+
$speed = $position->speed ?? 0;
|
|
117
|
+
|
|
118
|
+
// Track max speed
|
|
119
|
+
if ($speed > $maxSpeed) {
|
|
120
|
+
$maxSpeed = $speed;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Check for speeding (convert m/s to km/h)
|
|
124
|
+
$speedKmh = $speed * 3.6;
|
|
125
|
+
if ($speedKmh > $speedLimit) {
|
|
126
|
+
$speedingEvents[] = [
|
|
127
|
+
'position_uuid' => $position->uuid,
|
|
128
|
+
'speed' => round($speedKmh, 2),
|
|
129
|
+
'timestamp' => $position->created_at->toDateTimeString(),
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Calculate distance and dwell time
|
|
134
|
+
if ($previousPosition) {
|
|
135
|
+
// Calculate distance using Haversine formula
|
|
136
|
+
// $distance = $this->calculateDistance($previousPosition, $position);
|
|
137
|
+
$distance = Utils::vincentyGreatCircleDistance($previousPosition->coordinates, $position->coordinates);
|
|
138
|
+
// dd($previousPosition, $position, $distance);
|
|
139
|
+
$totalDistance += $distance;
|
|
140
|
+
|
|
141
|
+
// Check for dwell (low speed or no movement)
|
|
142
|
+
if ($speed < 0.5) { // Less than 0.5 m/s
|
|
143
|
+
if ($dwellStart === null) {
|
|
144
|
+
$dwellStart = $previousPosition->created_at;
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
if ($dwellStart !== null) {
|
|
148
|
+
$dwellDuration = $previousPosition->created_at->diffInSeconds($dwellStart);
|
|
149
|
+
if ($dwellDuration >= $dwellThreshold) {
|
|
150
|
+
$dwellTimes[] = [
|
|
151
|
+
'start' => $dwellStart->toDateTimeString(),
|
|
152
|
+
'end' => $previousPosition->created_at->toDateTimeString(),
|
|
153
|
+
'duration' => $dwellDuration,
|
|
154
|
+
];
|
|
155
|
+
}
|
|
156
|
+
$dwellStart = null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Calculate acceleration
|
|
161
|
+
if ($previousSpeed !== null) {
|
|
162
|
+
$timeDiff = $position->created_at->diffInSeconds($previousPosition->created_at);
|
|
163
|
+
if ($timeDiff > 0) {
|
|
164
|
+
$acceleration = abs($speed - $previousSpeed) / $timeDiff;
|
|
165
|
+
if ($acceleration > $accelerationThreshold) {
|
|
166
|
+
$accelerationEvents[] = [
|
|
167
|
+
'position_uuid' => $position->uuid,
|
|
168
|
+
'acceleration' => round($acceleration, 2),
|
|
169
|
+
'type' => $speed > $previousSpeed ? 'acceleration' : 'deceleration',
|
|
170
|
+
'timestamp' => $position->created_at->toDateTimeString(),
|
|
171
|
+
];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
$previousPosition = $position;
|
|
178
|
+
$previousSpeed = $speed;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Calculate average speed
|
|
182
|
+
$speeds = $positions->pluck('speed')->filter()->toArray();
|
|
183
|
+
$avgSpeed = count($speeds) > 0 ? array_sum($speeds) / count($speeds) : 0;
|
|
184
|
+
|
|
185
|
+
// Calculate total duration
|
|
186
|
+
$firstPosition = $positions->first();
|
|
187
|
+
$lastPosition = $positions->last();
|
|
188
|
+
$totalDuration = $lastPosition->created_at->diffInSeconds($firstPosition->created_at);
|
|
189
|
+
|
|
190
|
+
return [
|
|
191
|
+
'total_distance' => round($totalDistance / 1000, 2), // Convert to km
|
|
192
|
+
'total_duration' => $totalDuration, // seconds
|
|
193
|
+
'max_speed' => round($maxSpeed * 3.6, 2), // km/h
|
|
194
|
+
'avg_speed' => round($avgSpeed * 3.6, 2), // km/h
|
|
195
|
+
'speeding_events' => $speedingEvents,
|
|
196
|
+
'speeding_count' => count($speedingEvents),
|
|
197
|
+
'dwell_times' => $dwellTimes,
|
|
198
|
+
'dwell_count' => count($dwellTimes),
|
|
199
|
+
'acceleration_events' => $accelerationEvents,
|
|
200
|
+
'acceleration_count' => count($accelerationEvents),
|
|
201
|
+
'total_positions' => $positions->count(),
|
|
202
|
+
];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Calculate distance between two positions using Haversine formula.
|
|
207
|
+
*
|
|
208
|
+
* @param Position $pos1
|
|
209
|
+
* @param Position $pos2
|
|
210
|
+
*
|
|
211
|
+
* @return float Distance in meters
|
|
212
|
+
*/
|
|
213
|
+
private function calculateDistance($pos1, $pos2)
|
|
214
|
+
{
|
|
215
|
+
$coords1 = [$pos1->latitude, $pos1->longitude];
|
|
216
|
+
$coords2 = [$pos2->latitude, $pos2->longitude];
|
|
217
|
+
|
|
218
|
+
if (!isset($coords1[0]) || !isset($coords2[0]) || !isset($coords1[1]) || !isset($coords2[1])) {
|
|
219
|
+
return 0;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
$lat1 = deg2rad($coords1[0]);
|
|
223
|
+
$lon1 = deg2rad($coords1[1]);
|
|
224
|
+
$lat2 = deg2rad($coords2[0]);
|
|
225
|
+
$lon2 = deg2rad($coords2[1]);
|
|
226
|
+
|
|
227
|
+
$earthRadius = 6371000; // meters
|
|
228
|
+
|
|
229
|
+
$dLat = $lat2 - $lat1;
|
|
230
|
+
$dLon = $lon2 - $lon1;
|
|
231
|
+
|
|
232
|
+
$a = sin($dLat / 2) * sin($dLat / 2) +
|
|
233
|
+
cos($lat1) * cos($lat2) *
|
|
234
|
+
sin($dLon / 2) * sin($dLon / 2);
|
|
235
|
+
|
|
236
|
+
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
|
|
237
|
+
|
|
238
|
+
return $earthRadius * $c;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -12,4 +12,15 @@ class SensorController extends FleetOpsController
|
|
|
12
12
|
* @var string
|
|
13
13
|
*/
|
|
14
14
|
public $resource = 'sensor';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Query callback when querying record.
|
|
18
|
+
*
|
|
19
|
+
* @param \Illuminate\Database\Query\Builder $query
|
|
20
|
+
* @param Request $request
|
|
21
|
+
*/
|
|
22
|
+
public static function onQueryRecord($query, $request): void
|
|
23
|
+
{
|
|
24
|
+
$query->with(['telematic', 'warranty']);
|
|
25
|
+
}
|
|
15
26
|
}
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
namespace Fleetbase\FleetOps\Http\Controllers\Internal\v1;
|
|
4
4
|
|
|
5
5
|
use Fleetbase\FleetOps\Http\Controllers\FleetOpsController;
|
|
6
|
+
use Fleetbase\FleetOps\Models\Telematic;
|
|
7
|
+
use Fleetbase\FleetOps\Support\Telematics\TelematicProviderRegistry;
|
|
8
|
+
use Fleetbase\FleetOps\Support\Telematics\TelematicService;
|
|
9
|
+
use Illuminate\Http\JsonResponse;
|
|
10
|
+
use Illuminate\Http\Request;
|
|
6
11
|
|
|
7
12
|
class TelematicController extends FleetOpsController
|
|
8
13
|
{
|
|
@@ -12,4 +17,140 @@ class TelematicController extends FleetOpsController
|
|
|
12
17
|
* @var string
|
|
13
18
|
*/
|
|
14
19
|
public $resource = 'telematic';
|
|
20
|
+
|
|
21
|
+
protected TelematicService $telematicService;
|
|
22
|
+
protected TelematicProviderRegistry $registry;
|
|
23
|
+
|
|
24
|
+
public function __construct(TelematicService $service, TelematicProviderRegistry $registry)
|
|
25
|
+
{
|
|
26
|
+
parent::__construct();
|
|
27
|
+
$this->telematicService = $service;
|
|
28
|
+
$this->registry = $registry;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Query callback when querying record.
|
|
33
|
+
*
|
|
34
|
+
* @param \Illuminate\Database\Query\Builder $query
|
|
35
|
+
* @param Request $request
|
|
36
|
+
*/
|
|
37
|
+
public static function onQueryRecord($query, $request): void
|
|
38
|
+
{
|
|
39
|
+
$query->with(['warranty']);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* List available providers.
|
|
44
|
+
*/
|
|
45
|
+
public function providers(): JsonResponse
|
|
46
|
+
{
|
|
47
|
+
$providers = $this->registry->all()->map(fn ($p) => $p->toArray());
|
|
48
|
+
|
|
49
|
+
return response()->json($providers->values());
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Test connection to provider.
|
|
54
|
+
*/
|
|
55
|
+
public function testConnection(Request $request, string $id): JsonResponse
|
|
56
|
+
{
|
|
57
|
+
$telematic = Telematic::where('uuid', $id)
|
|
58
|
+
->where('company_uuid', session('company'))
|
|
59
|
+
->firstOrFail();
|
|
60
|
+
|
|
61
|
+
$async = $request->input('async', false);
|
|
62
|
+
|
|
63
|
+
$result = $this->telematicService->testConnection($telematic, $async);
|
|
64
|
+
|
|
65
|
+
if ($async) {
|
|
66
|
+
return response()->json($result, 202);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return response()->json($result);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Test connection to provider.
|
|
74
|
+
*/
|
|
75
|
+
public function testCredentials(Request $request, string $key): JsonResponse
|
|
76
|
+
{
|
|
77
|
+
$credentials = $request->array('credentials', []);
|
|
78
|
+
$async = $request->input('async', false);
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
$provider = $this->registry->resolve($key);
|
|
82
|
+
if (!$provider) {
|
|
83
|
+
return response()->error('Unable to resolve telematic provider.');
|
|
84
|
+
}
|
|
85
|
+
$result = $provider->testConnection($credentials);
|
|
86
|
+
} catch (\Exception $e) {
|
|
87
|
+
return response()->error($e->getMessage());
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if ($async) {
|
|
91
|
+
return response()->json($result, 202);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return response()->json($result);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Discover devices from provider.
|
|
99
|
+
*/
|
|
100
|
+
public function discover(Request $request, string $id): JsonResponse
|
|
101
|
+
{
|
|
102
|
+
$telematic = Telematic::where('uuid', $id)
|
|
103
|
+
->where('company_uuid', session('company'))
|
|
104
|
+
->firstOrFail();
|
|
105
|
+
|
|
106
|
+
$jobId = $this->telematicService->discoverDevices($telematic, [
|
|
107
|
+
'limit' => $request->input('limit', 100),
|
|
108
|
+
'filters' => $request->input('filters', []),
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
return response()->json([
|
|
112
|
+
'job_id' => $jobId,
|
|
113
|
+
'message' => 'Device discovery initiated',
|
|
114
|
+
], 202);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get devices for a telematic.
|
|
119
|
+
*/
|
|
120
|
+
public function devices(Request $request, string $id): JsonResponse
|
|
121
|
+
{
|
|
122
|
+
$telematic = Telematic::where('uuid', $id)
|
|
123
|
+
->where('company_uuid', session('company'))
|
|
124
|
+
->firstOrFail();
|
|
125
|
+
|
|
126
|
+
$devices = $this->telematicService->getDevices($telematic, [
|
|
127
|
+
'status' => $request->input('status'),
|
|
128
|
+
'search' => $request->input('search'),
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
return response()->json([
|
|
132
|
+
'data' => $devices,
|
|
133
|
+
]);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Link a device to a telematic.
|
|
138
|
+
*/
|
|
139
|
+
public function linkDevice(Request $request, string $id): JsonResponse
|
|
140
|
+
{
|
|
141
|
+
$telematic = Telematic::where('uuid', $id)
|
|
142
|
+
->where('company_uuid', session('company'))
|
|
143
|
+
->firstOrFail();
|
|
144
|
+
|
|
145
|
+
$request->validate([
|
|
146
|
+
'external_id' => 'required|string',
|
|
147
|
+
'device_name' => 'required|string',
|
|
148
|
+
]);
|
|
149
|
+
|
|
150
|
+
$device = $this->telematicService->linkDevice($telematic, $request->all());
|
|
151
|
+
|
|
152
|
+
return response()->json([
|
|
153
|
+
'device' => $device,
|
|
154
|
+
], 201);
|
|
155
|
+
}
|
|
15
156
|
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Http\Controllers;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Models\Telematic;
|
|
6
|
+
use Fleetbase\FleetOps\Support\Telematics\TelematicProviderRegistry;
|
|
7
|
+
use Fleetbase\FleetOps\Support\Telematics\TelematicService;
|
|
8
|
+
use Fleetbase\Support\IdempotencyManager;
|
|
9
|
+
use Illuminate\Http\JsonResponse;
|
|
10
|
+
use Illuminate\Http\Request;
|
|
11
|
+
use Illuminate\Routing\Controller;
|
|
12
|
+
use Illuminate\Support\Facades\Log;
|
|
13
|
+
use Illuminate\Support\Str;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Class TelematicWebhookController.
|
|
17
|
+
*
|
|
18
|
+
* Handles webhook ingestion from telematics providers.
|
|
19
|
+
*/
|
|
20
|
+
class TelematicWebhookController extends Controller
|
|
21
|
+
{
|
|
22
|
+
protected TelematicProviderRegistry $registry;
|
|
23
|
+
protected TelematicService $service;
|
|
24
|
+
protected IdempotencyManager $idempotency;
|
|
25
|
+
|
|
26
|
+
public function __construct(
|
|
27
|
+
TelematicProviderRegistry $registry,
|
|
28
|
+
TelematicService $service,
|
|
29
|
+
IdempotencyManager $idempotency,
|
|
30
|
+
) {
|
|
31
|
+
$this->registry = $registry;
|
|
32
|
+
$this->service = $service;
|
|
33
|
+
$this->idempotency = $idempotency;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Handle provider webhook.
|
|
38
|
+
*/
|
|
39
|
+
public function handle(Request $request, string $providerKey): JsonResponse
|
|
40
|
+
{
|
|
41
|
+
$correlationId = Str::uuid()->toString();
|
|
42
|
+
|
|
43
|
+
Log::info('Webhook received', [
|
|
44
|
+
'correlation_id' => $correlationId,
|
|
45
|
+
'provider' => $providerKey,
|
|
46
|
+
'headers' => $request->headers->all(),
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
// Check idempotency
|
|
50
|
+
$idempotencyKey = $request->header('X-Idempotency-Key');
|
|
51
|
+
if ($idempotencyKey && $this->idempotency->isDuplicate($idempotencyKey)) {
|
|
52
|
+
Log::info('Duplicate webhook detected', [
|
|
53
|
+
'correlation_id' => $correlationId,
|
|
54
|
+
'idempotency_key' => $idempotencyKey,
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
return response()->json(['status' => 'duplicate'], 200);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Get provider
|
|
61
|
+
$provider = $this->registry->resolve($providerKey);
|
|
62
|
+
|
|
63
|
+
// Find telematic for this provider
|
|
64
|
+
$telematic = Telematic::where('provider', $providerKey)->first();
|
|
65
|
+
|
|
66
|
+
if (!$telematic) {
|
|
67
|
+
Log::warning('No telematic found for provider', [
|
|
68
|
+
'correlation_id' => $correlationId,
|
|
69
|
+
'provider' => $providerKey,
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
return response()->json(['error' => 'No telematic configured'], 404);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Validate signature
|
|
76
|
+
$signature = $request->header('X-Webhook-Signature');
|
|
77
|
+
$credentials = $this->service->getCredentials($telematic);
|
|
78
|
+
|
|
79
|
+
if ($signature && !$provider->validateWebhookSignature($request->getContent(), $signature, $credentials)) {
|
|
80
|
+
Log::warning('Invalid webhook signature', [
|
|
81
|
+
'correlation_id' => $correlationId,
|
|
82
|
+
'provider' => $providerKey,
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
return response()->json(['error' => 'Invalid signature'], 403);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Process webhook
|
|
89
|
+
try {
|
|
90
|
+
$result = $provider->processWebhook($request->all(), $request->headers->all());
|
|
91
|
+
|
|
92
|
+
// Link devices
|
|
93
|
+
foreach ($result['devices'] as $deviceData) {
|
|
94
|
+
$this->service->linkDevice($telematic, $deviceData);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Store events (TODO: implement event storage)
|
|
98
|
+
// Store sensors (TODO: implement sensor storage)
|
|
99
|
+
|
|
100
|
+
// Mark as processed
|
|
101
|
+
if ($idempotencyKey) {
|
|
102
|
+
$this->idempotency->markProcessed($idempotencyKey);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
Log::info('Webhook processed successfully', [
|
|
106
|
+
'correlation_id' => $correlationId,
|
|
107
|
+
'devices_count' => count($result['devices']),
|
|
108
|
+
'events_count' => count($result['events']),
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
return response()->json(['status' => 'processed'], 200);
|
|
112
|
+
} catch (\Exception $e) {
|
|
113
|
+
Log::error('Webhook processing failed', [
|
|
114
|
+
'correlation_id' => $correlationId,
|
|
115
|
+
'error' => $e->getMessage(),
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
return response()->json(['error' => 'Processing failed'], 500);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Handle custom provider ingest.
|
|
124
|
+
*/
|
|
125
|
+
public function ingest(Request $request, string $id): JsonResponse
|
|
126
|
+
{
|
|
127
|
+
$telematic = Telematic::where('uuid', $id)->firstOrFail();
|
|
128
|
+
|
|
129
|
+
$correlationId = Str::uuid()->toString();
|
|
130
|
+
|
|
131
|
+
Log::info('Custom ingest received', [
|
|
132
|
+
'correlation_id' => $correlationId,
|
|
133
|
+
'telematic_uuid' => $id,
|
|
134
|
+
]);
|
|
135
|
+
|
|
136
|
+
// Check idempotency
|
|
137
|
+
$idempotencyKey = $request->header('X-Idempotency-Key');
|
|
138
|
+
if ($idempotencyKey && $this->idempotency->isDuplicate($idempotencyKey)) {
|
|
139
|
+
return response()->json(['status' => 'duplicate'], 200);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
// Process devices
|
|
144
|
+
if ($request->has('devices')) {
|
|
145
|
+
foreach ($request->input('devices') as $deviceData) {
|
|
146
|
+
$this->service->linkDevice($telematic, $deviceData);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Mark as processed
|
|
151
|
+
if ($idempotencyKey) {
|
|
152
|
+
$this->idempotency->markProcessed($idempotencyKey);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
Log::info('Custom ingest processed', [
|
|
156
|
+
'correlation_id' => $correlationId,
|
|
157
|
+
'devices_count' => count($request->input('devices', [])),
|
|
158
|
+
]);
|
|
159
|
+
|
|
160
|
+
return response()->json(['status' => 'ingested'], 200);
|
|
161
|
+
} catch (\Exception $e) {
|
|
162
|
+
Log::error('Custom ingest failed', [
|
|
163
|
+
'correlation_id' => $correlationId,
|
|
164
|
+
'error' => $e->getMessage(),
|
|
165
|
+
]);
|
|
166
|
+
|
|
167
|
+
return response()->json(['error' => 'Ingest failed'], 500);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Http\Filter;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Support\Utils;
|
|
6
|
+
use Fleetbase\Http\Filter\Filter;
|
|
7
|
+
|
|
8
|
+
class DeviceEventFilter extends Filter
|
|
9
|
+
{
|
|
10
|
+
public function queryForInternal()
|
|
11
|
+
{
|
|
12
|
+
$this->builder->where(
|
|
13
|
+
function ($query) {
|
|
14
|
+
$query->where('company_uuid', $this->session->get('company'));
|
|
15
|
+
}
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public function queryForPublic()
|
|
20
|
+
{
|
|
21
|
+
$this->builder->where('company_uuid', $this->session->get('company'));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public function query(?string $searchQuery)
|
|
25
|
+
{
|
|
26
|
+
$this->builder->search($searchQuery);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public function telematic(?string $telematic)
|
|
30
|
+
{
|
|
31
|
+
$this->builder->whereHas('device', function ($query) use ($telematic) {
|
|
32
|
+
$query->whereHas('telematic', function ($query) use ($telematic) {
|
|
33
|
+
$query->where('uuid', $telematic);
|
|
34
|
+
$query->orWhere('public_id', $telematic);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public function device(?string $device)
|
|
40
|
+
{
|
|
41
|
+
$this->builder->whereHas('device', function ($query) use ($device) {
|
|
42
|
+
$query->where('uuid', $device);
|
|
43
|
+
$query->orWhere('public_id', $device);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public function createdAt($createdAt)
|
|
48
|
+
{
|
|
49
|
+
$createdAt = Utils::dateRange($createdAt);
|
|
50
|
+
|
|
51
|
+
if (is_array($createdAt)) {
|
|
52
|
+
$this->builder->whereBetween('created_at', $createdAt);
|
|
53
|
+
} else {
|
|
54
|
+
$this->builder->whereDate('created_at', $createdAt);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public function updatedAt($updatedAt)
|
|
59
|
+
{
|
|
60
|
+
$updatedAt = Utils::dateRange($updatedAt);
|
|
61
|
+
|
|
62
|
+
if (is_array($updatedAt)) {
|
|
63
|
+
$this->builder->whereBetween('updated_at', $updatedAt);
|
|
64
|
+
} else {
|
|
65
|
+
$this->builder->whereDate('updated_at', $updatedAt);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|