@fleetbase/fleetops-engine 0.6.19 → 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 (130) 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/kanban.hbs +12 -10
  20. package/addon/components/order/kanban.js +27 -3
  21. package/addon/components/order-config-manager/custom-fields.js +1 -1
  22. package/addon/components/positions-replay.hbs +333 -0
  23. package/addon/components/positions-replay.js +372 -0
  24. package/addon/components/sensor/details.hbs +64 -38
  25. package/addon/components/sensor/form.hbs +112 -63
  26. package/addon/components/sensor/form.js +36 -24
  27. package/addon/components/sensor/panel-header.hbs +32 -0
  28. package/addon/components/sensor/panel-header.js +3 -0
  29. package/addon/components/telematic/details.hbs +40 -16
  30. package/addon/components/telematic/form.hbs +63 -64
  31. package/addon/components/telematic/form.js +73 -4
  32. package/addon/components/vehicle/card.hbs +1 -1
  33. package/addon/controllers/analytics/reports/index/edit.js +1 -1
  34. package/addon/controllers/connectivity/devices/index/details.js +22 -1
  35. package/addon/controllers/connectivity/devices/index/edit.js +66 -1
  36. package/addon/controllers/connectivity/devices/index.js +51 -9
  37. package/addon/controllers/connectivity/events/index.js +65 -16
  38. package/addon/controllers/connectivity/sensors/index/details.js +22 -1
  39. package/addon/controllers/connectivity/sensors/index/edit.js +66 -1
  40. package/addon/controllers/connectivity/sensors/index.js +66 -6
  41. package/addon/controllers/connectivity/telematics/index/details.js +22 -1
  42. package/addon/controllers/connectivity/telematics/index/edit.js +66 -1
  43. package/addon/controllers/connectivity/telematics/index.js +20 -11
  44. package/addon/controllers/management/fleets/index/details.js +26 -21
  45. package/addon/controllers/management/fleets/index/edit.js +9 -6
  46. package/addon/controllers/management/vehicles/index/details.js +21 -13
  47. package/addon/controllers/operations/orders/index/new.js +4 -2
  48. package/addon/controllers/operations/orders/index.js +50 -45
  49. package/addon/controllers/settings/custom-fields.js +6 -0
  50. package/addon/helpers/get-fleet-ops-option-label.js +11 -0
  51. package/addon/routes/connectivity/devices/index/details.js +27 -1
  52. package/addon/routes/connectivity/devices/index/edit.js +27 -1
  53. package/addon/routes/connectivity/sensors/index/details.js +27 -1
  54. package/addon/routes/connectivity/sensors/index/edit.js +27 -1
  55. package/addon/routes/connectivity/telematics/index/details.js +27 -1
  56. package/addon/routes/connectivity/telematics/index/edit.js +27 -1
  57. package/addon/routes/management/vehicles/index/details/positions.js +3 -0
  58. package/addon/routes/operations/orders/index.js +0 -3
  59. package/addon/routes.js +1 -0
  60. package/addon/services/movement-tracker.js +81 -30
  61. package/addon/services/order-creation.js +4 -8
  62. package/addon/services/order-validation.js +3 -3
  63. package/addon/styles/fleetops-engine.css +192 -0
  64. package/addon/templates/connectivity/devices/index/details/index.hbs +2 -2
  65. package/addon/templates/connectivity/devices/index/details.hbs +15 -2
  66. package/addon/templates/connectivity/devices/index/edit.hbs +1 -1
  67. package/addon/templates/connectivity/events/index.hbs +1 -1
  68. package/addon/templates/connectivity/sensors/index/details/index.hbs +2 -2
  69. package/addon/templates/connectivity/sensors/index/details.hbs +15 -2
  70. package/addon/templates/connectivity/sensors/index/edit.hbs +1 -1
  71. package/addon/templates/connectivity/telematics/index/details/index.hbs +2 -2
  72. package/addon/templates/connectivity/telematics/index/details.hbs +14 -2
  73. package/addon/templates/connectivity/telematics/index/edit.hbs +1 -1
  74. package/addon/templates/management/vehicles/index/details/positions.hbs +1 -0
  75. package/addon/templates/operations/orders/index.hbs +26 -2
  76. package/addon/utils/fleet-ops-options.js +95 -0
  77. package/addon/utils/setup-customer-portal.js +7 -0
  78. package/app/components/device/panel-header.js +1 -0
  79. package/app/components/map/drawer/device-event-listing.js +1 -0
  80. package/app/components/map/drawer/position-listing.js +1 -0
  81. package/app/components/positions-replay.js +1 -0
  82. package/app/components/sensor/panel-header.js +1 -0
  83. package/app/helpers/get-fleet-ops-option-label.js +1 -0
  84. package/app/routes/management/vehicles/index/details/positions.js +1 -0
  85. package/app/templates/management/vehicles/index/details/positions.js +1 -0
  86. package/composer.json +1 -1
  87. package/extension.json +1 -1
  88. package/package.json +4 -4
  89. package/server/config/telematics.php +111 -0
  90. package/server/migrations/2025_10_27_000001_add_telematics_integration_fields.php +70 -0
  91. package/server/migrations/2025_10_27_171322_fix_device_column_names.php +107 -0
  92. package/server/migrations/2025_10_27_203023_add_company_uuid_to_device_events_table.php +28 -0
  93. package/server/src/Console/Commands/ReplayVehicleLocations.php +225 -0
  94. package/server/src/Contracts/TelematicProviderDescriptor.php +72 -0
  95. package/server/src/Contracts/TelematicProviderInterface.php +119 -0
  96. package/server/src/Exceptions/TelematicProviderException.php +14 -0
  97. package/server/src/Exceptions/TelematicRateLimitExceededException.php +12 -0
  98. package/server/src/Http/Controllers/Api/v1/DriverController.php +24 -14
  99. package/server/src/Http/Controllers/Api/v1/VehicleController.php +27 -7
  100. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +22 -0
  101. package/server/src/Http/Controllers/Internal/v1/OrderController.php +50 -68
  102. package/server/src/Http/Controllers/Internal/v1/PositionController.php +240 -0
  103. package/server/src/Http/Controllers/Internal/v1/SensorController.php +11 -0
  104. package/server/src/Http/Controllers/Internal/v1/TelematicController.php +141 -0
  105. package/server/src/Http/Controllers/Internal/v1/TelematicWebhookController.php +170 -0
  106. package/server/src/Http/Filter/DeviceEventFilter.php +68 -0
  107. package/server/src/Http/Filter/PositionFilter.php +35 -0
  108. package/server/src/Http/Resources/v1/Position.php +44 -0
  109. package/server/src/Jobs/ReplayPositions.php +64 -0
  110. package/server/src/Jobs/SendPositionReplay.php +65 -0
  111. package/server/src/Jobs/SyncTelematicDevicesJob.php +106 -0
  112. package/server/src/Jobs/TestTelematicConnectionJob.php +102 -0
  113. package/server/src/Models/Device.php +72 -10
  114. package/server/src/Models/DeviceEvent.php +7 -0
  115. package/server/src/Models/Driver.php +28 -1
  116. package/server/src/Models/Payload.php +11 -3
  117. package/server/src/Models/Place.php +9 -2
  118. package/server/src/Models/Position.php +17 -17
  119. package/server/src/Models/Sensor.php +78 -13
  120. package/server/src/Models/Telematic.php +116 -6
  121. package/server/src/Models/Vehicle.php +104 -1
  122. package/server/src/Providers/FleetOpsServiceProvider.php +2 -0
  123. package/server/src/Support/Telematics/Providers/AbstractProvider.php +151 -0
  124. package/server/src/Support/Telematics/Providers/FlespiProvider.php +182 -0
  125. package/server/src/Support/Telematics/Providers/GeotabProvider.php +181 -0
  126. package/server/src/Support/Telematics/Providers/SamsaraProvider.php +177 -0
  127. package/server/src/Support/Telematics/TelematicProviderRegistry.php +147 -0
  128. package/server/src/Support/Telematics/TelematicService.php +223 -0
  129. package/server/src/Support/Utils.php +1 -1
  130. package/server/src/routes.php +12 -1
