@fleetbase/fleetops-engine 0.6.53 → 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 (212) 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/details.hbs +106 -135
  19. package/addon/components/device/details.js +174 -1
  20. package/addon/components/device/form.hbs +2 -2
  21. package/addon/components/device/form.js +7 -0
  22. package/addon/components/device/panel-header.hbs +46 -21
  23. package/addon/components/device/panel-header.js +49 -1
  24. package/addon/components/device/panel-tabs/events.hbs +30 -0
  25. package/addon/components/device/panel-tabs/events.js +128 -0
  26. package/addon/components/device/panel-tabs/sensors.hbs +30 -0
  27. package/addon/components/device/panel-tabs/sensors.js +93 -0
  28. package/addon/components/device/panel-tabs/vehicle.hbs +68 -0
  29. package/addon/components/device/panel-tabs/vehicle.js +90 -0
  30. package/addon/components/equipment/form.js +8 -1
  31. package/addon/components/layout/fleet-ops-sidebar/operations-monitor.js +76 -11
  32. package/addon/components/layout/fleet-ops-sidebar.hbs +1 -1
  33. package/addon/components/layout/fleet-ops-sidebar.js +101 -17
  34. package/addon/components/map/drawer/device-event-listing.js +2 -0
  35. package/addon/components/map/drawer/driver-listing.js +2 -0
  36. package/addon/components/map/drawer/place-listing.js +2 -0
  37. package/addon/components/map/drawer/position-listing.hbs +11 -6
  38. package/addon/components/map/drawer/position-listing.js +17 -4
  39. package/addon/components/map/drawer/vehicle-listing.js +2 -0
  40. package/addon/components/order/form/details.hbs +1 -43
  41. package/addon/components/order/form/details.js +18 -53
  42. package/addon/components/order/form/orchestrator-constraints.hbs +40 -0
  43. package/addon/components/order/form/orchestrator-constraints.js +61 -0
  44. package/addon/components/order/form/payload.js +48 -2
  45. package/addon/components/order/form/route.js +15 -0
  46. package/addon/components/order/form/service-rate.hbs +62 -11
  47. package/addon/components/order/form/service-rate.js +124 -2
  48. package/addon/components/order/form.hbs +2 -0
  49. package/addon/components/sensor/panel-header.hbs +47 -15
  50. package/addon/components/sensor/panel-header.js +59 -1
  51. package/addon/components/table/cell/dropdown/action-item.hbs +31 -0
  52. package/addon/components/table/cell/dropdown/action-item.js +62 -0
  53. package/addon/components/table/cell/dropdown.hbs +28 -0
  54. package/addon/components/table/cell/dropdown.js +91 -0
  55. package/addon/components/telematic/details.hbs +7 -1
  56. package/addon/components/telematic/form.js +16 -8
  57. package/addon/components/vendor/panel-header.hbs +82 -0
  58. package/addon/components/vendor/panel-header.js +48 -0
  59. package/addon/components/work-order/details.hbs +5 -1
  60. package/addon/components/work-order/form.hbs +26 -4
  61. package/addon/components/work-order/form.js +5 -1
  62. package/addon/controllers/connectivity/devices/index/details/events.js +244 -1
  63. package/addon/controllers/connectivity/devices/index/details/sensors.js +199 -0
  64. package/addon/controllers/connectivity/devices/index/details/vehicle.js +122 -0
  65. package/addon/controllers/connectivity/devices/index/details.js +12 -0
  66. package/addon/controllers/connectivity/devices/index.js +144 -129
  67. package/addon/controllers/connectivity/events/details.js +3 -0
  68. package/addon/controllers/connectivity/events/index.js +187 -11
  69. package/addon/controllers/connectivity/sensors/index.js +156 -14
  70. package/addon/controllers/connectivity/telematics/details/attachments.js +220 -54
  71. package/addon/controllers/connectivity/telematics/details/devices.js +170 -129
  72. package/addon/controllers/connectivity/telematics/details/events.js +39 -11
  73. package/addon/controllers/connectivity/telematics/details/sensors.js +39 -19
  74. package/addon/controllers/maintenance/equipment/index.js +1 -2
  75. package/addon/controllers/maintenance/parts/index.js +1 -2
  76. package/addon/controllers/maintenance/work-orders/index.js +18 -1
  77. package/addon/controllers/management/drivers/index.js +82 -42
  78. package/addon/controllers/management/fuel-reports/index.js +63 -4
  79. package/addon/controllers/management/issues/index.js +63 -4
  80. package/addon/controllers/management/vehicles/index.js +33 -6
  81. package/addon/routes/connectivity/devices/index/details/events.js +37 -1
  82. package/addon/routes/connectivity/devices/index/details/sensors.js +35 -0
  83. package/addon/routes/connectivity/devices/index/details/vehicle.js +29 -0
  84. package/addon/routes/connectivity/devices/index.js +7 -0
  85. package/addon/routes/connectivity/events/details.js +3 -0
  86. package/addon/routes/connectivity/events/index.js +6 -2
  87. package/addon/routes/connectivity/sensors/index.js +7 -2
  88. package/addon/routes/connectivity/telematics/details/attachments.js +72 -9
  89. package/addon/routes/connectivity/telematics/details/devices.js +6 -0
  90. package/addon/routes/connectivity/telematics/details/sensors.js +1 -0
  91. package/addon/routes/connectivity/telematics/details.js +1 -1
  92. package/addon/routes/connectivity/telematics/edit.js +1 -1
  93. package/addon/routes/maintenance/work-orders/index.js +1 -0
  94. package/addon/routes.js +2 -0
  95. package/addon/services/device-actions.js +30 -0
  96. package/addon/services/device-event-actions.js +1 -1
  97. package/addon/services/equipment-actions.js +1 -0
  98. package/addon/services/order-creation.js +54 -1
  99. package/addon/services/order-validation.js +3 -2
  100. package/addon/services/sensor-actions.js +3 -0
  101. package/addon/services/service-rate-actions.js +40 -1
  102. package/addon/services/vehicle-actions.js +24 -12
  103. package/addon/services/vendor-actions.js +3 -0
  104. package/addon/styles/fleetops-engine.css +28 -4
  105. package/addon/templates/connectivity/devices/index/details/events.hbs +85 -1
  106. package/addon/templates/connectivity/devices/index/details/sensors.hbs +73 -0
  107. package/addon/templates/connectivity/devices/index/details/vehicle.hbs +147 -0
  108. package/addon/templates/connectivity/telematics/details/attachments.hbs +198 -121
  109. package/addon/templates/connectivity/telematics/details/devices.hbs +4 -1
  110. package/addon/templates/connectivity/telematics/details/events.hbs +4 -1
  111. package/addon/templates/connectivity/telematics/details/sensors.hbs +3 -0
  112. package/addon/templates/management/vehicles/index/edit.hbs +1 -1
  113. package/addon/templates/management/vendors/index/details.hbs +2 -2
  114. package/addon/templates/settings/notifications.hbs +2 -1
  115. package/addon/templates/settings/routing.hbs +1 -0
  116. package/addon/utils/device-table-columns.js +288 -0
  117. package/addon/utils/fleet-ops-options.js +38 -0
  118. package/addon/utils/identity-cell-resource.js +20 -0
  119. package/addon/utils/map-drawer-dropdown-position.js +47 -0
  120. package/addon/utils/trackable-option.js +93 -0
  121. package/app/components/cell/attached-vehicle.js +1 -0
  122. package/app/components/cell/device-identity.js +1 -0
  123. package/app/components/cell/driver-identity.js +1 -0
  124. package/app/components/cell/equipment-identity.js +1 -0
  125. package/app/components/cell/part-identity.js +1 -0
  126. package/app/components/cell/telematic-device.js +1 -0
  127. package/app/components/cell/vehicle-identity.js +1 -0
  128. package/app/components/device/panel-tabs/events.hbs +30 -0
  129. package/app/components/device/panel-tabs/events.js +128 -0
  130. package/app/components/device/panel-tabs/sensors.hbs +30 -0
  131. package/app/components/device/panel-tabs/sensors.js +93 -0
  132. package/app/components/device/panel-tabs/vehicle.hbs +68 -0
  133. package/app/components/device/panel-tabs/vehicle.js +90 -0
  134. package/app/components/order/form/orchestrator-constraints.js +1 -0
  135. package/app/components/table/cell/dropdown/action-item.js +1 -0
  136. package/app/components/table/cell/dropdown.js +1 -0
  137. package/app/components/vendor/panel-header.js +1 -0
  138. package/app/controllers/connectivity/devices/index/details/sensors.js +1 -0
  139. package/app/controllers/connectivity/devices/index/details/vehicle.js +1 -0
  140. package/app/controllers/connectivity/events/{index/details.js → details.js} +1 -1
  141. package/app/routes/connectivity/devices/index/details/sensors.js +1 -0
  142. package/app/routes/connectivity/devices/index/details/vehicle.js +1 -0
  143. package/app/routes/connectivity/events/{index/details.js → details.js} +1 -1
  144. package/app/templates/connectivity/devices/index/details/sensors.js +1 -0
  145. package/app/templates/connectivity/devices/index/details/vehicle.js +1 -0
  146. package/app/templates/connectivity/events/{index/details.js → details.js} +1 -1
  147. package/app/utils/map-drawer-dropdown-position.js +1 -0
  148. package/app/utils/trackable-option.js +1 -0
  149. package/composer.json +1 -1
  150. package/extension.json +1 -1
  151. package/package.json +4 -4
  152. package/server/migrations/2025_08_28_054930_create_work_orders_table.php +1 -0
  153. package/server/migrations/2026_06_18_000001_add_category_to_work_orders_table.php +32 -0
  154. package/server/seeders/Testing/MaintenanceSeeder.php +5 -4
  155. package/server/src/Console/Commands/ProcessMaintenanceTriggers.php +1 -0
  156. package/server/src/Console/Commands/SyncTelematics.php +89 -0
  157. package/server/src/Exceptions/TelematicProviderException.php +13 -2
  158. package/server/src/Http/Controllers/Api/v1/OrderController.php +6 -21
  159. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +55 -1
  160. package/server/src/Http/Controllers/Internal/v1/DeviceEventController.php +17 -6
  161. package/server/src/Http/Controllers/Internal/v1/FleetController.php +9 -0
  162. package/server/src/Http/Controllers/Internal/v1/LiveController.php +178 -1
  163. package/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php +1 -0
  164. package/server/src/Http/Controllers/Internal/v1/OrderController.php +4 -32
  165. package/server/src/Http/Controllers/Internal/v1/SearchController.php +2 -2
  166. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +4 -4
  167. package/server/src/Http/Controllers/Internal/v1/ServiceRateController.php +82 -13
  168. package/server/src/Http/Filter/DeviceEventFilter.php +65 -0
  169. package/server/src/Http/Filter/DeviceFilter.php +80 -0
  170. package/server/src/Http/Filter/SensorFilter.php +125 -0
  171. package/server/src/Http/Filter/VehicleFilter.php +13 -0
  172. package/server/src/Http/Requests/CreateDriverRequest.php +14 -13
  173. package/server/src/Http/Requests/Internal/CreateDriverRequest.php +1 -0
  174. package/server/src/Http/Resources/v1/Driver.php +11 -2
  175. package/server/src/Http/Resources/v1/Index/Vehicle.php +19 -0
  176. package/server/src/Http/Resources/v1/WorkOrder.php +1 -0
  177. package/server/src/Jobs/FinalizeApiOrderCreation.php +47 -0
  178. package/server/src/Jobs/FinalizeInternalOrderCreation.php +35 -0
  179. package/server/src/Jobs/NotifyBulkAssignedDriver.php +47 -0
  180. package/server/src/Jobs/SyncTelematicDevicesJob.php +180 -123
  181. package/server/src/Models/Device.php +17 -1
  182. package/server/src/Models/DeviceEvent.php +82 -1
  183. package/server/src/Models/Driver.php +16 -0
  184. package/server/src/Models/Equipment.php +55 -0
  185. package/server/src/Models/Sensor.php +15 -2
  186. package/server/src/Models/Telematic.php +1 -1
  187. package/server/src/Models/Vehicle.php +5 -1
  188. package/server/src/Models/WorkOrder.php +5 -2
  189. package/server/src/Observers/DriverObserver.php +3 -3
  190. package/server/src/Observers/FleetObserver.php +23 -0
  191. package/server/src/Observers/VehicleObserver.php +3 -3
  192. package/server/src/Providers/FleetOpsServiceProvider.php +2 -0
  193. package/server/src/Support/LiveCacheService.php +1 -1
  194. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +143 -20
  195. package/server/src/Support/Telematics/Providers/FlespiProvider.php +89 -15
  196. package/server/src/Support/Telematics/Providers/GeotabProvider.php +123 -35
  197. package/server/src/Support/Telematics/Providers/SafeeProvider.php +70 -5
  198. package/server/src/Support/Telematics/Providers/SamsaraProvider.php +122 -14
  199. package/server/src/Support/Telematics/TelematicService.php +330 -30
  200. package/server/src/routes.php +1 -1
  201. package/server/tests/DeviceFilterTest.php +45 -0
  202. package/server/tests/DriverVehicleStatusDefaultTest.php +19 -2
  203. package/server/tests/EquipmentAssignmentMorphTypeTest.php +35 -0
  204. package/server/tests/OperationsMonitorEndpointTest.php +38 -0
  205. package/server/tests/OrderQueuedClosureRegressionTest.php +36 -0
  206. package/server/tests/ServiceRateRouteValidationTest.php +101 -0
  207. package/server/tests/TelematicsHardeningTest.php +598 -6
  208. package/server/tests/VehicleDriverAssignmentTest.php +27 -0
  209. package/translations/en-us.yaml +1 -0
  210. package/addon/controllers/connectivity/events/index/details.js +0 -3
  211. package/addon/routes/connectivity/events/index/details.js +0 -3
  212. /package/addon/templates/connectivity/events/{index/details.hbs → details.hbs} +0 -0
