@fleetbase/fleetops-engine 0.6.54 → 0.6.55

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 (127) hide show
  1. package/addon/components/admin/routing-settings.hbs +2 -1
  2. package/addon/components/cell/attached-vehicle.hbs +10 -0
  3. package/addon/components/cell/attached-vehicle.js +58 -0
  4. package/addon/components/cell/device-identity.hbs +24 -0
  5. package/addon/components/cell/device-identity.js +129 -0
  6. package/addon/components/cell/driver-identity.hbs +33 -0
  7. package/addon/components/cell/driver-identity.js +143 -0
  8. package/addon/components/cell/equipment-identity.hbs +5 -0
  9. package/addon/components/cell/equipment-identity.js +48 -0
  10. package/addon/components/cell/part-identity.hbs +5 -0
  11. package/addon/components/cell/part-identity.js +63 -0
  12. package/addon/components/cell/telematic-device.hbs +38 -0
  13. package/addon/components/cell/telematic-device.js +44 -0
  14. package/addon/components/cell/telematic-provider.hbs +33 -16
  15. package/addon/components/cell/telematic-provider.js +50 -8
  16. package/addon/components/cell/vehicle-identity.hbs +33 -0
  17. package/addon/components/cell/vehicle-identity.js +138 -0
  18. package/addon/components/device/form.hbs +2 -2
  19. package/addon/components/device/form.js +7 -0
  20. package/addon/components/device/panel-header.hbs +45 -20
  21. package/addon/components/device/panel-header.js +49 -1
  22. package/addon/components/device/panel-tabs/events.hbs +30 -0
  23. package/addon/components/device/panel-tabs/events.js +128 -0
  24. package/addon/components/device/panel-tabs/sensors.hbs +30 -0
  25. package/addon/components/device/panel-tabs/sensors.js +93 -0
  26. package/addon/components/device/panel-tabs/vehicle.hbs +68 -0
  27. package/addon/components/device/panel-tabs/vehicle.js +90 -0
  28. package/addon/components/equipment/form.js +8 -1
  29. package/addon/components/layout/fleet-ops-sidebar.hbs +1 -1
  30. package/addon/components/layout/fleet-ops-sidebar.js +8 -0
  31. package/addon/components/map/drawer/device-event-listing.js +2 -0
  32. package/addon/components/map/drawer/driver-listing.js +2 -0
  33. package/addon/components/map/drawer/place-listing.js +2 -0
  34. package/addon/components/map/drawer/position-listing.hbs +11 -6
  35. package/addon/components/map/drawer/position-listing.js +17 -4
  36. package/addon/components/map/drawer/vehicle-listing.js +2 -0
  37. package/addon/components/sensor/panel-header.hbs +47 -15
  38. package/addon/components/sensor/panel-header.js +59 -1
  39. package/addon/components/table/cell/dropdown/action-item.hbs +31 -0
  40. package/addon/components/table/cell/dropdown/action-item.js +62 -0
  41. package/addon/components/table/cell/dropdown.hbs +28 -0
  42. package/addon/components/table/cell/dropdown.js +91 -0
  43. package/addon/components/vendor/panel-header.hbs +82 -0
  44. package/addon/components/vendor/panel-header.js +48 -0
  45. package/addon/controllers/connectivity/devices/index/details/vehicle.js +49 -1
  46. package/addon/controllers/connectivity/devices/index.js +144 -129
  47. package/addon/controllers/connectivity/events/details.js +3 -0
  48. package/addon/controllers/connectivity/events/index.js +187 -11
  49. package/addon/controllers/connectivity/sensors/index.js +156 -14
  50. package/addon/controllers/connectivity/telematics/details/devices.js +126 -134
  51. package/addon/controllers/maintenance/equipment/index.js +1 -2
  52. package/addon/controllers/maintenance/parts/index.js +1 -2
  53. package/addon/controllers/management/drivers/index.js +82 -42
  54. package/addon/controllers/management/fuel-reports/index.js +63 -4
  55. package/addon/controllers/management/issues/index.js +63 -4
  56. package/addon/controllers/management/vehicles/index.js +33 -6
  57. package/addon/routes/connectivity/devices/index/details/vehicle.js +24 -2
  58. package/addon/routes/connectivity/devices/index.js +7 -0
  59. package/addon/routes/connectivity/events/details.js +3 -0
  60. package/addon/routes/connectivity/events/index.js +6 -2
  61. package/addon/routes/connectivity/sensors/index.js +7 -2
  62. package/addon/routes/connectivity/telematics/details/devices.js +2 -0
  63. package/addon/services/device-actions.js +30 -0
  64. package/addon/services/device-event-actions.js +1 -1
  65. package/addon/services/equipment-actions.js +1 -0
  66. package/addon/services/sensor-actions.js +3 -0
  67. package/addon/services/service-rate-actions.js +39 -1
  68. package/addon/services/vehicle-actions.js +24 -12
  69. package/addon/services/vendor-actions.js +3 -0
  70. package/addon/styles/fleetops-engine.css +8 -0
  71. package/addon/templates/connectivity/devices/index/details/vehicle.hbs +64 -1
  72. package/addon/templates/management/vendors/index/details.hbs +2 -2
  73. package/addon/templates/settings/notifications.hbs +2 -1
  74. package/addon/templates/settings/routing.hbs +1 -0
  75. package/addon/utils/device-table-columns.js +288 -0
  76. package/addon/utils/identity-cell-resource.js +20 -0
  77. package/addon/utils/map-drawer-dropdown-position.js +47 -0
  78. package/addon/utils/trackable-option.js +93 -0
  79. package/app/components/cell/attached-vehicle.js +1 -0
  80. package/app/components/cell/device-identity.js +1 -0
  81. package/app/components/cell/driver-identity.js +1 -0
  82. package/app/components/cell/equipment-identity.js +1 -0
  83. package/app/components/cell/part-identity.js +1 -0
  84. package/app/components/cell/telematic-device.js +1 -0
  85. package/app/components/cell/vehicle-identity.js +1 -0
  86. package/app/components/device/panel-tabs/events.hbs +30 -0
  87. package/app/components/device/panel-tabs/events.js +128 -0
  88. package/app/components/device/panel-tabs/sensors.hbs +30 -0
  89. package/app/components/device/panel-tabs/sensors.js +93 -0
  90. package/app/components/device/panel-tabs/vehicle.hbs +68 -0
  91. package/app/components/device/panel-tabs/vehicle.js +90 -0
  92. package/app/components/table/cell/dropdown/action-item.js +1 -0
  93. package/app/components/table/cell/dropdown.js +1 -0
  94. package/app/components/vendor/panel-header.js +1 -0
  95. package/app/controllers/connectivity/events/{index/details.js → details.js} +1 -1
  96. package/app/routes/connectivity/events/{index/details.js → details.js} +1 -1
  97. package/app/templates/connectivity/events/{index/details.js → details.js} +1 -1
  98. package/app/utils/map-drawer-dropdown-position.js +1 -0
  99. package/app/utils/trackable-option.js +1 -0
  100. package/composer.json +1 -1
  101. package/extension.json +1 -1
  102. package/package.json +4 -4
  103. package/server/src/Console/Commands/SyncTelematics.php +4 -4
  104. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +18 -1
  105. package/server/src/Http/Controllers/Internal/v1/DeviceEventController.php +1 -1
  106. package/server/src/Http/Controllers/Internal/v1/LiveController.php +1 -1
  107. package/server/src/Http/Controllers/Internal/v1/ServiceRateController.php +82 -13
  108. package/server/src/Http/Filter/DeviceEventFilter.php +21 -0
  109. package/server/src/Http/Filter/DeviceFilter.php +24 -1
  110. package/server/src/Http/Resources/v1/Index/Vehicle.php +19 -0
  111. package/server/src/Models/Device.php +2 -0
  112. package/server/src/Models/DeviceEvent.php +81 -0
  113. package/server/src/Models/Equipment.php +55 -0
  114. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +1 -0
  115. package/server/src/Support/Telematics/Providers/FlespiProvider.php +89 -15
  116. package/server/src/Support/Telematics/Providers/GeotabProvider.php +123 -35
  117. package/server/src/Support/Telematics/Providers/SafeeProvider.php +26 -2
  118. package/server/src/Support/Telematics/Providers/SamsaraProvider.php +122 -14
  119. package/server/src/routes.php +0 -1
  120. package/server/tests/DeviceFilterTest.php +9 -0
  121. package/server/tests/DriverVehicleStatusDefaultTest.php +19 -2
  122. package/server/tests/EquipmentAssignmentMorphTypeTest.php +35 -0
  123. package/server/tests/ServiceRateRouteValidationTest.php +101 -0
  124. package/server/tests/TelematicsHardeningTest.php +238 -7
  125. package/addon/controllers/connectivity/events/index/details.js +0 -3
  126. package/addon/routes/connectivity/events/index/details.js +0 -3
  127. /package/addon/templates/connectivity/events/{index/details.hbs → details.hbs} +0 -0
