@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
@@ -106,6 +106,116 @@ return [
106
106
  'pagination' => 'cursor',
107
107
  ],
108
108
  ],
109
+
110
+ [
111
+ 'key' => 'afaqy',
112
+ 'label' => 'AFAQY',
113
+ 'type' => 'native',
114
+ 'driver_class' => \Fleetbase\FleetOps\Support\Telematics\Providers\AfaqyProvider::class,
115
+ 'icon' => 'https://api.afaqy.sa/docs/img/favicon.ico',
116
+ 'description' => 'AFAQY AVL REST integration for unit discovery, live position, speed, heading, odometer, fuel, and signal data.',
117
+ 'docs_url' => 'https://api.afaqy.sa/docs/',
118
+ 'required_fields' => [
119
+ [
120
+ 'name' => 'base_url',
121
+ 'label' => 'Base URL',
122
+ 'type' => 'text',
123
+ 'placeholder' => 'https://api.afaqy.sa',
124
+ 'required' => false,
125
+ 'validation' => 'nullable|url',
126
+ ],
127
+ [
128
+ 'name' => 'username',
129
+ 'label' => 'Username',
130
+ 'type' => 'text',
131
+ 'placeholder' => 'Enter your AFAQY username',
132
+ 'required' => false,
133
+ 'validation' => 'required_without:token|string',
134
+ ],
135
+ [
136
+ 'name' => 'password',
137
+ 'label' => 'Password',
138
+ 'type' => 'password',
139
+ 'placeholder' => 'Enter your AFAQY password',
140
+ 'required' => false,
141
+ 'validation' => 'required_without:token|string',
142
+ ],
143
+ [
144
+ 'name' => 'token',
145
+ 'label' => 'Token',
146
+ 'type' => 'password',
147
+ 'placeholder' => 'Use an existing AFAQY token',
148
+ 'required' => false,
149
+ 'validation' => 'required_without:username|string',
150
+ ],
151
+ ],
152
+ 'supports_webhooks' => false,
153
+ 'supports_discovery' => true,
154
+ 'metadata' => [
155
+ 'rate_limit' => 60,
156
+ 'pagination' => 'offset',
157
+ 'capabilities' => ['tracking', 'speed', 'heading', 'odometer', 'fuel_level', 'ignition'],
158
+ ],
159
+ ],
160
+
161
+ [
162
+ 'key' => 'safee',
163
+ 'label' => 'Safee Tracking',
164
+ 'type' => 'native',
165
+ 'driver_class' => \Fleetbase\FleetOps\Support\Telematics\Providers\SafeeProvider::class,
166
+ 'description' => 'Safee Tracking REST integration for vehicle discovery, last state, live positions, odometer, fuel, and sensor data.',
167
+ 'required_fields' => [
168
+ [
169
+ 'name' => 'server_uri',
170
+ 'label' => 'Server URI',
171
+ 'type' => 'text',
172
+ 'placeholder' => 'https://tracking.example.com',
173
+ 'required' => true,
174
+ 'validation' => 'required|url',
175
+ ],
176
+ [
177
+ 'name' => 'realm_id',
178
+ 'label' => 'Realm ID',
179
+ 'type' => 'text',
180
+ 'placeholder' => 'Enter Safee realm ID',
181
+ 'required' => true,
182
+ ],
183
+ [
184
+ 'name' => 'client_id',
185
+ 'label' => 'Client ID',
186
+ 'type' => 'text',
187
+ 'placeholder' => 'Enter Safee client ID',
188
+ 'required' => true,
189
+ ],
190
+ [
191
+ 'name' => 'client_secret',
192
+ 'label' => 'Client Secret',
193
+ 'type' => 'password',
194
+ 'placeholder' => 'Enter Safee client secret',
195
+ 'required' => true,
196
+ ],
197
+ [
198
+ 'name' => 'username',
199
+ 'label' => 'Username',
200
+ 'type' => 'text',
201
+ 'placeholder' => 'Enter Safee username',
202
+ 'required' => true,
203
+ ],
204
+ [
205
+ 'name' => 'password',
206
+ 'label' => 'Password',
207
+ 'type' => 'password',
208
+ 'placeholder' => 'Enter Safee password',
209
+ 'required' => true,
210
+ ],
211
+ ],
212
+ 'supports_webhooks' => false,
213
+ 'supports_discovery' => true,
214
+ 'metadata' => [
215
+ 'rate_limit' => 3000,
216
+ 'pagination' => 'none',
217
+ 'capabilities' => ['tracking', 'speed', 'heading', 'odometer', 'fuel_level', 'ignition'],
218
+ ],
219
+ ],
109
220
  ]