@@ -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')
@@ -66,14 +66,27 @@ class Sensor extends Model
66
66
  *
67
67
  * @var array
68
68
  */
69
- protected $searchableColumns = ['name', 'type', 'internal_id', 'unit', 'public_id'];
69
+ protected $searchableColumns = ['name', 'type', 'internal_id', 'serial_number', 'imei', 'unit', 'public_id'];
70
70
 
71
71
  /**
72
72
  * The attributes that can be used for filtering.
73
73
  *
74
74
  * @var array
75
75
  */
76
- protected $filterParams = ['type', 'sensor_type', 'status', 'device_uuid', 'warranty_uuid', 'sensorable_type', 'telematic_uuid'];
76
+ protected $filterParams = [
77
+ 'type',
78
+ 'sensor_type',
79
+ 'status',
80
+ 'device_uuid',
81
+ 'serial_number',
82
+ 'imei',
83
+ 'warranty_uuid',
84
+ 'sensorable_type',
85
+ 'telematic_uuid',
86
+ 'last_reading_at',
87
+ 'created_at',
88
+ 'updated_at',
89
+ ];
77
90
 
78
91
  /**
79
92
  * The attributes that are mass assignable.
@@ -63,7 +63,7 @@ class Telematic extends Model
63
63
  *
64
64
  * @var array
65
65
  */
