@fleetbase/fleetops-engine 0.6.68 → 0.6.70

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 (294) hide show
  1. package/RELEASE.md +44 -9
  2. package/addon/components/admin/telematics-settings.hbs +143 -0
  3. package/addon/components/admin/telematics-settings.js +145 -0
  4. package/addon/components/contact/form.hbs +13 -2
  5. package/addon/components/customer/details.hbs +17 -17
  6. package/addon/components/customer/form.hbs +13 -2
  7. package/addon/components/customer/form.js +0 -56
  8. package/addon/components/driver/details.hbs +13 -19
  9. package/addon/components/driver/form.hbs +20 -35
  10. package/addon/components/driver/form.js +0 -58
  11. package/addon/components/fuel-integration/hub.hbs +1 -1
  12. package/addon/components/layout/fleet-ops-sidebar.js +37 -17
  13. package/addon/components/map/leaflet-live-map.hbs +4 -242
  14. package/addon/components/map/leaflet-live-map.js +2 -1
  15. package/addon/components/map/marker-card/driver.hbs +57 -0
  16. package/addon/components/map/marker-card/driver.js +3 -0
  17. package/addon/components/map/marker-card/vehicle.hbs +78 -0
  18. package/addon/components/map/marker-card/vehicle.js +3 -0
  19. package/addon/components/modals/driver-form.hbs +2 -2
  20. package/addon/components/modals/reset-profile-credentials.hbs +23 -0
  21. package/addon/components/modals/reset-profile-credentials.js +70 -0
  22. package/addon/components/profile-identity-hint.hbs +13 -0
  23. package/addon/components/profile-identity-hint.js +49 -0
  24. package/addon/components/telematic/hub.hbs +1 -1
  25. package/addon/components/widget/live-fleet.hbs +8 -40
  26. package/addon/controllers/analytics/reports/index.js +5 -0
  27. package/addon/controllers/connectivity/devices/index.js +4 -0
  28. package/addon/controllers/connectivity/fuel-providers/index.js +6 -4
  29. package/addon/controllers/connectivity/sensors/index.js +4 -0
  30. package/addon/controllers/connectivity/telematics/index.js +4 -0
  31. package/addon/controllers/maintenance/equipment/index.js +11 -4
  32. package/addon/controllers/maintenance/inspection-forms/index.js +2 -2
  33. package/addon/controllers/maintenance/inspection-submissions/index.js +8 -2
  34. package/addon/controllers/maintenance/maintenances/index.js +4 -0
  35. package/addon/controllers/maintenance/parts/index.js +11 -4
  36. package/addon/controllers/maintenance/schedules/index.js +17 -4
  37. package/addon/controllers/maintenance/work-orders/index.js +11 -4
  38. package/addon/controllers/management/contacts/index.js +4 -0
  39. package/addon/controllers/management/drivers/index.js +8 -0
  40. package/addon/controllers/management/fleets/index.js +4 -0
  41. package/addon/controllers/management/fuel-reports/index.js +4 -0
  42. package/addon/controllers/management/fuel-transactions/index.js +9 -7
  43. package/addon/controllers/management/issues/index.js +4 -0
  44. package/addon/controllers/management/places/index.js +4 -0
  45. package/addon/controllers/management/vehicles/index.js +4 -0
  46. package/addon/controllers/management/vendors/index.js +4 -0
  47. package/addon/controllers/operations/orders/index/details.js +30 -1
  48. package/addon/controllers/operations/orders/index.js +7 -0
  49. package/addon/controllers/operations/service-rates/index.js +3 -0
  50. package/addon/controllers/settings/index.js +13 -0
  51. package/addon/controllers/settings/telematics.js +113 -0
  52. package/addon/extension.js +42 -8
  53. package/addon/routes/analytics/index.js +14 -1
  54. package/addon/routes/analytics/reports/index/details.js +1 -1
  55. package/addon/routes/analytics/reports/index/edit.js +1 -1
  56. package/addon/routes/analytics/reports/index/new.js +14 -1
  57. package/addon/routes/analytics/reports.js +14 -1
  58. package/addon/routes/connectivity/devices/index/new.js +14 -1
  59. package/addon/routes/connectivity/devices.js +14 -1
  60. package/addon/routes/connectivity/events.js +14 -1
  61. package/addon/routes/connectivity/fuel-providers/details/transactions.js +12 -0
  62. package/addon/routes/connectivity/fuel-providers/details.js +9 -0
  63. package/addon/routes/connectivity/fuel-providers/edit.js +9 -0
  64. package/addon/routes/connectivity/fuel-providers/new.js +14 -1
  65. package/addon/routes/connectivity/fuel-providers.js +14 -1
  66. package/addon/routes/connectivity/sensors/index/new.js +14 -1
  67. package/addon/routes/connectivity/sensors.js +14 -1
  68. package/addon/routes/connectivity/telematics/new.js +14 -1
  69. package/addon/routes/connectivity/telematics.js +14 -1
  70. package/addon/routes/connectivity/tracking.js +14 -1
  71. package/addon/routes/maintenance/equipment/index/details.js +2 -2
  72. package/addon/routes/maintenance/equipment/index/edit.js +2 -2
  73. package/addon/routes/maintenance/equipment/index/new.js +14 -1
  74. package/addon/routes/maintenance/equipment.js +14 -1
  75. package/addon/routes/maintenance/index.js +14 -1
  76. package/addon/routes/maintenance/inspection-forms/index/details.js +10 -1
  77. package/addon/routes/maintenance/inspection-forms/index/edit.js +10 -1
  78. package/addon/routes/maintenance/inspection-forms/index/new.js +14 -1
  79. package/addon/routes/maintenance/inspection-forms.js +14 -1
  80. package/addon/routes/maintenance/inspection-submissions/index/details.js +10 -1
  81. package/addon/routes/maintenance/inspection-submissions/index/edit.js +10 -1
  82. package/addon/routes/maintenance/inspection-submissions/index/new.js +14 -1
  83. package/addon/routes/maintenance/inspection-submissions.js +14 -1
  84. package/addon/routes/maintenance/maintenances/index/details.js +2 -2
  85. package/addon/routes/maintenance/maintenances/index/edit.js +2 -2
  86. package/addon/routes/maintenance/maintenances/index/new.js +14 -1
  87. package/addon/routes/maintenance/maintenances.js +14 -1
  88. package/addon/routes/maintenance/parts/index/details.js +2 -2
  89. package/addon/routes/maintenance/parts/index/edit.js +2 -2
  90. package/addon/routes/maintenance/parts/index/new.js +14 -1
  91. package/addon/routes/maintenance/parts.js +14 -1
  92. package/addon/routes/maintenance/schedules/index/details.js +2 -2
  93. package/addon/routes/maintenance/schedules/index/edit.js +2 -2
  94. package/addon/routes/maintenance/schedules/index/new.js +15 -1
  95. package/addon/routes/maintenance/schedules.js +15 -1
  96. package/addon/routes/maintenance/work-orders/index/details.js +2 -2
  97. package/addon/routes/maintenance/work-orders/index/edit.js +2 -2
  98. package/addon/routes/maintenance/work-orders/index/new.js +14 -1
  99. package/addon/routes/maintenance/work-orders.js +14 -1
  100. package/addon/routes/management/fuel-transactions/index/details.js +9 -0
  101. package/addon/routes/management/fuel-transactions.js +14 -1
  102. package/addon/routes/management/index.js +13 -0
  103. package/addon/routes/management/trailers.js +16 -0
  104. package/addon/routes/management/vendors/integrated/details.js +14 -1
  105. package/addon/routes/management/vendors/integrated/edit.js +14 -1
  106. package/addon/routes/management/vendors/integrated/new.js +14 -1
  107. package/addon/routes/management/vendors/integrated.js +14 -1
  108. package/addon/routes/operations/orders.js +40 -1
  109. package/addon/routes/operations/routes/index/details.js +14 -1
  110. package/addon/routes/operations/routes/index/new.js +13 -0
  111. package/addon/routes/operations/routes.js +14 -1
  112. package/addon/routes/operations/service-rates/index/new.js +11 -0
  113. package/addon/routes/operations/service-rates.js +14 -1
  114. package/addon/routes/settings/avatars.js +14 -1
  115. package/addon/routes/settings/custom-fields.js +14 -1
  116. package/addon/routes/settings/map.js +14 -1
  117. package/addon/routes/settings/notifications.js +11 -0
  118. package/addon/routes/settings/orchestrator.js +1 -1
  119. package/addon/routes/settings/payments/index.js +1 -1
  120. package/addon/routes/settings/routing.js +14 -1
  121. package/addon/routes/settings/scheduling.js +14 -1
  122. package/addon/routes/settings/telematics.js +21 -0
  123. package/addon/routes/virtual.js +11 -0
  124. package/addon/routes.js +1 -0
  125. package/addon/services/contact-actions.js +50 -89
  126. package/addon/services/driver-actions.js +98 -0
  127. package/addon/templates/settings/telematics.hbs +70 -0
  128. package/addon/utils/profile-login-actions.js +172 -0
  129. package/app/components/admin/telematics-settings.js +1 -0
  130. package/app/components/{modals/reset-customer-credentials.js → map/marker-card/driver.js} +1 -1
  131. package/app/components/map/marker-card/vehicle.js +1 -0
  132. package/app/components/modals/reset-profile-credentials.js +1 -0
  133. package/app/components/profile-identity-hint.js +1 -0
  134. package/app/controllers/settings/telematics.js +1 -0
  135. package/app/routes/settings/telematics.js +1 -0
  136. package/app/templates/settings/telematics.js +1 -0
  137. package/composer.json +2 -2
  138. package/docs/AFAQY.md +1 -1
  139. package/docs/TELEMATICS_QUEUES.md +1 -1
  140. package/docs/TELEMETRY_ARCHITECTURE.md +72 -1
  141. package/extension.json +1 -1
  142. package/package.json +3 -3
  143. package/scripts/pest-bootstrap.php +12 -0
  144. package/server/config/telemetry.php +12 -0
  145. package/server/migrations/2026_09_21_000001_convert_profile_only_users_to_managed_accounts.php +132 -0
  146. package/server/migrations/2026_09_23_000001_add_retention_index_to_device_events.php +40 -0
  147. package/server/migrations/2026_09_23_000002_add_retention_index_to_positions.php +40 -0
  148. package/server/migrations/2026_09_23_000003_add_retention_index_to_telematic_sync_runs.php +40 -0
  149. package/server/resources/views/mail/driver-credentials.blade.php +21 -0
  150. package/server/src/Auth/Schemas/FleetOps.php +104 -5
  151. package/server/src/Console/Commands/DrainTelematicInbox.php +3 -13
  152. package/server/src/Console/Commands/FixCustomerCompanies.php +1 -1
  153. package/server/src/Console/Commands/FixDriverCompanies.php +1 -1
  154. package/server/src/Console/Commands/ProcessOperationalAlerts.php +3 -3
  155. package/server/src/Console/Commands/PruneTelematicsData.php +318 -0
  156. package/server/src/Exceptions/ProfileIdentityConflictException.php +34 -0
  157. package/server/src/Http/Controllers/Api/v1/CustomerController.php +14 -0
  158. package/server/src/Http/Controllers/Api/v1/DriverController.php +56 -39
  159. package/server/src/Http/Controllers/Api/v1/OrchestrationController.php +9 -0
  160. package/server/src/Http/Controllers/FleetOpsController.php +21 -0
  161. package/server/src/Http/Controllers/Internal/v1/AnalyticsController.php +11 -0
  162. package/server/src/Http/Controllers/Internal/v1/ContactController.php +16 -17
  163. package/server/src/Http/Controllers/Internal/v1/CustomerController.php +54 -4
  164. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +13 -0
  165. package/server/src/Http/Controllers/Internal/v1/DeviceEventController.php +11 -0
  166. package/server/src/Http/Controllers/Internal/v1/DriverController.php +204 -162
  167. package/server/src/Http/Controllers/Internal/v1/FleetOpsLookupController.php +47 -0
  168. package/server/src/Http/Controllers/Internal/v1/FuelProviderConnectionController.php +13 -0
  169. package/server/src/Http/Controllers/Internal/v1/FuelProviderTransactionController.php +15 -0
  170. package/server/src/Http/Controllers/Internal/v1/FuelReportController.php +9 -3
  171. package/server/src/Http/Controllers/Internal/v1/InspectionFormController.php +15 -0
  172. package/server/src/Http/Controllers/Internal/v1/LiveController.php +16 -0
  173. package/server/src/Http/Controllers/Internal/v1/MaintenanceController.php +15 -0
  174. package/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php +15 -0
  175. package/server/src/Http/Controllers/Internal/v1/ManifestController.php +15 -0
  176. package/server/src/Http/Controllers/Internal/v1/MetricsController.php +11 -0
  177. package/server/src/Http/Controllers/Internal/v1/NavigatorController.php +42 -1
  178. package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +12 -0
  179. package/server/src/Http/Controllers/Internal/v1/OrderController.php +178 -27
  180. package/server/src/Http/Controllers/Internal/v1/PaymentController.php +13 -0
  181. package/server/src/Http/Controllers/Internal/v1/RadarController.php +19 -0
  182. package/server/src/Http/Controllers/Internal/v1/ServiceAreaController.php +9 -3
  183. package/server/src/Http/Controllers/Internal/v1/SettingController.php +465 -14
  184. package/server/src/Http/Controllers/Internal/v1/TelematicController.php +21 -0
  185. package/server/src/Http/Controllers/Internal/v1/TrailerController.php +21 -0
  186. package/server/src/Http/Controllers/Internal/v1/VehicleController.php +21 -0
  187. package/server/src/Http/Controllers/Internal/v1/VendorController.php +17 -0
  188. package/server/src/Http/Controllers/Internal/v1/WorkOrderController.php +11 -0
  189. package/server/src/Http/Requests/CreateDriverRequest.php +6 -2
  190. package/server/src/Http/Requests/Internal/CreateDriverRequest.php +9 -18
  191. package/server/src/Http/Resources/v1/Contact.php +3 -0
  192. package/server/src/Http/Resources/v1/Driver.php +3 -0
  193. package/server/src/Jobs/DispatchTelematicsRetentionJobs.php +70 -0
  194. package/server/src/Jobs/PruneTelematicsDataJob.php +74 -0
  195. package/server/src/Listeners/NotifyDriverOnShiftChange.php +3 -3
  196. package/server/src/Mail/CustomerCredentialsMail.php +1 -1
  197. package/server/src/Mail/DriverCredentialsMail.php +55 -0
  198. package/server/src/Models/Contact.php +55 -90
  199. package/server/src/Models/Sensor.php +3 -5
  200. package/server/src/Observers/ContactObserver.php +2 -22
  201. package/server/src/Observers/DriverObserver.php +5 -6
  202. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +9 -1
  203. package/server/src/Providers/FleetOpsServiceProvider.php +18 -0
  204. package/server/src/Support/Ai/Capabilities/AbstractFleetOpsAICapability.php +62 -5
  205. package/server/src/Support/Ai/Capabilities/AssetStatusCapability.php +24 -5
  206. package/server/src/Support/Ai/Capabilities/CreateOrderPreviewCapability.php +6 -2
  207. package/server/src/Support/Ai/Capabilities/OrderInsightsCapability.php +40 -7
  208. package/server/src/Support/Ai/Capabilities/SearchResourcesCapability.php +86 -24
  209. package/server/src/Support/Ai/FleetOpsAiConsoleCommands.php +122 -0
  210. package/server/src/Support/Ai/Tools/CreateOrderTool.php +86 -0
  211. package/server/src/Support/Ai/Tools/ImportOrdersTool.php +53 -0
  212. package/server/src/Support/Ai/Tools/OptimizeOrderRouteTool.php +70 -0
  213. package/server/src/Support/Ai/Tools/SearchResourcesTool.php +73 -0
  214. package/server/src/Support/Analytics/LiveFleet.php +18 -2
  215. package/server/src/Support/Authorization.php +103 -0
  216. package/server/src/Support/Database/TableIndexes.php +66 -0
  217. package/server/src/Support/OrderTracker.php +2 -2
  218. package/server/src/Support/ProfileAccountManager.php +405 -0
  219. package/server/src/Support/Reporting/FleetOpsReportSchema.php +476 -116
  220. package/server/src/Support/Telematics/Retention/RetentionPolicy.php +300 -0
  221. package/server/src/Support/Telematics/Retention/TelemetryActivity.php +40 -0
  222. package/server/src/Support/Telematics/TelematicService.php +12 -2
  223. package/server/src/Support/Telematics/Telemetry/Ingestor.php +4 -2
  224. package/server/src/Tracking/TrackingIntelligenceService.php +1 -1
  225. package/server/src/Tracking/TrackingOptions.php +2 -2
  226. package/server/src/Traits/AuthorizesMethods.php +37 -0
  227. package/server/src/routes.php +14 -1
  228. package/server/tests/AiOperationalQueryCapabilityTest.php +1 -0
  229. package/server/tests/AnalyticsRoutesTest.php +29 -2
  230. package/server/tests/ApiCustomerControllerContractsTest.php +21 -0
  231. package/server/tests/ApiDriverControllerContractsTest.php +102 -20
  232. package/server/tests/CommandContractsTest.php +4 -4
  233. package/server/tests/ControllerHelperContractsTest.php +3 -11
  234. package/server/tests/Feature/Http/AfaqyRealtimeIngestionTest.php +21 -1
  235. package/server/tests/Feature/Http/Api/DriverControllerAuthFlowsTest.php +36 -0
  236. package/server/tests/Feature/Http/Api/DriverControllerSessionHelpersTest.php +19 -6
  237. package/server/tests/Feature/Http/Api/ProfileOnlyUserConversionMigrationTest.php +201 -0
  238. package/server/tests/Feature/Http/Api/TelematicsRetentionIndexMigrationTest.php +115 -0
  239. package/server/tests/Feature/Http/Internal/ContactControllerEndpointsTest.php +6 -4
  240. package/server/tests/Feature/Http/Internal/CustomerControllerCredentialsTest.php +6 -2
  241. package/server/tests/Feature/Http/Internal/DriverControllerContractsTest.php +23 -0
  242. package/server/tests/Feature/Http/Internal/DriverControllerCreateRecordTest.php +42 -17
  243. package/server/tests/Feature/Http/Internal/DriverControllerExistingUserAdoptionTest.php +168 -277
  244. package/server/tests/Feature/Http/Internal/DriverControllerLoginManagementTest.php +716 -0
  245. package/server/tests/Feature/Http/Internal/OrderControllerContractsTest.php +10 -3
  246. package/server/tests/Feature/Http/Internal/OrderControllerImportFromFilesTest.php +18 -0
  247. package/server/tests/Feature/Http/Internal/OrderControllerTenantScopingTest.php +411 -0
  248. package/server/tests/Feature/Http/Internal/OrderControllerUpstreamNotFoundTest.php +8 -20
  249. package/server/tests/Feature/Http/Internal/SettingControllerHelpersTest.php +71 -0
  250. package/server/tests/Feature/Http/Internal/TelematicsStorageDeadlinesTest.php +119 -0
  251. package/server/tests/InternalCustomerControllerContractsTest.php +32 -1
  252. package/server/tests/InternalImportExportControllerContractsTest.php +5 -8
  253. package/server/tests/ModelAccessorContractsTest.php +82 -20
  254. package/server/tests/NavigatorControllerContractsTest.php +36 -4
  255. package/server/tests/NotificationAndMailContractsTest.php +31 -0
  256. package/server/tests/ObserverContractsTest.php +28 -3
  257. package/server/tests/OrchestratorConsumableApiTest.php +42 -0
  258. package/server/tests/ProviderContractsTest.php +10 -0
  259. package/server/tests/ReportSchemaContractsTest.php +120 -6
  260. package/server/tests/RequestContractsTest.php +19 -11
  261. package/server/tests/SettingControllerContractsTest.php +429 -18
  262. package/server/tests/SmallControllerContractsTest.php +8 -5
  263. package/server/tests/Support/PruneTelematicsDataProbe.php +55 -0
  264. package/server/tests/SupportJobAndAiCoverageTest.php +3 -3
  265. package/server/tests/TelematicControllerContractsTest.php +22 -4
  266. package/server/tests/Unit/Console/Commands/ProcessOperationalAlertsCommandTest.php +2 -2
  267. package/server/tests/Unit/Console/PruneTelematicsDataCommandTest.php +353 -0
  268. package/server/tests/Unit/Jobs/DispatchTelematicsRetentionJobsTest.php +114 -0
  269. package/server/tests/Unit/Jobs/PruneTelematicsDataJobTest.php +60 -0
  270. package/server/tests/Unit/Models/ContactCustomerUserTest.php +90 -14
  271. package/server/tests/Unit/Models/ContactTest.php +48 -5
  272. package/server/tests/Unit/Models/SensorTest.php +2 -2
  273. package/server/tests/Unit/Models/SmallModelSurfacesTest.php +31 -32
  274. package/server/tests/Unit/Observers/ObserverListenerSeamsTest.php +7 -2
  275. package/server/tests/Unit/Orchestration/VroomEngineTransportAndSettingsTest.php +20 -0
  276. package/server/tests/Unit/Providers/FleetOpsAiRegistrationTest.php +15 -2
  277. package/server/tests/Unit/Support/Ai/AssetStatusCapabilityQueriesTest.php +42 -0
  278. package/server/tests/Unit/Support/Ai/Capabilities/OrderInsightsCapabilityTest.php +19 -1
  279. package/server/tests/Unit/Support/Ai/Capabilities/SearchResourcesCapabilityTest.php +85 -1
  280. package/server/tests/Unit/Support/Ai/FleetOpsAiConsoleCommandsTest.php +31 -0
  281. package/server/tests/Unit/Support/Ai/Tools/FleetOpsAiToolsTest.php +246 -0
  282. package/server/tests/Unit/Support/AiCapabilityPermissionsTest.php +3 -3
  283. package/server/tests/Unit/Support/Analytics/LiveFleetTest.php +6 -0
  284. package/server/tests/Unit/Support/AuthorizationTest.php +142 -0
  285. package/server/tests/Unit/Support/MaintainableAndOperationalAlertsTest.php +12 -1
  286. package/server/tests/Unit/Support/MoreQuerySeamsTest.php +11 -2
  287. package/server/tests/Unit/Support/ProfileAccountManagerTest.php +643 -0
  288. package/server/tests/Unit/Support/Telematics/Retention/RetentionPolicyTest.php +190 -0
  289. package/server/tests/Unit/Support/Telematics/Retention/TelemetryActivityTest.php +68 -0
  290. package/server/tests/Unit/Tracking/TrackingOptionsCompanySettingsTest.php +109 -0
  291. package/translations/en-us.yaml +85 -2
  292. package/translations/uk-ua.yaml +3475 -0
  293. package/addon/components/modals/reset-customer-credentials.hbs +0 -18
  294. package/addon/components/modals/reset-customer-credentials.js +0 -55
