@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
@@ -0,0 +1,35 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Jobs;
4
+
5
+ use Fleetbase\FleetOps\Events\OrderReady;
6
+ use Fleetbase\FleetOps\Models\Order;
7
+ use Illuminate\Bus\Queueable;
8
+ use Illuminate\Contracts\Queue\ShouldQueue;
9
+ use Illuminate\Foundation\Bus\Dispatchable;
10
+ use Illuminate\Queue\InteractsWithQueue;
11
+ use Illuminate\Queue\SerializesModels;
12
+
13
+ class FinalizeInternalOrderCreation implements ShouldQueue
14
+ {
15
+ use Dispatchable;
16
+ use InteractsWithQueue;
17
+ use Queueable;
18
+ use SerializesModels;
19
+
20
+ public function __construct(public string $orderUuid)
21
+ {
22
+ }
23
+
24
+ public function handle(): void
25
+ {
26
+ $order = Order::where('uuid', $this->orderUuid)->first();
27
+ if (!$order) {
28
+ return;
29
+ }
30
+
31
+ $order->notifyDriverAssigned();
32
+
33
+ event(new OrderReady($order));
34
+ }
35
+ }
@@ -0,0 +1,47 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Jobs;
4
+
5
+ use Fleetbase\FleetOps\Models\Driver;
6
+ use Fleetbase\FleetOps\Models\Order;
7
+ use Illuminate\Bus\Queueable;
8
+ use Illuminate\Contracts\Queue\ShouldQueue;
9
+ use Illuminate\Foundation\Bus\Dispatchable;
10
+ use Illuminate\Queue\InteractsWithQueue;
11
+ use Illuminate\Queue\SerializesModels;
12
+
13
+ class NotifyBulkAssignedDriver implements ShouldQueue
14
+ {
15
+ use Dispatchable;
16
+ use InteractsWithQueue;
17
+ use Queueable;
18
+ use SerializesModels;
19
+
20
+ public function __construct(public array $orderUuids, public string $driverUuid)
21
+ {
22
+ }
23
+
24
+ public function handle(): void
25
+ {
26
+ $driver = Driver::where('uuid', $this->driverUuid)->first();
27
+ if (!$driver) {
28
+ return;
29
+ }
30
+
31
+ Order::whereIn('uuid', $this->orderUuids)
32
+ ->cursor()
33
+ ->each(function (Order $order) use ($driver): void {
34
+ $order->setRelation('driverAssigned', $driver);
35
+ $order->driver_assigned_uuid = $driver->uuid;
36
+
37
+ try {
38
+ $order->notifyDriverAssigned();
39
+ } catch (\Throwable $e) {
40
+ logger()->warning(
41
+ 'Failed notifying driver on order ' . $order->uuid,
42
+ ['error' => $e->getMessage()]
43
+ );
44
+ }
45
+ });
46
+ }
47
+ }
@@ -10,6 +10,7 @@ use Illuminate\Contracts\Queue\ShouldQueue;
10
10
  use Illuminate\Foundation\Bus\Dispatchable;
11
11
  use Illuminate\Queue\InteractsWithQueue;
12
12
  use Illuminate\Queue\SerializesModels;
13
+ use Illuminate\Support\Facades\Cache;
13
14
  use Illuminate\Support\Facades\Log;
14
15
 