@@ -8,6 +8,7 @@ use Fleetbase\FleetOps\Casts\Point;
8
8
  use Fleetbase\FleetOps\Support\Utils;
9
9
  use Fleetbase\FleetOps\Support\VehicleData;
10
10
  use Fleetbase\LaravelMysqlSpatial\Eloquent\SpatialTrait;
11
+ use Fleetbase\LaravelMysqlSpatial\Types\Point as SpatialPoint;
11
12
  use Fleetbase\Models\Category;
12
13
  use Fleetbase\Models\File;
13
14
  use Fleetbase\Models\Model;
@@ -23,6 +24,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
23
24
  use Illuminate\Database\Eloquent\Relations\HasManyThrough;
24
25
  use Illuminate\Database\Eloquent\Relations\HasOne;
25
26
  use Illuminate\Database\Eloquent\Relations\MorphMany;
27
+ use Illuminate\Support\Arr;
26
28
  use Illuminate\Support\Str;
27
29
  use Spatie\Activitylog\LogOptions;
28
30
  use Spatie\Activitylog\Traits\LogsActivity;
@@ -61,7 +63,7 @@ class Vehicle extends Model
61
63
  *
62
64
  * @var array
63
65
  */
64
- protected $searchableColumns = ['make', 'model', 'year', 'plate_number', 'vin', 'public_id'];
66
+ protected $searchableColumns = ['name', 'description', 'make', 'model', 'trim', 'model_type', 'body_type', 'body_sub_type', 'year', 'plate_number', 'vin', 'call_sign', 'public_id'];
65
67
 
