@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,405 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support;
4
+
5
+ use Fleetbase\FleetOps\Exceptions\ProfileIdentityConflictException;
6
+ use Fleetbase\FleetOps\Mail\CustomerCredentialsMail;
7
+ use Fleetbase\FleetOps\Mail\DriverCredentialsMail;
8
+ use Fleetbase\FleetOps\Models\Contact;
9
+ use Fleetbase\FleetOps\Models\Driver;
10
+ use Fleetbase\Models\Company;
11
+ use Fleetbase\Models\CompanyUser;
12
+ use Fleetbase\Models\User;
13
+ use Fleetbase\Services\SmsService;
14
+ use Illuminate\Support\Facades\Mail;
15
+ use Illuminate\Support\Str;
16
+
17
+ /**
18
+ * Manages the login accounts behind driver, contact and customer profiles.
19
+ *
20
+ * A profile either owns a managed account (user type `driver`, `contact` or
21
+ * `customer`), which it creates, updates and deletes, or it is linked to a
22
+ * staff account (type `user` or `admin`) of the same organization. A staff
23
+ * account is managed in IAM, so a profile never changes its email or phone,
24
+ * resets its password, deactivates it or deletes it.
25
+ */
26
+ class ProfileAccountManager
27
+ {
28
+ /**
29
+ * User types owned by a profile.
30
+ */
31
+ public const MANAGED_TYPES = ['driver', 'contact', 'customer'];
32
+
33
+ /**
34
+ * The organization role given to each managed account type.
35
+ */
36
+ public const ROLES = [
37
+ 'driver' => 'Driver',
38
+ 'contact' => 'Fleet-Ops Contact',
39
+ 'customer' => 'Fleet-Ops Customer',
40
+ ];
41
+
42
+ /**
43
+ * Whether the account is owned by a driver, contact or customer profile.
44
+ */
45
+ public static function isManagedAccount(?object $user): bool
46
+ {
47
+ return $user !== null && in_array(data_get($user, 'type'), static::MANAGED_TYPES, true);
48
+ }
49
+
50
+ /**
51
+ * Whether the account is a team member's account managed in IAM.
52
+ */
53
+ public static function isStaffAccount(?object $user): bool
54
+ {
55
+ return $user !== null && !static::isManagedAccount($user);
56
+ }
57
+
58
+ /**
59
+ * Find the active account holding the given email or phone.
60
+ */
61
+ public static function findAccountByIdentity(?string $email, ?string $phone, ?string $ignoreUserUuid = null): ?User
62
+ {
63
+ $email = static::normalizeEmail($email);
64
+ $phone = static::normalizePhone($phone);
65
+
66
+ if (!$email && !$phone) {
67
+ return null;
68
+ }
69
+
70
+ return User::where(function ($query) use ($email, $phone) {
71
+ if ($email) {
72
+ $query->orWhere('email', $email);
73
+ }
74
+
75
+ if ($phone) {
76
+ $query->orWhere('phone', $phone);
77
+ }
78
+ })
79
+ ->when($ignoreUserUuid, fn ($query) => $query->where('uuid', '!=', $ignoreUserUuid))
80
+ ->whereNull('deleted_at')
81
+ ->first();
82
+ }
83
+
84
+ /**
85
+ * Describe what would happen to an email/phone entered on a new or existing
86
+ * profile: `staff` is the team member the profile would link to, `conflict`
87
+ * is why the email/phone cannot be used.
88
+ *
89
+ * @return array{account: ?User, staff: ?User, conflict: ?string}
90
+ */
91
+ public static function lookup(string $companyUuid, string $type, ?string $email, ?string $phone, ?User $currentUser = null): array
92
+ {
93
+ $result = ['account' => null, 'staff' => null, 'conflict' => null];
94
+ $account = static::findAccountByIdentity($email, $phone, $currentUser?->uuid);
95
+
96
+ if (!$account) {
97
+ return $result;
98
+ }
99
+
100
+ $result['account'] = $account;
101
+ $field = static::matchedField($account, $email);
102
+
103
+ // An existing profile can't swap its account for another one
104
+ if ($currentUser) {
105
+ $result['conflict'] = static::takenMessage($field);
106
+
107
+ return $result;
108
+ }
109
+
110
+ try {
111
+ static::assertAccountCanHoldProfile($account, $companyUuid, $type, $field);
112
+ } catch (ProfileIdentityConflictException $e) {
113
+ $result['conflict'] = $e->getMessage();
114
+
115
+ return $result;
116
+ }
117
+
118
+ if (static::isStaffAccount($account)) {
119
+ $result['staff'] = $account;
120
+ }
121
+
122
+ return $result;
123
+ }
124
+
125
+ /**
126
+ * Resolve the account for a new profile. A staff member of the organization
127
+ * or a managed account of the same type with the email/phone is linked;
128
+ * otherwise a managed account is created.
129
+ *
130
+ * @param array $attributes extra user attributes: password, status, timezone, avatar_uuid, country, ip_address, meta
131
+ *
132
+ * @throws ProfileIdentityConflictException
133
+ */
134
+ public static function resolveForProfile(string $companyUuid, string $type, ?string $name, ?string $email, ?string $phone, array $attributes = []): User
135
+ {
136
+ $account = static::findAccountByIdentity($email, $phone);
137
+
138
+ if ($account) {
139
+ static::assertAccountCanHoldProfile($account, $companyUuid, $type, static::matchedField($account, $email));
140
+
141
+ if (static::isManagedAccount($account)) {
142
+ static::attachToCompany($account, $companyUuid, $type);
143
+ }
144
+
145
+ return $account;
146
+ }
147
+
148
+ return static::createManagedAccount($companyUuid, $type, $name, $email, $phone, $attributes);
149
+ }
150
+
151
+ /**
152
+ * Create a managed account and add it to the organization without an invite.
153
+ */
154
+ public static function createManagedAccount(string $companyUuid, string $type, ?string $name, ?string $email, ?string $phone, array $attributes = []): User
155
+ {
156
+ $company = Company::where('uuid', $companyUuid)->first();
157
+ $password = $attributes['password'] ?? null;
158
+ $userData = array_filter([
159
+ 'company_uuid' => $companyUuid,
160
+ 'name' => $name,
161
+ 'email' => static::normalizeEmail($email),
162
+ 'phone' => static::normalizePhone($phone),
163
+ 'username' => User::generateUsername($name ?: Str::random(6)),
164
+ 'timezone' => $attributes['timezone'] ?? $company?->timezone ?? date_default_timezone_get(),
165
+ 'status' => $attributes['status'] ?? 'pending',
166
+ 'avatar_uuid' => $attributes['avatar_uuid'] ?? null,
167
+ 'country' => $attributes['country'] ?? null,
168
+ 'ip_address' => $attributes['ip_address'] ?? null,
169
+ 'meta' => $attributes['meta'] ?? null,
170
+ ], fn ($value) => $value !== null);
171
+
172
+ $user = new User($userData);
173
+ $user->password = $password ?: Str::random(16);
174
+ $user->save();
175
+ $user->setType($type);
176
+
177
+ static::attachToCompany($user, $companyUuid, $type);
178
+
179
+ return $user;
180
+ }
181
+
182
+ /**
183
+ * Push the profile's proxy fields to its account. A staff account only
184
+ * takes the name: its email and phone are its console identity.
185
+ *
186
+ * @throws ProfileIdentityConflictException
187
+ */
188
+ public static function syncProxyFields(?User $user, array $attributes): bool
189
+ {
190
+ if (!$user) {
191
+ return false;
192
+ }
193
+
194
+ $fields = static::isStaffAccount($user) ? ['name'] : ['name', 'email', 'phone', 'timezone', 'avatar_uuid', 'password'];
195
+ $input = array_intersect_key($attributes, array_flip($fields));
196
+
197
+ if (array_key_exists('email', $input)) {
198
+ $input['email'] = static::normalizeEmail($input['email']);
199
+ }
200
+
201
+ if (array_key_exists('phone', $input)) {
202
+ $input['phone'] = static::normalizePhone($input['phone']);
203
+ }
204
+
205
+ $input = array_filter($input, fn ($value, $key) => $value !== null || in_array($key, ['email', 'phone'], true), ARRAY_FILTER_USE_BOTH);
206
+ $input = array_filter($input, fn ($value, $key) => $key === 'password' || $user->{$key} !== $value, ARRAY_FILTER_USE_BOTH);
207
+
208
+ if (empty($input)) {
209
+ return false;
210
+ }
211
+
212
+ $changedEmail = array_key_exists('email', $input) ? $input['email'] : null;
213
+ $changedPhone = array_key_exists('phone', $input) ? $input['phone'] : null;
214
+ $taken = static::findAccountByIdentity($changedEmail, $changedPhone, $user->uuid);
215
+ if ($taken) {
216
+ throw new ProfileIdentityConflictException(static::takenMessage(static::matchedField($taken, $changedEmail)), static::matchedField($taken, $changedEmail));
217
+ }
218
+
219
+ if (isset($input['password'])) {
220
+ $user->password = $input['password'];
221
+ unset($input['password']);
222
+ }
223
+
224
+ $user->fill($input);
225
+
226
+ return $user->save();
227
+ }
228
+
229
+ /**
230
+ * Release a profile's account after the profile is deleted. A managed
231
+ * account with no other profile is deleted, which frees its email and
232
+ * phone; one still used by another organization's profile only leaves this
233
+ * organization. Staff accounts are never touched.
234
+ */
235
+ public static function releaseForProfile(?User $user, ?string $companyUuid = null): void
236
+ {
237
+ if (!static::isManagedAccount($user) || $user->trashed()) {
238
+ return;
239
+ }
240
+
241
+ $remainingProfiles = static::profileCompanies($user);
242
+ if ($remainingProfiles->isEmpty()) {
243
+ $user->delete();
244
+
245
+ return;
246
+ }
247
+
248
+ if ($companyUuid && !$remainingProfiles->contains($companyUuid)) {
249
+ CompanyUser::where(['company_uuid' => $companyUuid, 'user_uuid' => $user->uuid])->delete();
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Generate a new password for the account and send it to the profile.
255
+ *
256
+ * @return string how the credentials were sent: `email` or `sms`
257
+ */
258
+ public static function sendCredentials(Driver|Contact $profile, User $user, ?string $password = null): string
259
+ {
260
+ $password ??= Str::random(12);
261
+ $user->changePassword($password);
262
+
263
+ if ($user->status !== 'active') {
264
+ $user->activate();
265
+ }
266
+
267
+ return static::deliverCredentials($profile, $user, $password);
268
+ }
269
+
270
+ /**
271
+ * Send already set credentials to the profile by email, or by SMS when the
272
+ * account has no email.
273
+ */
274
+ public static function deliverCredentials(Driver|Contact $profile, User $user, string $password): string
275
+ {
276
+ if ($user->email) {
277
+ Mail::to($user)->send($profile instanceof Driver ? new DriverCredentialsMail($password, $profile, $user) : new CustomerCredentialsMail($password, $profile));
278
+
279
+ return 'email';
280
+ }
281
+
282
+ if ($user->phone) {
283
+ $profile->loadMissing('company');
284
+ $companyName = $profile->company?->name ?? config('app.name');
285
+ $identity = $user->phone;
286
+ $result = app(SmsService::class)->send($user->phone, "Your {$companyName} sign-in details. Login: {$identity} Password: {$password}");
287
+
288
+ if (is_array($result) && array_key_exists('success', $result) && !$result['success']) {
289
+ throw new \Exception('The credentials could not be texted: ' . ($result['error'] ?? $result['message'] ?? 'the SMS provider refused it.'));
290
+ }
291
+
292
+ return 'sms';
293
+ }
294
+
295
+ throw new \Exception('This profile has no email or phone to send credentials to.');
296
+ }
297
+
298
+ /**
299
+ * Throw when the account can't hold the kind of profile being created.
300
+ *
301
+ * @throws ProfileIdentityConflictException
302
+ */
303
+ public static function assertAccountCanHoldProfile(User $account, string $companyUuid, string $type, string $field = 'email'): void
304
+ {
305
+ if (static::isStaffAccount($account)) {
306
+ if (!static::isCompanyMember($account, $companyUuid)) {
307
+ throw new ProfileIdentityConflictException(static::takenMessage($field), $field);
308
+ }
309
+ } elseif ($account->type !== $type) {
310
+ throw new ProfileIdentityConflictException('This ' . static::fieldLabel($field) . ' is already used by a ' . $account->type . '.', $field);
311
+ }
312
+
313
+ if (static::hasProfileInCompany($account, $companyUuid, $type)) {
314
+ throw new ProfileIdentityConflictException('A ' . $type . ' with this ' . static::fieldLabel($field) . ' already exists.', $field);
315
+ }
316
+ }
317
+
318
+ public static function hasProfileInCompany(User $user, string $companyUuid, string $type): bool
319
+ {
320
+ if ($type === 'driver') {
321
+ return Driver::withoutGlobalScopes()->where(['user_uuid' => $user->uuid, 'company_uuid' => $companyUuid])->whereNull('deleted_at')->exists();
322
+ }
323
+
324
+ return Contact::withoutGlobalScopes()->where(['user_uuid' => $user->uuid, 'company_uuid' => $companyUuid])->whereNull('deleted_at')->exists();
325
+ }
326
+
327
+ /**
328
+ * The organizations the account still has a driver or contact profile in.
329
+ */
330
+ public static function profileCompanies(User $user): \Illuminate\Support\Collection
331
+ {
332
+ $drivers = Driver::withoutGlobalScopes()->where('user_uuid', $user->uuid)->whereNull('deleted_at')->pluck('company_uuid');
333
+ $contacts = Contact::withoutGlobalScopes()->where('user_uuid', $user->uuid)->whereNull('deleted_at')->pluck('company_uuid');
334
+
335
+ return $drivers->merge($contacts)->filter()->unique()->values();
336
+ }
337
+
338
+ public static function isCompanyMember(User $user, ?string $companyUuid): bool
339
+ {
340
+ if (!$companyUuid) {
341
+ return false;
342
+ }
343
+
344
+ return $user->company_uuid === $companyUuid || CompanyUser::where(['company_uuid' => $companyUuid, 'user_uuid' => $user->uuid])->exists();
345
+ }
346
+
347
+ /**
348
+ * Add a managed account to the organization with its profile role, without
349
+ * sending an organization invite.
350
+ */
351
+ public static function attachToCompany(User $user, string $companyUuid, string $type): ?CompanyUser
352
+ {
353
+ $company = Company::where('uuid', $companyUuid)->first();
354
+ if (!$company) {
355
+ return null;
356
+ }
357
+
358
+ $role = static::ROLES[$type] ?? static::ROLES['contact'];
359
+ $companyUser = CompanyUser::where(['company_uuid' => $companyUuid, 'user_uuid' => $user->uuid])->first();
360
+ if (!$companyUser) {
361
+ $companyUser = $company->addUser($user, $role);
362
+ } elseif (static::isManagedAccount($user)) {
363
+ $companyUser->assignSingleRole($role);
364
+ }
365
+
366
+ if (!$user->company_uuid) {
367
+ $user->forceFill(['company_uuid' => $companyUuid])->saveQuietly();
368
+ }
369
+
370
+ $user->setRelation('companyUser', $companyUser);
371
+
372
+ return $companyUser;
373
+ }
374
+
375
+ public static function normalizeEmail(?string $email): ?string
376
+ {
377
+ $email = is_string($email) ? strtolower(trim($email)) : null;
378
+
379
+ return $email ?: null;
380
+ }
381
+
382
+ public static function normalizePhone(?string $phone): ?string
383
+ {
384
+ $phone = is_string($phone) ? trim($phone) : null;
385
+
386
+ return $phone ? Utils::formatPhoneNumber($phone) : null;
387
+ }
388
+
389
+ private static function matchedField(User $account, ?string $email): string
390
+ {
391
+ $email = static::normalizeEmail($email);
392
+
393
+ return $email && strtolower((string) $account->email) === $email ? 'email' : 'phone';
394
+ }
395
+
396
+ private static function fieldLabel(string $field): string
397
+ {
398
+ return $field === 'phone' ? 'phone number' : 'email';
399
+ }
400
+
401
+ private static function takenMessage(string $field): string
402
+ {
403
+ return 'This ' . static::fieldLabel($field) . ' is already in use by another account.';
404
+ }
405
+ }