@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
@@ -0,0 +1,177 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Telematics\Providers;
4
+
5
+ /**
6
+ * Class SamsaraProvider.
7
+ *
8
+ * Samsara telematics provider implementation.
9
+ * https://www.samsara.com/
10
+ */
11
+ class SamsaraProvider extends AbstractProvider
12
+ {
13
+ protected string $baseUrl = 'https://api.samsara.com';
14
+ protected int $requestsPerMinute = 60;
15
+
16
+ protected function prepareAuthentication(): void
17
+ {
18
+ $this->headers = [
19
+ 'Authorization' => 'Bearer ' . $this->credentials['api_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 user info
31
+ $response = $this->request('GET', '/fleet/users');
32
+
33
+ return [
34
+ 'success' => true,
35
+ 'message' => 'Connection successful',
36
+ 'metadata' => [
37
+ 'users_count' => count($response['data'] ?? []),
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 = ['limit' => $limit];
55
+ if ($cursor) {
56
+ $params['after'] = $cursor;
57
+ }
58
+
59
+ $response = $this->request('GET', '/fleet/vehicles', $params);
60
+
61
+ $devices = $response['data'] ?? [];
62
+ $nextCursor = $response['pagination']['endCursor'] ?? null;
63
+ $hasMore = $response['pagination']['hasNextPage'] ?? false;
64
+
65
+ return [
66
+ 'devices' => $devices,
67
+ 'next_cursor' => $hasMore ? $nextCursor : null,
68
+ 'has_more' => $hasMore,
69
+ ];
70
+ }
71
+
72
+ public function fetchDeviceDetails(string $externalId): array
73
+ {
74
+ $response = $this->request('GET', "/fleet/vehicles/{$externalId}");
75
+
76
+ return $response['data'] ?? [];
77
+ }
78
+
79
+ public function normalizeDevice(array $payload): array
80
+ {
81
+ return [
82
+ 'external_id' => $payload['id'],
83
+ 'device_name' => $payload['name'] ?? 'Unknown Device',
84
+ 'device_provider' => 'samsara',
85
+ 'device_model' => $payload['make'] ?? null,
86
+ 'vin' => $payload['vin'] ?? null,
87
+ 'license_plate' => $payload['licensePlate'] ?? null,
88
+ 'status' => 'active',
89
+ 'meta' => $payload,
90
+ ];
91
+ }
92
+
93
+ public function normalizeEvent(array $payload): array
94
+ {
95
+ return [
96
+ 'external_id' => $payload['id'] ?? null,
97
+ 'event_type' => $payload['eventType'] ?? 'vehicle_update',
98
+ 'occurred_at' => $payload['time'] ?? now(),
99
+ 'location' => [
100
+ 'lat' => $payload['location']['latitude'] ?? null,
101
+ 'lng' => $payload['location']['longitude'] ?? null,
102
+ ],
103
+ 'meta' => $payload,
104
+ ];
105
+ }
106
+
107
+ public function normalizeSensor(array $payload): array
108
+ {
109
+ return [
110
+ 'sensor_type' => $payload['sensorType'] ?? 'generic',
111
+ 'value' => $payload['value'] ?? null,
112
+ 'unit' => $payload['unit'] ?? null,
113
+ 'recorded_at' => $payload['time'] ?? now(),
114
+ 'meta' => $payload,
115
+ ];
116
+ }
117
+
118
+ public function validateWebhookSignature(string $payload, string $signature, array $credentials): bool
119
+ {
120
+ if (!isset($credentials['webhook_secret'])) {
121
+ return true;
122
+ }
123
+
124
+ $expectedSignature = hash_hmac('sha256', $payload, $credentials['webhook_secret']);
125
+
126
+ return hash_equals($expectedSignature, $signature);
127
+ }
128
+
129
+ public function processWebhook(array $payload, array $headers = []): array
130
+ {
131
+ $devices = [];
132
+ $events = [];
133
+
134
+ // Samsara webhook structure
135
+ if (isset($payload['data'])) {
136
+ foreach ($payload['data'] as $item) {
137
+ if (isset($item['vehicle'])) {
138
+ $devices[] = $this->normalizeDevice($item['vehicle']);
139
+ }
140
+
141
+ $events[] = $this->normalizeEvent($item);
142
+ }
143
+ }
144
+
145
+ return [
146
+ 'devices' => $devices,
147
+ 'events' => $events,
148
+ 'sensors' => [],
149
+ ];
150
+ }
151
+
152
+ public function getCredentialSchema(): array
153
+ {
154
+ return [
155
+ [
156
+ 'name' => 'api_token',
157
+ 'label' => 'API Token',
158
+ 'type' => 'password',
159
+ 'placeholder' => 'Enter your Samsara API token',
160
+ 'required' => true,
161
+ 'validation' => 'required|string|min:20',
162
+ ],
163
+ [
164
+ 'name' => 'webhook_secret',
165
+ 'label' => 'Webhook Secret (Optional)',
166
+ 'type' => 'password',
167
+ 'placeholder' => 'Enter webhook secret for signature validation',
168
+ 'required' => false,
169
+ ],
170
+ ];
171
+ }
172
+
173
+ public function supportsWebhooks(): bool
174
+ {
175
+ return true;
176
+ }
177
+ }
@@ -0,0 +1,147 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Telematics;
4
+
5
+ use Fleetbase\FleetOps\Contracts\TelematicProviderDescriptor;
6
+ use Fleetbase\FleetOps\Contracts\TelematicProviderInterface;
7
+ use Illuminate\Support\Collection;
8
+
9
+ /**
10
+ * Class ProviderRegistry.
11
+ *
12
+ * Central registry for all telematics providers.
13
+ * Manages provider discovery, registration, and instantiation.
14
+ */
15
+ class TelematicProviderRegistry
16
+ {
17
+ /**
18
+ * @var Collection<TelematicProviderDescriptor>
19
+ */
20
+ protected Collection $providers;
21
+
22
+ /**
23
+ * Create a new ProviderRegistry instance.
24
+ */
25
+ public function __construct()
26
+ {
27
+ $this->providers = collect();
28
+ $this->loadNativeProviders();
29
+ }
30
+
31
+ /**
32
+ * Load native providers from configuration.
33
+ */
34
+ protected function loadNativeProviders(): void
35
+ {
36
+ $config = config('telematics.providers', []);
37
+
38
+ foreach ($config as $providerConfig) {
39
+ $descriptor = new TelematicProviderDescriptor($providerConfig);
40
+ $this->register($descriptor);
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Register a provider.
46
+ */
47
+ public function register(TelematicProviderDescriptor $descriptor): void
48
+ {
49
+ $this->providers->put($descriptor->key, $descriptor);
50
+ }
51
+
52
+ /**
53
+ * Get all registered providers.
54
+ *
55
+ * @return Collection<TelematicProviderDescriptor>
56
+ */
57
+ public function all(): Collection
58
+ {
59
+ return $this->providers;
60
+ }
61
+
62
+ /**
63
+ * Find a provider by key.
64
+ */
65
+ public function findByKey(string $key): ?TelematicProviderDescriptor
66
+ {
67
+ return $this->providers->get($key);
68
+ }
69
+
70
+ /**
71
+ * Check if a provider exists.
72
+ */
73
+ public function has(string $key): bool
74
+ {
75
+ return $this->providers->has($key);
76
+ }
77
+
78
+ /**
79
+ * Resolve a provider instance by key.
80
+ *
81
+ * @throws \InvalidArgumentException
82
+ */
83
+ public function resolve(string $key): TelematicProviderInterface
84
+ {
85
+ $descriptor = $this->findByKey($key);
86
+
87
+ if (!$descriptor) {
88
+ throw new \InvalidArgumentException("Provider '{$key}' not found in registry.");
89
+ }
90
+
91
+ if (!$descriptor->driverClass) {
92
+ throw new \InvalidArgumentException("Provider '{$key}' does not have a driver class.");
93
+ }
94
+
95
+ if (!class_exists($descriptor->driverClass)) {
96
+ throw new \InvalidArgumentException("Provider driver class '{$descriptor->driverClass}' does not exist.");
97
+ }
98
+
99
+ $provider = app($descriptor->driverClass);
100
+
101
+ if (!$provider instanceof TelematicProviderInterface) {
102
+ throw new \InvalidArgumentException('Provider driver class must implement TelematicProviderInterface.');
103
+ }
104
+
105
+ return $provider;
106
+ }
107
+
108
+ /**
109
+ * Get providers that support webhooks.
110
+ *
111
+ * @return Collection<TelematicProviderDescriptor>
112
+ */
113
+ public function getWebhookProviders(): Collection
114
+ {
115
+ return $this->providers->filter(fn ($p) => $p->supportsWebhooks);
116
+ }
117
+
118
+ /**
119
+ * Get providers that support discovery.
120
+ *
121
+ * @return Collection<TelematicProviderDescriptor>
122
+ */
123
+ public function getDiscoveryProviders(): Collection
124
+ {
125
+ return $this->providers->filter(fn ($p) => $p->supportsDiscovery);
126
+ }
127
+
128
+ /**
129
+ * Get native providers only.
130
+ *
131
+ * @return Collection<TelematicProviderDescriptor>
132
+ */
133
+ public function getNativeProviders(): Collection
134
+ {
135
+ return $this->providers->filter(fn ($p) => $p->type === 'native');
136
+ }
137
+
138
+ /**
139
+ * Get custom providers only.
140
+ *
141
+ * @return Collection<TelematicProviderDescriptor>
142
+ */
143
+ public function getCustomProviders(): Collection
144
+ {
145
+ return $this->providers->filter(fn ($p) => $p->type === 'custom');
146
+ }
147
+ }
@@ -0,0 +1,223 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Telematics;
4
+
5
+ use Fleetbase\FleetOps\Jobs\SyncDevicesJob;
6
+ use Fleetbase\FleetOps\Jobs\TestConnectionJob;
7
+ use Fleetbase\FleetOps\Models\Device;
8
+ use Fleetbase\FleetOps\Models\Telematic;
9
+ use Illuminate\Support\Facades\Crypt;
10
+ use Illuminate\Support\Facades\Validator;
11
+ use Illuminate\Validation\ValidationException;
12
+
13
+ /**
14
+ * Class TelematicService.
15
+ *
16
+ * Business logic for telematics management.
17
+ * Handles CRUD operations, connection testing, and device discovery.
18
+ */
19
+ class TelematicService
20
+ {
21
+ protected TelematicProviderRegistry $registry;
22
+
23
+ public function __construct(TelematicProviderRegistry $registry)
24
+ {
25
+ $this->registry = $registry;
26
+ }
27
+
28
+ /**
29
+ * Create a new telematic integration.
30
+ *
31
+ * @throws ValidationException
32
+ */
33
+ public function create(array $data): Telematic
34
+ {
35
+ // Validate provider exists
36
+ $providerKey = $data['provider_key'];
37
+ $descriptor = $this->registry->findByKey($providerKey);
38
+
39
+ if (!$descriptor) {
40
+ throw ValidationException::withMessages(['provider_key' => ['Invalid provider key.']]);
41
+ }
42
+
43
+ // Validate credentials against provider schema
44
+ $this->validateCredentials($data['credentials'] ?? [], $descriptor->requiredFields);
45
+
46
+ // Test connection if requested
47
+ if ($data['test_connection'] ?? false) {
48
+ $provider = $this->registry->resolve($providerKey);
49
+ $result = $provider->testConnection($data['credentials']);
50
+
51
+ if (!$result['success']) {
52
+ throw ValidationException::withMessages(['credentials' => [$result['message']]]);
53
+ }
54
+ }
55
+
56
+ // Create telematic record
57
+ $telematic = new Telematic();
58
+ $telematic->company_uuid = session('company');
59
+ $telematic->name = $data['name'];
60
+ $telematic->provider = $providerKey;
61
+ $telematic->credentials = Crypt::encryptString(json_encode($data['credentials']));
62
+ $telematic->status = 'active';
63
+ $telematic->meta = $data['meta'] ?? [];
64
+ $telematic->save();
65
+
66
+ return $telematic;
67
+ }
68
+
69
+ /**
70
+ * Update a telematic integration.
71
+ */
72
+ public function update(Telematic $telematic, array $data): Telematic
73
+ {
74
+ if (isset($data['name'])) {
75
+ $telematic->name = $data['name'];
76
+ }
77
+
78
+ if (isset($data['credentials'])) {
79
+ $descriptor = $this->registry->findByKey($telematic->provider);
80
+ $this->validateCredentials($data['credentials'], $descriptor->requiredFields);
81
+ $telematic->credentials = Crypt::encryptString(json_encode($data['credentials']));
82
+ }
83
+
84
+ if (isset($data['status'])) {
85
+ $telematic->status = $data['status'];
86
+ }
87
+
88
+ if (isset($data['meta'])) {
89
+ $telematic->meta = array_merge($telematic->meta ?? [], $data['meta']);
90
+ }
91
+
92
+ $telematic->save();
93
+
94
+ return $telematic;
95
+ }
96
+
97
+ /**
98
+ * Delete a telematic integration.
99
+ */
100
+ public function delete(Telematic $telematic): bool
101
+ {
102
+ return $telematic->delete();
103
+ }
104
+
105
+ /**
106
+ * Test connection to a provider.
107
+ *
108
+ * @return array|string
109
+ */
110
+ public function testConnection(Telematic $telematic, bool $async = false)
111
+ {
112
+ if ($async) {
113
+ $job = new TestConnectionJob($telematic);
114
+ dispatch($job);
115
+
116
+ return ['job_id' => $job->getJobId(), 'message' => 'Connection test queued'];
117
+ }
118
+
119
+ $provider = $this->registry->resolve($telematic->provider);
120
+ $provider->connect($telematic);
121
+
122
+ $credentials = json_decode(Crypt::decryptString($telematic->credentials), true);
123
+
124
+ return $provider->testConnection($credentials);
125
+ }
126
+
127
+ /**
128
+ * Discover devices from a provider.
129
+ *
130
+ * @return string Job ID
131
+ */
132
+ public function discoverDevices(Telematic $telematic, array $options = []): string
133
+ {
134
+ $job = new SyncDevicesJob($telematic, $options);
135
+ dispatch($job);
136
+
137
+ return $job->getJobId();
138
+ }
139
+
140
+ /**
141
+ * Link a device to a telematic.
142
+ */
143
+ public function linkDevice(Telematic $telematic, array $deviceData): Device
144
+ {
145
+ $device = Device::firstOrNew([
146
+ 'telematic_uuid' => $telematic->uuid,
147
+ 'external_id' => $deviceData['external_id'],
148
+ ]);
149
+
150
+ $device->device_name = $deviceData['device_name'] ?? 'Unknown Device';
151
+ $device->device_model = $deviceData['device_model'] ?? null;
152
+ $device->device_provider = $telematic->provider;
153
+ $device->status = $deviceData['status'] ?? 'active';
154
+ $device->meta = array_merge($device->meta ?? [], $deviceData['meta'] ?? []);
155
+
156
+ $device->save();
157
+
158
+ return $device;
159
+ }
160
+
161
+ /**
162
+ * Get devices for a telematic.
163
+ *
164
+ * @return \Illuminate\Database\Eloquent\Collection
165
+ */
166
+ public function getDevices(Telematic $telematic, array $filters = [])
167
+ {
168
+ $query = Device::where('telematic_uuid', $telematic->uuid);
169
+
170
+ if (isset($filters['status'])) {
171
+ $query->where('status', $filters['status']);
172
+ }
173
+
174
+ if (isset($filters['search'])) {
175
+ $query->where(function ($q) use ($filters) {
176
+ $q->where('device_name', 'like', "%{$filters['search']}%")
177
+ ->orWhere('external_id', 'like', "%{$filters['search']}%");
178
+ });
179
+ }
180
+
181
+ return $query->get();
182
+ }
183
+
184
+ /**
185
+ * Validate credentials against provider schema.
186
+ *
187
+ * @throws ValidationException
188
+ */
189
+ protected function validateCredentials(array $credentials, array $schema): void
190
+ {
191
+ $rules = [];
192
+
193
+ foreach ($schema as $field) {
194
+ $fieldRules = [];
195
+
196
+ if ($field['required'] ?? true) {
197
+ $fieldRules[] = 'required';
198
+ }
199
+
200
+ if (isset($field['validation'])) {
201
+ $fieldRules[] = $field['validation'];
202
+ } else {
203
+ $fieldRules[] = 'string';
204
+ }
205
+
206
+ $rules[$field['name']] = implode('|', $fieldRules);
207
+ }
208
+
209
+ $validator = Validator::make($credentials, $rules);
210
+
211
+ if ($validator->fails()) {
212
+ throw new ValidationException($validator);
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Get decrypted credentials for a telematic.
218
+ */
219
+ public function getCredentials(Telematic $telematic): array
220
+ {
221
+ return json_decode(Crypt::decryptString($telematic->credentials), true);
222
+ }
223
+ }
@@ -908,7 +908,7 @@ class Utils extends FleetbaseUtils
908
908
 
909
909
  /**
910
910
  * Calculates the great-circle distance between two points, with
911
- * the Vincenty formula. (Using over haversine tdue to antipodal point issues).
911
+ * the Vincenty formula. (Using over haversine due to antipodal point issues).
912
912
  *
913
913
  * https://en.wikipedia.org/wiki/Great-circle_distance#Formulas
914
914
  * https://en.wikipedia.org/wiki/Antipodal_point
@@ -345,6 +345,10 @@ Route::prefix(config('fleetops.api.routing.prefix', null))->namespace('Fleetbase
345
345
  $router->fleetbaseRoutes('proofs');
346
346
  $router->fleetbaseRoutes('purchase-rates');
347
347
  $router->fleetbaseRoutes('routes');
348
+ $router->fleetbaseRoutes('positions', function ($router, $controller) {
349
+ $router->post('replay', $controller('replay'));
350
+ $router->post('metrics', $controller('metrics'));
351
+ });
348
352
  $router->fleetbaseRoutes(
349
353
  'service-areas',
350
354
  function ($router, $controller) {
@@ -399,7 +403,14 @@ Route::prefix(config('fleetops.api.routing.prefix', null))->namespace('Fleetbase
399
403
  $router->fleetbaseRoutes('devices');
400
404
  $router->fleetbaseRoutes('device-events');
401
405
  $router->fleetbaseRoutes('sensors');
402
- $router->fleetbaseRoutes('telematics');
406
+ $router->fleetbaseRoutes('telematics', function ($router, $controller) {
407
+ $router->get('providers', $controller('providers'));
408
+ $router->get('devices', $controller('devices'));
409
+ $router->post('link-device', $controller('linkDevice'));
410
+ $router->post('discover', $controller('discover'));
411
+ $router->post('{id}/test-connection', $controller('testConnection'));
412
+ $router->post('{key}/test-credentials', $controller('testCredentials'));
413
+ });
403
414
  $router->fleetbaseRoutes('work-orders');
404
415
  $router->fleetbaseRoutes('maintenance');
405
416
  $router->fleetbaseRoutes('equipment');