66
68
  /**
67
69
  * Attributes that is filterable on this model.
@@ -533,6 +535,31 @@ class Vehicle extends Model
533
535
  return ($isFirstPosition || $isPast50Meters) ? Position::create($positionData) : null;
534
536
  }
535
537
 
538
+ /**
539
+ * Creates a new position for the vehicle.
540
+ */
541
+ public function createPosition(array $attributes = [], Model|string|null $destination = null): ?Position
542
+ {
543
+ if (!isset($attributes['coordinates']) && isset($attributes['location'])) {
544
+ $attributes['coordinates'] = $attributes['location'];
545
+ }
546
+
547
+ if (!isset($attributes['coordinates']) && isset($attributes['latitude']) && isset($attributes['longitude'])) {
548
+ $attributes['coordinates'] = new SpatialPoint($attributes['latitude'], $attributes['longitude']);
549
+ }
550
+
551
+ // handle destination if set
552
+ $destinationUuid = Str::isUuid($destination) ? $destination : data_get($destination, 'uuid');
553
+
554
+ return Position::create([
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,
560
+ ]);
561
+ }
562
+
536
563
  public static function createFromImport(array $row, bool $saveInstance = false): Vehicle
537
564
  {
538
565
  // Filter array for null key values
@@ -660,4 +687,80 @@ class Vehicle extends Model
660
687
 
661
688
  return $details;
662
689
  }
