@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.
Files changed (120) hide show
  1. package/addon/components/custom-entity/form.hbs +14 -14
  2. package/addon/components/device/details.hbs +92 -43
  3. package/addon/components/device/form.hbs +108 -60
  4. package/addon/components/device/form.js +36 -8
  5. package/addon/components/device/panel-header.hbs +32 -0
  6. package/addon/components/device/panel-header.js +3 -0
  7. package/addon/components/driver/form.hbs +1 -1
  8. package/addon/components/driver/form.js +49 -47
  9. package/addon/components/entity/form.hbs +7 -5
  10. package/addon/components/layout/fleet-ops-sidebar.js +12 -12
  11. package/addon/components/map/drawer/device-event-listing.hbs +58 -0
  12. package/addon/components/map/drawer/device-event-listing.js +181 -0
  13. package/addon/components/map/drawer/position-listing.hbs +84 -0
  14. package/addon/components/map/drawer/position-listing.js +289 -0
  15. package/addon/components/map/drawer.js +2 -0
  16. package/addon/components/map/leaflet-live-map.hbs +7 -2
  17. package/addon/components/order/details/payload.hbs +6 -4
  18. package/addon/components/order/details/payload.js +2 -0
  19. package/addon/components/order-config-manager/custom-fields.js +1 -1
  20. package/addon/components/positions-replay.hbs +333 -0
  21. package/addon/components/positions-replay.js +372 -0
  22. package/addon/components/sensor/details.hbs +64 -38
  23. package/addon/components/sensor/form.hbs +112 -63
  24. package/addon/components/sensor/form.js +36 -24
  25. package/addon/components/sensor/panel-header.hbs +32 -0
  26. package/addon/components/sensor/panel-header.js +3 -0
  27. package/addon/components/telematic/details.hbs +40 -16
  28. package/addon/components/telematic/form.hbs +63 -64
  29. package/addon/components/telematic/form.js +73 -4
  30. package/addon/components/vehicle/card.hbs +1 -1
  31. package/addon/controllers/analytics/reports/index/edit.js +1 -1
  32. package/addon/controllers/connectivity/devices/index/details.js +22 -1
  33. package/addon/controllers/connectivity/devices/index/edit.js +66 -1
  34. package/addon/controllers/connectivity/devices/index.js +51 -9
  35. package/addon/controllers/connectivity/events/index.js +65 -16
  36. package/addon/controllers/connectivity/sensors/index/details.js +22 -1
  37. package/addon/controllers/connectivity/sensors/index/edit.js +66 -1
  38. package/addon/controllers/connectivity/sensors/index.js +66 -6
  39. package/addon/controllers/connectivity/telematics/index/details.js +22 -1
  40. package/addon/controllers/connectivity/telematics/index/edit.js +66 -1
  41. package/addon/controllers/connectivity/telematics/index.js +20 -11
  42. package/addon/controllers/management/fleets/index/details.js +26 -21
  43. package/addon/controllers/management/fleets/index/edit.js +9 -6
  44. package/addon/controllers/management/vehicles/index/details.js +21 -13
  45. package/addon/controllers/settings/custom-fields.js +6 -0
  46. package/addon/helpers/get-fleet-ops-option-label.js +11 -0
  47. package/addon/routes/connectivity/devices/index/details.js +27 -1
  48. package/addon/routes/connectivity/devices/index/edit.js +27 -1
  49. package/addon/routes/connectivity/sensors/index/details.js +27 -1
  50. package/addon/routes/connectivity/sensors/index/edit.js +27 -1
  51. package/addon/routes/connectivity/telematics/index/details.js +27 -1
  52. package/addon/routes/connectivity/telematics/index/edit.js +27 -1
  53. package/addon/routes/management/vehicles/index/details/positions.js +3 -0
  54. package/addon/routes.js +1 -0
  55. package/addon/services/movement-tracker.js +81 -30
  56. package/addon/styles/fleetops-engine.css +157 -0
  57. package/addon/templates/connectivity/devices/index/details/index.hbs +2 -2
  58. package/addon/templates/connectivity/devices/index/details.hbs +15 -2
  59. package/addon/templates/connectivity/devices/index/edit.hbs +1 -1
  60. package/addon/templates/connectivity/events/index.hbs +1 -1
  61. package/addon/templates/connectivity/sensors/index/details/index.hbs +2 -2
  62. package/addon/templates/connectivity/sensors/index/details.hbs +15 -2
  63. package/addon/templates/connectivity/sensors/index/edit.hbs +1 -1
  64. package/addon/templates/connectivity/telematics/index/details/index.hbs +2 -2
  65. package/addon/templates/connectivity/telematics/index/details.hbs +14 -2
  66. package/addon/templates/connectivity/telematics/index/edit.hbs +1 -1
  67. package/addon/templates/management/vehicles/index/details/positions.hbs +1 -0
  68. package/addon/utils/fleet-ops-options.js +95 -0
  69. package/app/components/device/panel-header.js +1 -0
  70. package/app/components/map/drawer/device-event-listing.js +1 -0
  71. package/app/components/map/drawer/position-listing.js +1 -0
  72. package/app/components/positions-replay.js +1 -0
  73. package/app/components/sensor/panel-header.js +1 -0
  74. package/app/helpers/get-fleet-ops-option-label.js +1 -0
  75. package/app/routes/management/vehicles/index/details/positions.js +1 -0
  76. package/app/templates/management/vehicles/index/details/positions.js +1 -0
  77. package/composer.json +1 -1
  78. package/extension.json +1 -1
  79. package/package.json +4 -4
  80. package/server/config/telematics.php +111 -0
  81. package/server/migrations/2025_10_27_000001_add_telematics_integration_fields.php +70 -0
  82. package/server/migrations/2025_10_27_171322_fix_device_column_names.php +107 -0
  83. package/server/migrations/2025_10_27_203023_add_company_uuid_to_device_events_table.php +28 -0
  84. package/server/src/Console/Commands/ReplayVehicleLocations.php +225 -0
  85. package/server/src/Contracts/TelematicProviderDescriptor.php +72 -0
  86. package/server/src/Contracts/TelematicProviderInterface.php +119 -0
  87. package/server/src/Exceptions/TelematicProviderException.php +14 -0
  88. package/server/src/Exceptions/TelematicRateLimitExceededException.php +12 -0
  89. package/server/src/Http/Controllers/Api/v1/DriverController.php +24 -14
  90. package/server/src/Http/Controllers/Api/v1/VehicleController.php +27 -7
  91. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +22 -0
  92. package/server/src/Http/Controllers/Internal/v1/PositionController.php +240 -0
  93. package/server/src/Http/Controllers/Internal/v1/SensorController.php +11 -0
  94. package/server/src/Http/Controllers/Internal/v1/TelematicController.php +141 -0
  95. package/server/src/Http/Controllers/Internal/v1/TelematicWebhookController.php +170 -0
  96. package/server/src/Http/Filter/DeviceEventFilter.php +68 -0
  97. package/server/src/Http/Filter/PositionFilter.php +35 -0
  98. package/server/src/Http/Resources/v1/Position.php +44 -0
  99. package/server/src/Jobs/ReplayPositions.php +64 -0
  100. package/server/src/Jobs/SendPositionReplay.php +65 -0
  101. package/server/src/Jobs/SyncTelematicDevicesJob.php +106 -0
  102. package/server/src/Jobs/TestTelematicConnectionJob.php +102 -0
  103. package/server/src/Models/Device.php +72 -10
  104. package/server/src/Models/DeviceEvent.php +7 -0
  105. package/server/src/Models/Driver.php +28 -1
  106. package/server/src/Models/Payload.php +0 -1
  107. package/server/src/Models/Place.php +4 -1
  108. package/server/src/Models/Position.php +17 -17
  109. package/server/src/Models/Sensor.php +78 -13
  110. package/server/src/Models/Telematic.php +116 -6
  111. package/server/src/Models/Vehicle.php +8 -11
  112. package/server/src/Providers/FleetOpsServiceProvider.php +2 -0
  113. package/server/src/Support/Telematics/Providers/AbstractProvider.php +151 -0
  114. package/server/src/Support/Telematics/Providers/FlespiProvider.php +182 -0
  115. package/server/src/Support/Telematics/Providers/GeotabProvider.php +181 -0
  116. package/server/src/Support/Telematics/Providers/SamsaraProvider.php +177 -0
  117. package/server/src/Support/Telematics/TelematicProviderRegistry.php +147 -0
  118. package/server/src/Support/Telematics/TelematicService.php +223 -0
  119. package/server/src/Support/Utils.php +1 -1
  120. 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
+ }