@fleetbase/fleetops-engine 0.6.20 → 0.6.22

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 (162) hide show
  1. package/addon/components/custom-entity/form.hbs +14 -14
  2. package/addon/components/device/card.hbs +1 -0
  3. package/addon/components/device/card.js +3 -0
  4. package/addon/components/device/details.hbs +92 -43
  5. package/addon/components/device/form.hbs +108 -60
  6. package/addon/components/device/form.js +36 -8
  7. package/addon/components/device/manager.hbs +29 -0
  8. package/addon/components/device/manager.js +95 -0
  9. package/addon/components/device/panel-header.hbs +32 -0
  10. package/addon/components/device/panel-header.js +3 -0
  11. package/addon/components/device/pill.hbs +16 -0
  12. package/addon/components/device/pill.js +3 -0
  13. package/addon/components/driver/details.hbs +4 -0
  14. package/addon/components/driver/details.js +19 -1
  15. package/addon/components/driver/form.hbs +14 -3
  16. package/addon/components/driver/form.js +49 -47
  17. package/addon/components/driver/pill.hbs +17 -0
  18. package/addon/components/driver/pill.js +3 -0
  19. package/addon/components/entity/form.hbs +7 -5
  20. package/addon/components/layout/fleet-ops-sidebar.js +12 -12
  21. package/addon/components/map/drawer/device-event-listing.hbs +64 -0
  22. package/addon/components/map/drawer/device-event-listing.js +181 -0
  23. package/addon/components/map/drawer/position-listing.hbs +100 -0
  24. package/addon/components/map/drawer/position-listing.js +455 -0
  25. package/addon/components/map/drawer.js +2 -0
  26. package/addon/components/map/leaflet-live-map.hbs +7 -2
  27. package/addon/components/modals/attach-device.hbs +18 -0
  28. package/addon/components/modals/attach-device.js +3 -0
  29. package/addon/components/order/details/detail.hbs +2 -54
  30. package/addon/components/order/details/detail.js +1 -0
  31. package/addon/components/order/details/payload.hbs +6 -4
  32. package/addon/components/order/details/payload.js +2 -0
  33. package/addon/components/order/pill.hbs +34 -0
  34. package/addon/components/order/pill.js +3 -0
  35. package/addon/components/order-config-manager/custom-fields.js +1 -1
  36. package/addon/components/positions-replay.hbs +339 -0
  37. package/addon/components/positions-replay.js +409 -0
  38. package/addon/components/sensor/details.hbs +64 -38
  39. package/addon/components/sensor/form.hbs +112 -63
  40. package/addon/components/sensor/form.js +36 -24
  41. package/addon/components/sensor/panel-header.hbs +32 -0
  42. package/addon/components/sensor/panel-header.js +3 -0
  43. package/addon/components/telematic/details.hbs +40 -16
  44. package/addon/components/telematic/form.hbs +63 -64
  45. package/addon/components/telematic/form.js +73 -4
  46. package/addon/components/vehicle/card.hbs +2 -2
  47. package/addon/components/vehicle/details.hbs +4 -0
  48. package/addon/components/vehicle/details.js +19 -1
  49. package/addon/components/vehicle/form.hbs +4 -0
  50. package/addon/components/vehicle/pill.hbs +34 -0
  51. package/addon/components/vehicle/pill.js +3 -0
  52. package/addon/controllers/analytics/reports/index/edit.js +1 -1
  53. package/addon/controllers/connectivity/devices/index/details.js +22 -1
  54. package/addon/controllers/connectivity/devices/index/edit.js +66 -1
  55. package/addon/controllers/connectivity/devices/index.js +51 -9
  56. package/addon/controllers/connectivity/events/index.js +65 -16
  57. package/addon/controllers/connectivity/sensors/index/details.js +22 -1
  58. package/addon/controllers/connectivity/sensors/index/edit.js +66 -1
  59. package/addon/controllers/connectivity/sensors/index.js +66 -6
  60. package/addon/controllers/connectivity/telematics/index/details.js +22 -1
  61. package/addon/controllers/connectivity/telematics/index/edit.js +66 -1
  62. package/addon/controllers/connectivity/telematics/index.js +20 -11
  63. package/addon/controllers/management/fleets/index/details.js +26 -21
  64. package/addon/controllers/management/fleets/index/edit.js +9 -6
  65. package/addon/controllers/management/vehicles/index/details.js +26 -13
  66. package/addon/controllers/settings/custom-fields.js +6 -0
  67. package/addon/helpers/get-fleet-ops-option-label.js +11 -0
  68. package/addon/routes/connectivity/devices/index/details.js +27 -1
  69. package/addon/routes/connectivity/devices/index/edit.js +27 -1
  70. package/addon/routes/connectivity/sensors/index/details.js +27 -1
  71. package/addon/routes/connectivity/sensors/index/edit.js +27 -1
  72. package/addon/routes/connectivity/telematics/index/details.js +27 -1
  73. package/addon/routes/connectivity/telematics/index/edit.js +27 -1
  74. package/addon/routes/management/drivers/index/details/positions.js +3 -0
  75. package/addon/routes/management/vehicles/index/details/positions.js +3 -0
  76. package/addon/routes.js +4 -0
  77. package/addon/services/movement-tracker.js +81 -30
  78. package/addon/services/position-playback.js +486 -0
  79. package/addon/services/resource-metadata.js +46 -0
  80. package/addon/styles/fleetops-engine.css +157 -0
  81. package/addon/templates/connectivity/devices/index/details/index.hbs +2 -2
  82. package/addon/templates/connectivity/devices/index/details.hbs +15 -2
  83. package/addon/templates/connectivity/devices/index/edit.hbs +1 -1
  84. package/addon/templates/connectivity/events/index.hbs +1 -1
  85. package/addon/templates/connectivity/sensors/index/details/index.hbs +2 -2
  86. package/addon/templates/connectivity/sensors/index/details.hbs +15 -2
  87. package/addon/templates/connectivity/sensors/index/edit.hbs +1 -1
  88. package/addon/templates/connectivity/telematics/index/details/index.hbs +2 -2
  89. package/addon/templates/connectivity/telematics/index/details.hbs +14 -2
  90. package/addon/templates/connectivity/telematics/index/edit.hbs +1 -1
  91. package/addon/templates/management/drivers/index/details/positions.hbs +2 -0
  92. package/addon/templates/management/vehicles/index/details/devices.hbs +1 -2
  93. package/addon/templates/management/vehicles/index/details/positions.hbs +1 -0
  94. package/addon/utils/fleet-ops-options.js +95 -0
  95. package/app/components/device/card.js +1 -0
  96. package/app/components/device/manager.js +1 -0
  97. package/app/components/device/panel-header.js +1 -0
  98. package/app/components/device/pill.js +1 -0
  99. package/app/components/driver/pill.js +1 -0
  100. package/app/components/map/drawer/device-event-listing.js +1 -0
  101. package/app/components/map/drawer/position-listing.js +1 -0
  102. package/app/components/modals/attach-device.js +1 -0
  103. package/app/components/order/pill.js +1 -0
  104. package/app/components/positions-replay.js +1 -0
  105. package/app/components/sensor/panel-header.js +1 -0
  106. package/app/components/vehicle/pill.js +1 -0
  107. package/app/helpers/get-fleet-ops-option-label.js +1 -0
  108. package/app/routes/management/drivers/index/details/positions.js +1 -0
  109. package/app/routes/management/vehicles/index/details/positions.js +1 -0
  110. package/app/services/position-playback.js +1 -0
  111. package/app/services/resource-metadata.js +1 -0
  112. package/app/templates/management/drivers/index/details/positions.js +1 -0
  113. package/app/templates/management/vehicles/index/details/positions.js +1 -0
  114. package/composer.json +1 -1
  115. package/extension.json +1 -1
  116. package/package.json +4 -4
  117. package/server/config/telematics.php +111 -0
  118. package/server/migrations/2025_10_27_000001_add_telematics_integration_fields.php +70 -0
  119. package/server/migrations/2025_10_27_171322_fix_device_column_names.php +107 -0
  120. package/server/migrations/2025_10_27_203023_add_company_uuid_to_device_events_table.php +28 -0
  121. package/server/src/Console/Commands/ReplayVehicleLocations.php +225 -0
  122. package/server/src/Contracts/TelematicProviderDescriptor.php +72 -0
  123. package/server/src/Contracts/TelematicProviderInterface.php +119 -0
  124. package/server/src/Exceptions/TelematicProviderException.php +14 -0
  125. package/server/src/Exceptions/TelematicRateLimitExceededException.php +12 -0
  126. package/server/src/Http/Controllers/Api/v1/DriverController.php +24 -14
  127. package/server/src/Http/Controllers/Api/v1/VehicleController.php +27 -7
  128. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +22 -0
  129. package/server/src/Http/Controllers/Internal/v1/PositionController.php +240 -0
  130. package/server/src/Http/Controllers/Internal/v1/SensorController.php +11 -0
  131. package/server/src/Http/Controllers/Internal/v1/TelematicController.php +141 -0
  132. package/server/src/Http/Controllers/TelematicWebhookController.php +169 -0
  133. package/server/src/Http/Filter/DeviceEventFilter.php +68 -0
  134. package/server/src/Http/Filter/PositionFilter.php +35 -0
  135. package/server/src/Http/Resources/v1/Position.php +44 -0
  136. package/server/src/Jobs/ReplayPositions.php +64 -0
  137. package/server/src/Jobs/SendPositionReplay.php +65 -0
  138. package/server/src/Jobs/SyncTelematicDevicesJob.php +106 -0
  139. package/server/src/Jobs/TestTelematicConnectionJob.php +102 -0
  140. package/server/src/Models/Asset.php +10 -8
  141. package/server/src/Models/Device.php +79 -12
  142. package/server/src/Models/DeviceEvent.php +33 -3
  143. package/server/src/Models/Driver.php +28 -1
  144. package/server/src/Models/Maintenance.php +15 -12
  145. package/server/src/Models/Part.php +2 -0
  146. package/server/src/Models/Payload.php +0 -1
  147. package/server/src/Models/Place.php +4 -1
  148. package/server/src/Models/Position.php +27 -17
  149. package/server/src/Models/Sensor.php +78 -13
  150. package/server/src/Models/Telematic.php +116 -6
  151. package/server/src/Models/TrackingNumber.php +3 -1
  152. package/server/src/Models/Vehicle.php +8 -11
  153. package/server/src/Models/WorkOrder.php +8 -5
  154. package/server/src/Providers/FleetOpsServiceProvider.php +2 -0
  155. package/server/src/Support/Telematics/Providers/AbstractProvider.php +151 -0
  156. package/server/src/Support/Telematics/Providers/FlespiProvider.php +182 -0
  157. package/server/src/Support/Telematics/Providers/GeotabProvider.php +181 -0
  158. package/server/src/Support/Telematics/Providers/SamsaraProvider.php +177 -0
  159. package/server/src/Support/Telematics/TelematicProviderRegistry.php +147 -0
  160. package/server/src/Support/Telematics/TelematicService.php +223 -0
  161. package/server/src/Support/Utils.php +1 -1
  162. package/server/src/routes.php +24 -1
