@fleetbase/fleetops-engine 0.6.47 → 0.6.49

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 (223) hide show
  1. package/addon/components/admin/routing-settings.hbs +71 -0
  2. package/addon/components/admin/routing-settings.js +124 -0
  3. package/addon/components/customer/orders.js +1 -1
  4. package/addon/components/display-place.hbs +1 -1
  5. package/addon/components/driver/details.hbs +5 -1
  6. package/addon/components/driver/form.hbs +11 -1
  7. package/addon/components/fleet-listing-panel.hbs +2 -2
  8. package/addon/components/fleet-listing-panel.js +10 -0
  9. package/addon/components/map/google-live-map.js +12 -73
  10. package/addon/components/map/leaflet-live-map.hbs +149 -33
  11. package/addon/components/map/order-list-overlay/order.hbs +18 -3
  12. package/addon/components/map/toolbar.hbs +2 -2
  13. package/addon/components/map/toolbar.js +10 -2
  14. package/addon/components/orchestrator/phase-builder.hbs +49 -5
  15. package/addon/components/orchestrator/phase-builder.js +34 -1
  16. package/addon/components/orchestrator/resource-panel.hbs +19 -1
  17. package/addon/components/orchestrator/resource-panel.js +4 -0
  18. package/addon/components/orchestrator-workbench.js +4 -1
  19. package/addon/components/order/details/purchase-rate.hbs +19 -22
  20. package/addon/components/order/details/route.hbs +26 -1
  21. package/addon/components/order/details/route.js +50 -0
  22. package/addon/components/order/details/tracking.hbs +171 -27
  23. package/addon/components/order/details/tracking.js +457 -0
  24. package/addon/components/order/form.hbs +2 -2
  25. package/addon/components/order/kanban-card.hbs +18 -3
  26. package/addon/components/order-list-overlay/order.hbs +18 -3
  27. package/addon/components/order-progress-card.hbs +21 -6
  28. package/addon/components/order-tracking-lookup.hbs +7 -7
  29. package/addon/components/order-tracking-lookup.js +1 -1
  30. package/addon/components/route-list.hbs +78 -8
  31. package/addon/components/route-list.js +48 -6
  32. package/addon/components/tracking-stop-progress.hbs +45 -0
  33. package/addon/components/tracking-stop-progress.js +79 -0
  34. package/addon/components/widget/fleet-ops-key-metrics.hbs +19 -17
  35. package/addon/components/widget/fleet-ops-key-metrics.js +26 -0
  36. package/addon/components/widget/fuel-efficiency.hbs +26 -0
  37. package/addon/components/widget/fuel-efficiency.js +43 -0
  38. package/addon/components/widget/geofence-violations.hbs +44 -0
  39. package/addon/components/widget/geofence-violations.js +55 -0
  40. package/addon/components/widget/issues-insights.hbs +43 -0
  41. package/addon/components/widget/issues-insights.js +62 -0
  42. package/addon/components/widget/kpi-active-orders.hbs +10 -0
  43. package/addon/components/widget/kpi-active-orders.js +5 -0
  44. package/addon/components/widget/kpi-aov.hbs +10 -0
  45. package/addon/components/widget/kpi-aov.js +5 -0
  46. package/addon/components/widget/kpi-distance.hbs +10 -0
  47. package/addon/components/widget/kpi-distance.js +5 -0
  48. package/addon/components/widget/kpi-drivers-online.hbs +10 -0
  49. package/addon/components/widget/kpi-drivers-online.js +5 -0
  50. package/addon/components/widget/kpi-earnings.hbs +10 -0
  51. package/addon/components/widget/kpi-earnings.js +5 -0
  52. package/addon/components/widget/kpi-open-issues.hbs +11 -0
  53. package/addon/components/widget/kpi-open-issues.js +5 -0
  54. package/addon/components/widget/kpi-tile.hbs +59 -0
  55. package/addon/components/widget/kpi-tile.js +168 -0
  56. package/addon/components/widget/live-fleet.hbs +122 -0
  57. package/addon/components/widget/live-fleet.js +365 -0
  58. package/addon/components/widget/maintenance-overview.hbs +45 -0
  59. package/addon/components/widget/maintenance-overview.js +40 -0
  60. package/addon/components/widget/on-time-delivery.hbs +27 -0
  61. package/addon/components/widget/on-time-delivery.js +73 -0
  62. package/addon/components/widget/operations-pulse.hbs +51 -0
  63. package/addon/components/widget/operations-pulse.js +173 -0
  64. package/addon/components/widget/orders-by-status.hbs +26 -0
  65. package/addon/components/widget/orders-by-status.js +54 -0
  66. package/addon/components/widget/revenue-trend.hbs +34 -0
  67. package/addon/components/widget/revenue-trend.js +67 -0
  68. package/addon/components/widget/top-drivers.hbs +44 -0
  69. package/addon/components/widget/top-drivers.js +54 -0
  70. package/addon/controllers/settings/routing.js +124 -1
  71. package/addon/extension.js +172 -3
  72. package/addon/helpers/format-duration.js +38 -0
  73. package/addon/routes/application.js +6 -0
  74. package/addon/services/map-adapter/google.js +13 -2
  75. package/addon/services/order-list-overlay.js +2 -2
  76. package/addon/styles/fleetops-engine.css +806 -46
  77. package/addon/templates/maintenance/equipment/index.hbs +1 -1
  78. package/addon/templates/maintenance/parts/index.hbs +1 -1
  79. package/addon/templates/maintenance/schedules/index.hbs +1 -1
  80. package/addon/templates/management/vehicles/index.hbs +1 -1
  81. package/addon/templates/settings/avatars.hbs +1 -1
  82. package/addon/templates/settings/custom-fields.hbs +1 -1
  83. package/addon/templates/settings/map.hbs +1 -1
  84. package/addon/templates/settings/navigator-app.hbs +1 -1
  85. package/addon/templates/settings/notifications.hbs +1 -1
  86. package/addon/templates/settings/orchestrator.hbs +1 -1
  87. package/addon/templates/settings/payments/index.hbs +1 -1
  88. package/addon/templates/settings/payments/onboard.hbs +1 -1
  89. package/addon/templates/settings/routing.hbs +82 -4
  90. package/addon/templates/settings/scheduling.hbs +1 -1
  91. package/addon/utils/live-map-card-content.js +171 -0
  92. package/app/components/tracking-stop-progress.js +1 -0
  93. package/app/components/widget/fuel-efficiency.js +1 -0
  94. package/app/components/widget/geofence-violations.js +1 -0
  95. package/app/components/widget/issues-insights.js +1 -0
  96. package/app/components/widget/kpi-active-orders.js +1 -0
  97. package/app/components/widget/kpi-aov.js +1 -0
  98. package/app/components/widget/kpi-distance.js +1 -0
  99. package/app/components/widget/kpi-drivers-online.js +1 -0
  100. package/app/components/widget/kpi-earnings.js +1 -0
  101. package/app/components/widget/kpi-open-issues.js +1 -0
  102. package/app/components/widget/kpi-tile.js +1 -0
  103. package/app/components/widget/live-fleet.js +1 -0
  104. package/app/components/widget/maintenance-overview.js +1 -0
  105. package/app/components/widget/on-time-delivery.js +1 -0
  106. package/app/components/widget/operations-pulse.js +1 -0
  107. package/app/components/widget/orders-by-status.js +1 -0
  108. package/app/components/widget/revenue-trend.js +1 -0
  109. package/app/components/widget/top-drivers.js +1 -0
  110. package/app/helpers/format-duration.js +1 -0
  111. package/app/utils/live-map-card-content.js +1 -0
  112. package/composer.json +1 -1
  113. package/extension.json +1 -1
  114. package/package.json +3 -3
  115. package/pnpm-workspace.yaml +1 -0
  116. package/server/config/fleetops.php +32 -11
  117. package/server/config/telematics.php +111 -1
  118. package/server/migrations/2026_05_13_000001_repair_customer_contact_user_types.php +86 -0
  119. package/server/migrations/2026_05_18_000001_add_license_expiry_to_drivers_table.php +25 -0
  120. package/server/seeders/Testing/Concerns/SeedsTestingData.php +183 -0
  121. package/server/seeders/Testing/ConnectivitySeeder.php +212 -0
  122. package/server/seeders/Testing/FleetSeeder.php +341 -0
  123. package/server/seeders/Testing/MaintenanceSeeder.php +238 -0
  124. package/server/seeders/Testing/NetworkSeeder.php +250 -0
  125. package/server/seeders/Testing/OrdersSeeder.php +214 -0
  126. package/server/seeders/Testing/TestingSeeder.php +40 -0
  127. package/server/seeders/Testing/VroomOrchestrationSeeder.php +409 -0
  128. package/server/src/Console/Commands/ProcessOperationalAlerts.php +279 -0
  129. package/server/src/Http/Controllers/Api/v1/OrchestrationController.php +64 -0
  130. package/server/src/Http/Controllers/Api/v1/OrderController.php +26 -26
  131. package/server/src/Http/Controllers/Api/v1/PayloadController.php +10 -10
  132. package/server/src/Http/Controllers/Api/v1/ServiceQuoteController.php +1 -1
  133. package/server/src/Http/Controllers/Internal/v1/AnalyticsController.php +119 -0
  134. package/server/src/Http/Controllers/Internal/v1/ContactController.php +41 -0
  135. package/server/src/Http/Controllers/Internal/v1/LiveController.php +6 -42
  136. package/server/src/Http/Controllers/Internal/v1/MetricsController.php +82 -3
  137. package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +1 -1
  138. package/server/src/Http/Controllers/Internal/v1/OrderController.php +52 -21
  139. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +1 -1
  140. package/server/src/Http/Controllers/Internal/v1/SettingController.php +164 -12
  141. package/server/src/Http/Resources/v1/Driver.php +2 -0
  142. package/server/src/Http/Resources/v1/Fleet.php +4 -0
  143. package/server/src/Http/Resources/v1/Index/Driver.php +48 -0
  144. package/server/src/Http/Resources/v1/Index/Payload.php +2 -2
  145. package/server/src/Http/Resources/v1/Index/Vehicle.php +74 -1
  146. package/server/src/Models/Contact.php +122 -13
  147. package/server/src/Models/Driver.php +2 -0
  148. package/server/src/Models/Order.php +1 -1
  149. package/server/src/Models/OrderConfig.php +14 -11
  150. package/server/src/Models/Payload.php +8 -8
  151. package/server/src/Models/Place.php +0 -3
  152. package/server/src/Models/ServiceRate.php +1 -1
  153. package/server/src/Notifications/LateDeparture.php +48 -0
  154. package/server/src/Notifications/ProlongedStoppage.php +48 -0
  155. package/server/src/Notifications/RouteDeviation.php +48 -0
  156. package/server/src/Observers/ContactObserver.php +8 -0
  157. package/server/src/Orchestration/Engines/CapacityAllocationEngine.php +220 -0
  158. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +357 -38
  159. package/server/src/Orchestration/Support/OrchestrationPayloadBuilder.php +259 -51
  160. package/server/src/Providers/FleetOpsServiceProvider.php +32 -0
  161. package/server/src/Providers/NotificationServiceProvider.php +3 -0
  162. package/server/src/Support/Analytics/AbstractAnalytics.php +77 -0
  163. package/server/src/Support/Analytics/FuelEfficiency.php +83 -0
  164. package/server/src/Support/Analytics/GeofenceViolations.php +63 -0
  165. package/server/src/Support/Analytics/IssuesInsights.php +67 -0
  166. package/server/src/Support/Analytics/LiveFleet.php +127 -0
  167. package/server/src/Support/Analytics/MaintenanceOverview.php +70 -0
  168. package/server/src/Support/Analytics/OnTimeDelivery.php +75 -0
  169. package/server/src/Support/Analytics/OperationsPulse.php +86 -0
  170. package/server/src/Support/Analytics/OrdersByStatus.php +64 -0
  171. package/server/src/Support/Analytics/RevenueTrend.php +79 -0
  172. package/server/src/Support/Analytics/TopDrivers.php +92 -0
  173. package/server/src/Support/LiveOrderQuery.php +75 -0
  174. package/server/src/Support/Metrics/AbstractMetric.php +170 -0
  175. package/server/src/Support/Metrics/ActiveLiveOrdersMetric.php +28 -0
  176. package/server/src/Support/Metrics/AvgOrderValueMetric.php +53 -0
  177. package/server/src/Support/Metrics/DriversOnlineMetric.php +30 -0
  178. package/server/src/Support/Metrics/EarningsMetric.php +30 -0
  179. package/server/src/Support/Metrics/FuelCostsMetric.php +30 -0
  180. package/server/src/Support/Metrics/MoneyMetric.php +30 -0
  181. package/server/src/Support/Metrics/OpenIssuesMetric.php +35 -0
  182. package/server/src/Support/Metrics/OrdersCanceledMetric.php +35 -0
  183. package/server/src/Support/Metrics/OrdersCompletedMetric.php +35 -0
  184. package/server/src/Support/Metrics/OrdersInProgressMetric.php +51 -0
  185. package/server/src/Support/Metrics/OrdersScheduledMetric.php +37 -0
  186. package/server/src/Support/Metrics/Registry.php +46 -0
  187. package/server/src/Support/Metrics/ResolvedIssuesMetric.php +35 -0
  188. package/server/src/Support/Metrics/TotalCustomersMetric.php +29 -0
  189. package/server/src/Support/Metrics/TotalDistanceTraveledMetric.php +35 -0
  190. package/server/src/Support/Metrics/TotalDriversMetric.php +28 -0
  191. package/server/src/Support/Metrics/TotalTimeTraveledMetric.php +35 -0
  192. package/server/src/Support/Metrics.php +46 -234
  193. package/server/src/Support/OSRM.php +11 -6
  194. package/server/src/Support/OrderTracker.php +7 -582
  195. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +290 -0
  196. package/server/src/Support/Telematics/Providers/SafeeProvider.php +333 -0
  197. package/server/src/Support/Utils.php +1 -1
  198. package/server/src/Tracking/Contracts/TrackingProviderInterface.php +19 -0
  199. package/server/src/Tracking/Providers/CalculatedTrackingProvider.php +65 -0
  200. package/server/src/Tracking/Providers/GoogleRoutesTrackingProvider.php +118 -0
  201. package/server/src/Tracking/Providers/OsrmTrackingProvider.php +71 -0
  202. package/server/src/Tracking/README.md +25 -0
  203. package/server/src/Tracking/Support/FakeTrackingProvider.php +42 -0
  204. package/server/src/Tracking/TrackingContext.php +63 -0
  205. package/server/src/Tracking/TrackingContextBuilder.php +204 -0
  206. package/server/src/Tracking/TrackingIntelligenceService.php +180 -0
  207. package/server/src/Tracking/TrackingOptions.php +49 -0
  208. package/server/src/Tracking/TrackingProviderCapabilities.php +30 -0
  209. package/server/src/Tracking/TrackingProviderManager.php +89 -0
  210. package/server/src/Tracking/TrackingProviderRegistry.php +35 -0
  211. package/server/src/Tracking/TrackingProviderResult.php +20 -0
  212. package/server/src/Tracking/TrackingStop.php +59 -0
  213. package/server/src/routes.php +27 -0
  214. package/server/tests/AnalyticsRoutesTest.php +41 -0
  215. package/server/tests/CapacityAllocationEngineTest.php +108 -0
  216. package/server/tests/LiveOrderQueryTest.php +34 -0
  217. package/server/tests/MetricsRegistryTest.php +88 -0
  218. package/server/tests/OrchestrationPayloadBuilderTest.php +132 -0
  219. package/server/tests/OrchestratorConsumableApiTest.php +74 -0
  220. package/server/tests/PingDriverEndpointTest.php +19 -0
  221. package/server/tests/TrackingIntelligenceTest.php +176 -0
  222. package/server/tests/VroomOrchestrationEngineTest.php +250 -0
  223. package/translations/en-us.yaml +41 -0