690
+
691
+ /**
692
+ * Set or update a single key/value pair in the `specs` JSON column.
693
+ *
694
+ * Uses Laravel's `data_set` helper to allow dot notation for nested keys.
695
+ *
696
+ * @param string|array $key the key (or array path) to set within the specs
697
+ * @param mixed $value the value to assign to the given key
698
+ *
699
+ * @return array the updated specs array
700
+ */
701
+ public function setSpec(string|array $key, mixed $value): array
702
+ {
703
+ $specs = is_array($this->specs) ? $this->specs : (array) $this->specs;
704
+ data_set($specs, $key, $value);
705
+ $this->specs = $specs;
706
+
707
+ return $specs;
708
+ }
709
+
710
+ /**
711
+ * Merge multiple values into the `specs` JSON column.
712
+ *
713
+ * By default this performs a shallow merge (overwrites duplicate keys).
714
+ * Use `array_replace_recursive` if you need nested merges.
715
+ *
716
+ * @param array $newSpecs key/value pairs to merge into specs
717
+ *
718
+ * @return array the updated specs array
719
+ */
720
+ public function setSpecs(array $newSpecs = []): array
721
+ {
722
+ $specs = is_array($this->specs) ? $this->specs : (array) $this->specs;
723
+ $specs = array_merge($specs, $newSpecs);
724
+ $this->specs = $specs;
725
+
726
+ return $specs;
727
+ }
728
+
729
+ /**
730
+ * Set or update a single key/value pair in the `vin_data` JSON column.
731
+ *
732
+ * Uses Laravel's `data_set` helper to allow dot notation for nested keys.
733
+ *
734
+ * @param string|array $key the key (or array path) to set within the VIN data
735
+ * @param mixed $value the value to assign to the given key
736
+ *
737
+ * @return array the updated vin_data array
738
+ */
739
+ public function setVinData(string|array $key, mixed $value): array
740
+ {
741
+ $vinData = is_array($this->vin_data) ? $this->vin_data : (array) $this->vin_data;
742
+ data_set($vinData, $key, $value);
743
+ $this->vin_data = $vinData;
744
+
745
+ return $vinData;
746
+ }
747
+
748
+ /**
749
+ * Merge multiple values into the `vin_data` JSON column.
750
+ *
751
+ * By default this performs a shallow merge (overwrites duplicate keys).
752
+ * Use `array_replace_recursive` if you need nested merges.
753
+ *
754
+ * @param array $newVinData key/value pairs to merge into vin_data
755
+ *
756
+ * @return array the updated vin_data array
757
+ */
758
+ public function setVinDatas(array $newVinData = []): array
759
+ {
760
+ $vinData = is_array($this->vin_data) ? $this->vin_data : (array) $this->vin_data;
761
+ $vinData = array_merge($vinData, $newVinData);
762
+ $this->vin_data = $vinData;
763
+
764
+ return $vinData;
765
+ }
663
766
  }
