@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
@@ -0,0 +1,107 @@
1
+ <?php
2
+
3
+ use Illuminate\Database\Migrations\Migration;
4
+ use Illuminate\Database\Schema\Blueprint;
5
+ use Illuminate\Support\Facades\DB;
6
+ use Illuminate\Support\Facades\Schema;
7
+
8
+ return new class extends Migration {
9
+ public function up(): void
10
+ {
11
+ /**
12
+ * Devices: rename columns WITHOUT doctrine/dbal (MySQL 8+).
13
+ */
14
+ DB::statement('ALTER TABLE devices RENAME COLUMN device_name TO name');
15
+ DB::statement('ALTER TABLE devices RENAME COLUMN device_type TO type');
16
+ DB::statement('ALTER TABLE devices RENAME COLUMN device_location TO location');
17
+ DB::statement('ALTER TABLE devices RENAME COLUMN device_model TO model');
18
+ DB::statement('ALTER TABLE devices RENAME COLUMN device_provider TO provider');
19
+
20
+ /**
21
+ * Devices: add new scalar cols + spatial POINT (nullable first!).
22
+ */
23
+ Schema::table('devices', function (Blueprint $table) {
24
+ $table->string('internal_id')->nullable()->after('device_id');
25
+ $table->string('imei')->nullable()->after('device_id');
26
+ $table->string('imsi')->nullable()->after('device_id');
27
+ $table->string('firmware_version')->nullable()->after('device_id');
28
+
29
+ // must be nullable now; we'll backfill and then make NOT NULL
30
+ $table->point('last_position')->nullable()->after('serial_number');
31
+ });
32
+
33
+ // Backfill existing rows so NOT NULL will succeed
34
+ DB::statement('UPDATE devices SET last_position = ST_SRID(POINT(0, 0), 4326) WHERE last_position IS NULL');
35
+
36
+ // Make column NOT NULL (and optionally enforce SRID at column level)
37
+ // If you want to enforce SRID on the column itself, uncomment the SRID variant:
38
+ // DB::statement('ALTER TABLE devices MODIFY last_position POINT NOT NULL SRID 4326');
39
+ DB::statement('ALTER TABLE devices MODIFY last_position POINT NOT NULL');
40
+
41
+ // NOW add the spatial index (requires NOT NULL)
42
+ Schema::table('devices', function (Blueprint $table) {
43
+ $table->spatialIndex('last_position', 'devices_last_position_spx');
44
+ });
45
+
46
+ /**
47
+ * Sensors: drop legacy column, add fields + POINT (nullable), backfill, NOT NULL, index.
48
+ */
49
+ Schema::table('sensors', function (Blueprint $table) {
50
+ if (Schema::hasColumn('sensors', 'sensor_type')) {
51
+ $table->dropColumn('sensor_type');
52
+ }
53
+
54
+ $table->foreignUuid('telematic_uuid')->nullable()->after('company_uuid')->constrained('telematics', 'uuid')->nullOnDelete();
55
+ $table->string('firmware_version')->nullable()->after('name');
56
+ $table->string('imei')->nullable()->after('name');
57
+ $table->string('imsi')->nullable()->after('name');
58
+ $table->string('serial_number')->nullable()->after('name');
59
+ $table->string('internal_id')->nullable()->after('name');
60
+ $table->string('status')->nullable()->after('last_value');
61
+
62
+ $table->point('last_position')->nullable()->after('serial_number');
63
+ });
64
+
65
+ DB::statement('UPDATE sensors SET last_position = ST_SRID(POINT(0, 0), 4326) WHERE last_position IS NULL');
66
+ DB::statement('ALTER TABLE sensors MODIFY last_position POINT NOT NULL');
67
+
68
+ Schema::table('sensors', function (Blueprint $table) {
69
+ $table->spatialIndex('last_position', 'sensors_last_position_spx');
70
+ });
71
+ }
72
+
73
+ public function down(): void
74
+ {
75
+ /**
76
+ * Devices: drop spatial index then column, drop added scalars, rename back.
77
+ */
78
+ Schema::table('devices', function (Blueprint $table) {
79
+ // drop index BEFORE dropping column
80
+ $table->dropSpatialIndex('devices_last_position_spx');
81
+ $table->dropColumn('last_position');
82
+
83
+ $table->dropColumn(['internal_id', 'imei', 'imsi', 'firmware_version']);
84
+ });
85
+
86
+ DB::statement('ALTER TABLE devices RENAME COLUMN name TO device_name');
87
+ DB::statement('ALTER TABLE devices RENAME COLUMN type TO device_type');
88
+ DB::statement('ALTER TABLE devices RENAME COLUMN location TO device_location');
89
+ DB::statement('ALTER TABLE devices RENAME COLUMN model TO device_model');
90
+ DB::statement('ALTER TABLE devices RENAME COLUMN provider TO device_provider');
91
+
92
+ /**
93
+ * Sensors: drop spatial index/column, drop new fields, restore sensor_type.
94
+ */
95
+ Schema::table('sensors', function (Blueprint $table) {
96
+ $table->dropSpatialIndex('sensors_last_position_spx');
97
+ $table->dropColumn('last_position');
98
+
99
+ $table->dropForeign(['telematic_uuid']);
100
+ $table->dropColumn(['telematic_uuid']);
101
+
102
+ $table->dropColumn(['serial_number', 'internal_id', 'imei', 'imsi', 'firmware_version', 'status']);
103
+
104
+ $table->string('sensor_type')->nullable()->after('slug');
105
+ });
106
+ }
107
+ };
@@ -0,0 +1,28 @@
1
+ <?php
2
+
3
+ use Illuminate\Database\Migrations\Migration;
4
+ use Illuminate\Database\Schema\Blueprint;
5
+ use Illuminate\Support\Facades\Schema;
6
+
7
+ return new class extends Migration {
8
+ /**
9
+ * Run the migrations.
10
+ */
11
+ public function up(): void
12
+ {
13
+ Schema::table('device_events', function (Blueprint $table) {
14
+ $table->foreignUuid('company_uuid')->nullable()->after('_key')->constrained('companies', 'uuid')->nullOnDelete();
15
+ });
16
+ }
17
+
18
+ /**
19
+ * Reverse the migrations.
20
+ */
21
+ public function down(): void
22
+ {
23
+ Schema::table('device_events', function (Blueprint $table) {
24
+ $table->dropForeign(['company_uuid']);
25
+ $table->dropColumn(['company_uuid']);
26
+ });
27
+ }
28
+ };
@@ -0,0 +1,225 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Console\Commands;
4
+
5
+ use Carbon\Carbon;
6
+ use Fleetbase\FleetOps\Models\Vehicle;
7
+ use Fleetbase\Support\SocketCluster\SocketClusterService;
8
+ use Illuminate\Console\Command;
9
+
10
+ class ReplayVehicleLocations extends Command
11
+ {
12
+ /**
13
+ * The name and signature of the console command.
14
+ *
15
+ * @var string
16
+ */
17
+ protected $signature = 'vehicle:replay-locations
18
+ {file : Path to the JSON file containing vehicle location data}
19
+ {--speed=1 : Speed multiplier for replay (1 = real-time, 2 = 2x speed, 0.5 = half speed)}
20
+ {--vehicle= : Filter by specific vehicle ID (optional)}
21
+ {--limit= : Limit the number of events to process (optional)}
22
+ {--sleep= : Set a manual sleep for replay (in seconds)}
23
+ {--skip-sleep : Skip sleep delays and send all events immediately}';
24
+
25
+ /**
26
+ * The console command description.
27
+ *
28
+ * @var string
29
+ */
30
+ protected $description = 'Replay vehicle location events from JSON file with timing simulation via SocketCluster';
31
+
32
+ /**
33
+ * Execute the console command.
34
+ *
35
+ * @return int
36
+ */
37
+ public function handle()
38
+ {
39
+ $filePath = $this->argument('file');
40
+ $speedMultiplier = (float) $this->option('speed');
41
+ $vehicleFilter = $this->option('vehicle');
42
+ $limit = $this->option('limit') ? (int) $this->option('limit') : null;
43
+ $skipSleep = $this->option('skip-sleep');
44
+ $sleep = $this->option('sleep') ? (int) $this->option('sleep') : null;
45
+
46
+ // Validate file exists
47
+ if (!file_exists($filePath)) {
48
+ $this->error("File not found: {$filePath}");
49
+
50
+ return Command::FAILURE;
51
+ }
52
+
53
+ // Validate speed multiplier
54
+ if ($speedMultiplier <= 0) {
55
+ $this->error('Speed multiplier must be greater than 0');
56
+
57
+ return Command::FAILURE;
58
+ }
59
+
60
+ $this->info('Starting vehicle location replay...');
61
+ $this->info("File: {$filePath}");
62
+ $this->info("Speed: {$speedMultiplier}x");
63
+
64
+ if ($vehicleFilter) {
65
+ $this->info("Filtering for vehicle: {$vehicleFilter}");
66
+ }
67
+
68
+ if ($skipSleep) {
69
+ $this->warn('Sleep delays disabled - sending all events immediately');
70
+ }
71
+
72
+ // Load and parse JSON data
73
+ $this->info('Loading location data...');
74
+ $jsonContent = file_get_contents($filePath);
75
+ $locationEvents = json_decode($jsonContent, true);
76
+
77
+ if (json_last_error() !== JSON_ERROR_NONE) {
78
+ $this->error('Failed to parse JSON: ' . json_last_error_msg());
79
+
80
+ return Command::FAILURE;
81
+ }
82
+
83
+ if (!is_array($locationEvents) || empty($locationEvents)) {
84
+ $this->error('Invalid or empty location data');
85
+
86
+ return Command::FAILURE;
87
+ }
88
+
89
+ // Filter by vehicle if specified
90
+ if ($vehicleFilter) {
91
+ $locationEvents = array_filter($locationEvents, function ($event) use ($vehicleFilter) {
92
+ return isset($event['data']['id']) && $event['data']['id'] === $vehicleFilter;
93
+ });
94
+ $locationEvents = array_values($locationEvents); // Re-index array
95
+ }
96
+
97
+ $totalEvents = count($locationEvents);
98
+
99
+ if ($totalEvents === 0) {
100
+ $this->warn('No events found matching the criteria');
101
+
102
+ return Command::SUCCESS;
103
+ }
104
+
105
+ // Apply limit if specified
106
+ if ($limit && $limit < $totalEvents) {
107
+ $locationEvents = array_slice($locationEvents, 0, $limit);
108
+ $totalEvents = $limit;
109
+ }
110
+
111
+ $this->info("Total events to process: {$totalEvents}");
112
+ $this->newLine();
113
+
114
+ // Initialize SocketCluster client
115
+ $socketClusterClient = new SocketClusterService();
116
+
117
+ // Statistics tracking
118
+ $successCount = 0;
119
+ $errorCount = 0;
120
+ $startTime = microtime(true);
121
+ $previousTimestamp = null;
122
+
123
+ // Process each location event
124
+ foreach ($locationEvents as $index => $event) {
125
+ $eventNumber = $index + 1;
126
+ $vehicleId = $event['data']['id'] ?? 'unknown';
127
+ $eventId = $event['id'] ?? 'unknown';
128
+ $createdAt = $event['created_at'] ?? null;
129
+
130
+ // Get vehicle record
131
+ $vehicle = Vehicle::where('public_id', $vehicleId)->first();
132
+ if (!$vehicle) {
133
+ continue;
134
+ }
135
+
136
+ // Calculate sleep duration based on timestamp difference
137
+ if (!$skipSleep && $previousTimestamp !== null && $createdAt !== null) {
138
+ try {
139
+ $currentTime = Carbon::parse($createdAt);
140
+ $previousTime = Carbon::parse($previousTimestamp);
141
+ $diffInSeconds = $currentTime->diffInSeconds($previousTime);
142
+
143
+ // Apply speed multiplier
144
+ $sleepDuration = $diffInSeconds / $speedMultiplier;
145
+
146
+ if ($sleep) {
147
+ $this->info("[{$eventNumber}/{$totalEvents}] Waiting {$sleep}s (real: {$diffInSeconds}s)...");
148
+ sleep((int) $sleep);
149
+ } elseif ($sleepDuration > 0) {
150
+ $this->info("[{$eventNumber}/{$totalEvents}] Waiting {$sleepDuration}s (real: {$diffInSeconds}s)...");
151
+ sleep((int) $sleepDuration);
152
+
153
+ // Handle fractional seconds
154
+ $fractional = $sleepDuration - floor($sleepDuration);
155
+ if ($fractional > 0) {
156
+ usleep((int) ($fractional * 1000000));
157
+ }
158
+ }
159
+ } catch (\Exception $e) {
160
+ $this->warn("Failed to calculate time difference: {$e->getMessage()}");
161
+ }
162
+ }
163
+
164
+ // Update previous timestamp
165
+ $previousTimestamp = $createdAt;
166
+
167
+ // Prepare channel names
168
+ $channels = ["vehicle.{$vehicleId}", "vehicle.{$vehicle->uuid}"];
169
+
170
+ foreach ($channels as $channel) {
171
+ // Send event via SocketCluster
172
+ try {
173
+ $sent = $socketClusterClient->send($channel, $event);
174
+
175
+ $location = $event['data']['location']['coordinates'] ?? ['N/A', 'N/A'];
176
+ $speed = $event['data']['speed'] ?? 'N/A';
177
+ $heading = $event['data']['heading'] ?? 'N/A';
178
+
179
+ $this->line(sprintf(
180
+ "[{$eventNumber}/{$totalEvents}] ✓ Sent event %s for vehicle %s | Channel: %s | Coords: [%.6f, %.6f] | Speed: %s | Heading: %s | Time: %s",
181
+ $eventId,
182
+ $vehicleId,
183
+ $channel,
184
+ $location[0],
185
+ $location[1],
186
+ $speed,
187
+ $heading,
188
+ $createdAt ?? 'N/A'
189
+ ));
190
+
191
+ $successCount++;
192
+ } catch (\WebSocket\ConnectionException $e) {
193
+ $this->error("[{$eventNumber}/{$totalEvents}] ✗ Connection error for event {$eventId}: {$e->getMessage()}");
194
+ $errorCount++;
195
+ } catch (\WebSocket\TimeoutException $e) {
196
+ $this->error("[{$eventNumber}/{$totalEvents}] ✗ Timeout error for event {$eventId}: {$e->getMessage()}");
197
+ $errorCount++;
198
+ } catch (\Throwable $e) {
199
+ $this->error("[{$eventNumber}/{$totalEvents}] ✗ Error for event {$eventId}: {$e->getMessage()}");
200
+ $errorCount++;
201
+ }
202
+ }
203
+ }
204
+
205
+ // Summary
206
+ $endTime = microtime(true);
207
+ $duration = round($endTime - $startTime, 2);
208
+
209
+ $this->newLine();
210
+ $this->info('=== Replay Complete ===');
211
+ $this->info("Total events processed: {$totalEvents}");
212
+ $this->info("Successful: {$successCount}");
213
+
214
+ if ($errorCount > 0) {
215
+ $this->error("Failed: {$errorCount}");
216
+ } else {
217
+ $this->info("Failed: {$errorCount}");
218
+ }
219
+
220
+ $this->info("Duration: {$duration}s");
221
+ $this->info('Average rate: ' . round($totalEvents / max($duration, 0.001), 2) . ' events/second');
222
+
223
+ return $errorCount > 0 ? Command::FAILURE : Command::SUCCESS;
224
+ }
225
+ }
@@ -0,0 +1,72 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Contracts;
4
+
5
+ use Fleetbase\FleetOps\Support\Utils;
6
+
7
+ /**
8
+ * Class TelematicProviderDescriptor.
9
+ *
10
+ * Data Transfer Object for provider metadata.
11
+ * Used by the ProviderRegistry to describe available providers.
12
+ */
13
+ class TelematicProviderDescriptor
14
+ {
15
+ public string $key;
16
+ public string $label;
17
+ public string $type; // 'native' or 'custom'
18
+ public ?string $driverClass;
19
+ public ?string $icon;
20
+ public ?string $description;
21
+ public ?string $docsUrl;
22
+ public array $requiredFields;
23
+ public bool $supportsWebhooks;
24
+ public bool $supportsDiscovery;
25
+ public array $metadata;
26
+
27
+ /**
28
+ * Create a new ProviderDescriptor instance.
29
+ */
30
+ public function __construct(array $data)
31
+ {
32
+ $this->key = $data['key'];
33
+ $this->label = $data['label'];
34
+ $this->type = $data['type'] ?? 'native';
35
+ $this->driverClass = $data['driver_class'] ?? null;
36
+ $this->icon = $data['icon'] ?? null;
37
+ $this->description = $data['description'] ?? null;
38
+ $this->docsUrl = $data['docs_url'] ?? null;
39
+ $this->requiredFields = $data['required_fields'] ?? [];
40
+ $this->supportsWebhooks = $data['supports_webhooks'] ?? false;
41
+ $this->supportsDiscovery = $data['supports_discovery'] ?? false;
42
+ $this->metadata = $data['metadata'] ?? [];
43
+ }
44
+
45
+ /**
46
+ * Convert to array for JSON serialization.
47
+ */
48
+ public function toArray(): array
49
+ {
50
+ return [
51
+ 'key' => $this->key,
52
+ 'label' => $this->label,
53
+ 'type' => $this->type,
54
+ 'icon' => $this->icon,
55
+ 'description' => $this->description,
56
+ 'docs_url' => $this->docsUrl,
57
+ 'required_fields' => $this->requiredFields,
58
+ 'supports_webhooks' => $this->supportsWebhooks,
59
+ 'supports_discovery' => $this->supportsDiscovery,
60
+ 'metadata' => $this->metadata,
61
+ 'webhook_url' => Utils::apiUrl('webhooks/telematics/' . $this->key),
62
+ ];
63
+ }
64
+
65
+ /**
66
+ * Get JSON representation.
67
+ */
68
+ public function toJson(): string
69
+ {
70
+ return json_encode($this->toArray());
71
+ }
72
+ }
@@ -0,0 +1,119 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Contracts;
4
+
5
+ use Fleetbase\FleetOps\Models\Telematic;
6
+
7
+ /**
8
+ * Interface TelematicProviderInterface.
9
+ *
10
+ * Core contract that all telematics providers must implement.
11
+ * Defines the standard methods for authentication, device discovery,
12
+ * webhook handling, and data normalization.
13
+ */
14
+ interface TelematicProviderInterface
15
+ {
16
+ /**
17
+ * Connect to the provider using the given telematic configuration.
18
+ */
19
+ public function connect(Telematic $telematic): void;
20
+
21
+ /**
22
+ * Test the connection to the provider.
23
+ *
24
+ * @param array $credentials Provider credentials
25
+ *
26
+ * @return array ['success' => bool, 'message' => string, 'metadata' => array]
27
+ */
28
+ public function testConnection(array $credentials): array;
29
+
30
+ /**
31
+ * Fetch devices from the provider.
32
+ *
33
+ * @param array $options Options including limit, cursor, filters
34
+ *
35
+ * @return array ['devices' => array, 'next_cursor' => string|null, 'has_more' => bool]
36
+ */
37
+ public function fetchDevices(array $options = []): array;
38
+
39
+ /**
40
+ * Fetch detailed information for a specific device.
41
+ *
42
+ * @param string $externalId Provider's device identifier
43
+ *
44
+ * @return array Device details
45
+ */
46
+ public function fetchDeviceDetails(string $externalId): array;
47
+
48
+ /**
49
+ * Normalize a device payload from the provider into FleetOps format.
50
+ *
51
+ * @param array $payload Raw device data from provider
52
+ *
53
+ * @return array Normalized device data
54
+ */
55
+ public function normalizeDevice(array $payload): array;
56
+
57
+ /**
58
+ * Normalize an event payload from the provider into FleetOps format.
59
+ *
60
+ * @param array $payload Raw event data from provider
61
+ *
62
+ * @return array Normalized event data
63
+ */
64
+ public function normalizeEvent(array $payload): array;
65
+
66
+ /**
67
+ * Normalize sensor data from the provider into FleetOps format.
68
+ *
69
+ * @param array $payload Raw sensor data from provider
70
+ *
71
+ * @return array Normalized sensor data
72
+ */
73
+ public function normalizeSensor(array $payload): array;
74
+
75
+ /**
76
+ * Validate a webhook signature.
77
+ *
78
+ * @param string $payload Raw webhook payload
79
+ * @param string $signature Signature from webhook headers
80
+ * @param array $credentials Provider credentials
81
+ *
82
+ * @return bool True if signature is valid
83
+ */
84
+ public function validateWebhookSignature(string $payload, string $signature, array $credentials): bool;
85
+
86
+ /**
87
+ * Process a webhook payload from the provider.
88
+ *
89
+ * @param array $payload Webhook payload
90
+ * @param array $headers Webhook headers
91
+ *
92
+ * @return array ['devices' => array, 'events' => array, 'sensors' => array]
93
+ */
94
+ public function processWebhook(array $payload, array $headers = []): array;
95
+
96
+ /**
97
+ * Get the provider's credential schema.
98
+ *
99
+ * @return array Array of field definitions
100
+ */
101
+ public function getCredentialSchema(): array;
102
+
103
+ /**
104
+ * Check if the provider supports webhooks.
105
+ */
106
+ public function supportsWebhooks(): bool;
107
+
108
+ /**
109
+ * Check if the provider supports device discovery.
110
+ */
111
+ public function supportsDiscovery(): bool;
112
+
113
+ /**
114
+ * Get rate limit information for the provider.
115
+ *
116
+ * @return array ['requests_per_minute' => int, 'burst_size' => int]
117
+ */
118
+ public function getRateLimits(): array;
119
+ }
@@ -0,0 +1,14 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Exceptions;
4
+
5
+ use Exception;
6
+
7
+ /**
8
+ * Class TelematicProviderException.
9
+ *
10
+ * Base exception for provider-related errors.
11
+ */
12
+ class TelematicProviderException extends \Exception
13
+ {
14
+ }
@@ -0,0 +1,12 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Exceptions;
4
+
5
+ /**
6
+ * Class TelematicRateLimitExceededException.
7
+ *
8
+ * Exception thrown when provider rate limit is exceeded.
9
+ */
10
+ class TelematicRateLimitExceededException extends ProviderException
11
+ {
12
+ }
@@ -343,23 +343,34 @@ class DriverController extends Controller
343
343
  // check if driver needs a geocoded update to set city and country they are currently in