66
- protected $filterParams = ['provider', 'status', 'warranty_uuid'];
66
+ protected $filterParams = ['public_id', 'provider', 'status', 'warranty_uuid'];
67
67
 
68
68
  /**
69
69
  * The attributes that are mass assignable.
@@ -72,7 +72,7 @@ class Vehicle extends Model
72
72
  *
73
73
  * @var array
74
74
  */
75
- protected $filterParams = ['vendor', 'driver', 'driver_uuid', 'vehicle_make', 'vehicle_model'];
75
+ protected $filterParams = ['vendor', 'driver', 'driver_uuid', 'vehicle_make', 'vehicle_model', 'telematic_uuid'];
76
76
 
77
77
  /**
78
78
  * Relationships to auto load with driver.
@@ -407,6 +407,10 @@ class Vehicle extends Model
407
407
  */
408
408
  public function getDisplayNameAttribute()
409
409
  {
410
+ if (isset($this->name) && strlen($this->name)) {
411
+ return $this->name;
412
+ }
413
+
410
414
  // Initialize an empty array to hold the name segments
411
415
  $nameSegments = [];
412
416
 
@@ -59,14 +59,14 @@ class WorkOrder extends Model
59
59
  *
60
60
  * @var array
61
61
  */
62
- protected $searchableColumns = ['code', 'subject', 'instructions', 'public_id'];
62
+ protected $searchableColumns = ['code', 'subject', 'category', 'instructions', 'public_id'];
63
63
 
64
64
  /**
65
65
  * The attributes that can be used for filtering.
66
66
  *
67
67
  * @var array
68
68
  */
69
- protected $filterParams = ['status', 'priority', 'target_type', 'assignee_type'];
69
+ protected $filterParams = ['category', 'status', 'priority', 'target_type', 'assignee_type'];
70
70
 
71
71
  /**
72
72
  * The attributes that are mass assignable.
@@ -77,6 +77,7 @@ class WorkOrder extends Model
77
77
  'company_uuid',
78
78
  'code',
79
79
  'subject',
80
+ 'category',
80
81
  'status',
81
82
  'priority',
82
83
  'target_type',
@@ -575,6 +576,7 @@ class WorkOrder extends Model
575
576
  $row = array_filter($row);
576
577
 
577
578
  $subject = Utils::or($row, ['subject', 'title', 'name']);
579
+ $category = Utils::or($row, ['category', 'work_order_category', 'work_type', 'type']);
578
580
  $status = Utils::or($row, ['status'], 'open');
579
581
  $priority = Utils::or($row, ['priority'], 'normal');
580
582
  $instructions = Utils::or($row, ['instructions', 'description', 'notes']);
@@ -592,6 +594,7 @@ class WorkOrder extends Model
592
594
  $workOrder = new static([
593
595
  'company_uuid' => session('company'),
594
596
  'subject' => $subject,
597
+ 'category' => $category,
595
598
  'status' => $status,
596
599
  'priority' => $priority,
597
600
  'instructions' => $instructions,
@@ -30,7 +30,7 @@ class DriverObserver
30
30
  */
31
31
  public function created(Driver $driver)
32
32
  {
33
- LiveCacheService::invalidate('drivers');
33
+ LiveCacheService::invalidateMultiple(['drivers', 'operations-monitor']);
34
34
  }
35
35
 
36
36
  /**
@@ -40,7 +40,7 @@ class DriverObserver
40
40
  */
41
41
  public function updated(Driver $driver)
42
42
  {
43
- LiveCacheService::invalidate('drivers');
43
+ LiveCacheService::invalidateMultiple(['drivers', 'operations-monitor']);
44
44
  }
45
45
 
46
46
  /**
@@ -70,6 +70,6 @@ class DriverObserver
70
70
  $user->delete();
71
71
  }
72
72
 
73
- LiveCacheService::invalidate('drivers');
73
+ LiveCacheService::invalidateMultiple(['drivers', 'operations-monitor']);
74
74
  }
75
75
  }
@@ -3,9 +3,30 @@
3
3
  namespace Fleetbase\FleetOps\Observers;
4
4
 
5
5
  use Fleetbase\FleetOps\Models\Fleet;
6
+ use Fleetbase\FleetOps\Support\LiveCacheService;
6
7
 
7
8
  class FleetObserver
8
9
  {
10
+ /**
11
+ * Handle the Fleet "created" event.
12
+ *
13
+ * @return void
14
+ */
15
+ public function created(Fleet $fleet)
16
+ {
17
+ LiveCacheService::invalidate('operations-monitor');
18
+ }
19
+
20
+ /**
21
+ * Handle the Fleet "updated" event.
22
+ *
23
+ * @return void
24
+ */
25
+ public function updated(Fleet $fleet)
26
+ {
27
+ LiveCacheService::invalidate('operations-monitor');
28
+ }
29
+
9
30
  /**
10
31
  * Handle the Driver "deleted" event.
11
32
  *
@@ -15,5 +36,7 @@ class FleetObserver
15
36
  {
16
37
  // If the fleet being deleted is set as parent fleet, remove it as the parent fleet
17
38
  $subFleets = Fleet::where(['parent_fleet_uuid' => $fleet->uuid])->update(['parent_fleet_uuid' => null]);
39
+
40
+ LiveCacheService::invalidate('operations-monitor');
18
41
  }
19
42
  }
@@ -30,7 +30,7 @@ class VehicleObserver
30
30
  }
31
31
  }
32
32
 
33
- LiveCacheService::invalidate('vehicles');
33
+ LiveCacheService::invalidateMultiple(['vehicles', 'operations-monitor']);
34
34
  }
35
35
 
36
36
  /**
@@ -55,7 +55,7 @@ class VehicleObserver
55
55
  }
56
56
  }
57
57
 
58
- LiveCacheService::invalidate('vehicles');
58
+ LiveCacheService::invalidateMultiple(['vehicles', 'operations-monitor']);
59
59
  }
60
60
 
61
61
  /**
@@ -68,6 +68,6 @@ class VehicleObserver
68
68
  // Unassign the deleted vehicle from matching driver/(s)
69
69
  Driver::where(['vehicle_uuid' => $vehicle->uuid])->delete();
70
70
 
71
- LiveCacheService::invalidate('vehicles');
71
+ LiveCacheService::invalidateMultiple(['vehicles', 'operations-monitor']);
72
72
  }
73
73
  }
@@ -69,6 +69,7 @@ class FleetOpsServiceProvider extends CoreServiceProvider
69
69
  \Fleetbase\FleetOps\Console\Commands\ProcessMaintenanceTriggers::class,
70
70
  \Fleetbase\FleetOps\Console\Commands\SendMaintenanceReminders::class,
71
71
  \Fleetbase\FleetOps\Console\Commands\ProcessOperationalAlerts::class,
72
+ \Fleetbase\FleetOps\Console\Commands\SyncTelematics::class,
72
73
  ];
73
74
 
74
75
  /**
@@ -141,6 +142,7 @@ class FleetOpsServiceProvider extends CoreServiceProvider
141
142
  $schedule->command('fleetops:process-maintenance-triggers')->daily()->withoutOverlapping()->storeOutputInDb();
142
143
  $schedule->command('fleetops:send-maintenance-reminders')->daily()->withoutOverlapping()->storeOutputInDb();
143
144
  $schedule->command('fleetops:process-operational-alerts')->everyMinute()->withoutOverlapping()->storeOutputInDb();
145
+ $schedule->command('fleetops:sync-telematics')->everyMinute()->withoutOverlapping()->storeOutputInDb();
144
146
  });
145
147
  $this->registerNotifications();
146
148
  $this->registerExpansionsFrom(__DIR__ . '/../Expansions');
@@ -108,7 +108,7 @@ class LiveCacheService
108
108
  }
109
109
  } else {
110
110
  // Invalidate all endpoints
111
- $endpoints = ['orders', 'routes', 'coordinates', 'drivers', 'vehicles', 'places'];
111
+ $endpoints = ['orders', 'routes', 'coordinates', 'drivers', 'vehicles', 'places', 'operations-monitor'];
112
112
  foreach ($endpoints as $ep) {
113
113
  static::incrementVersion($ep);
114
114
  }
@@ -2,6 +2,9 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Support\Telematics\Providers;
4
4
 
5
+ use Fleetbase\FleetOps\Exceptions\TelematicProviderException;
6
+ use Illuminate\Http\Client\ConnectionException;
7
+ use Illuminate\Http\Client\Response;
5
8
  use Illuminate\Support\Carbon;
6
9
  use Illuminate\Support\Facades\Http;
7
10
  use Illuminate\Support\Facades\Log;
@@ -14,20 +17,23 @@ use Illuminate\Support\Facades\Log;
14
17
  */
15
18
  class AfaqyProvider extends AbstractProvider
16
19
  {
17
- protected string $baseUrl = 'https://api.afaqy.sa';
18
- protected int $requestsPerMinute = 60;
20
+ protected string $baseUrl = 'https://api.afaqy.sa';
21
+ protected int $requestsPerMinute = 60;
22
+ protected int $dataTimeout = 120;
23
+ protected int $connectTimeout = 15;
24
+ protected int $connectionTestTimeout = 30;
25
+ protected int $connectionTestConnectTimeout = 10;
19
26
 
20
27
  protected function prepareAuthentication(): void
21
28
  {
22
29
  $this->baseUrl = rtrim($this->credentials['base_url'] ?? $this->baseUrl, '/');
23
- $token = $this->credentials['token'] ?? $this->authenticate();
30
+ $token = $this->canRefreshToken() ? $this->authenticate() : ($this->credentials['token'] ?? null);
24
31
 
25
- $this->credentials['token'] = $token;
26
- $this->headers = [
27
- 'Accept' => 'application/json',
28
- 'Content-Type' => 'application/json',
29
- 'Authorization' => 'Bearer ' . $token,
30
- ];
32
+ if (!$token) {
33
+ throw new \InvalidArgumentException('AFAQY username/password or token is required.');
34
+ }
35
+
36
+ $this->setToken($token);
31
37
  }
32
38
 
33
39
  public function testConnection(array $credentials): array
@@ -41,7 +47,7 @@ class AfaqyProvider extends AbstractProvider
41
47
  'projection' => ['_id', 'name', 'imei', 'last_update'],
42
48
  'filters' => new \stdClass(),
43
49
  ],
44
- ], true);
50
+ ], true, $this->connectionTestTimeout, $this->connectionTestConnectTimeout);
45
51
 