@@ -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
+ }
@@ -0,0 +1,181 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Telematics\Providers;
4
+
5
+ use Illuminate\Support\Facades\Http;
6
+
7
+ /**
8
+ * Class GeotabProvider.
9
+ *
10
+ * Geotab telematics provider implementation.
11
+ * https://www.geotab.com/
12
+ */
13
+ class GeotabProvider extends AbstractProvider
14
+ {
15
+ protected string $baseUrl = 'https://my.geotab.com/apiv1';
16
+ protected int $requestsPerMinute = 50;
17
+ protected ?string $sessionId = null;
18
+
19
+ protected function prepareAuthentication(): void
20
+ {
21
+ // Geotab uses session-based authentication
22
+ if (!$this->sessionId) {
23
+ $this->authenticate();
24
+ }
25
+
26
+ $this->headers = [
27
+ 'Content-Type' => 'application/json',
28
+ ];
29
+ }
30
+
31
+ /**
32
+ * Authenticate with Geotab and get session ID.
33
+ */
34
+ protected function authenticate(): void
35
+ {
36
+ $response = Http::post($this->baseUrl, [
37
+ 'method' => 'Authenticate',
38
+ 'params' => [
39
+ 'database' => $this->credentials['database'],
40
+ 'userName' => $this->credentials['username'],
41
+ 'password' => $this->credentials['password'],
42
+ ],
43
+ ])->json();
44
+
45
+ if (isset($response['result']['credentials']['sessionId'])) {
46
+ $this->sessionId = $response['result']['credentials']['sessionId'];
47
+ } else {
48
+ throw new \Exception('Geotab authentication failed');
49
+ }
50
+ }
51
+
52
+ public function testConnection(array $credentials): array
53
+ {
54
+ try {
55
+ $this->credentials = $credentials;
56
+ $this->authenticate();
57
+
58
+ return [
59
+ 'success' => true,
60
+ 'message' => 'Connection successful',
61
+ 'metadata' => [
62
+ 'session_id' => substr($this->sessionId, 0, 10) . '...',
63
+ ],
64
+ ];
65
+ } catch (\Exception $e) {
66
+ return [
67
+ 'success' => false,
68
+ 'message' => $e->getMessage(),
69
+ 'metadata' => [],
70
+ ];
71
+ }
72
+ }
73
+
74
+ public function fetchDevices(array $options = []): array
75
+ {
76
+ $limit = $options['limit'] ?? 100;
77
+
78
+ $response = Http::post($this->baseUrl, [
79
+ 'method' => 'Get',
80
+ 'params' => [
81
+ 'credentials' => [
82
+ 'database' => $this->credentials['database'],
83
+ 'sessionId' => $this->sessionId,
84
+ ],
85
+ 'typeName' => 'Device',
86
+ 'resultsLimit' => $limit,
87
+ ],
88
+ ])->json();
89
+
90
+ $devices = $response['result'] ?? [];
91
+
92
+ return [
93
+ 'devices' => $devices,
94
+ 'next_cursor' => null,
95
+ 'has_more' => false,
96
+ ];
97
+ }
98
+
99
+ public function fetchDeviceDetails(string $externalId): array
100
+ {
101
+ $response = Http::post($this->baseUrl, [
102
+ 'method' => 'Get',
103
+ 'params' => [
104
+ 'credentials' => [
105
+ 'database' => $this->credentials['database'],
106
+ 'sessionId' => $this->sessionId,
107
+ ],
108
+ 'typeName' => 'Device',
109
+ 'search' => ['id' => $externalId],
110
+ ],
111
+ ])->json();
112
+
113
+ return $response['result'][0] ?? [];
114
+ }
115
+
116
+ public function normalizeDevice(array $payload): array
117
+ {
118
+ return [
119
+ 'external_id' => $payload['id'],
120
+ 'device_name' => $payload['name'] ?? 'Unknown Device',
121
+ 'device_provider' => 'geotab',
122
+ 'device_model' => $payload['deviceType'] ?? null,
123
+ 'imei' => $payload['serialNumber'] ?? null,
124
+ 'vin' => $payload['vehicleIdentificationNumber'] ?? null,
125
+ 'status' => 'active',
126
+ 'meta' => $payload,
127
+ ];
128
+ }
129
+
130
+ public function normalizeEvent(array $payload): array
131
+ {
132
+ return [
133
+ 'external_id' => $payload['id'] ?? null,
134
+ 'event_type' => $payload['type'] ?? 'status_data',
135
+ 'occurred_at' => $payload['dateTime'] ?? now(),
136
+ 'meta' => $payload,
137
+ ];
138
+ }
139
+
140
+ public function normalizeSensor(array $payload): array
141
+ {
142
+ return [
143
+ 'sensor_type' => $payload['diagnosticType'] ?? 'generic',
144
+ 'value' => $payload['data'] ?? null,
145
+ 'recorded_at' => $payload['dateTime'] ?? now(),
146
+ 'meta' => $payload,
147
+ ];
148
+ }
149
+
150
+ public function getCredentialSchema(): array
151
+ {
152
+ return [
153
+ [
154
+ 'name' => 'database',
155
+ 'label' => 'Database Name',
156
+ 'type' => 'text',
157
+ 'placeholder' => 'Enter your Geotab database name',
158
+ 'required' => true,
159
+ ],
160
+ [
161
+ 'name' => 'username',
162
+ 'label' => 'Username',
163
+ 'type' => 'text',
164
+ 'placeholder' => 'Enter your Geotab username',
165
+ 'required' => true,
166
+ ],
167
+ [
168
+ 'name' => 'password',
169
+ 'label' => 'Password',
170
+ 'type' => 'password',
171
+ 'placeholder' => 'Enter your Geotab password',
172
+ 'required' => true,
173
+ ],
174
+ ];
175
+ }
176
+
177
+ public function supportsWebhooks(): bool
178
+ {
179
+ return false;
180
+ }
181
+ }