15
16
  /**
@@ -27,7 +28,7 @@ class SyncTelematicDevicesJob implements ShouldQueue
27
28
  public Telematic $telematic;
28
29
  public array $options;
29
30
  public string $jobId;
30
- public int $tries = 3;
31
+ public int $tries = 1;
31
32
  public int $timeout = 300;
32
33
 
33
34
  /**
@@ -46,141 +47,186 @@ class SyncTelematicDevicesJob implements ShouldQueue
46
47
  public function handle(TelematicProviderRegistry $registry, TelematicService $service): void
47
48
  {
48
49
  $correlationId = \Illuminate\Support\Str::uuid()->toString();
50
+ $lockKey = 'fleetops:sync-telematic-devices:' . $this->telematic->uuid;
51
+ $lock = Cache::lock($lockKey, $this->timeout + 60);
49
52
 
50
- Log::info('Device discovery started', [
51
- 'correlation_id' => $correlationId,
52
- 'telematic_uuid' => $this->telematic->uuid,
53
- 'provider' => $this->telematic->provider,
54
- ]);
53
+ if (!$lock->get()) {
54
+ Log::info('Device discovery skipped because another sync is already running', [
55
+ 'correlation_id' => $correlationId,
56
+ 'telematic_uuid' => $this->telematic->uuid,
57
+ 'provider' => $this->telematic->provider,
58
+ 'lock_key' => $lockKey,
59
+ ]);
60
+
61
+ $this->telematic->meta = array_merge($this->telematic->meta ?? [], [
62
+ 'last_sync_job_id' => $this->jobId,
63
+ 'last_sync_result' => 'skipped',
64
+ 'last_sync_skipped_reason' => 'sync_already_running',
65
+ 'last_sync_skipped_at' => now()->toDateTimeString(),
66
+ ]);
67
+ $this->telematic->save();
55
68
 
56
- $cursor = null;
57
- $totalFetched = 0;
58
- $totalLinked = 0;
59
- $totalSkipped = 0;
60
- $pageCount = 0;
61
- $lastProviderAllCount = null;
62
- $lastProviderFiltersCount = null;
69
+ return;
70
+ }
63
71
 
64
72
  try {
65
- $provider = $registry->resolve($this->telematic->provider);
66
- $provider->connect($this->telematic);
67
-
68
- do {
69
- $response = $provider->fetchDevices([
70
- 'limit' => $this->options['limit'] ?? null,
71
- 'cursor' => $cursor,
72
- 'filters' => $this->options['filters'] ?? [],
73
- ]);
73
+ Log::info('Device discovery started', [
74
+ 'correlation_id' => $correlationId,
75
+ 'telematic_uuid' => $this->telematic->uuid,
76
+ 'provider' => $this->telematic->provider,
77
+ ]);
74
78
 
75
- $devices = $response['devices'] ?? [];
76
- $pagination = $response['pagination'] ?? [];
77
- $pageCount++;
78
- $totalFetched += count($devices);
79
-
80
- if (isset($pagination['allCount'])) {
81
- $lastProviderAllCount = $pagination['allCount'];
82
- }
83
-
84
- if (isset($pagination['filtersCount'])) {
85
- $lastProviderFiltersCount = $pagination['filtersCount'];
86
- }
87
-
88
- Log::info('Device discovery page fetched', [
89
- 'correlation_id' => $correlationId,
90
- 'telematic_uuid' => $this->telematic->uuid,
91
- 'provider' => $this->telematic->provider,
92
- 'page' => $pageCount,
93
- 'cursor' => $cursor,
94
- 'limit' => $pagination['limit'] ?? ($this->options['limit'] ?? null),
95
- 'result_count' => count($devices),
96
- 'all_count' => $lastProviderAllCount,
97
- 'filters_count' => $lastProviderFiltersCount,
98
- 'next_cursor' => $response['next_cursor'] ?? null,
99
- 'has_more' => $response['has_more'] ?? false,
100
- ]);
79
+ $cursor = null;
80
+ $totalFetched = 0;
81
+ $totalLinked = 0;
82
+ $totalEvents = 0;
83
+ $totalSensors = 0;
84
+ $totalSkipped = 0;
85
+ $pageCount = 0;
86
+ $lastProviderAllCount = null;
87
+ $lastProviderFiltersCount = null;
88
+
89
+ try {
90
+ $provider = $registry->resolve($this->telematic->provider);
91
+ $provider->connect($this->telematic);
92
+
93
+ do {
94
+ $response = $provider->fetchDevices([
95
+ 'limit' => $this->options['limit'] ?? null,
96
+ 'cursor' => $cursor,
97
+ 'filters' => $this->options['filters'] ?? [],
98
+ ]);
99
+
100
+ $devices = $response['devices'] ?? [];
101
+ $pagination = $response['pagination'] ?? [];
102
+ $pageCount++;
103
+ $totalFetched += count($devices);
104
+
105
+ if (isset($pagination['allCount'])) {
106
+ $lastProviderAllCount = $pagination['allCount'];
107
+ }
101
108
 
102
- foreach ($devices as $devicePayload) {
103
- $normalizedDevice = $provider->normalizeDevice($devicePayload);
104
- try {
105
- $service->linkDevice($this->telematic, $normalizedDevice);
106
- $totalLinked++;
107
- } catch (\Illuminate\Validation\ValidationException $e) {
108
- $totalSkipped++;
109
- Log::warning('Skipping telematics device without provider identity', [
110
- 'correlation_id' => $correlationId,
111
- 'provider' => $this->telematic->provider,
112
- 'provider_unit_id' => $devicePayload['_id'] ?? $devicePayload['id'] ?? null,
113
- 'device_id' => $normalizedDevice['device_id'] ?? null,
114
- 'name' => $normalizedDevice['name'] ?? $devicePayload['name'] ?? null,
115
- 'imei' => $devicePayload['imei'] ?? null,
116
- ]);
109
+ if (isset($pagination['filtersCount'])) {
110
+ $lastProviderFiltersCount = $pagination['filtersCount'];
117
111
  }
118
- }
119
112
 
120
- $cursor = $response['next_cursor'] ?? null;
113
+ Log::info('Device discovery page fetched', [
114
+ 'correlation_id' => $correlationId,
115
+ 'telematic_uuid' => $this->telematic->uuid,
116
+ 'provider' => $this->telematic->provider,
117
+ 'page' => $pageCount,
118
+ 'cursor' => $cursor,
119
+ 'limit' => $pagination['limit'] ?? ($this->options['limit'] ?? null),
120
+ 'result_count' => count($devices),
121
+ 'all_count' => $lastProviderAllCount,
122
+ 'filters_count' => $lastProviderFiltersCount,
123
+ 'next_cursor' => $response['next_cursor'] ?? null,
124
+ 'has_more' => $response['has_more'] ?? false,
125
+ ]);
126
+
127
+ foreach ($devices as $devicePayload) {
128
+ $normalizedDevice = $provider->normalizeDevice($devicePayload);
129
+ try {
130
+ $result = $service->ingestDeviceSnapshot($this->telematic, $provider, $devicePayload);
131
+ $totalLinked++;
132
+ if ($result['event']) {
133
+ $totalEvents++;
134
+ }
135
+ $totalSensors += $result['sensors'] ?? 0;
136
+ } catch (\Illuminate\Validation\ValidationException $e) {
137
+ $totalSkipped++;
138
+ Log::warning('Skipping telematics device without provider identity', [
139
+ 'correlation_id' => $correlationId,
140
+ 'provider' => $this->telematic->provider,
141
+ 'provider_unit_id' => $devicePayload['_id'] ?? $devicePayload['id'] ?? null,
142
+ 'device_id' => $normalizedDevice['device_id'] ?? null,
143
+ 'name' => $normalizedDevice['name'] ?? $devicePayload['name'] ?? null,
144
+ 'imei' => $devicePayload['imei'] ?? null,
145
+ ]);
146
+ }
147
+ }
121
148
 
122
- Log::info('Device discovery progress', [
123
- 'correlation_id' => $correlationId,
124
- 'fetched' => $totalFetched,
125
- 'linked' => $totalLinked,
126
- 'skipped' => $totalSkipped,
127
- 'has_more' => $response['has_more'] ?? false,
149
+ $cursor = $response['next_cursor'] ?? null;
150
+
151
+ Log::info('Device discovery progress', [
152
+ 'correlation_id' => $correlationId,
153
+ 'fetched' => $totalFetched,
154
+ 'linked' => $totalLinked,
155
+ 'events' => $totalEvents,
156
+ 'sensors' => $totalSensors,
157
+ 'skipped' => $totalSkipped,
158
+ 'has_more' => $response['has_more'] ?? false,
159
+ ]);
160
+
161
+ // Broadcast progress (TODO: implement WebSocket broadcasting)
162
+ } while (($response['has_more'] ?? false) && $cursor);
163
+
164
+ Log::info('Device discovery completed', [
165
+ 'correlation_id' => $correlationId,
166
+ 'total_fetched' => $totalFetched,
167
+ 'total_linked' => $totalLinked,
168
+ 'total_events' => $totalEvents,
169
+ 'total_sensors' => $totalSensors,
170
+ 'total_skipped' => $totalSkipped,
171
+ 'page_count' => $pageCount,
172
+ 'provider_all_count' => $lastProviderAllCount,
173
+ 'provider_filter_count' => $lastProviderFiltersCount,
128
174
  ]);
129
175
 
130
- // Broadcast progress (TODO: implement WebSocket broadcasting)
131
- } while (($response['has_more'] ?? false) && $cursor);
132
-
133
- Log::info('Device discovery completed', [
134
- 'correlation_id' => $correlationId,
135
- 'total_fetched' => $totalFetched,
136
- 'total_linked' => $totalLinked,
137
- 'total_skipped' => $totalSkipped,
138
- 'page_count' => $pageCount,
139
- 'provider_all_count' => $lastProviderAllCount,
140
- 'provider_filter_count' => $lastProviderFiltersCount,
141
- ]);
142
-
143
- $this->telematic->status = 'active';
144
- $this->telematic->meta = array_merge($this->telematic->meta ?? [], [
145
- 'last_sync_job_id' => $this->jobId,
146
- 'last_sync_completed_at' => now()->toDateTimeString(),
147
- 'last_sync_result' => 'success',
148
- 'last_sync_total' => $totalLinked,
149
- 'last_sync_fetched_total' => $totalFetched,
150
- 'last_sync_linked_total' => $totalLinked,
151
- 'last_sync_skipped_total' => $totalSkipped,
152
- 'last_sync_page_count' => $pageCount,
153
- 'last_sync_provider_total' => $lastProviderFiltersCount ?? $lastProviderAllCount,
154
- 'last_sync_provider_all_count' => $lastProviderAllCount,
155
- 'last_sync_provider_filters_count' => $lastProviderFiltersCount,
156
- 'last_sync_error' => null,
157
- ]);
158
- $this->telematic->save();
159
- } catch (\Exception $e) {
160
- Log::error('Device discovery failed', [
161
- 'correlation_id' => $correlationId,
162
- 'error' => $e->getMessage(),
163
- 'exception' => get_class($e),
164
- ]);
176
+ $this->telematic->status = 'active';
177
+ $this->telematic->meta = array_merge($this->telematic->meta ?? [], [
178
+ 'last_sync_job_id' => $this->jobId,
179
+ 'last_sync_completed_at' => now()->toDateTimeString(),
180
+ 'last_sync_result' => 'success',
181
+ 'last_sync_total' => $totalLinked,
182
+ 'last_sync_fetched_total' => $totalFetched,
183
+ 'last_sync_linked_total' => $totalLinked,
184
+ 'last_sync_events_total' => $totalEvents,
185
+ 'last_sync_sensors_total' => $totalSensors,
186
+ 'last_sync_skipped_total' => $totalSkipped,
187
+ 'last_sync_page_count' => $pageCount,
188
+ 'last_sync_provider_total' => $lastProviderFiltersCount ?? $lastProviderAllCount,
189
+ 'last_sync_provider_all_count' => $lastProviderAllCount,
190
+ 'last_sync_provider_filters_count' => $lastProviderFiltersCount,
191
+ 'last_sync_error' => null,
192
+ 'last_sync_error_context' => null,
193
+ ]);
194
+ $this->telematic->save();
195
+ } catch (\Exception $e) {
196
+ $failureContext = method_exists($e, 'context') ? $e->context() : [];
197
+ $failureMessage = $this->safeSyncErrorMessage($e);
198
+
199
+ Log::error('Device discovery failed', [
200
+ 'correlation_id' => $correlationId,
201
+ 'error' => $failureMessage,
202
+ 'exception' => get_class($e),
203
+ 'provider_context' => $failureContext,
204
+ ]);
165
205
 
166
- $this->telematic->status = 'error';
167
- $this->telematic->meta = array_merge($this->telematic->meta ?? [], [
168
- 'last_sync_job_id' => $this->jobId,
169
- 'last_sync_result' => 'failed',
170
- 'last_sync_fetched_total' => $totalFetched,
171
- 'last_sync_linked_total' => $totalLinked,
172
- 'last_sync_skipped_total' => $totalSkipped,
173
- 'last_sync_page_count' => $pageCount,
174
- 'last_sync_provider_total' => $lastProviderFiltersCount ?? $lastProviderAllCount,
175
- 'last_sync_provider_all_count' => $lastProviderAllCount,
176
- 'last_sync_provider_filters_count' => $lastProviderFiltersCount,
177
- 'last_sync_error' => 'Device sync failed. Review the provider connection and server logs, then try again.',
178
- 'last_sync_error_type' => class_basename($e),
179
- 'last_sync_failed_at' => now()->toDateTimeString(),
180
- ]);
181
- $this->telematic->save();
206
+ $this->telematic->status = 'error';
207
+ $this->telematic->meta = array_merge($this->telematic->meta ?? [], [
208
+ 'last_sync_job_id' => $this->jobId,
209
+ 'last_sync_result' => 'failed',
210
+ 'last_sync_fetched_total' => $totalFetched,
211
+ 'last_sync_linked_total' => $totalLinked,
212
+ 'last_sync_events_total' => $totalEvents,
213
+ 'last_sync_sensors_total' => $totalSensors,
214
+ 'last_sync_skipped_total' => $totalSkipped,
215
+ 'last_sync_page_count' => $pageCount,
216
+ 'last_sync_provider_total' => $lastProviderFiltersCount ?? $lastProviderAllCount,
217
+ 'last_sync_provider_all_count' => $lastProviderAllCount,
218
+ 'last_sync_provider_filters_count' => $lastProviderFiltersCount,
219
+ 'last_sync_error' => $failureMessage,
220
+ 'last_sync_error_type' => class_basename($e),
221
+ 'last_sync_error_context' => $failureContext,
222
+ 'last_sync_failed_at' => now()->toDateTimeString(),
223
+ ]);
224
+ $this->telematic->save();
182
225
 
183
- throw $e;
226
+ throw $e;
227
+ }
228
+ } finally {
229
+ $lock->release();
184
230
  }
185
231
  }
186
232
 
@@ -191,4 +237,15 @@ class SyncTelematicDevicesJob implements ShouldQueue
191
237
  {
192
238
  return $this->jobId;
193
239
  }
240
+
241
+ protected function safeSyncErrorMessage(\Throwable $e): string
242
+ {
243
+ $message = $e->getMessage();
244
+
245
+ if (!$message || preg_match('/(token=|password|client_secret|authorization|bearer\s+[a-z0-9._-]+)/i', $message)) {
246
+ return 'Device sync failed. Review the provider connection and safe sync metadata, then try again.';
247
+ }
248
+
249
+ return $message;
250
+ }
194
251
  }
@@ -74,7 +74,23 @@ class Device extends Model
74
74
  *
75
75
  * @var array
76
76
  */