46
52
  return [
47
53
  'success' => true,
@@ -55,7 +61,7 @@ class AfaqyProvider extends AbstractProvider
55
61
  return [
56
62
  'success' => false,
57
63
  'message' => $e->getMessage(),
58
- 'metadata' => [],
64
+ 'metadata' => method_exists($e, 'context') ? $e->context() : [],
59
65
  ];
60
66
  }
61
67
  }
@@ -145,6 +151,7 @@ class AfaqyProvider extends AbstractProvider
145
151
 
146
152
  return [
147
153
  'device_id' => $payload['_id'] ?? $payload['id'] ?? null,
154
+ 'external_id' => $payload['_id'] ?? $payload['id'] ?? null,
148
155
  'name' => $payload['name'] ?? data_get($payload, 'profile.plate_number') ?? 'Unknown Unit',
149
156
  'provider' => 'afaqy',
150
157
  'model' => data_get($payload, 'profile.model') ?? $payload['device'] ?? null,
@@ -187,12 +194,14 @@ class AfaqyProvider extends AbstractProvider
187
194
  'event_type' => $payload['event'] ?? $payload['event_type'] ?? 'telemetry_update',
188
195
  'message' => $payload['message'] ?? $payload['event'] ?? null,
189
196
  'occurred_at' => $this->parseTimestamp($lastUpdate['dtt'] ?? $lastUpdate['dts'] ?? null),
197
+ 'online' => $payload['active'] ?? null,
190
198
  'location' => [
191
199
  'lat' => $lastUpdate['lat'] ?? data_get($lastUpdate, 'loc.coordinates.1'),
192
200
  'lng' => $lastUpdate['lng'] ?? data_get($lastUpdate, 'loc.coordinates.0'),
193
201
  ],
194
202
  'speed' => $lastUpdate['speed'] ?? $lastUpdate['spd'] ?? null,
195
203
  'heading' => $lastUpdate['angle'] ?? $lastUpdate['ang'] ?? null,
204
+ 'altitude' => $lastUpdate['alt'] ?? null,
196
205
  'odometer' => data_get($payload, 'counters.odometer'),
197
206
  'ignition' => $this->extractIgnition($payload),
198
207
  'fuel_level' => $this->extractFuelLevel($payload),
@@ -271,19 +280,64 @@ class AfaqyProvider extends AbstractProvider
271
280
  ]);