@@ -8,6 +8,7 @@ use Fleetbase\FleetOps\Http\Controllers\FleetOpsController;
8
8
  use Fleetbase\FleetOps\Models\ServiceRate;
9
9
  use Fleetbase\Http\Requests\ExportRequest;
10
10
  use Illuminate\Http\Request;
11
+ use Illuminate\Support\Collection;
11
12
  use Illuminate\Support\Str;
12
13
  use Maatwebsite\Excel\Facades\Excel;
13
14
 
@@ -27,24 +28,30 @@ class ServiceRateController extends FleetOpsController
27
28
  */
28
29
  public function getServicesForRoute(Request $request)
29
30
  {
30
- $coordinates = explode(';', $request->input('coordinates')); // ex. 1.3621663,103.8845049;1.353151,103.86458
31
+ $coordinates = $request->input('coordinates');
31
32
 
32
- // convert coordinates to points
33
- $waypoints = collect($coordinates)->map(
34
- function ($coord) {
35
- $coord = explode(',', $coord);
36
- [$latitude, $longitude] = $coord;
33
+ if (!is_string($coordinates) || trim($coordinates) === '') {
34
+ return response()->error('Route coordinates are required to query service rates.', 422);
35
+ }
37
36
 
38
- return Point::fromText("POINT($longitude $latitude)", 4326);
39
- }
40
- );
37
+ $waypoints = $this->parseRouteCoordinates($coordinates);
38
+
39
+ if ($waypoints === null) {
40
+ return response()->error('Invalid route coordinates provided.', 422);
41
+ }
42
+
43
+ if ($waypoints->count() < 2) {
44
+ return response()->error('At least two route coordinates are required to query service rates.', 422);
45
+ }
41
46
 
42
- $applicableServiceRates = ServiceRate::getServicableForWaypoints(
47
+ $serviceType = $this->normalizeOptionalQueryValue($request->input('service_type'));
48
+
49
+ $applicableServiceRates = $this->getServicableForWaypoints(
43
50
  $waypoints,
44
- function ($query) use ($request) {
51
+ function ($query) use ($request, $serviceType) {
45
52
  $query->where('company_uuid', $request->session()->get('company'));
46
- if ($request->filled('service_type')) {
47
- $query->where('service_type', $request->input('service_type'));
53
+ if ($serviceType) {
54
+ $query->where('service_type', $serviceType);
48
55
  }
49
56
  }
50
57
  );
@@ -52,6 +59,68 @@ class ServiceRateController extends FleetOpsController
52
59
  return response()->json($applicableServiceRates);
53
60
  }
54
61
 
62
+ /**
63
+ * Parse semicolon-delimited latitude,longitude coordinate pairs.
64
+ */
65
+ protected function parseRouteCoordinates(string $coordinates): ?Collection
66
+ {
67
+ $coordinates = collect(explode(';', $coordinates))
68
+ ->map(fn ($coordinate) => trim($coordinate));
69
+
70
+ if ($coordinates->contains('')) {
71
+ return null;
72
+ }
73
+
74
+ $waypoints = $coordinates
75
+ ->map(function ($coordinate) {
76
+ $parts = array_map('trim', explode(',', $coordinate));
77
+
78
+ if (count($parts) !== 2) {
79
+ return null;
80
+ }
81
+
82
+ [$latitude, $longitude] = $parts;
83
+
84
+ if (!is_numeric($latitude) || !is_numeric($longitude)) {
85
+ return null;
86
+ }
87
+
88
+ return Point::fromText("POINT($longitude $latitude)", 4326);
89
+ });
90
+
91
+ if ($waypoints->contains(null)) {
92
+ return null;
93
+ }
94
+
95
+ return $waypoints->values();
96
+ }
97
+
98
+ /**
99
+ * Treat placeholder query-string values from the UI as absent.
100
+ */
101
+ protected function normalizeOptionalQueryValue($value): ?string
102
+ {
103
+ if (!is_string($value)) {
104
+ return null;
105
+ }
106
+
107
+ $value = trim($value);
108
+
109
+ if ($value === '' || in_array(strtolower($value), ['null', 'undefined'], true)) {
110
+ return null;
111
+ }
112
+
113
+ return $value;
114
+ }
115
+
116
+ /**
117
+ * Resolve applicable service rates for route waypoints.
118
+ */
119
+ protected function getServicableForWaypoints(Collection $waypoints, \Closure $queryCallback): array
120
+ {
121
+ return ServiceRate::getServicableForWaypoints($waypoints, $queryCallback);
122
+ }
123
+
55
124
  /**
56
125
  * Export the service rate to excel or csv.
57
126
  *
@@ -26,6 +26,27 @@ class DeviceEventFilter extends Filter
26
26
  $this->builder->search($searchQuery);
27
27
  }
28
28
 
29
+ public function eventType(?string $eventType)
30
+ {
31
+ if ($eventType) {
32
+ $this->builder->where('event_type', 'like', '%' . $eventType . '%');
33
+ }
34
+ }
35
+
36
+ public function provider(?string $provider)
37
+ {
38
+ if ($provider) {
39
+ $this->builder->where('provider', 'like', '%' . $provider . '%');
40
+ }
41
+ }
42
+
43
+ public function code(?string $code)
44
+ {
45
+ if ($code) {
46
+ $this->builder->where('code', 'like', '%' . $code . '%');
47
+ }
48
+ }
49
+
29
50
  public function telematic(?string $telematic)
30
51
  {
31
52
  $this->builder->whereHas('device', function ($query) use ($telematic) {
@@ -2,6 +2,7 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Http\Filter;
4
4
 
5
+ use Fleetbase\FleetOps\Models\Vehicle;
5
6
  use Fleetbase\FleetOps\Support\Utils;
6
7
  use Fleetbase\Http\Filter\Filter;
7
8
 
@@ -38,6 +39,22 @@ class DeviceFilter extends Filter
38
39
  }
39
40
  }
40
41
 
42
+ public function type(string|array|null $type)
43
+ {
44
+ $type = Utils::arrayFrom($type);
45
+
46
+ if ($type) {
47
+ $this->builder->whereIn('type', $type);
48
+ }
49
+ }
50
+
51
+ public function serialNumber(?string $serialNumber)
52
+ {
53
+ if ($serialNumber) {
54
+ $this->builder->where('serial_number', 'like', '%' . $serialNumber . '%');
55
+ }
56
+ }
57
+
41
58
  public function telematic(?string $telematic)
42
59
  {
43
60
  $this->builder->where('telematic_uuid', $telematic);
@@ -71,7 +88,13 @@ class DeviceFilter extends Filter
71
88
  public function vehicle(?string $vehicle)
72
89
  {
73
90
  if ($vehicle) {
74
- $this->builder->where('attachable_uuid', $vehicle);
91
+ $this->builder->where(function ($query) use ($vehicle) {
92
+ $query->where('attachable_uuid', $vehicle)
93
+ ->orWhereIn('attachable_uuid', Vehicle::query()
94
+ ->where('company_uuid', $this->session->get('company'))
95
+ ->where('public_id', $vehicle)
96
+ ->pluck('uuid'));
97
+ });
75
98
  }
76
99
  }
77
100
 
@@ -46,6 +46,7 @@ class Vehicle extends FleetbaseResource
46
46
  'altitude' => (int) data_get($this, 'altitude', 0),
47
47
  'speed' => (int) data_get($this, 'speed', 0),
48
48
  'online' => (bool) data_get($this, 'online', false),
49
+ 'devices' => $this->whenLoaded('devices', fn () => $this->compactDevices()),
49
50
  'assigned_orders_count' => $this->when($isInternal, $this->assignedOrdersCount()),
50
51
 
51
52
  // Meta flag to indicate this is an index resource
@@ -65,6 +66,24 @@ class Vehicle extends FleetbaseResource
65
66
  return Order::where('vehicle_assigned_uuid', $this->uuid)->count();
66
67
  }
67
68
 
69
+ protected function compactDevices(): array
70
+ {
71
+ return $this->devices->map(
72
+ fn ($device) => [
73
+ 'id' => $device->uuid,
74
+ 'uuid' => $device->uuid,
75
+ 'public_id' => $device->public_id,
76
+ 'name' => $device->name,
77
+ 'display_name' => $device->display_name,
78
+ 'device_id' => $device->device_id,
79
+ 'serial_number' => $device->serial_number,
80
+ 'imei' => $device->imei,
81
+ 'provider' => $device->provider,
82
+ 'status' => $device->status,
83
+ ]
84
+ )->values()->all();
85
+ }
86
+
68
87
  protected function currentOrderReference(): ?string
69
88
  {
70
89
  $this->loadMissing('driver.currentOrder');
@@ -80,6 +80,8 @@ class Device extends Model
80
80
  'vehicle',
81
81
  'connection_status',
82
82
  'device_id',
83
+ 'type',
84
+ 'serial_number',
83
85
  'last_online_at',
84
86
  'updated_at',
85
87
  'warranty_uuid',
@@ -103,6 +103,15 @@ class DeviceEvent extends Model
103
103
  */
104
104
  protected $appends = [
105
105
  'device_name',
106
+ 'device_id',
107
+ 'device_imei',
108
+ 'device_serial_number',
109
+ 'device_connection_status',
110
+ 'device_status',
111
+ 'device_photo_url',
112
+ 'telematic_uuid',
113
+ 'telematic_name',
114
+ 'provider_descriptor',
106
115
  'is_processed',
107
116
  'age_minutes',
108
117
  'processing_delay_minutes',
@@ -208,6 +217,78 @@ class DeviceEvent extends Model
208
217
  return $this->device?->name;
209
218
  }
210
219
 
220
+ /**
221
+ * Get the provider device identifier.
222
+ */
223
+ public function getDeviceIdAttribute(): ?string
224
+ {
225
+ return $this->device?->device_id ?? $this->ident;
226
+ }
227
+
228
+ /**
229
+ * Get the device IMEI.
230
+ */
231
+ public function getDeviceImeiAttribute(): ?string
232
+ {
233
+ return $this->device?->imei;
234
+ }
235
+
236
+ /**
237
+ * Get the device serial number.
238
+ */
239
+ public function getDeviceSerialNumberAttribute(): ?string
240
+ {
241
+ return $this->device?->serial_number;
242
+ }
243
+
244
+ /**
245
+ * Get the device connection status.
246
+ */
247
+ public function getDeviceConnectionStatusAttribute(): ?string
248
+ {
249
+ return $this->device?->connection_status;
250
+ }
251
+
252
+ /**
253
+ * Get the device operational status.
254
+ */
255
+ public function getDeviceStatusAttribute(): ?string
256
+ {
257
+ return $this->device?->status;
258
+ }
259
+
260
+ /**
261
+ * Get the device photo URL.
262
+ */
263
+ public function getDevicePhotoUrlAttribute(): ?string
264
+ {
265
+ return $this->device?->photo_url;
266
+ }
267
+
268
+ /**
269
+ * Get the related telematic UUID.
270
+ */
271
+ public function getTelematicUuidAttribute(): ?string
272
+ {
273
+ return $this->device?->telematic_uuid;
274
+ }
275
+
276
+ /**
277
+ * Get the related telematic name.
278
+ */
279
+ public function getTelematicNameAttribute(): ?string
280
+ {
281
+ return $this->device?->telematic?->name;
282
+ }
283
+
284
+ /**
285
+ * Get the related telematic provider descriptor.
286
+ */
287
+ public function getProviderDescriptorAttribute(): array
288
+ {
289
+ return $this->device?->telematic?->provider_descriptor ?? [];
290
+ }
291
+
211
292
  /**
212
293
  * Check if the event has been processed.
213
294
  */
@@ -18,6 +18,7 @@ use Fleetbase\Traits\TracksApiCredential;
18
18
  use Illuminate\Database\Eloquent\Relations\BelongsTo;
19
19
  use Illuminate\Database\Eloquent\Relations\HasMany;
20
20
  use Illuminate\Database\Eloquent\Relations\MorphTo;
21
+ use Illuminate\Database\Eloquent\Relations\Relation;
21
22
  use Spatie\Activitylog\LogOptions;
22
23
  use Spatie\Activitylog\Traits\LogsActivity;
23
24
  use Spatie\Sluggable\HasSlug;
@@ -94,6 +95,15 @@ class Equipment extends Model
94
95
  'slug',
95
96
  ];
96
97
 
98
+ /**
99
+ * Set attributes and defaults.
100
+ *
101
+ * @var array
102
+ */
103
+ protected $attributes = [
104
+ 'status' => 'available',
105
+ ];
106
+
97
107
  /**
98
108
  * Dynamic attributes that are appended to object.
99
109
  *
@@ -148,6 +158,19 @@ class Equipment extends Model
148
158
  */
149
159
  protected static $logName = 'equipment';
150
160
 
161
+ /**
162
+ * Enforce assignment morph aliases for existing short type values.
163
+ */
164
+ public static function boot(): void
165
+ {
166
+ parent::boot();
167
+
168
+ Relation::morphMap([
169
+ 'fleet-ops:vehicle' => Vehicle::class,
170
+ 'fleet-ops:driver' => Driver::class,
171
+ ]);
172
+ }
173
+
151
174
  /**
152
175
  * Get the options for generating the slug.
153
176
  */
@@ -192,6 +215,38 @@ class Equipment extends Model
192
215
  return $this->morphTo(__FUNCTION__, 'equipable_type', 'equipable_uuid');
193
216
  }
194
217
 
218
+ /**
219
+ * Set the equipment status attribute.
220
+ */
221
+ public function setStatusAttribute(?string $status = 'available'): void
222
+ {
223
+ $this->attributes['status'] = $status === null || $status === 'active' ? 'available' : $status;
224
+ }
225
+
226
+ /**
227
+ * Normalize equipment assignment type aliases to Eloquent morph classes.
228
+ */
229
+ public function setEquipableTypeAttribute(?string $type): void
230
+ {
231
+ if ($type === null || $type === '') {
232
+ $this->attributes['equipable_type'] = null;
233
+
234
+ return;
235
+ }
236
+
237
+ if (str_contains($type, '\\')) {
238
+ $this->attributes['equipable_type'] = $type;
239
+
240
+ return;
241
+ }
242
+
243
+ $this->attributes['equipable_type'] = match ($type) {
244
+ 'fleet-ops:vehicle', 'vehicle' => Utils::getMutationType('fleet-ops:vehicle'),
245
+ 'fleet-ops:driver', 'driver' => Utils::getMutationType('fleet-ops:driver'),
246
+ default => $type,
247
+ };
248
+ }
249
+
195
250
  public function maintenances(): HasMany
196
251
  {
197
252
  return $this->hasMany(Maintenance::class, 'maintainable_uuid', 'uuid')
@@ -151,6 +151,7 @@ class AfaqyProvider extends AbstractProvider
151
151
 
152
152
  return [
153
153
  'device_id' => $payload['_id'] ?? $payload['id'] ?? null,
154
+ 'external_id' => $payload['_id'] ?? $payload['id'] ?? null,
154
155
  'name' => $payload['name'] ?? data_get($payload, 'profile.plate_number') ?? 'Unknown Unit',
155
156
  'provider' => 'afaqy',
156
157
  'model' => data_get($payload, 'profile.model') ?? $payload['device'] ?? null,
@@ -2,6 +2,8 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Support\Telematics\Providers;
4
4
 
5
+ use Illuminate\Support\Carbon;
6
+
5
7
  /**
6
8
  * Class FlespiProvider.
7
9
  *
@@ -77,34 +79,71 @@ class FlespiProvider extends AbstractProvider
77
79
 
78
80
  public function normalizeDevice(array $payload): array
79
81
  {
82
+ $telemetry = $payload['telemetry'] ?? $payload;
83
+ $externalId = $payload['id'] ?? $payload['device.id'] ?? null;
84
+ $occurredAt = $this->parseTimestamp($telemetry['timestamp'] ?? $payload['last_active'] ?? null);
85
+
80
86
  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,
87
+ 'device_id' => $externalId,
88
+ 'external_id' => $externalId,
89
+ 'name' => $payload['name'] ?? $telemetry['device.name'] ?? 'Unknown Device',
90
+ 'provider' => 'flespi',
91
+ 'model' => $payload['device_type_id'] ?? $payload['device_type_name'] ?? null,
92
+ 'imei' => $payload['configuration']['ident'] ?? $payload['ident'] ?? $telemetry['ident'] ?? null,
93
+ 'serial_number' => $payload['configuration']['serial'] ?? $payload['serial'] ?? null,
94
+ 'phone' => $payload['configuration']['phone'] ?? null,
95
+ 'status' => isset($payload['telemetry']) ? 'active' : 'inactive',
96
+ 'online' => $this->resolveOnline($telemetry),
97
+ 'last_seen_at' => $occurredAt,
98
+ 'location' => [
99
+ 'lat' => $telemetry['position.latitude'] ?? null,
100
+ 'lng' => $telemetry['position.longitude'] ?? null,
101
+ ],
102
+ 'speed' => $telemetry['position.speed'] ?? $telemetry['vehicle.speed'] ?? null,
103
+ 'heading' => $telemetry['position.direction'] ?? $telemetry['position.heading'] ?? null,
104
+ 'altitude' => $telemetry['position.altitude'] ?? null,
105
+ 'odometer' => $telemetry['vehicle.mileage'] ?? $telemetry['vehicle.odometer'] ?? null,
106
+ 'ignition' => $this->extractIgnition($telemetry),
107
+ 'fuel_level' => $telemetry['fuel.level'] ?? $telemetry['can.fuel.level'] ?? null,
108
+ 'meta' => [
109
+ 'raw' => $payload,
110
+ 'provider_status' => array_filter([
111
+ 'status' => $payload['status'] ?? $telemetry['status'] ?? null,
112
+ 'online' => $telemetry['online'] ?? null,
113
+ 'connected' => $telemetry['device.connected'] ?? $telemetry['connected'] ?? null,
114
+ ], fn ($value) => $value !== null),
91
115
  ],
92
- 'meta' => $payload,
93
116
  ];
94
117
  }
95
118
 
96
119
  public function normalizeEvent(array $payload): array
97
120
  {
121
+ $deviceId = $payload['device.id'] ?? $payload['id'] ?? null;
122
+
98
123
  return [
99
- 'external_id' => $payload['id'] ?? null,
100
- 'device_id' => $payload['device.id'] ?? null,
124
+ 'external_id' => $payload['id'] ?? $deviceId,
125
+ 'device_id' => $deviceId,
101
126
  'event_type' => $payload['event.enum'] ?? 'telemetry_update',
102
- 'occurred_at' => isset($payload['timestamp']) ? date('Y-m-d H:i:s', $payload['timestamp']) : now(),
127
+ 'occurred_at' => $this->parseTimestamp($payload['timestamp'] ?? null) ?? now(),
128
+ 'online' => $this->resolveOnline($payload),
103
129
  'location' => [
104
130
  'lat' => $payload['position.latitude'] ?? null,
105
131
  'lng' => $payload['position.longitude'] ?? null,
106
132
  ],
107
- 'meta' => $payload,
133
+ 'speed' => $payload['position.speed'] ?? $payload['vehicle.speed'] ?? null,
134
+ 'heading' => $payload['position.direction'] ?? $payload['position.heading'] ?? null,
135
+ 'altitude' => $payload['position.altitude'] ?? null,
136
+ 'odometer' => $payload['vehicle.mileage'] ?? $payload['vehicle.odometer'] ?? null,
137
+ 'ignition' => $this->extractIgnition($payload),
138
+ 'fuel_level' => $payload['fuel.level'] ?? $payload['can.fuel.level'] ?? null,
139
+ 'meta' => [
140
+ 'raw' => $payload,
141
+ 'provider_status' => array_filter([
142
+ 'status' => $payload['status'] ?? null,
143
+ 'online' => $payload['online'] ?? null,
144
+ 'connected' => $payload['device.connected'] ?? $payload['connected'] ?? null,
145
+ ], fn ($value) => $value !== null),
146
+ ],
108
147
  ];
109
148
  }
110
149
 
@@ -180,4 +219,39 @@ class FlespiProvider extends AbstractProvider
180
219
  {
181
220
  return true;
182
221
  }
222
+
223
+ protected function parseTimestamp($value): ?string
224
+ {
225
+ if (!$value) {
226
+ return null;
227
+ }
228
+
229
+ if (is_numeric($value)) {
230
+ return Carbon::createFromTimestamp((float) $value)->toDateTimeString();
231
+ }
232
+
233
+ return Carbon::parse($value)->toDateTimeString();
234
+ }
235
+
236
+ protected function resolveOnline(array $payload): ?bool
237
+ {
238
+ $value = $payload['online'] ?? $payload['device.connected'] ?? $payload['connected'] ?? null;
239
+
240
+ if ($value === null) {
241
+ return isset($payload['timestamp']);
242
+ }
243
+
244
+ return filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? (bool) $value;
245
+ }
246
+
247
+ protected function extractIgnition(array $payload): ?bool
248
+ {
249
+ $value = $payload['engine.ignition.status'] ?? $payload['ignition.status'] ?? null;
250
+
251
+ if ($value === null) {
252
+ return null;
253
+ }
254
+
255
+ return filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? (bool) $value;
256
+ }
183
257
  }