@@ -3,6 +3,7 @@
3
3
  namespace Fleetbase\FleetOps\Models;
4
4
 
5
5
  use Fleetbase\Casts\Json;
6
+ use Fleetbase\FleetOps\Support\Telematics\TelematicProviderRegistry;
6
7
  use Fleetbase\Models\Model;
7
8
  use Fleetbase\Models\User;
8
9
  use Fleetbase\Traits\HasApiModelBehavior;
@@ -85,8 +86,8 @@ class Telematic extends Model
85
86
  'last_seen_at',
86
87
  'last_metrics',
87
88
  'config',
89
+ 'credentials',
88
90
  'meta',
89
- 'slug',
90
91
  ];
91
92
 
92
93
  /**
@@ -94,7 +95,7 @@ class Telematic extends Model
94
95
  *
95
96
  * @var array
96
97
  */
97
- protected $appends = ['warranty_name', 'is_online', 'signal_strength', 'last_location'];
98
+ protected $appends = ['warranty_name', 'is_online', 'signal_strength', 'last_location', 'provider_descriptor'];
98
99
 
99
100
  /**
100
101
  * The attributes excluded from the model's JSON form.
@@ -109,10 +110,97 @@ class Telematic extends Model
109
110
  * @var array
110
111
  */
111
112
  protected $casts = [
112
- 'last_seen_at' => 'datetime',
113
- 'last_metrics' => Json::class,
114
- 'config' => Json::class,
115
- 'meta' => Json::class,
113
+ 'last_seen_at' => 'datetime',
114
+ 'last_metrics' => Json::class,
115
+ 'config' => Json::class,
116
+ 'credentials' => Json::class,
117
+ 'meta' => Json::class,
118
+ ];
119
+
120
+ /**
121
+ * Telematic statuses.
122
+ *
123
+ * @var array
124
+ */
125
+ public static $statuses = [
126
+ [
127
+ 'key' => 'initialized',
128
+ 'label' => 'Initialized',
129
+ 'description' => 'Provider entry has been created but not yet configured.',
130
+ ],
131
+ [
132
+ 'key' => 'configured',
133
+ 'label' => 'Configured',
134
+ 'description' => 'Provider credentials and settings are valid but connection not yet tested.',
135
+ ],
136
+ [
137
+ 'key' => 'connecting',
138
+ 'label' => 'Connecting',
139
+ 'description' => 'Attempting to establish a connection with provider API.',
140
+ ],
141
+ [
142
+ 'key' => 'connected',
143
+ 'label' => 'Connected',
144
+ 'description' => 'Successfully authenticated and connected to provider API.',
145
+ ],
146
+ [
147
+ 'key' => 'synchronizing',
148
+ 'label' => 'Synchronizing',
149
+ 'description' => 'Currently syncing data (devices, vehicles, positions, etc.) from provider.',
150
+ ],
151
+ [
152
+ 'key' => 'active',
153
+ 'label' => 'Active',
154
+ 'description' => 'Integration is healthy and data syncs are occurring normally.',
155
+ ],
156
+ [
157
+ 'key' => 'degraded',
158
+ 'label' => 'Degraded',
159
+ 'description' => 'Integration partially working; intermittent errors or missing data.',
160
+ ],
161
+ [
162
+ 'key' => 'disconnected',
163
+ 'label' => 'Disconnected',
164
+ 'description' => 'Connection lost or failed authentication.',
165
+ ],
166
+ [
167
+ 'key' => 'error',
168
+ 'label' => 'Error',
169
+ 'description' => 'Provider integration encountered a fatal issue.',
170
+ ],
171
+ [
172
+ 'key' => 'disabled',
173
+ 'label' => 'Disabled',
174
+ 'description' => 'Manually disabled by the user.',
175
+ ],
176
+ [
177
+ 'key' => 'archived',
178
+ 'label' => 'Archived',
179
+ 'description' => 'Deprecated or replaced integration, kept for record.',
180
+ ],
181
+ ];
182
+
183
+ /**
184
+ * Telematic health statuses.
185
+ *
186
+ * @var array
187
+ */
188
+ public static $healthStates = [
189
+ [
190
+ 'key' => 'healthy',
191
+ 'label' => 'Healthy',
192
+ 'description' => 'Integration tested and stable.',
193
+ ],
194
+ [
195
+ 'key' => 'warning',
196
+ 'label' => 'Warning',
197
+ 'description' => 'Minor issues detected (e.g., slow response, nearing quota).',
198
+ ],
199
+ [
200
+ 'key' => 'critical',
201
+ 'label' => 'Critical',
202
+ 'description' => 'Persistent failure or no data received in X hours.',
203
+ ],
116
204
  ];