@@ -0,0 +1,115 @@
1
+ <?php
2
+
3
+ use Fleetbase\FleetOps\Support\Database\TableIndexes;
4
+ use Illuminate\Database\SQLiteConnection;
5
+ use Illuminate\Support\Facades\Schema;
6
+
7
+ /**
8
+ * The retention sweep ranges over (company_uuid, created_at) and
9
+ * (telematic_uuid, updated_at); these migrations add those indexes once,
10
+ * skip equivalent existing ones, and roll back only what they created.
11
+ */
12
+ function fleetopsRetentionIndexDatabase(): SQLiteConnection
13
+ {
14
+ $connection = new SQLiteConnection(new PDO('sqlite::memory:'), ':memory:', '', ['driver' => 'sqlite']);
15
+ Schema::swap($connection->getSchemaBuilder());
16
+
17
+ return $connection;
18
+ }
19
+
20
+ function fleetopsRetentionIndexMigrations(): array
21
+ {
22
+ $migrations = [];
23
+ foreach (glob(dirname(__DIR__, 4) . '/migrations/2026_09_23_00000*_add_retention_index_to_*.php') as $path) {
24
+ $migrations[basename($path)] = fn () => require $path;
25
+ }
26
+
27
+ return $migrations;
28
+ }
29
+
30
+ test('retention index migrations add composite indexes once and roll them back', function () {
31
+ fleetopsRetentionIndexDatabase();
32
+ Schema::create('device_events', function ($table) {
33
+ $table->increments('id');
34
+ $table->uuid('company_uuid')->nullable();
35
+ $table->timestamps();
36
+ });
37
+ Schema::create('positions', function ($table) {
38
+ $table->increments('id');
39
+ $table->uuid('company_uuid')->nullable();
40
+ $table->timestamps();
41
+ });
42
+ Schema::create('telematic_sync_runs', function ($table) {
43
+ $table->uuid('uuid')->primary();
44
+ $table->uuid('telematic_uuid')->index();
45
+ $table->timestamps();
46
+ });
47
+
48
+ $expected = [
49
+ 'device_events' => ['device_events_company_created_at_index', ['company_uuid', 'created_at']],
50
+ 'positions' => ['positions_company_created_at_index', ['company_uuid', 'created_at']],
51
+ 'telematic_sync_runs' => ['telematic_sync_runs_telematic_updated_at_index', ['telematic_uuid', 'updated_at']],
52
+ ];
53
+ $migrations = fleetopsRetentionIndexMigrations();
54
+ expect($migrations)->toHaveCount(3);
55
+ foreach ($migrations as $load) {
56
+ $load()->up();
57
+ $load()->up();
58
+ }
59
+ foreach ($expected as $table => [$name, $columns]) {
60
+ expect(TableIndexes::for($table)[$name] ?? null)->toBe($columns)
61
+ ->and(TableIndexes::covers($table, $columns))->toBeTrue()
62
+ ->and(TableIndexes::covers($table, ['created_at']))->toBeFalse();
63
+ }
64
+ // The existing single-column index on telematic_uuid does not cover the composite.
65
+ expect(count(array_filter(TableIndexes::for('telematic_sync_runs'), fn ($columns) => $columns === ['telematic_uuid'])))->toBe(1);
66
+
67
+ foreach ($migrations as $load) {
68
+ $load()->down();
69
+ }
70
+ foreach ($expected as $table => [$name]) {
71
+ expect(TableIndexes::for($table))->not->toHaveKey($name);
72
+ }
73
+ });
74
+
75
+ test('retention index migrations skip missing tables, missing columns and equivalent indexes', function () {
76
+ fleetopsRetentionIndexDatabase();
77
+ $migrations = fleetopsRetentionIndexMigrations();
78
+
79
+ // No tables at all: nothing to do either way.
80
+ foreach ($migrations as $load) {
81
+ $load()->up();
82
+ $load()->down();
83
+ }
84
+
85
+ Schema::create('device_events', fn ($table) => $table->increments('id'));
86
+ Schema::create('positions', function ($table) {
87
+ $table->increments('id');
88
+ $table->uuid('company_uuid')->nullable();
89
+ $table->timestamps();
90
+ $table->index(['company_uuid', 'created_at', 'id'], 'positions_existing_company_created');
91
+ });
92
+ Schema::create('telematic_sync_runs', function ($table) {
93
+ $table->uuid('uuid')->primary();
94
+ $table->uuid('telematic_uuid');
95
+ $table->timestamps();
96
+ $table->index(['telematic_uuid', 'updated_at'], 'telematic_sync_runs_telematic_updated_at_index');
97
+ });
98
+ Schema::table('telematic_sync_runs', fn ($table) => $table->index('updated_at', 'unrelated_updated_at'));
99
+
100
+ foreach ($migrations as $load) {
101
+ $load()->up();
102
+ }
103
+ expect(TableIndexes::for('device_events'))->toBe([])
104
+ ->and(array_keys(TableIndexes::for('positions')))->toBe(['positions_existing_company_created'])
105
+ ->and(TableIndexes::for('telematic_sync_runs'))->toHaveKeys(['telematic_sync_runs_telematic_updated_at_index', 'unrelated_updated_at']);
106
+
107
+ // Rolling back never drops an index the migration did not create, even under the reserved name when its shape differs.
108
+ Schema::table('telematic_sync_runs', fn ($table) => $table->dropIndex('telematic_sync_runs_telematic_updated_at_index'));
109
+ Schema::table('telematic_sync_runs', fn ($table) => $table->index('updated_at', 'telematic_sync_runs_telematic_updated_at_index'));
110
+ foreach ($migrations as $load) {
111
+ $load()->down();
112
+ }
113
+ expect(array_keys(TableIndexes::for('positions')))->toBe(['positions_existing_company_created'])
114
+ ->and(TableIndexes::for('telematic_sync_runs')['telematic_sync_runs_telematic_updated_at_index'])->toBe(['updated_at']);
115
+ });
@@ -252,10 +252,12 @@ test('contact update guards types resolve users and portal seams persist', funct
252
252
  $probe->callProtected('assertContactInputIsValid', [$request, &$input, $contact]);
253
253
  })->toThrow(Exception::class);