@@ -0,0 +1,290 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Telematics\Providers;
4
+
5
+ use Illuminate\Support\Carbon;
6
+ use Illuminate\Support\Facades\Http;
7
+
8
+ /**
9
+ * AFAQY telematics provider implementation.
10
+ *
11
+ * AFAQY uses token-based REST endpoints for authentication, unit discovery,
12
+ * latest unit telemetry, historical signals, and track generation.
13
+ */
14
+ class AfaqyProvider extends AbstractProvider
15
+ {
16
+ protected string $baseUrl = 'https://api.afaqy.sa';
17
+ protected int $requestsPerMinute = 60;
18
+
19
+ protected function prepareAuthentication(): void
20
+ {
21
+ $this->baseUrl = rtrim($this->credentials['base_url'] ?? $this->baseUrl, '/');
22
+ $token = $this->credentials['token'] ?? $this->authenticate();
23
+
24
+ $this->credentials['token'] = $token;
25
+ $this->headers = [
26
+ 'Accept' => 'application/json',
27
+ 'Content-Type' => 'application/json',
28
+ ];
29
+ }
30
+
31
+ public function testConnection(array $credentials): array
32
+ {
33
+ try {
34
+ $this->credentials = $credentials;
35
+ $this->prepareAuthentication();
36
+
37
+ $response = $this->afaqyPost('/units/lists', [
38
+ 'data' => [
39
+ 'projection' => ['_id', 'name', 'imei', 'last_update'],
40
+ 'filters' => new \stdClass(),
41
+ ],
42
+ ]);
43
+
44
+ return [
45
+ 'success' => true,
46
+ 'message' => 'Connection successful',
47
+ 'metadata' => [
48
+ 'units_count' => count($response['data'] ?? []),
49
+ 'status_code' => $response['status_code'] ?? null,
50
+ ],
51
+ ];
52
+ } catch (\Throwable $e) {
53
+ return [
54
+ 'success' => false,
55
+ 'message' => $e->getMessage(),
56
+ 'metadata' => [],
57
+ ];
58
+ }
59
+ }
60
+
61
+ public function fetchDevices(array $options = []): array
62
+ {
63
+ $limit = (int) ($options['limit'] ?? 100);
64
+ $offset = (int) ($options['cursor'] ?? 0);
65
+
66
+ $response = $this->afaqyPost('/units/lists', [
67
+ 'data' => [
68
+ 'address' => $options['address'] ?? false,
69
+ 'filters' => $options['filters'] ?? new \stdClass(),
70
+ 'projection' => $options['projection'] ?? [
71
+ '_id',
72
+ 'name',
73
+ 'imei',
74
+ 'sim_number',
75
+ 'device_serial',
76
+ 'active',
77
+ 'driver_id',
78
+ 'last_update',
79
+ 'counters',
80
+ 'profile',
81
+ 'sensors',
82
+ 'sensors_last_val',
83
+ ],
84
+ ],
85
+ 'limit' => $limit,
86
+ 'offset' => $offset,
87
+ ]);
88
+
89
+ $devices = $response['data'] ?? [];
90
+ $pagination = $response['pagination'] ?? [];
91
+ $total = (int) ($pagination['filtersCount'] ?? $pagination['allCount'] ?? count($devices));
92
+ $nextCursor = ($offset + $limit) < $total ? $offset + $limit : null;
93
+
94
+ return [
95
+ 'devices' => $devices,
96
+ 'next_cursor' => $nextCursor,
97
+ 'has_more' => $nextCursor !== null,
98
+ ];
99
+ }
100
+
101
+ public function fetchDeviceDetails(string $externalId): array
102
+ {
103
+ $response = $this->afaqyPost('/units/view', [
104
+ 'data' => ['id' => $externalId],
105
+ ]);
106
+
107
+ return $response['data'] ?? [];
108
+ }
109
+
110
+ public function normalizeDevice(array $payload): array
111
+ {
112
+ $lastUpdate = $payload['last_update'] ?? [];
113
+
114
+ return [
115
+ 'external_id' => $payload['_id'] ?? $payload['id'] ?? null,
116
+ 'device_name' => $payload['name'] ?? data_get($payload, 'profile.plate_number') ?? 'Unknown Unit',
117
+ 'device_provider' => 'afaqy',
118
+ 'device_model' => data_get($payload, 'profile.model') ?? $payload['device'] ?? null,
119
+ 'imei' => $payload['imei'] ?? null,
120
+ 'phone' => $payload['sim_number'] ?? null,
121
+ 'vin' => data_get($payload, 'profile.vin'),
122
+ 'status' => ($payload['active'] ?? false) ? 'active' : 'inactive',
123
+ 'location' => [
124
+ 'lat' => $lastUpdate['lat'] ?? null,
125
+ 'lng' => $lastUpdate['lng'] ?? null,
126
+ ],
127
+ 'meta' => [
128
+ 'raw' => $payload,
129
+ 'plate_number' => data_get($payload, 'profile.plate_number'),
130
+ 'driver_id' => $payload['driver_id'] ?? null,
131
+ 'last_update' => $this->normalizeEvent($payload),
132
+ 'capabilities' => [
133
+ 'tracking' => isset($lastUpdate['lat'], $lastUpdate['lng']),
134
+ 'odometer' => data_get($payload, 'counters.odometer') !== null,
135
+ 'fuel_level' => $this->extractFuelLevel($payload) !== null,
136
+ 'ignition_state' => $this->extractIgnition($payload) !== null,
137
+ ],
138
+ ],
139
+ ];
140
+ }
141
+
142
+ public function normalizeEvent(array $payload): array
143
+ {
144
+ $lastUpdate = $payload['last_update'] ?? $payload;
145
+
146
+ return [
147
+ 'external_id' => $payload['_id'] ?? $payload['id'] ?? null,
148
+ 'event_type' => $payload['event'] ?? $payload['event_type'] ?? 'telemetry_update',
149
+ 'occurred_at' => $this->parseTimestamp($lastUpdate['dtt'] ?? $lastUpdate['dts'] ?? null),
150
+ 'location' => [
151
+ 'lat' => $lastUpdate['lat'] ?? data_get($lastUpdate, 'loc.coordinates.1'),
152
+ 'lng' => $lastUpdate['lng'] ?? data_get($lastUpdate, 'loc.coordinates.0'),
153
+ ],
154
+ 'speed' => $lastUpdate['speed'] ?? $lastUpdate['spd'] ?? null,
155
+ 'heading' => $lastUpdate['angle'] ?? $lastUpdate['ang'] ?? null,
156
+ 'odometer' => data_get($payload, 'counters.odometer'),
157
+ 'ignition' => $this->extractIgnition($payload),
158
+ 'fuel_level' => $this->extractFuelLevel($payload),
159
+ 'meta' => $payload,
160
+ ];
161
+ }
162
+
163
+ public function normalizeSensor(array $payload): array
164
+ {
165
+ return [
166
+ 'sensor_type' => $payload['type'] ?? $payload['name'] ?? $payload['param'] ?? 'generic',
167
+ 'value' => $payload['value'] ?? $payload['last_val']['value'] ?? $payload['last_update'] ?? null,
168
+ 'unit' => $payload['units'] ?? null,
169
+ 'recorded_at' => $this->parseTimestamp($payload['updated_at'] ?? $payload['created_at'] ?? null),
170
+ 'meta' => $payload,
171
+ ];
172
+ }
173
+
174
+ public function getCredentialSchema(): array
175
+ {
176
+ return [
177
+ [
178
+ 'name' => 'base_url',
179
+ 'label' => 'Base URL',
180
+ 'type' => 'text',
181
+ 'placeholder' => 'https://api.afaqy.sa',
182
+ 'required' => false,
183
+ 'validation' => 'nullable|url',
184
+ ],
185
+ [
186
+ 'name' => 'username',
187
+ 'label' => 'Username',
188
+ 'type' => 'text',
189
+ 'placeholder' => 'Enter your AFAQY username',
190
+ 'required' => false,
191
+ 'validation' => 'required_without:token|string',
192
+ ],
193
+ [
194
+ 'name' => 'password',
195
+ 'label' => 'Password',
196
+ 'type' => 'password',
197
+ 'placeholder' => 'Enter your AFAQY password',
198
+ 'required' => false,
199
+ 'validation' => 'required_without:token|string',
200
+ ],
201
+ [
202
+ 'name' => 'token',
203
+ 'label' => 'Token',
204
+ 'type' => 'password',
205
+ 'placeholder' => 'Use an existing AFAQY token',
206
+ 'required' => false,
207
+ 'validation' => 'required_without:username|string',
208
+ ],
209
+ ];
210
+ }
211
+
212
+ protected function authenticate(): string
213
+ {
214
+ if (empty($this->credentials['username']) || empty($this->credentials['password'])) {
215
+ throw new \InvalidArgumentException('AFAQY username/password or token is required.');
216
+ }
217
+
218
+ $response = Http::asJson()
219
+ ->acceptJson()
220
+ ->timeout(30)
221
+ ->post($this->baseUrl . '/auth/login', [
222
+ 'data' => [
223
+ 'username' => $this->credentials['username'],
224
+ 'password' => $this->credentials['password'],
225
+ 'expire' => $this->credentials['expire'] ?? 1,
226
+ ],
227
+ ]);
228
+
229
+ if ($response->failed()) {
230
+ throw new \RuntimeException('AFAQY authentication failed: ' . $response->body());
231
+ }
232
+
233
+ $token = data_get($response->json(), 'data.token');
234
+
235
+ if (!$token) {
236
+ throw new \RuntimeException('AFAQY authentication did not return a token.');
237
+ }
238
+
239
+ return $token;
240
+ }
241
+
242
+ protected function afaqyPost(string $endpoint, array $payload = []): array
243
+ {
244
+ $response = Http::withHeaders($this->headers)
245
+ ->timeout(30)
246
+ ->post($this->baseUrl . $endpoint . '?token=' . urlencode($this->credentials['token']), $payload);
247
+
248
+ if ($response->failed()) {
249
+ throw new \RuntimeException('AFAQY API request failed: ' . $response->body());
250
+ }
251
+
252
+ return $response->json() ?? [];
253
+ }
254
+
255
+ protected function parseTimestamp($value): ?string
256
+ {
257
+ if (!$value) {
258
+ return null;
259
+ }
260
+
261
+ if (is_numeric($value)) {
262
+ $timestamp = (float) $value;
263
+ $seconds = $timestamp > 9999999999 ? $timestamp / 1000 : $timestamp;
264
+
265
+ return Carbon::createFromTimestamp($seconds)->toDateTimeString();
266
+ }
267
+
268
+ return Carbon::parse($value)->toDateTimeString();
269
+ }
270
+
271
+ protected function extractIgnition(array $payload): ?bool
272
+ {
273
+ $params = data_get($payload, 'last_update.params', []);
274
+ $value = data_get($params, 'acc') ?? data_get($params, 'di1') ?? data_get($payload, 'counters.last_acc');
275
+
276
+ if ($value === null) {
277
+ return null;
278
+ }
279
+
280
+ return filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? ((int) $value === 1);
281
+ }
282
+
283
+ protected function extractFuelLevel(array $payload): mixed
284
+ {
285
+ return data_get($payload, 'last_update.params.fuel')
286
+ ?? data_get($payload, 'last_update.params.fuel_level')
287
+ ?? data_get($payload, 'fc.level')
288
+ ?? null;
289
+ }
290
+ }
@@ -0,0 +1,333 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Telematics\Providers;
4
+
5
+ use Illuminate\Support\Carbon;
6
+ use Illuminate\Support\Facades\Http;
7
+
8
+ /**
9
+ * Safee Tracking REST provider implementation.
10
+ *
11
+ * Safee authenticates through an OpenID Connect token endpoint and exposes
12
+ * vehicle discovery plus latest state/position endpoints under api/v2.
13
+ */
14
+ class SafeeProvider extends AbstractProvider
15
+ {
16
+ protected string $baseUrl = '';
17
+ protected int $requestsPerMinute = 3000;
18
+ protected ?string $accessToken = null;
19
+
20
+ protected function prepareAuthentication(): void
21
+ {
22
+ $this->baseUrl = rtrim($this->credentials['api_base_url'] ?? $this->credentials['server_uri'] ?? '', '/');
23
+ $this->accessToken = $this->credentials['access_token'] ?? $this->authenticate();
24
+ $scheme = $this->credentials['authorization_scheme'] ?? 'Bearer';
25
+
26
+ $this->headers = [
27
+ 'Accept' => 'application/json',
28
+ 'Content-Type' => 'application/json',
29
+ 'Accept-Language' => $this->credentials['language'] ?? 'en',
30
+ 'Authorization' => trim($scheme . ' ' . $this->accessToken),
31
+ ];
32
+ }
33
+
34
+ public function testConnection(array $credentials): array
35
+ {
36
+ try {
37
+ $this->credentials = $credentials;
38
+ $this->prepareAuthentication();
39
+
40
+ $response = $this->safeeGet('/api/v2/status');
41
+
42
+ return [
43
+ 'success' => (int) ($response['code'] ?? 0) === 0,
44
+ 'message' => $response['message'] ?? 'Connection successful',
45
+ 'metadata' => [
46
+ 'status' => $response['status'] ?? null,
47
+ 'time' => $response['time'] ?? null,
48
+ ],
49
+ ];
50
+ } catch (\Throwable $e) {
51
+ return [
52
+ 'success' => false,
53
+ 'message' => $e->getMessage(),
54
+ 'metadata' => [],
55
+ ];
56
+ }
57
+ }
58
+
59
+ public function fetchDevices(array $options = []): array
60
+ {
61
+ if (!empty($options['vehicles'])) {
62
+ $response = $this->safeePost('/api/v2/vehicle/last-state', [
63
+ 'live' => $options['live'] ?? true,
64
+ 'startDate' => $options['start_date'] ?? null,
65
+ 'endDate' => $options['end_date'] ?? null,
66
+ 'vehicles' => $options['vehicles'],
67
+ ]);
68
+
69
+ $devices = $response['result'] ?? [];
70
+ } else {
71
+ $response = $this->safeePost('/api/v2/vehicle/list-info', $options['filter'] ?? new \stdClass());
72
+ $devices = $response['result'] ?? [];
73
+ }
74
+
75
+ return [
76
+ 'devices' => $devices,
77
+ 'next_cursor' => null,
78
+ 'has_more' => false,
79
+ ];
80
+ }
81
+
82
+ public function fetchDeviceDetails(string $externalId): array
83
+ {
84
+ $response = $this->safeePost('/api/v2/vehicle/info', [
85
+ 'id' => is_numeric($externalId) ? (int) $externalId : $externalId,
86
+ ]);
87
+
88
+ return $response['result'] ?? [];
89
+ }
90
+
91
+ public function normalizeDevice(array $payload): array
92
+ {
93
+ $position = $this->extractPosition($payload);
94
+
95
+ return [
96
+ 'external_id' => $payload['id'] ?? data_get($payload, 'vehicle.id') ?? null,
97
+ 'device_name' => $payload['name'] ?? $payload['plateNumber'] ?? data_get($payload, 'vehicle.name') ?? 'Unknown Vehicle',
98
+ 'device_provider' => 'safee',
99
+ 'device_model' => $payload['model'] ?? data_get($payload, 'device.model') ?? null,
100
+ 'imei' => data_get($payload, 'device.imei') ?? data_get($payload, 'device.serial') ?? null,
101
+ 'vin' => $payload['vin'] ?? null,
102
+ 'status' => $this->normalizeVehicleStatus($payload['status'] ?? $payload['vehicleStatus'] ?? null),
103
+ 'location' => [
104
+ 'lat' => $position['lat'] ?? null,
105
+ 'lng' => $position['lng'] ?? null,
106
+ ],
107
+ 'meta' => [
108
+ 'raw' => $payload,
109
+ 'plate_number' => $payload['plateNumber'] ?? $payload['plate_number'] ?? null,
110
+ 'door_number' => $payload['doorNumber'] ?? null,
111
+ 'driver' => $payload['driver'] ?? null,
112
+ 'last_update' => $this->normalizeEvent($payload),
113
+ 'capabilities' => [
114
+ 'tracking' => isset($position['lat'], $position['lng']),
115
+ 'odometer' => $this->extractOdometer($payload) !== null,
116
+ 'fuel_level' => $this->extractFuelLevel($payload) !== null,
117
+ 'ignition_state' => $this->extractIgnition($payload) !== null,
118
+ ],
119
+ ],
120
+ ];
121
+ }
122
+
123
+ public function normalizeEvent(array $payload): array
124
+ {
125
+ $position = $this->extractPosition($payload);
126
+
127
+ return [
128
+ 'external_id' => $payload['id'] ?? data_get($payload, 'vehicle.id') ?? null,
129
+ 'event_type' => data_get($payload, 'event.code') ?? data_get($payload, 'event.name') ?? 'telemetry_update',
130
+ 'occurred_at' => $this->parseTimestamp($payload['date'] ?? $payload['deviceTime'] ?? $payload['time'] ?? null),
131
+ 'location' => [
132
+ 'lat' => $position['lat'] ?? null,
133
+ 'lng' => $position['lng'] ?? null,
134
+ ],
135
+ 'speed' => $payload['speed'] ?? $payload['lastSpeed'] ?? null,
136
+ 'heading' => $payload['heading'] ?? $payload['angle'] ?? null,
137
+ 'odometer' => $this->extractOdometer($payload),
138
+ 'ignition' => $this->extractIgnition($payload),
139
+ 'fuel_level' => $this->extractFuelLevel($payload),
140
+ 'meta' => $payload,
141
+ ];
142
+ }
143
+
144
+ public function normalizeSensor(array $payload): array
145
+ {
146
+ return [
147
+ 'sensor_type' => $payload['type'] ?? $payload['name'] ?? 'generic',
148
+ 'value' => $payload['value'] ?? $payload['lastValue'] ?? null,
149
+ 'unit' => $payload['unit'] ?? null,
150
+ 'recorded_at' => $this->parseTimestamp($payload['date'] ?? $payload['deviceTime'] ?? null),
151
+ 'meta' => $payload,
152
+ ];
153
+ }
154
+
155
+ public function getCredentialSchema(): array
156
+ {
157
+ return [
158
+ [
159
+ 'name' => 'server_uri',
160
+ 'label' => 'Server URI',
161
+ 'type' => 'text',
162
+ 'placeholder' => 'https://tracking.example.com',
163
+ 'required' => true,
164
+ 'validation' => 'required|url',
165
+ ],
166
+ [
167
+ 'name' => 'realm_id',
168
+ 'label' => 'Realm ID',
169
+ 'type' => 'text',
170
+ 'placeholder' => 'Enter Safee realm ID',
171
+ 'required' => true,
172
+ ],
173
+ [
174
+ 'name' => 'client_id',
175
+ 'label' => 'Client ID',
176
+ 'type' => 'text',
177
+ 'placeholder' => 'Enter Safee client ID',
178
+ 'required' => true,
179
+ ],
180
+ [
181
+ 'name' => 'client_secret',
182
+ 'label' => 'Client Secret',
183
+ 'type' => 'password',
184
+ 'placeholder' => 'Enter Safee client secret',
185
+ 'required' => true,
186
+ ],
187
+ [
188
+ 'name' => 'username',
189
+ 'label' => 'Username',
190
+ 'type' => 'text',
191
+ 'placeholder' => 'Enter Safee username',
192
+ 'required' => true,
193
+ ],
194
+ [
195
+ 'name' => 'password',
196
+ 'label' => 'Password',
197
+ 'type' => 'password',
198
+ 'placeholder' => 'Enter Safee password',
199
+ 'required' => true,
200
+ ],
201
+ ];
202
+ }
203
+
204
+ protected function authenticate(): string
205
+ {
206
+ foreach (['server_uri', 'realm_id', 'client_id', 'client_secret', 'username', 'password'] as $field) {
207
+ if (empty($this->credentials[$field])) {
208
+ throw new \InvalidArgumentException("Safee credential '{$field}' is required.");
209
+ }
210
+ }
211
+
212
+ $tokenUrl = rtrim($this->credentials['server_uri'], '/') . '/auth/realms/' . $this->credentials['realm_id'] . '/protocol/openid-connect/token';
213
+
214
+ $response = Http::asForm()
215
+ ->acceptJson()
216
+ ->timeout(30)
217
+ ->post($tokenUrl, [
218
+ 'grant_type' => 'password',
219
+ 'client_secret' => $this->credentials['client_secret'],
220
+ 'client_id' => $this->credentials['client_id'],
221
+ 'username' => $this->credentials['username'],
222
+ 'password' => $this->credentials['password'],
223
+ ]);
224
+
225
+ if ($response->failed()) {
226
+ throw new \RuntimeException('Safee authentication failed: ' . $response->body());
227
+ }
228
+
229
+ $token = $response->json('access_token');
230
+
231
+ if (!$token) {
232
+ throw new \RuntimeException('Safee authentication did not return an access token.');
233
+ }
234
+
235
+ return $token;
236
+ }
237
+
238
+ protected function safeeGet(string $endpoint): array
239
+ {
240
+ $response = Http::withHeaders($this->headers)
241
+ ->timeout(30)
242
+ ->get($this->baseUrl . $endpoint);
243
+
244
+ if ($response->failed()) {
245
+ throw new \RuntimeException('Safee API request failed: ' . $response->body());
246
+ }
247
+
248
+ return $response->json() ?? [];
249
+ }
250
+
251
+ protected function safeePost(string $endpoint, array|\stdClass $payload = []): array
252
+ {
253
+ $response = Http::withHeaders($this->headers)
254
+ ->timeout(30)
255
+ ->post($this->baseUrl . $endpoint, $payload);
256
+
257
+ if ($response->failed()) {
258
+ throw new \RuntimeException('Safee API request failed: ' . $response->body());
259
+ }
260
+
261
+ return $response->json() ?? [];
262
+ }
263
+
264
+ protected function extractPosition(array $payload): array
265
+ {
266
+ $position = $payload['position'] ?? $payload['lastPosition'] ?? $payload['location'] ?? $payload;
267
+
268
+ return [
269
+ 'lat' => $position['lat'] ?? $position['latitude'] ?? data_get($position, 'loc.coordinates.1'),
270
+ 'lng' => $position['lon'] ?? $position['lng'] ?? $position['longitude'] ?? data_get($position, 'loc.coordinates.0'),
271
+ ];
272
+ }
273
+
274
+ protected function parseTimestamp($value): ?string
275
+ {
276
+ if (!$value) {
277
+ return null;
278
+ }
279
+
280
+ if (is_numeric($value)) {
281
+ $timestamp = (float) $value;
282
+ $seconds = $timestamp > 9999999999 ? $timestamp / 1000 : $timestamp;
283
+
284
+ return Carbon::createFromTimestamp($seconds)->toDateTimeString();
285
+ }
286
+
287
+ return Carbon::parse($value)->toDateTimeString();
288
+ }
289
+
290
+ protected function normalizeVehicleStatus(?string $status): string
291
+ {
292
+ if (!$status) {
293
+ return 'active';
294
+ }
295
+
296
+ return in_array(strtolower($status), ['offline', 'inactive', 'deleted', 'expired'], true) ? 'inactive' : 'active';
297
+ }
298
+
299
+ protected function extractOdometer(array $payload): mixed
300
+ {
301
+ return data_get($payload, 'canbus.odometer')
302
+ ?? data_get($payload, 'vehicleCanbus.odometer')
303
+ ?? data_get($payload, 'odometer')
304
+ ?? null;
305
+ }
306
+
307
+ protected function extractIgnition(array $payload): ?bool
308
+ {
309
+ $value = $payload['ignition'] ?? data_get($payload, 'event.code');
310
+
311
+ if ($value === null) {
312
+ return null;
313
+ }
314
+
315
+ if (is_string($value) && str_contains(strtolower($value), 'ignition_on')) {
316
+ return true;
317
+ }
318
+
319
+ if (is_string($value) && str_contains(strtolower($value), 'ignition_off')) {
320
+ return false;
321
+ }
322
+
323
+ return filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
324
+ }
325
+
326
+ protected function extractFuelLevel(array $payload): mixed
327
+ {
328
+ return data_get($payload, 'fuel.level')
329
+ ?? data_get($payload, 'fuelLevel')
330
+ ?? data_get($payload, 'vehicleFuel.level')
331
+ ?? null;
332
+ }
333
+ }
@@ -392,7 +392,7 @@ class Utils extends FleetbaseUtils
392
392
  return null;