117
205
 
118
206
  /**
@@ -169,6 +257,20 @@ class Telematic extends Model
169
257
  return $this->hasMany(Asset::class, 'telematic_uuid', 'uuid');
170
258
  }
171
259
 
260
+ /**
261
+ * Get the provider config.
262
+ */
263
+ public function getProviderDescriptorAttribute(): array
264
+ {
265
+ $registry = app(TelematicProviderRegistry::class);
266
+ $provider = $registry->findByKey($this->provider);
267
+ if ($provider) {
268
+ return $provider->toArray();
269
+ }
270
+
271
+ return [];
272
+ }
273
+
172
274
  /**
173
275
  * Get the warranty name.
174
276
  */
@@ -333,4 +435,12 @@ class Telematic extends Model
333
435
 
334
436
  return true;
335
437
  }
438
+
439
+ /**
440
+ * Sets a default status if none input.
441
+ */
442
+ public function setStatusAttribute(?string $status = null): void
443
+ {
444
+ $this->attributes['status'] = $status ?? 'initialized';
445
+ }
336
446
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Models;
4
4
 
5
+ use Fleetbase\Casts\PolymorphicType;
5
6
  use Fleetbase\FleetOps\Casts\Point;
6
7
  use Fleetbase\FleetOps\Support\Utils;