254
254
 
255
- // User references resolve through uuid or public id
256
- expect($probe->callProtected('resolveUserUuid', ['user_contactseam']))->toBe('88888888-8888-4888-8888-888888888881')
257
- ->and($probe->callProtected('resolveUserUuid', ['88888888-8888-4888-8888-888888888881']))->toBe('88888888-8888-4888-8888-888888888881')
258
- ->and($probe->callProtected('resolveUserUuid', ['unknown-user']))->toBe('unknown-user');
255
+ // The login account is managed by the profile, so user references are dropped
256
+ $userInput = ['name' => 'Seam Contact', 'user_uuid' => '88888888-8888-4888-8888-888888888881', 'user' => 'user_contactseam'];
257
+ Closure::bind(function (array &$input) {
258
+ $this->resolveUserInput(Request::create('/x', 'PUT', []), $input);
259
+ }, $probe, ContactController::class)($userInput);
260
+ expect($userInput)->toBe(['name' => 'Seam Contact']);
259
261
 
260
262
  // Portal seams read users, mint passwords, and persist meta quietly
261
263
  expect($probe->callProtected('contactUser', [$contact])?->uuid)->toBe('88888888-8888-4888-8888-888888888881')
@@ -125,7 +125,9 @@ function fleetopsCustomerCredentialsBoot(): SQLiteConnection
125
125
  'contacts' => ['uuid', 'public_id', 'company_uuid', 'user_uuid', 'name', 'email', 'phone', 'type', 'title', 'meta', 'photo_uuid', 'place_uuid', 'slug', '_key'],