77
- protected $filterParams = ['status', 'attachment_state', 'warranty_uuid', 'attachable_type', 'attachable_uuid', 'telematic', 'telematic_uuid', 'provider'];
77
+ protected $filterParams = [
78
+ 'status',
79
+ 'attachment_state',
80
+ 'vehicle',
81
+ 'connection_status',
82
+ 'device_id',
83
+ 'type',
84
+ 'serial_number',
85
+ 'last_online_at',
86
+ 'updated_at',
87
+ 'warranty_uuid',
88
+ 'attachable_type',
89
+ 'attachable_uuid',
90
+ 'telematic',
91
+ 'telematic_uuid',
92
+ 'provider',
93
+ ];
78
94
 
79
95
  /**
80
96
  * The attributes that are mass assignable.
@@ -65,7 +65,7 @@ class DeviceEvent extends Model
65
65
  *
66
66
  * @var array
67
67
  */
68
- protected $filterParams = ['event_type', 'severity', 'device_uuid', 'provider', 'code'];
68
+ protected $filterParams = ['event_type', 'severity', 'device_uuid', 'provider', 'code', 'processed', 'occurred_at', 'created_at', 'updated_at', 'telematic'];
69
69
 
70
70
  /**
71
71
  * The attributes that are mass assignable.
@@ -103,6 +103,15 @@ class DeviceEvent extends Model
103
103
  */