7
8
  use Fleetbase\LaravelMysqlSpatial\Eloquent\SpatialTrait;
@@ -66,7 +67,8 @@ class TrackingNumber extends Model
66
67
  * @var array
67
68
  */
68
69
  protected $casts = [
69
- 'location' => Point::class,
70
+ 'location' => Point::class,
71
+ 'owner_type' => PolymorphicType::class,
70
72
  ];
71
73
 
72
74
  /**
@@ -536,10 +536,7 @@ class Vehicle extends Model
536
536
  }
537
537
 
538
538
  /**
539
- * Creates a new position for the vehicle
540
- *
541
- * @param array $attributes
542
- * @return Position|null
539
+ * Creates a new position for the vehicle.
543
540
  */
544
541
  public function createPosition(array $attributes = [], Model|string|null $destination = null): ?Position
545
542
  {
@@ -555,11 +552,11 @@ class Vehicle extends Model
555
552
  $destinationUuid = Str::isUuid($destination) ? $destination : data_get($destination, 'uuid');
556
553
 
557
554
  return Position::create([
558
- ...Arr::only($attributes, ['coordinates', 'heading', 'bearing', 'speed', 'altitude']),
559
- 'subject_uuid' => $this->uuid,
560
- 'subject_type' => $this->getMorphClass(),
561
- 'company_uuid' => $this->company_uuid,
562
- 'destination_uuid' => $destinationUuid
555
+ ...Arr::only($attributes, ['coordinates', 'heading', 'bearing', 'speed', 'altitude', 'order_uuid']),
556
+ 'subject_uuid' => $this->uuid,
557
+ 'subject_type' => $this->getMorphClass(),
558
+ 'company_uuid' => $this->company_uuid,
559
+ 'destination_uuid' => $destinationUuid,
563
560
  ]);
564
561
  }