126
126
  'users' => ['uuid', 'public_id', 'company_uuid', 'name', 'email', 'phone', 'password', 'type', 'status', 'timezone', 'slug', 'username', 'avatar_uuid', 'meta', '_key'],
127
127
  'companies' => ['uuid', 'public_id', 'name', 'country'],
128
- 'files' => ['uuid', 'public_id', 'company_uuid', 'subject_uuid', 'subject_type', 'name', 'path', 'disk', 'type', '_key'],
128
+ // session_status reads the user's membership of the session company
129
+ 'company_users' => ['uuid', 'company_uuid', 'user_uuid', 'status'],
130
+ 'files' => ['uuid', 'public_id', 'company_uuid', 'subject_uuid', 'subject_type', 'name', 'path', 'disk', 'type', '_key'],
129
131
  ];
130
132
  foreach ($tables as $table => $columns) {
131
133
  $schema->create($table, function ($blueprint) use ($columns) {
@@ -192,5 +194,7 @@ test('customer resolution and payload helpers expose user details', function ()
192
194
 
193
195
  $payload = $probe->callHelper('customerPayload', $customer);
194
196
  expect($payload['uuid'])->toBe('55555555-5555-4555-8555-555555555555')
195
- ->and($payload)->toHaveKey('user');
197
+ ->and($payload['user']['uuid'])->toBe('user-1')
198
+ ->and($payload['is_staff_linked'])->toBeFalse()
199
+ ->and($payload['login_status'])->toBe('active');
196
200
  });
@@ -1131,6 +1131,29 @@ test('internal driver controller verify code returns driver resource and handles
1131
1131
  ]);
1132
1132
  });