104
104
  protected $appends = [
105
105
  'device_name',
106
+ 'device_id',
107
+ 'device_imei',
108
+ 'device_serial_number',
109
+ 'device_connection_status',
110
+ 'device_status',
111
+ 'device_photo_url',
112
+ 'telematic_uuid',
113
+ 'telematic_name',
114
+ 'provider_descriptor',
106
115
  'is_processed',
107
116
  'age_minutes',
108
117
  'processing_delay_minutes',
@@ -208,6 +217,78 @@ class DeviceEvent extends Model
208
217
  return $this->device?->name;
209
218
  }
210
219
 
220
+ /**
221
+ * Get the provider device identifier.
222
+ */
223
+ public function getDeviceIdAttribute(): ?string
224
+ {
225
+ return $this->device?->device_id ?? $this->ident;
226
+ }
227
+
228
+ /**
229
+ * Get the device IMEI.
230
+ */
231
+ public function getDeviceImeiAttribute(): ?string
232
+ {
233
+ return $this->device?->imei;
234
+ }
235
+
236
+ /**
237
+ * Get the device serial number.
238
+ */
239
+ public function getDeviceSerialNumberAttribute(): ?string
240
+ {
241
+ return $this->device?->serial_number;
242
+ }
243
+
244
+ /**
245
+ * Get the device connection status.
246
+ */
247
+ public function getDeviceConnectionStatusAttribute(): ?string
248
+ {
249
+ return $this->device?->connection_status;
250
+ }
251
+
252
+ /**
253
+ * Get the device operational status.
254
+ */
255
+ public function getDeviceStatusAttribute(): ?string
256
+ {
257
+ return $this->device?->status;
258
+ }
259
+
260
+ /**
261
+ * Get the device photo URL.
262
+ */
263
+ public function getDevicePhotoUrlAttribute(): ?string
264
+ {
265
+ return $this->device?->photo_url;
266
+ }
267
+
268
+ /**
269
+ * Get the related telematic UUID.
270
+ */
271
+ public function getTelematicUuidAttribute(): ?string
272
+ {
273
+ return $this->device?->telematic_uuid;
274
+ }
275
+
276
+ /**
277
+ * Get the related telematic name.
278
+ */
279
+ public function getTelematicNameAttribute(): ?string
280
+ {
281
+ return $this->device?->telematic?->name;
282
+ }
283
+
284
+ /**
285
+ * Get the related telematic provider descriptor.
286
+ */
287
+ public function getProviderDescriptorAttribute(): array
288
+ {
289
+ return $this->device?->telematic?->provider_descriptor ?? [];
290
+ }
291
+
211
292
  /**
212
293
  * Check if the event has been processed.
213
294
  */
@@ -32,6 +32,7 @@ use Illuminate\Database\Eloquent\Relations\MorphMany;
32
32
  use Illuminate\Database\Eloquent\Relations\MorphOne;
33
33
  use Illuminate\Notifications\Notifiable;
34
34
  use Illuminate\Support\Arr;
35
+ use Illuminate\Support\Carbon;
35
36
  use Illuminate\Support\Collection;
36
37
  use Illuminate\Support\Facades\DB;
37
38
  use Illuminate\Support\Str;
@@ -235,6 +236,21 @@ class Driver extends Model
235
236
  static::addGlobalScope(new DriverScope());
236
237
  }
237
238
 
239
+ public function setLicenseExpiryAttribute($value): void
240
+ {
241
+ if (empty($value)) {
242
+ if ($this->exists && !empty($this->getOriginal('license_expiry'))) {
243
+ return;
244
+ }
245
+
246
+ $this->attributes['license_expiry'] = null;
247
+
248
+ return;
249
+ }
250
+
251
+ $this->attributes['license_expiry'] = Carbon::parse($value)->toDateString();
252
+ }
253
+
238
254
  /**
239
255
  * @return BelongsTo
240
256
  */