565
562
 
@@ -760,8 +757,8 @@ class Vehicle extends Model
760
757
  */
761
758
  public function setVinDatas(array $newVinData = []): array
762
759
  {
763
- $vinData = is_array($this->vin_data) ? $this->vin_data : (array) $this->vin_data;
764
- $vinData = array_merge($vinData, $newVinData);
760
+ $vinData = is_array($this->vin_data) ? $this->vin_data : (array) $this->vin_data;
761
+ $vinData = array_merge($vinData, $newVinData);
765
762
  $this->vin_data = $vinData;
766
763
 
767
764
  return $vinData;
@@ -3,6 +3,7 @@
3
3
  namespace Fleetbase\FleetOps\Models;
4
4
 
5
5
  use Fleetbase\Casts\Json;
6
+ use Fleetbase\Casts\PolymorphicType;
6
7
  use Fleetbase\Models\File;
7
8
  use Fleetbase\Models\Model;
8
9
  use Fleetbase\Models\User;
@@ -115,11 +116,13 @@ class WorkOrder extends Model
115
116
  * @var array
116
117
  */
117
118
  protected $casts = [
118
- 'opened_at' => 'datetime',
119
- 'due_at' => 'datetime',
120
- 'closed_at' => 'datetime',
121
- 'checklist' => Json::class,
122
- 'meta' => Json::class,
119
+ 'opened_at' => 'datetime',
120
+ 'due_at' => 'datetime',
121
+ 'closed_at' => 'datetime',
122
+ 'checklist' => Json::class,
123
+ 'meta' => Json::class,
124
+ 'target_type' => PolymorphicType::class,
125
+ 'assignee_type' => PolymorphicType::class,
123
126
  ];
124
127
 
125
128
  /**
@@ -59,6 +59,7 @@ class FleetOpsServiceProvider extends CoreServiceProvider
59
59
  \Fleetbase\FleetOps\Console\Commands\DebugOrderTracker::class,
60
60
  \Fleetbase\FleetOps\Console\Commands\PurgeUnpurchasedServiceQuotes::class,
61
61
  \Fleetbase\FleetOps\Console\Commands\SendDriverNotification::class,
62
+ \Fleetbase\FleetOps\Console\Commands\ReplayVehicleLocations::class,
62
63
  ];
63
64
 
64
65
  /**
@@ -103,6 +104,7 @@ class FleetOpsServiceProvider extends CoreServiceProvider
103
104
  $this->loadMigrationsFrom(__DIR__ . '/../../migrations');
104
105
  $this->loadViewsFrom(__DIR__ . '/../../resources/views', 'fleetops');
105
106
  $this->mergeConfigFrom(__DIR__ . '/../../config/fleetops.php', 'fleetops');
107
+ $this->mergeConfigFrom(__DIR__ . '/../../config/telematics.php', 'telematics');
106
108
  $this->mergeConfigFrom(__DIR__ . '/../../config/api.php', 'api');
107
109
  $this->mergeConfigFrom(__DIR__ . '/../../config/cache.stores.php', 'cache.stores');
108
110
  $this->mergeConfigFrom(__DIR__ . '/../../config/geocoder.php', 'geocoder');
@@ -0,0 +1,151 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Telematics\Providers;
4
+
5
+ use Fleetbase\FleetOps\Contracts\TelematicProviderInterface;
6
+ use Fleetbase\FleetOps\Exceptions\TelematicRateLimitExceededException;
7
+ use Fleetbase\FleetOps\Models\Telematic;
8
+ use Illuminate\Support\Facades\Cache;
9
+ use Illuminate\Support\Facades\Crypt;
10
+ use Illuminate\Support\Facades\Http;
11
+ use Illuminate\Support\Facades\Log;
12
+ use Illuminate\Support\Str;
13
+
14
+ /**
15
+ * Class AbstractProvider.
16
+ *
17
+ * Base implementation for all telematics providers.
18
+ * Provides common functionality for HTTP requests, rate limiting,
19
+ * and credential management.
20
+ */
21
+ abstract class AbstractProvider implements TelematicProviderInterface
22
+ {
23
+ protected Telematic $telematic;
24
+ protected array $credentials = [];
25
+ protected array $headers = [];
26
+ protected string $baseUrl = '';
27
+ protected int $requestsPerMinute = 60;
28
+ protected int $burstSize = 10;
29
+
30
+ /**
31
+ * Connect to the provider.
32
+ */
33
+ public function connect(Telematic $telematic): void
34
+ {
35
+ $this->telematic = $telematic;
36
+ $this->credentials = json_decode(Crypt::decryptString($telematic->credentials), true);
37
+ $this->prepareAuthentication();
38
+ }
39
+
40
+ /**
41
+ * Prepare authentication headers/tokens.
42
+ * Override this in provider implementations.
43
+ */
44
+ abstract protected function prepareAuthentication(): void;
45
+
46
+ /**
47
+ * Make an HTTP request to the provider API.
48
+ *
49
+ * @throws TelematicRateLimitExceededException
50
+ */
51
+ protected function request(string $method, string $endpoint, array $data = []): array
52
+ {
53
+ $this->checkRateLimit();
54
+
55
+ $url = $this->baseUrl . $endpoint;
56
+ $correlationId = Str::uuid()->toString();
57
+
58
+ Log::info('Provider API request', [
59
+ 'correlation_id' => $correlationId,
60
+ 'provider' => class_basename($this),
61
+ 'method' => $method,
62
+ 'url' => $url,
63
+ ]);
64
+
65
+ $response = Http::withHeaders($this->headers)
66
+ ->timeout(30)
67
+ ->{strtolower($method)}($url, $data);
68
+
69
+ $this->recordRequest();
70
+
71
+ if ($response->failed()) {
72
+ Log::error('Provider API request failed', [
73
+ 'correlation_id' => $correlationId,
74
+ 'status' => $response->status(),
75
+ 'body' => $response->body(),
76
+ ]);
77
+
78
+ throw new \Exception('API request failed: ' . $response->body());
79
+ }
80
+
81
+ return $response->json();
82
+ }
83
+
84
+ /**
85
+ * Check rate limit using token bucket algorithm.
86
+ *
87
+ * @throws TelematicRateLimitExceededException
88
+ */
89
+ protected function checkRateLimit(): void
90
+ {
91
+ $key = 'rate_limit:' . class_basename($this) . ':' . $this->telematic->uuid;
92
+ $tokens = Cache::get($key, $this->burstSize);
93
+
94
+ if ($tokens <= 0) {
95
+ throw new TelematicRateLimitExceededException('Rate limit exceeded for provider');
96
+ }
97
+
98
+ Cache::put($key, $tokens - 1, 60);
99
+ }
100
+
101
+ /**
102
+ * Record a request for metrics.
103
+ */
104
+ protected function recordRequest(): void
105
+ {
106
+ $key = 'rate_limit:' . class_basename($this) . ':' . $this->telematic->uuid;
107
+ $tokens = Cache::get($key, 0);
108
+
109
+ // Refill tokens gradually
110
+ if ($tokens < $this->burstSize) {
111
+ Cache::put($key, min($tokens + 1, $this->burstSize), 60);
112
+ }
113
+ }
114
+
115
+ public function supportsWebhooks(): bool
116
+ {
117
+ return false;
118
+ }
119
+
120
+ public function supportsDiscovery(): bool
121
+ {
122
+ return true;
123
+ }
124
+
125
+ public function getRateLimits(): array
126
+ {
127
+ return [
128
+ 'requests_per_minute' => $this->requestsPerMinute,
129
+ 'burst_size' => $this->burstSize,
130
+ ];
131
+ }
132
+
133
+ public function validateWebhookSignature(string $payload, string $signature, array $credentials): bool
134
+ {
135
+ return false;
136
+ }
137
+
138
+ public function processWebhook(array $payload, array $headers = []): array
139
+ {
140
+ return [
141
+ 'devices' => [],
142
+ 'events' => [],
143
+ 'sensors' => [],
144
+ ];
145
+ }
146
+
147
+ public function getCredentialSchema(): array
148
+ {
149
+ return [];
150
+ }
151
+ }
@@ -0,0 +1,182 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Telematics\Providers;
4
+
5
+ /**
6
+ * Class FlespiProvider.
7
+ *
8
+ * Flespi telematics provider implementation.
9
+ * https://flespi.com/
10
+ */
11
+ class FlespiProvider extends AbstractProvider
12
+ {
13
+ protected string $baseUrl = 'https://flespi.io/gw';
14
+ protected int $requestsPerMinute = 100;
15
+
16
+ protected function prepareAuthentication(): void
17
+ {
18
+ $this->headers = [
19
+ 'Authorization' => 'FlespiToken ' . $this->credentials['token'],
20
+ 'Accept' => 'application/json',
21
+ ];
22
+ }
23
+
24
+ public function testConnection(array $credentials): array
25
+ {
26
+ try {
27
+ $this->credentials = $credentials;
28
+ $this->prepareAuthentication();
29
+
30
+ // Test by fetching channels
31
+ $response = $this->request('GET', '/channels/all');
32
+
33
+ return [
34
+ 'success' => true,
35
+ 'message' => 'Connection successful',
36
+ 'metadata' => [
37
+ 'channels_count' => count($response['result'] ?? []),
38
+ ],
39
+ ];
40
+ } catch (\Exception $e) {
41
+ return [
42
+ 'success' => false,
43
+ 'message' => $e->getMessage(),
44
+ 'metadata' => [],
45
+ ];
46
+ }
47
+ }
48
+
49
+ public function fetchDevices(array $options = []): array
50
+ {
51
+ $limit = $options['limit'] ?? 100;
52
+ $cursor = $options['cursor'] ?? null;
53
+
54
+ $params = ['count' => $limit];
55
+ if ($cursor) {
56
+ $params['offset'] = $cursor;
57
+ }
58
+
59
+ $response = $this->request('GET', '/devices/all', $params);
60
+
61
+ $devices = $response['result'] ?? [];
62
+ $nextCursor = count($devices) >= $limit ? ((int) ($cursor ?? 0) + $limit) : null;
63
+
64
+ return [
65
+ 'devices' => $devices,
66
+ 'next_cursor' => $nextCursor,
67
+ 'has_more' => $nextCursor !== null,
68
+ ];
69
+ }
70
+
71
+ public function fetchDeviceDetails(string $externalId): array
72
+ {
73
+ $response = $this->request('GET', "/devices/{$externalId}");
74
+
75
+ return $response['result'][0] ?? [];
76
+ }
77
+
78
+ public function normalizeDevice(array $payload): array
79
+ {
80
+ return [
81
+ 'external_id' => $payload['id'],
82
+ 'device_name' => $payload['name'] ?? 'Unknown Device',
83
+ 'device_provider' => 'flespi',
84
+ 'device_model' => $payload['device_type_id'] ?? null,
85
+ 'imei' => $payload['configuration']['ident'] ?? null,
86
+ 'phone' => $payload['configuration']['phone'] ?? null,
87
+ 'status' => isset($payload['telemetry']) ? 'active' : 'inactive',
88
+ 'location' => [
89
+ 'lat' => $payload['telemetry']['position.latitude'] ?? null,
90
+ 'lng' => $payload['telemetry']['position.longitude'] ?? null,
91
+ ],
92
+ 'meta' => $payload,
93
+ ];
94
+ }
95
+
96
+ public function normalizeEvent(array $payload): array
97
+ {
98
+ return [
99
+ 'external_id' => $payload['id'] ?? null,
100
+ 'event_type' => $payload['event.enum'] ?? 'telemetry_update',
101
+ 'occurred_at' => isset($payload['timestamp']) ? date('Y-m-d H:i:s', $payload['timestamp']) : now(),
102
+ 'location' => [
103
+ 'lat' => $payload['position.latitude'] ?? null,
104
+ 'lng' => $payload['position.longitude'] ?? null,
105
+ ],
106
+ 'meta' => $payload,
107
+ ];
108
+ }
109
+
110
+ public function normalizeSensor(array $payload): array
111
+ {
112
+ return [
113
+ 'sensor_type' => $payload['sensor_type'] ?? 'generic',
114
+ 'value' => $payload['value'] ?? null,
115
+ 'unit' => $payload['unit'] ?? null,
116
+ 'recorded_at' => isset($payload['timestamp']) ? date('Y-m-d H:i:s', $payload['timestamp']) : now(),
117
+ 'meta' => $payload,
118
+ ];
119
+ }
120
+
121
+ public function validateWebhookSignature(string $payload, string $signature, array $credentials): bool
122
+ {
123
+ if (!isset($credentials['webhook_secret'])) {
124
+ return true; // No secret configured, skip validation
125
+ }
126
+
127
+ $expectedSignature = hash_hmac('sha256', $payload, $credentials['webhook_secret']);
128
+
129
+ return hash_equals($expectedSignature, $signature);
130
+ }
131
+
132
+ public function processWebhook(array $payload, array $headers = []): array
133
+ {
134
+ $devices = [];
135
+ $events = [];
136
+
137
+ // Flespi sends array of messages
138
+ foreach ($payload as $message) {
139
+ if (isset($message['device.id'])) {
140
+ $devices[] = $this->normalizeDevice([
141
+ 'id' => $message['device.id'],
142
+ 'name' => $message['device.name'] ?? null,
143
+ 'telemetry' => $message,
144
+ ]);
145
+
146
+ $events[] = $this->normalizeEvent($message);
147
+ }
148
+ }
149
+
150
+ return [
151
+ 'devices' => $devices,
152
+ 'events' => $events,
153
+ 'sensors' => [],
154
+ ];
155
+ }
156
+
157
+ public function getCredentialSchema(): array
158
+ {
159
+ return [
160
+ [
161
+ 'name' => 'token',
162
+ 'label' => 'Flespi Token',
163
+ 'type' => 'password',
164
+ 'placeholder' => 'Enter your Flespi API token',
165
+ 'required' => true,
166
+ 'validation' => 'required|string|min:20',
167
+ ],
168
+ [
169
+ 'name' => 'webhook_secret',
170
+ 'label' => 'Webhook Secret (Optional)',
171
+ 'type' => 'password',
172
+ 'placeholder' => 'Enter webhook secret for signature validation',
173
+ 'required' => false,
174
+ ],
175
+ ];
176
+ }
177
+
178
+ public function supportsWebhooks(): bool
179
+ {
180
+ return true;
181
+ }
182
+ }