1133
1133
 
1134
+ test('internal driver controller refuses phone login and code verification for deactivated drivers', function () {
1135
+ FleetOpsInternalDriverAuthControllerProbe::resetProbe();
1136
+ app()->instance('request', Request::create('/', 'POST', ['phone' => '15551234567']));
1137
+
1138
+ $deactivated = fleetopsInternalDriverAuthUser();
1139
+ $deactivated->setAttribute('status', 'inactive');
1140
+ FleetOpsInternalDriverAuthControllerProbe::$loginUser = $deactivated;
1141
+ FleetOpsInternalDriverAuthControllerProbe::$verificationUser = $deactivated;
1142
+ FleetOpsInternalDriverAuthControllerProbe::$verificationExists = true;
1143
+
1144
+ $controller = new FleetOpsInternalDriverAuthControllerProbe();
1145
+ $login = $controller->loginWithPhone();
1146
+ $verify = $controller->verifyCode(new Request(['identity' => '+15551234567', 'code' => '111111']));
1147
+
1148
+ expect($login->getStatusCode())->toBe(403)
1149
+ ->and($login->getData(true))->toBe(['error' => 'This driver login has been deactivated.'])
1150
+ ->and($verify->getStatusCode())->toBe(403)
1151
+ ->and($verify->getData(true))->toBe(['error' => 'This driver login has been deactivated.'])
1152
+ // No code is sent and no token is issued
1153
+ ->and(FleetOpsInternalDriverAuthControllerProbe::$verifications)->toBe([])
1154
+ ->and(FleetOpsInternalDriverAuthControllerProbe::$tokens)->toBe([]);
1155
+ });
1156
+
1134
1157
  test('internal driver controller delegates login and create driver verification flows to api controller', function () {
1135
1158
  $request = new Request(['for' => 'create_driver']);
1136
1159
 
@@ -10,11 +10,12 @@ use Illuminate\Http\Request;
10
10
  /**
11
11
  * Covers the internal DriverController createRecord and updateRecord flows
12
12
  * against an in-memory SQLite fixture. The creation closure executes end to
13
- * end: company resolution, user creation with driver typing, existing-user
14
- * adoption and conflict detection, organization membership checks, and the
15
- * exception-to-error-response branches. Role assignment requires the full
16
- * spatie permission boot unavailable in the harness, so flows settle in the
17
- * documented error branch after the user record is persisted.
13
+ * end: company resolution, managed account creation with driver typing
14
+ * through ProfileAccountManager, and the exception-to-error-response
15
+ * branches. Role assignment requires the full spatie permission boot, which
16
+ * this fixture leaves out, so flows settle in the documented error branch
17
+ * after the user record is persisted (see
18
+ * DriverControllerExistingUserAdoptionTest for the full flow).
18
19
  */
19
20
  if (!function_exists('Fleetbase\Support\session')) {
20
21
  eval('namespace Fleetbase\Support; function session($key = null, $default = null) { if ($key === null) { return new class { public function has($k) { return \session($k) !== null; } public function get($k, $d = null) { return \session($k, $d); } }; } return \session($key, $default); }');
@@ -101,6 +102,28 @@ function fleetopsInternalDriverCreateBoot(): SQLiteConnection
101
102
  }
102
103
  });