344
344
  $isGeocodable = Carbon::parse($driver->updated_at)->diffInMinutes(Carbon::now(), false) > 10 || empty($driver->country) || empty($driver->city);
345
345
 
346
- $driver->update([
347
- 'location' => new Point($latitude, $longitude),
348
- 'altitude' => $altitude,
349
- 'heading' => $heading,
350
- 'speed' => $speed,
351
- ]);
346
+ $positionData = [
347
+ 'location' => new Point($latitude, $longitude),
348
+ 'latitude' => $latitude,
349
+ 'longitude' => $longitude,
350
+ 'altitude' => $altitude,
351
+ 'heading' => $heading,
352
+ 'speed' => $speed,
353
+ ];
354
+
355
+ // Append current order to data if applicable
356
+ $order = $driver->getCurrentOrder();
357
+ if ($order) {
358
+ $positionData['order_uuid'] = $order->uuid;
359
+ // Get destination
360
+ $destination = $order->payload?->getPickupOrCurrentWaypoint();
361
+ if ($destination) {
362
+ $positionData['destination_uuid'] = $destination->uuid;
363
+ }
364
+ }
365
+
366
+ $driver->update($positionData);
367
+ $driver->createPosition($positionData);
352
368
 
353
369
  // If vehicle is assigned to driver load it and sync position data