272
281
 
273
282
  if ($response->failed()) {
274
- throw new \RuntimeException('AFAQY authentication failed with status ' . $response->status());
283
+ throw new TelematicProviderException('AFAQY authentication failed with status ' . $response->status(), $this->providerErrorContext('/auth/login', $response, false));
275
284
  }
276
285
 
277
286
  $token = data_get($response->json(), 'data.token');
278
287
 
279
288
  if (!$token) {
280
- throw new \RuntimeException('AFAQY authentication did not return a token.');
289
+ throw new TelematicProviderException('AFAQY authentication did not return a token.', $this->providerErrorContext('/auth/login', $response, false));
281
290
  }
282
291
 
283
292
  return $token;
284
293
  }
285
294
 
286
- protected function afaqyPost(string $endpoint, array $payload = [], bool $tokenInQuery = false): array
295
+ protected function afaqyPost(string $endpoint, array $payload = [], bool $tokenInQuery = false, ?int $timeout = null, ?int $connectTimeout = null): array
296
+ {
297
+ return $this->authenticatedPost($endpoint, $payload, $tokenInQuery, true, $timeout, $connectTimeout);
298
+ }
299
+
300
+ protected function authenticatedPost(string $endpoint, array $payload = [], bool $tokenInQuery = false, bool $allowRetry = true, ?int $timeout = null, ?int $connectTimeout = null): array
301
+ {
302
+ [$url, $body] = $this->buildAuthenticatedRequest($endpoint, $payload, $tokenInQuery);
303
+
304
+ $startedAt = microtime(true);
305
+ $timeout ??= $this->dataTimeout;
306
+ $connectTimeout ??= $this->connectTimeout;
307
+
308
+ try {
309
+ $response = Http::withHeaders($this->headers)
310
+ ->timeout($timeout)
311
+ ->connectTimeout($connectTimeout)
312
+ ->post($url, $body);
313
+ } catch (ConnectionException $e) {
314
+ throw new TelematicProviderException('AFAQY API request timed out while waiting for provider response.', $this->transportErrorContext($endpoint, $payload, $e, !$allowRetry, $startedAt, $timeout, $connectTimeout), previous: $e);
315
+ }
316
+
317
+ if ($this->isTokenRejected($response)) {
318
+ if (!$allowRetry || !$this->canRefreshToken()) {
319
+ $message = $this->canRefreshToken()
320
+ ? 'AFAQY token rejected after refresh with status ' . $response->status()
321
+ : 'AFAQY token rejected and username/password credentials are required to refresh it.';
322
+
323
+ throw new TelematicProviderException($message, $this->providerErrorContext($endpoint, $response, !$allowRetry));
324
+ }
325
+
326
+ Log::warning('AFAQY token rejected; refreshing token and retrying request', $this->providerErrorContext($endpoint, $response, true));
327
+
328
+ $this->refreshToken();
329
+
330
+ return $this->authenticatedPost($endpoint, $payload, $tokenInQuery, false, $timeout, $connectTimeout);
331
+ }
332
+
333
+ if ($response->failed()) {
334
+ throw new TelematicProviderException('AFAQY API request failed with status ' . $response->status(), $this->providerErrorContext($endpoint, $response, !$allowRetry));
335
+ }
336
+
337
+ return $response->json() ?? [];
338
+ }
339
+
340
+ protected function buildAuthenticatedRequest(string $endpoint, array $payload = [], bool $tokenInQuery = false): array
287
341
  {
288
342
  $url = $this->baseUrl . $endpoint;
289
343
  $body = $tokenInQuery ? $payload : array_merge(['token' => $this->credentials['token']], $payload);
@@ -292,15 +346,84 @@ class AfaqyProvider extends AbstractProvider
292
346
  $url .= (str_contains($url, '?') ? '&' : '?') . http_build_query(['token' => $this->credentials['token']]);
293
347
  }