103
104
  Illuminate\Support\Facades\DB::clearResolvedInstance('db');
105
+ app()->instance('hash', new class implements Illuminate\Contracts\Hashing\Hasher {
106
+ public function info($hashedValue): array
107
+ {
108
+ return [];
109
+ }
110
+
111
+ public function make($value, array $options = []): string
112
+ {
113
+ return 'hashed:' . $value;
114
+ }
115
+
116
+ public function check($value, $hashedValue, array $options = []): bool
117
+ {
118
+ return 'hashed:' . $value === $hashedValue;
119
+ }
120
+
121
+ public function needsRehash($hashedValue, array $options = []): bool
122
+ {
123
+ return false;
124
+ }
125
+ });
126
+ Illuminate\Support\Facades\Hash::clearResolvedInstance('hash');
104
127
 
105
128
  $schema = $connection->getSchemaBuilder();
106
129
  $tables = [
@@ -158,31 +181,33 @@ test('create record provisions a driver-typed user before role assignment', func
158
181
  ->and($connection->table('company_users')->count())->toBeGreaterThanOrEqual(0);
159
182
  });
160
183
 
161
- test('create record rejects users that already belong to a driver', function () {
184
+ test('create record ignores a picked user account and resolves the login from the email', function () {
162
185
  $connection = fleetopsInternalDriverCreateBoot();
163
- $connection->table('users')->insert(['uuid' => '11111111-1111-4111-8111-111111111111', 'company_uuid' => 'company-1', 'name' => 'Existing', 'type' => 'driver']);
164
- $connection->table('drivers')->insert(['uuid' => 'driver-1', 'company_uuid' => 'company-1', 'user_uuid' => '11111111-1111-4111-8111-111111111111']);
186
+ $connection->table('users')->insert(['uuid' => '11111111-1111-4111-8111-111111111111', 'company_uuid' => 'company-1', 'name' => 'Existing', 'email' => 'existing@example.com', 'type' => 'driver']);
165
187
 
166
- $result = (new DriverController())->createRecord(fleetopsInternalDriverCreateRequest([
167
- 'name' => 'Existing',
188
+ (new DriverController())->createRecord(fleetopsInternalDriverCreateRequest([
189
+ 'name' => 'Someone Else',
190
+ 'email' => 'someone@example.com',
168
191
  'user_uuid' => '11111111-1111-4111-8111-111111111111',
169
192
  ]));
170
193
 
171
- expect($result)->toBeInstanceOf(JsonResponse::class)
172
- ->and($result->getData(true))->toBe(['error' => 'This user account already belongs to a driver.']);
194
+ // The login account is managed by the profile: a new driver account is
195
+ // created for the email rather than the picked account being taken over
196
+ expect($connection->table('users')->count())->toBe(2)
197
+ ->and($connection->table('users')->where('email', 'someone@example.com')->value('type'))->toBe('driver')
198
+ ->and($connection->table('users')->where('uuid', '11111111-1111-4111-8111-111111111111')->value('name'))->toBe('Existing');
173
199
  });
174
200
 
175
- test('create record adopts an existing user and applies photo avatars', function () {
201
+ test('create record applies photo avatars to the new driver account', function () {
176
202
  $connection = fleetopsInternalDriverCreateBoot();
177
- $connection->table('users')->insert(['uuid' => '11111111-1111-4111-8111-111111111111', 'company_uuid' => 'company-1', 'name' => 'Adoptable', 'type' => 'driver']);
178
203
 
179
204
  $result = (new DriverController())->createRecord(fleetopsInternalDriverCreateRequest([
180
- 'name' => 'Adoptable',
181
- 'user_uuid' => '11111111-1111-4111-8111-111111111111',
205
+ 'name' => 'Photo Driver',
182
206
  'photo_uuid' => '22222222-2222-4222-8222-222222222222',
183
207
  ]));
184
208
 
185
- // Avatar update applies before the harness role-assignment limitation.
209
+ // Avatar is set when the account is created, before the harness
210
+ // role-assignment limitation.
186
211
  expect($connection->table('users')->value('avatar_uuid'))->toBe('22222222-2222-4222-8222-222222222222')
187
212
  ->and($result)->toBeInstanceOf(JsonResponse::class)
188
213
  ->and($result->getData(true))->toHaveKey('error');