354
370
  $driver->loadMissing('vehicle');
355
371
  if ($driver->vehicle) {
356
- $driver->vehicle->update([
357
- 'location' => new Point($latitude, $longitude),
358
- 'altitude' => $altitude,
359
- 'heading' => $heading,
360
- 'speed' => $speed,
361
- ]);
362
- $driver->vehicle->createPositionWithOrderContext();
372
+ $driver->vehicle->update($positionData);
373
+ $driver->vehicle->createPosition($positionData);
363
374
  broadcast(new VehicleLocationChanged($driver->vehicle, ['driver' => $driver->public_id]));
364
375
  }
365
376
 
@@ -376,7 +387,6 @@ class DriverController extends Controller
376
387
  }
377
388
 
378
389
  broadcast(new DriverLocationChanged($driver));
379
- $driver->createPositionWithOrderContext();
380
390
 
381
391
  return new DriverResource($driver);
382
392
  }
@@ -235,15 +235,35 @@ class VehicleController extends Controller
235
235
  );
236
236
  }
237
237
 
238
- $vehicle->update([
239
- 'location' => new Point($latitude, $longitude),
240
- 'altitude' => $altitude,
241
- 'heading' => $heading,
242
- 'speed' => $speed,
243
- ]);
238
+ $positionData = [
239
+ 'location' => new Point($latitude, $longitude),
240
+ 'latitude' => $latitude,
241
+ 'longitude' => $longitude,
242
+ 'altitude' => $altitude,
243
+ 'heading' => $heading,
244
+ 'speed' => $speed,
245
+ ];
246
+
247
+ // Get vehicle driver
248
+ $vehicle->loadMissing('driver');
249
+ $driver = $vehicle->driver;
250
+ if ($driver) {
251
+ // Append current order to data if applicable
252
+ $order = $driver->getCurrentOrder();
253
+ if ($order) {
254
+ $positionData['order_uuid'] = $order->uuid;
255
+ // Get destination
256
+ $destination = $order->payload?->getPickupOrCurrentWaypoint();
257
+ if ($destination) {
258
+ $positionData['destination_uuid'] = $destination->uuid;
259
+ }
260
+ }
261
+ }
262
+
263
+ $vehicle->update($positionData);
264
+ $vehicle->createPosition($positionData);
244
265
 
245
266
  broadcast(new VehicleLocationChanged($vehicle));
246
- $vehicle->createPositionWithOrderContext();
247
267
 
248
268
  return new VehicleResource($vehicle);
249
269
  }