294
348
 
295
- $response = Http::withHeaders($this->headers)
296
- ->timeout(30)
297
- ->post($url, $body);
349
+ return [$url, $body];
350
+ }
298
351
 
299
- if ($response->failed()) {
300
- throw new \RuntimeException('AFAQY API request failed with status ' . $response->status());
352
+ protected function setToken(string $token): void
353
+ {
354
+ $this->credentials['token'] = $token;
355
+ $this->headers = [
356
+ 'Accept' => 'application/json',
357
+ 'Content-Type' => 'application/json',
358
+ 'Authorization' => 'Bearer ' . $token,
359
+ ];
360
+ }
361
+
362
+ protected function refreshToken(): void
363
+ {
364
+ unset($this->credentials['token']);
365
+
366
+ $this->setToken($this->authenticate());
367
+ }
368
+
369
+ protected function canRefreshToken(): bool
370
+ {
371
+ return !empty($this->credentials['username']) && !empty($this->credentials['password']);
372
+ }
373
+
374
+ protected function isTokenRejected(Response $response): bool
375
+ {
376
+ return in_array($response->status(), [401, 403], true);
377
+ }
378
+
379
+ protected function providerErrorContext(string $endpoint, Response $response, bool $retryAttempted): array
380
+ {
381
+ $json = $response->json() ?? [];
382
+ $json = is_array($json) ? $json : [];
383
+
384
+ return array_filter([
385
+ 'provider' => 'afaqy',
386
+ 'endpoint' => $endpoint,
387
+ 'status_code' => $response->status(),
388
+ 'provider_code' => data_get($json, 'code') ?? data_get($json, 'error.code') ?? data_get($json, 'error_code'),
389
+ 'provider_message' => $this->providerErrorMessage($json),
390
+ 'retry_attempted' => $retryAttempted,
391
+ ], fn ($value) => $value !== null);
392
+ }
393
+
394
+ protected function providerErrorMessage(array $json): ?string
395
+ {
396
+ $message = data_get($json, 'message')
397
+ ?? data_get($json, 'error.message')
398
+ ?? data_get($json, 'error_description')
399
+ ?? data_get($json, 'error');
400
+
401
+ return is_scalar($message) ? (string) $message : null;
402
+ }
403
+
404
+ protected function transportErrorContext(string $endpoint, array $payload, ConnectionException $e, bool $retryAttempted, float $startedAt, int $timeout, int $connectTimeout): array
405
+ {
406
+ return array_filter([
407
+ 'provider' => 'afaqy',
408
+ 'endpoint' => $endpoint,
409
+ 'requested_limit' => data_get($payload, 'data.limit'),
410
+ 'requested_offset' => data_get($payload, 'data.offset'),
411
+ 'timeout' => $timeout,
412
+ 'connect_timeout' => $connectTimeout,
413
+ 'elapsed_ms' => (int) ((microtime(true) - $startedAt) * 1000),
414
+ 'bytes_received' => $this->extractBytesReceived($e->getMessage()),
415
+ 'retry_attempted' => $retryAttempted,
416
+ 'transport_error' => 'connection_exception',
417
+ ], fn ($value) => $value !== null);
418
+ }
419
+
420
+ protected function extractBytesReceived(string $message): ?int
421
+ {
422
+ if (preg_match('/with\s+(\d+)\s+bytes\s+received/i', $message, $matches)) {
423
+ return (int) $matches[1];
301
424
  }
302
425
 
303
- return $response->json() ?? [];
426
+ return null;
304
427
  }
305
428
 
306
429
  protected function compactLastUpdate(array $lastUpdate): array