393
393
  }
394
394
 
395
- if (preg_match("/POINT\\(\\s*([-+]?\\d*\\.?\\d+)\\s+([-+]?\\d*\\.?\\d+)\\s*\\)/i", $expressionValue, $matches)) {
395
+ if (preg_match('/POINT\\(\\s*([-+]?\\d*\\.?\\d+)\\s+([-+]?\\d*\\.?\\d+)\\s*\\)/i', $expressionValue, $matches)) {
396
396
  return sprintf('POINT(%s %s)', $matches[1], $matches[2]);
397
397
  }
398
398
 
@@ -0,0 +1,19 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Tracking\Contracts;
4
+
5
+ use Fleetbase\FleetOps\Tracking\TrackingContext;
6
+ use Fleetbase\FleetOps\Tracking\TrackingOptions;
7
+ use Fleetbase\FleetOps\Tracking\TrackingProviderCapabilities;
8
+ use Fleetbase\FleetOps\Tracking\TrackingProviderResult;
9
+
10
+ interface TrackingProviderInterface
11
+ {
12
+ public function key(): string;
13
+
14
+ public function capabilities(): TrackingProviderCapabilities;
15
+
16
+ public function canTrack(TrackingContext $context): bool;
17
+
18
+ public function track(TrackingContext $context, TrackingOptions $options): TrackingProviderResult;
19
+ }