110
221
  ];
111
-
@@ -0,0 +1,86 @@
1
+ <?php
2
+
3
+ use Fleetbase\FleetOps\Models\Contact;
4
+ use Fleetbase\Models\CompanyUser;
5
+ use Illuminate\Database\Migrations\Migration;
6
+ use Illuminate\Support\Facades\DB;
7
+ use Illuminate\Support\Facades\Schema;
8
+
9
+ return new class extends Migration {
10
+ /**
11
+ * Run the migrations.
12
+ *
13
+ * @return void
14
+ */
15
+ public function up()
16
+ {
17
+ Contact::withoutGlobalScopes()
18
+ ->where('type', 'customer')
19
+ ->whereNull('deleted_at')
20
+ ->chunkById(100, function ($contacts) {
21
+ foreach ($contacts as $contact) {
22
+ try {
23
+ $contact->repairCustomerTypeInvariant(true);
24
+ } catch (Throwable $e) {
25
+ // Skip ambiguous or unrecoverable records; runtime invariants handle new writes.
26
+ }
27
+ }
28
+ });
29
+
30
+ if (!$this->canUseCustomerRoleHint()) {
31
+ return;
32
+ }
33
+
34
+ DB::table('contacts')
35
+ ->select('contacts.id')
36
+ ->join('company_users', function ($join) {
37
+ $join->on('company_users.user_uuid', '=', 'contacts.user_uuid');
38
+ $join->on('company_users.company_uuid', '=', 'contacts.company_uuid');
39
+ })
40
+ ->join('model_has_roles', function ($join) {
41
+ $join->on('model_has_roles.model_uuid', '=', 'company_users.uuid');
42
+ $join->where('model_has_roles.model_type', CompanyUser::class);
43
+ })
44
+ ->join('roles', 'roles.id', '=', 'model_has_roles.role_id')
45
+ ->where('roles.name', 'Fleet-Ops Customer')
46
+ ->whereNull('contacts.deleted_at')
47
+ ->where(function ($query) {
48
+ $query->whereNull('contacts.type');
49
+ $query->orWhere('contacts.type', '!=', 'customer');
50
+ })
51
+ ->distinct()
52
+ ->orderBy('contacts.id')
53
+ ->chunk(100, function ($rows) {
54
+ foreach ($rows as $row) {
55
+ $contact = Contact::withoutGlobalScopes()->find($row->id);
56
+ if (!$contact) {
57
+ continue;
58
+ }
59
+
60
+ try {
61
+ $contact->repairCustomerTypeInvariant(true);
62
+ } catch (Throwable $e) {
63
+ // Skip ambiguous or unrecoverable records; role is only a recovery hint.
64
+ }
65
+ }
66
+ });
67
+ }
68
+
69
+ /**
70
+ * Reverse the migrations.
71
+ *
72
+ * @return void
73
+ */
74
+ public function down()
75
+ {
76
+ // Data repair is intentionally not reversible.
77
+ }
78
+
79
+ private function canUseCustomerRoleHint(): bool
80
+ {
81
+ return Schema::hasTable('contacts')
82
+ && Schema::hasTable('company_users')
83
+ && Schema::hasTable('model_has_roles')
84
+ && Schema::hasTable('roles');
85
+ }
86
+ };
@@ -0,0 +1,25 @@
1
+ <?php
2
+
3
+ use Illuminate\Database\Migrations\Migration;
4
+ use Illuminate\Database\Schema\Blueprint;
5
+ use Illuminate\Support\Facades\Schema;
6
+
7
+ return new class extends Migration {
8
+ public function up()
9
+ {
10
+ Schema::table('drivers', function (Blueprint $table) {
11
+ if (!Schema::hasColumn('drivers', 'license_expiry')) {
12
+ $table->date('license_expiry')->nullable()->after('drivers_license_number')->index();
13
+ }
14
+ });
15
+ }
16
+
17
+ public function down()
18
+ {
19
+ Schema::table('drivers', function (Blueprint $table) {
20
+ if (Schema::hasColumn('drivers', 'license_expiry')) {
21
+ $table->dropColumn('license_expiry');
22
+ }
23
+ });
24
+ }
25
+ };
@@ -0,0 +1,183 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Seeders\Testing\Concerns;
4
+
5
+ use Fleetbase\FleetOps\Models\ServiceArea;
6
+ use Fleetbase\FleetOps\Models\Zone;
7
+ use Fleetbase\LaravelMysqlSpatial\Types\LineString;
8
+ use Fleetbase\LaravelMysqlSpatial\Types\MultiPolygon;
9
+ use Fleetbase\LaravelMysqlSpatial\Types\Point;
10
+ use Fleetbase\LaravelMysqlSpatial\Types\Polygon;
11
+ use Fleetbase\Models\Company;
12
+ use Fleetbase\Seeders\Concerns\ResolvesSeedCompany;
13
+ use Illuminate\Database\Eloquent\Model;
14
+ use Illuminate\Database\Eloquent\SoftDeletes;
15
+ use Illuminate\Support\Carbon;
16
+ use Illuminate\Support\Facades\Schema;
17
+ use Illuminate\Support\Str;
18
+
19
+ trait SeedsTestingData
20
+ {
21
+ use ResolvesSeedCompany;
22
+
23
+ protected const SEED_NAME = 'fleetops-testing';
24
+
25
+ protected function resolveCompany(): ?Company
26
+ {
27
+ return $this->resolveSeedCompany();
28
+ }
29
+
30
+ protected function prepareCompany(): ?Company
31
+ {
32
+ $company = $this->resolveCompany();
33
+ if (!$company) {
34
+ $this->command?->error('No company found. Create a Fleetbase company before running FleetOps testing seeders.');
35
+
36
+ return null;
37
+ }
38
+
39
+ session(['company' => $company->uuid]);
40
+
41
+ return $company;
42
+ }
43
+
44
+ protected function fixtureKey(string $seedId): string
45
+ {
46
+ return static::SEED_NAME . ':' . $seedId;
47
+ }
48
+
49
+ protected function meta(string $seedId, array $extra = []): array
50
+ {
51
+ return array_merge([
52
+ 'seed' => static::SEED_NAME,
53
+ 'seed_id' => $seedId,
54
+ ], $extra);
55
+ }
56
+
57
+ protected function timestamp(int $hoursOffset = 0): Carbon
58
+ {
59
+ return Carbon::parse('2026-01-15 08:00:00', 'Asia/Singapore')->addHours($hoursOffset);
60
+ }
61
+
62
+ protected function point(float $lat, float $lng): Point
63
+ {
64
+ return new Point($lat, $lng);
65
+ }
66
+
67
+ protected function polygon(array $coordinates): Polygon
68
+ {
69
+ $points = array_map(fn (array $coordinate) => $this->point($coordinate[0], $coordinate[1]), $coordinates);
70
+ if ($points[0]->getLat() !== end($points)->getLat() || $points[0]->getLng() !== end($points)->getLng()) {
71
+ $points[] = $points[0];
72
+ }
73
+
74
+ return new Polygon([new LineString($points)]);
75
+ }
76
+
77
+ protected function multiPolygon(array $coordinates): MultiPolygon
78
+ {
79
+ return new MultiPolygon([$this->polygon($coordinates)]);
80
+ }
81
+
82
+ protected function createRecord(string $modelClass, array $attributes, bool $withoutEvents = false): Model
83
+ {
84
+ /** @var Model $model */
85
+ $model = new $modelClass();
86
+ $attributes = $this->filterColumns($model, array_merge([
87
+ 'uuid' => (string) Str::uuid(),
88
+ 'created_at' => $this->timestamp(),
89
+ 'updated_at' => $this->timestamp(),
90
+ ], $attributes));
91
+
92
+ $model->forceFill($attributes);
93
+
94
+ if ($withoutEvents) {
95
+ $modelClass::withoutEvents(fn () => $model->save());
96
+ } else {
97
+ $model->save();
98
+ }
99
+
100
+ return $model;
101
+ }
102
+
103
+ protected function filterColumns(Model $model, array $attributes): array
104
+ {
105
+ $table = $model->getTable();
106
+
107
+ if (!Schema::hasTable($table)) {
108
+ return $attributes;
109
+ }
110
+
111
+ return array_filter(
112
+ $attributes,
113
+ fn (string $column) => Schema::hasColumn($table, $column),
114
+ ARRAY_FILTER_USE_KEY
115
+ );
116
+ }
117
+
118
+ protected function seededQuery(string $modelClass)
119
+ {
120
+ /** @var Model $model */
121
+ $model = new $modelClass();
122
+ $table = $model->getTable();
123
+ $query = $modelClass::query();
124
+
125
+ if (Schema::hasColumn($table, 'meta')) {
126
+ return $query->where('meta->seed', static::SEED_NAME);
127
+ }
128
+
129
+ if (Schema::hasColumn($table, '_key')) {
130
+ return $query->where('_key', 'like', static::SEED_NAME . ':%');
131
+ }
132
+
133
+ return $query->whereRaw('1 = 0');
134
+ }
135
+
136
+ protected function purgeModel(string $modelClass): void
137
+ {
138
+ /** @var Model $model */
139
+ $model = new $modelClass();
140
+ $query = $this->seededQuery($modelClass);
141
+
142
+ if (in_array(SoftDeletes::class, class_uses_recursive($modelClass), true)) {
143
+ $query->forceDelete();
144
+
145
+ return;
146
+ }
147
+
148
+ $query->delete();
149
+ }
150
+
151
+ protected function seededModel(string $modelClass, string $seedId): ?Model
152
+ {
153
+ /** @var Model $model */
154
+ $model = new $modelClass();
155
+ $table = $model->getTable();
156
+
157
+ if (Schema::hasColumn($table, 'meta')) {
158
+ return $modelClass::where('meta->seed', static::SEED_NAME)->where('meta->seed_id', $seedId)->first();
159
+ }
160
+
161
+ if (Schema::hasColumn($table, '_key')) {
162
+ return $modelClass::where('_key', $this->fixtureKey($seedId))->first();
163
+ }
164
+
165
+ return null;
166
+ }
167
+
168
+ protected function seededServiceArea(string $seedId): ?ServiceArea
169
+ {
170
+ /** @var ServiceArea|null $serviceArea */
171
+ $serviceArea = $this->seededModel(ServiceArea::class, $seedId);
172
+
173
+ return $serviceArea;
174
+ }
175
+
176
+ protected function seededZone(string $seedId): ?Zone
177
+ {
178
+ /** @var Zone|null $zone */
179
+ $zone = $this->seededModel(Zone::class, $seedId);
180
+
181
+ return $zone;
182
+ }
183
+ }
@@ -0,0 +1,212 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Seeders\Testing;
4
+
5
+ use Fleetbase\FleetOps\Models\Device;
6
+ use Fleetbase\FleetOps\Models\DeviceEvent;
7
+ use Fleetbase\FleetOps\Models\Driver;
8
+ use Fleetbase\FleetOps\Models\GeofenceEventLog;
9
+ use Fleetbase\FleetOps\Models\Order;
10
+ use Fleetbase\FleetOps\Models\Sensor;
11
+ use Fleetbase\FleetOps\Models\Telematic;
12
+ use Fleetbase\FleetOps\Models\Vehicle;
13
+ use Fleetbase\FleetOps\Seeders\Testing\Concerns\SeedsTestingData;
14
+ use Fleetbase\Models\Company;
15
+ use Illuminate\Database\Seeder;
16
+ use Illuminate\Support\Facades\DB;
17
+
18
+ class ConnectivitySeeder extends Seeder
19
+ {
20
+ use SeedsTestingData;
21
+
22
+ public function run(): void
23
+ {
24
+ $company = $this->prepareCompany();
25
+ if (!$company) {
26
+ return;
27
+ }
28
+
29
+ DB::transaction(function () use ($company) {
30
+ $this->purgeSeedData();
31
+ $telematics = $this->seedTelematics($company);
32
+ $devices = $this->seedDevices($company, $telematics);
33
+ $this->seedSensors($company, $devices);
34
+ $this->seedDeviceEvents($company, $devices);
35
+ $this->seedGeofenceEvents($company);
36
+
37
+ $this->command?->info('Seeded FleetOps testing connectivity fixtures for company: ' . $company->public_id);
38
+ });
39
+ }
40
+
41
+ public function purgeSeedData(): void
42
+ {
43
+ if ($zone = $this->seededZone('cbd_zone')) {
44
+ GeofenceEventLog::where('geofence_uuid', $zone->uuid)
45
+ ->whereIn('event_type', ['entered', 'dwelled', 'exited'])
46
+ ->delete();
47
+ }
48
+
49
+ $this->purgeModel(DeviceEvent::class);
50
+ $this->purgeModel(Sensor::class);
51
+ $this->purgeModel(Device::class);
52
+ $this->purgeModel(Telematic::class);
53
+ }
54
+
55
+ protected function seedTelematics(Company $company): array
56
+ {
57
+ $telematics = [
58
+ 'telematic_samsara' => ['Samsara Sandbox', 'samsara', 'VG34', 'TM-SAM-001', 'active'],
59
+ 'telematic_calculated' => ['Calculated Provider', 'calculated', 'SIM', 'TM-CALC-001', 'connected'],
60
+ ];
61
+
62
+ $models = [];
63
+ foreach ($telematics as $seedId => [$name, $provider, $model, $serial, $status]) {
64
+ $models[$seedId] = $this->createRecord(Telematic::class, [
65
+ '_key' => $this->fixtureKey($seedId),
66
+ 'company_uuid' => $company->uuid,
67
+ 'name' => $name,
68
+ 'provider' => $provider,
69
+ 'model' => $model,
70
+ 'serial_number' => $serial,
71
+ 'firmware_version' => '2026.01',
72
+ 'status' => $status,
73
+ 'imei' => '3569380356438' . ($provider === 'samsara' ? '01' : '02'),
74
+ 'last_seen_at' => $this->timestamp(1),
75
+ 'last_metrics' => ['signal' => 88, 'battery' => 94],
76
+ 'config' => ['fixture' => true],
77
+ 'credentials' => ['token' => 'testing-fixture-redacted'],
78
+ 'meta' => $this->meta($seedId),
79
+ ]);
80
+ }
81
+
82
+ return $models;
83
+ }
84
+
85
+ protected function seedDevices(Company $company, array $telematics): array
86
+ {
87
+ $vehicleCentral = $this->seededModel(Vehicle::class, 'van_central');
88
+ $vehicleEast = $this->seededModel(Vehicle::class, 'van_east');
89
+ $devices = [
90
+ 'device_van_central' => ['CEN-01 OBD', 'OBD-II', 'Fleetbase Labs', 'DEV-CEN-001', 'telematic_samsara', $vehicleCentral, 1.2821, 103.8510, true, 'online'],
91
+ 'device_van_east' => ['EAS-01 Reefer', 'ColdChain-GW', 'Fleetbase Labs', 'DEV-EAST-001', 'telematic_calculated', $vehicleEast, 1.3522, 103.9442, true, 'online'],
92
+ ];
93
+
94
+ $models = [];
95
+ foreach ($devices as $seedId => [$name, $model, $manufacturer, $serial, $telematicSeedId, $vehicle, $lat, $lng, $online, $status]) {
96
+ $models[$seedId] = $this->createRecord(Device::class, [
97
+ '_key' => $this->fixtureKey($seedId),
98
+ 'company_uuid' => $company->uuid,
99
+ 'telematic_uuid' => $telematics[$telematicSeedId]?->uuid,
100
+ 'attachable_uuid' => $vehicle?->uuid,
101
+ 'attachable_type' => Vehicle::class,
102
+ 'type' => 'tracker',
103
+ 'device_id' => strtoupper($seedId),
104
+ 'internal_id' => strtoupper($seedId),
105
+ 'name' => $name,
106
+ 'model' => $model,
107
+ 'manufacturer' => $manufacturer,
108
+ 'serial_number' => $serial,
109
+ 'last_position' => $this->point($lat, $lng),
110
+ 'installation_date' => $this->timestamp(-720)->toDateString(),
111
+ 'last_online_at' => $this->timestamp(2),
112
+ 'online' => $online,
113
+ 'status' => $status,
114
+ 'data_frequency' => 60,
115
+ 'notes' => 'FleetOps testing fixture device.',
116
+ 'data' => ['speed_kmh' => 28, 'heading' => 92],
117
+ 'options' => ['fixture' => true],
118
+ 'meta' => $this->meta($seedId),
119
+ ]);
120
+ }
121
+
122
+ return $models;
123
+ }
124
+
125
+ protected function seedSensors(Company $company, array $devices): void
126
+ {
127
+ $sensors = [
128
+ 'sensor_temperature' => ['device_van_east', 'Cargo Temperature', 'temperature', 'C', -2, 8, '4.2'],
129
+ 'sensor_door' => ['device_van_central', 'Cargo Door', 'door', 'state', 0, 1, 'closed'],
130
+ ];
131
+
132
+ foreach ($sensors as $seedId => [$deviceSeedId, $name, $sensorType, $unit, $min, $max, $lastValue]) {
133
+ $device = $devices[$deviceSeedId] ?? null;
134
+ $this->createRecord(Sensor::class, [
135
+ '_key' => $this->fixtureKey($seedId),
136
+ 'company_uuid' => $company->uuid,
137
+ 'device_uuid' => $device?->uuid,
138
+ 'name' => $name,
139
+ 'sensor_type' => $sensorType,
140
+ 'type' => $sensorType,
141
+ 'unit' => $unit,
142
+ 'min_threshold' => $min,
143
+ 'max_threshold' => $max,
144
+ 'threshold_inclusive' => true,
145
+ 'last_reading_at' => $this->timestamp(2),
146
+ 'last_value' => $lastValue,
147
+ 'sensorable_type' => Device::class,
148
+ 'sensorable_uuid' => $device?->uuid,
149
+ 'status' => 'active',
150
+ 'meta' => $this->meta($seedId),
151
+ ]);
152
+ }
153
+ }
154
+
155
+ protected function seedDeviceEvents(Company $company, array $devices): void
156
+ {
157
+ $events = [
158
+ 'event_ignition_on' => ['device_van_central', 'ignition_on', 'info', 'Vehicle ignition on'],
159
+ 'event_temperature_warning' => ['device_van_east', 'temperature_threshold', 'warning', 'Cargo temperature near threshold'],
160
+ ];
161
+
162
+ foreach ($events as $seedId => [$deviceSeedId, $eventType, $severity, $reason]) {
163
+ $device = $devices[$deviceSeedId] ?? null;
164
+ $this->createRecord(DeviceEvent::class, [
165
+ '_key' => $this->fixtureKey($seedId),
166
+ 'company_uuid' => $company->uuid,
167
+ 'device_uuid' => $device?->uuid,
168
+ 'payload' => ['fixture' => true, 'event' => $eventType],
169
+ 'meta' => $this->meta($seedId),
170
+ 'event_type' => $eventType,
171
+ 'severity' => $severity,
172
+ 'ident' => strtoupper($seedId),
173
+ 'protocol' => 'testing',
174
+ 'provider' => 'fleetops-testing',
175
+ 'state' => $severity,
176
+ 'code' => strtoupper($eventType),
177
+ 'reason' => $reason,
178
+ 'occurred_at' => $this->timestamp(2),
179
+ ]);
180
+ }
181
+ }
182
+
183
+ protected function seedGeofenceEvents(Company $company): void
184
+ {
185
+ $driver = $this->seededModel(Driver::class, 'driver_ava');
186
+ $vehicle = $this->seededModel(Vehicle::class, 'van_central');
187
+ $order = $this->seededModel(Order::class, 'order_dispatched');
188
+ $zone = $this->seededZone('cbd_zone');
189
+
190
+ if (!$driver || !$zone) {
191
+ return;
192
+ }
193
+
194
+ foreach (['entered', 'dwelled', 'exited'] as $index => $eventType) {
195
+ $this->createRecord(GeofenceEventLog::class, [
196
+ 'company_uuid' => $company->uuid,
197
+ 'driver_uuid' => $driver->uuid,
198
+ 'vehicle_uuid' => $vehicle?->uuid,
199
+ 'order_uuid' => $order?->uuid,
200
+ 'geofence_uuid' => $zone->uuid,
201
+ 'geofence_type' => 'zone',
202
+ 'geofence_name' => 'Marina Bay',
203
+ 'event_type' => $eventType,
204
+ 'latitude' => 1.2821,
205
+ 'longitude' => 103.8510,
206
+ 'speed_kmh' => $eventType === 'dwelled' ? 0 : 22,
207
+ 'dwell_duration_minutes' => $eventType === 'dwelled' ? 18 : null,
208
+ 'occurred_at' => $this->timestamp($index),
209
+ ]);
210
+ }
211
+ }
212
+ }