@fleetbase/fleetops-engine 0.6.58 → 0.6.60

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 (701) hide show
  1. package/README.md +3 -0
  2. package/RELEASE.md +36 -0
  3. package/addon/components/customer/form.hbs +0 -49
  4. package/addon/components/entity/card.hbs +1 -0
  5. package/addon/components/modals/order-label.hbs +1 -1
  6. package/addon/components/place/form.hbs +1 -1
  7. package/addon/components/vehicle/details/schedules.hbs +2 -2
  8. package/addon/components/vehicle/details/schedules.js +7 -1
  9. package/addon/controllers/maintenance/schedules/index/edit.js +10 -1
  10. package/addon/services/entity-actions.js +32 -0
  11. package/addon/services/vehicle-actions.js +4 -2
  12. package/addon/styles/fleetops-engine.css +0 -1
  13. package/composer.json +8 -3
  14. package/extension.json +1 -1
  15. package/index.js +7 -1
  16. package/package.json +2 -1
  17. package/scripts/coverage-file-runner.php +240 -0
  18. package/scripts/coverage-runner.php +2 -1
  19. package/scripts/coverage-summary.php +41 -2
  20. package/scripts/pest-bootstrap.php +237 -3
  21. package/scripts/pest-file-runner.php +13 -1
  22. package/scripts/pest-runner.php +57 -2
  23. package/server/config/fleetops.php +44 -0
  24. package/server/config/geocoder.php +1 -1
  25. package/server/migrations/2026_08_08_000001_make_sku_and_provider_transaction_unique_indexes_soft_delete_aware.php +152 -0
  26. package/server/resources/views/labels/entity-label.php +4 -4
  27. package/server/src/Casts/MultiPolygon.php +8 -3
  28. package/server/src/Casts/OrderConfigEntities.php +10 -3
  29. package/server/src/Casts/Point.php +10 -6
  30. package/server/src/Casts/Polygon.php +5 -2
  31. package/server/src/Console/Commands/AssignCustomerRoles.php +28 -6
  32. package/server/src/Console/Commands/AssignDriverRoles.php +36 -6
  33. package/server/src/Console/Commands/AuditCustomerUserConflicts.php +6 -1
  34. package/server/src/Console/Commands/DispatchAdhocOrders.php +12 -2
  35. package/server/src/Console/Commands/FixCustomerCompanies.php +46 -11
  36. package/server/src/Console/Commands/FixDriverCompanies.php +18 -3
  37. package/server/src/Console/Commands/FixLegacyOrderConfigs.php +31 -6
  38. package/server/src/Console/Commands/ProcessMaintenanceTriggers.php +94 -42
  39. package/server/src/Console/Commands/PurgeUnpurchasedServiceQuotes.php +42 -12
  40. package/server/src/Console/Commands/ReplayVehicleLocations.php +120 -55
  41. package/server/src/Console/Commands/SendDriverNotification.php +25 -4
  42. package/server/src/Console/Commands/SendMaintenanceReminders.php +43 -23
  43. package/server/src/Console/Commands/SimulateOrderRouteNavigation.php +36 -6
  44. package/server/src/Console/Commands/TrackOrderDistanceAndTime.php +6 -1
  45. package/server/src/Events/DriverLocationChanged.php +1 -1
  46. package/server/src/Events/DriverSimulatedLocationChanged.php +1 -1
  47. package/server/src/Events/EntityActivityChanged.php +1 -1
  48. package/server/src/Events/EntityCompleted.php +1 -1
  49. package/server/src/Events/EntityDriverAssigned.php +1 -1
  50. package/server/src/Events/FuelProviderTransactionImported.php +1 -1
  51. package/server/src/Events/FuelProviderTransactionMatched.php +1 -1
  52. package/server/src/Events/FuelProviderTransactionUnmatched.php +1 -1
  53. package/server/src/Events/FuelReportCreatedFromProvider.php +1 -1
  54. package/server/src/Events/GeofenceDwelled.php +1 -1
  55. package/server/src/Events/GeofenceEntered.php +1 -1
  56. package/server/src/Events/GeofenceExited.php +1 -1
  57. package/server/src/Events/VehicleLocationChanged.php +1 -1
  58. package/server/src/Events/WaypointActivityChanged.php +1 -1
  59. package/server/src/Events/WaypointCompleted.php +1 -1
  60. package/server/src/Exceptions/IntegratedVendorException.php +2 -2
  61. package/server/src/Exceptions/TelematicRateLimitExceededException.php +1 -1
  62. package/server/src/Http/Controllers/Api/v1/ContactController.php +89 -22
  63. package/server/src/Http/Controllers/Api/v1/CustomerController.php +346 -101
  64. package/server/src/Http/Controllers/Api/v1/DeviceController.php +48 -14
  65. package/server/src/Http/Controllers/Api/v1/DriverController.php +224 -64
  66. package/server/src/Http/Controllers/Api/v1/EntityController.php +112 -66
  67. package/server/src/Http/Controllers/Api/v1/EquipmentController.php +40 -10
  68. package/server/src/Http/Controllers/Api/v1/FleetController.php +54 -14
  69. package/server/src/Http/Controllers/Api/v1/FuelReportController.php +54 -14
  70. package/server/src/Http/Controllers/Api/v1/FuelTransactionController.php +36 -11
  71. package/server/src/Http/Controllers/Api/v1/GeofenceController.php +73 -23
  72. package/server/src/Http/Controllers/Api/v1/IssueController.php +54 -14
  73. package/server/src/Http/Controllers/Api/v1/LabelController.php +61 -22
  74. package/server/src/Http/Controllers/Api/v1/NavigatorController.php +32 -15
  75. package/server/src/Http/Controllers/Api/v1/OrderConfigController.php +35 -5
  76. package/server/src/Http/Controllers/Api/v1/OrderController.php +388 -203
  77. package/server/src/Http/Controllers/Api/v1/PartController.php +40 -10
  78. package/server/src/Http/Controllers/Api/v1/PayloadController.php +90 -30
  79. package/server/src/Http/Controllers/Api/v1/PlaceController.php +188 -91
  80. package/server/src/Http/Controllers/Api/v1/PurchaseRateController.php +100 -36
  81. package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +96 -26
  82. package/server/src/Http/Controllers/Api/v1/ServiceQuoteController.php +169 -81
  83. package/server/src/Http/Controllers/Api/v1/ServiceRateController.php +104 -75
  84. package/server/src/Http/Controllers/Api/v1/TrackingNumberController.php +92 -14
  85. package/server/src/Http/Controllers/Api/v1/TrackingStatusController.php +66 -16
  86. package/server/src/Http/Controllers/Api/v1/VehicleController.php +121 -67
  87. package/server/src/Http/Controllers/Api/v1/VendorController.php +54 -14
  88. package/server/src/Http/Controllers/Api/v1/WorkOrderController.php +47 -12
  89. package/server/src/Http/Controllers/Api/v1/ZoneController.php +84 -24
  90. package/server/src/Http/Controllers/Concerns/ResolvesReviewAccountBypass.php +62 -0
  91. package/server/src/Http/Controllers/Internal/v1/AnalyticsController.php +15 -5
  92. package/server/src/Http/Controllers/Internal/v1/ContactController.php +113 -28
  93. package/server/src/Http/Controllers/Internal/v1/CustomerController.php +36 -11
  94. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +9 -4
  95. package/server/src/Http/Controllers/Internal/v1/DriverController.php +168 -70
  96. package/server/src/Http/Controllers/Internal/v1/EquipmentController.php +18 -3
  97. package/server/src/Http/Controllers/Internal/v1/FleetController.php +137 -60
  98. package/server/src/Http/Controllers/Internal/v1/FleetOpsLookupController.php +24 -9
  99. package/server/src/Http/Controllers/Internal/v1/FuelReportController.php +18 -3
  100. package/server/src/Http/Controllers/Internal/v1/GeocoderController.php +26 -8
  101. package/server/src/Http/Controllers/Internal/v1/GettingStartedController.php +12 -2
  102. package/server/src/Http/Controllers/Internal/v1/IntegratedVendorController.php +27 -6
  103. package/server/src/Http/Controllers/Internal/v1/IssueController.php +82 -29
  104. package/server/src/Http/Controllers/Internal/v1/MaintenanceController.php +34 -26
  105. package/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php +141 -97
  106. package/server/src/Http/Controllers/Internal/v1/ManifestController.php +28 -8
  107. package/server/src/Http/Controllers/Internal/v1/MetricsController.php +21 -12
  108. package/server/src/Http/Controllers/Internal/v1/MorphController.php +90 -18
  109. package/server/src/Http/Controllers/Internal/v1/NavigatorController.php +88 -35
  110. package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +149 -39
  111. package/server/src/Http/Controllers/Internal/v1/OrderConfigController.php +54 -18
  112. package/server/src/Http/Controllers/Internal/v1/OrderController.php +298 -86
  113. package/server/src/Http/Controllers/Internal/v1/PartController.php +18 -3
  114. package/server/src/Http/Controllers/Internal/v1/PaymentController.php +46 -14
  115. package/server/src/Http/Controllers/Internal/v1/PlaceController.php +31 -5
  116. package/server/src/Http/Controllers/Internal/v1/ProofController.php +88 -48
  117. package/server/src/Http/Controllers/Internal/v1/SearchController.php +1 -1
  118. package/server/src/Http/Controllers/Internal/v1/SensorController.php +6 -1
  119. package/server/src/Http/Controllers/Internal/v1/ServiceAreaController.php +6 -1
  120. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +255 -105
  121. package/server/src/Http/Controllers/Internal/v1/SettingController.php +88 -35
  122. package/server/src/Http/Controllers/Internal/v1/TelematicController.php +1 -4
  123. package/server/src/Http/Controllers/Internal/v1/Traits/DriverSchedulingTrait.php +42 -14
  124. package/server/src/Http/Controllers/Internal/v1/VendorController.php +103 -33
  125. package/server/src/Http/Controllers/Internal/v1/WorkOrderController.php +42 -12
  126. package/server/src/Http/Filter/DriverFilter.php +2 -2
  127. package/server/src/Http/Filter/MaintenanceScheduleFilter.php +40 -0
  128. package/server/src/Http/Filter/OrderFilter.php +1 -1
  129. package/server/src/Http/Filter/VehicleFilter.php +21 -7
  130. package/server/src/Http/Middleware/AuthenticateCustomerToken.php +12 -2
  131. package/server/src/Http/Requests/BulkDispatchRequest.php +3 -9
  132. package/server/src/Http/Requests/CancelOrderRequest.php +2 -6
  133. package/server/src/Http/Requests/CreateContactRequest.php +2 -6
  134. package/server/src/Http/Requests/CreateCustomerRequest.php +9 -1
  135. package/server/src/Http/Requests/CreateDeviceRequest.php +2 -2
  136. package/server/src/Http/Requests/CreateDriverRequest.php +3 -9
  137. package/server/src/Http/Requests/CreateEntityRequest.php +2 -6
  138. package/server/src/Http/Requests/CreateEquipmentRequest.php +2 -2
  139. package/server/src/Http/Requests/CreateFleetRequest.php +2 -6
  140. package/server/src/Http/Requests/CreateFuelReportRequest.php +16 -9
  141. package/server/src/Http/Requests/CreateFuelTransactionRequest.php +66 -3
  142. package/server/src/Http/Requests/CreateIssueRequest.php +2 -6
  143. package/server/src/Http/Requests/CreateOrderRequest.php +7 -8
  144. package/server/src/Http/Requests/CreatePartRequest.php +41 -3
  145. package/server/src/Http/Requests/CreatePayloadRequest.php +7 -9
  146. package/server/src/Http/Requests/CreatePlaceRequest.php +2 -6
  147. package/server/src/Http/Requests/CreatePurchaseRateRequest.php +2 -6
  148. package/server/src/Http/Requests/CreateSensorRequest.php +2 -2
  149. package/server/src/Http/Requests/CreateServiceAreaRequest.php +2 -6
  150. package/server/src/Http/Requests/CreateServiceQuoteRequest.php +2 -6
  151. package/server/src/Http/Requests/CreateServiceRateRequest.php +2 -6
  152. package/server/src/Http/Requests/CreateTrackingNumberRequest.php +2 -6
  153. package/server/src/Http/Requests/CreateTrackingStatusRequest.php +2 -6
  154. package/server/src/Http/Requests/CreateVehicleRequest.php +2 -6
  155. package/server/src/Http/Requests/CreateVendorRequest.php +2 -6
  156. package/server/src/Http/Requests/CreateWorkOrderRequest.php +2 -2
  157. package/server/src/Http/Requests/CreateZoneRequest.php +2 -6
  158. package/server/src/Http/Requests/DecodeTrackingNumberQR.php +2 -6
  159. package/server/src/Http/Requests/DriverSimulationRequest.php +2 -6
  160. package/server/src/Http/Requests/Internal/AssignOrderRequest.php +3 -7
  161. package/server/src/Http/Requests/Internal/CreateDriverRequest.php +4 -12
  162. package/server/src/Http/Requests/Internal/CreateOrderConfigRequest.php +2 -6
  163. package/server/src/Http/Requests/Internal/CreateOrderRequest.php +7 -8
  164. package/server/src/Http/Requests/Internal/FleetActionRequest.php +18 -12
  165. package/server/src/Http/Requests/Internal/UpdateDriverRequest.php +6 -3
  166. package/server/src/Http/Requests/QueryServiceQuotesRequest.php +2 -6
  167. package/server/src/Http/Requests/ScheduleOrderRequest.php +2 -6
  168. package/server/src/Http/Requests/UpdateIssueRequest.php +1 -3
  169. package/server/src/Http/Resources/v1/Customer.php +2 -2
  170. package/server/src/Http/Resources/v1/Maintenance.php +3 -4
  171. package/server/src/Http/Resources/v1/MaintenanceSchedule.php +3 -4
  172. package/server/src/Http/Resources/v1/Order.php +3 -7
  173. package/server/src/Http/Resources/v1/PurchaseRate.php +4 -6
  174. package/server/src/Http/Resources/v1/TrackingNumber.php +33 -0
  175. package/server/src/Http/Resources/v1/TrackingStatus.php +4 -6
  176. package/server/src/Http/Resources/v1/WorkOrder.php +3 -4
  177. package/server/src/Imports/ContactImport.php +10 -1
  178. package/server/src/Imports/DriverImport.php +10 -1
  179. package/server/src/Imports/EquipmentImport.php +6 -1
  180. package/server/src/Imports/FleetImport.php +10 -1
  181. package/server/src/Imports/FuelReportImport.php +10 -1
  182. package/server/src/Imports/IssueImport.php +10 -1
  183. package/server/src/Imports/MaintenanceImport.php +6 -1
  184. package/server/src/Imports/MaintenanceScheduleImport.php +6 -1
  185. package/server/src/Imports/PartImport.php +6 -1
  186. package/server/src/Imports/PlaceImport.php +10 -1
  187. package/server/src/Imports/VehicleImport.php +10 -1
  188. package/server/src/Imports/VendorImport.php +10 -1
  189. package/server/src/Imports/WorkOrderImport.php +6 -1
  190. package/server/src/Integrations/Lalamove/Lalamove.php +3 -7
  191. package/server/src/Integrations/Lalamove/LalamoveServiceType.php +5 -8
  192. package/server/src/Jobs/CheckGeofenceDwell.php +41 -16
  193. package/server/src/Jobs/FinalizeApiOrderCreation.php +17 -2
  194. package/server/src/Jobs/FinalizeInternalOrderCreation.php +11 -1
  195. package/server/src/Jobs/NotifyBulkAssignedDriver.php +29 -16
  196. package/server/src/Jobs/ProcessAllocationJob.php +59 -25
  197. package/server/src/Jobs/ReplayPositions.php +17 -10
  198. package/server/src/Jobs/SendPositionReplay.php +18 -7
  199. package/server/src/Jobs/SimulateDrivingRoute.php +14 -3
  200. package/server/src/Jobs/SyncFuelProviderTransactionsJob.php +12 -2
  201. package/server/src/Listeners/HandleDeliveryCompletion.php +11 -1
  202. package/server/src/Listeners/HandleGeofenceDwelled.php +6 -1
  203. package/server/src/Listeners/HandleGeofenceEntered.php +4 -2
  204. package/server/src/Listeners/HandleGeofenceExited.php +6 -1
  205. package/server/src/Listeners/HandleOrderCanceled.php +18 -3
  206. package/server/src/Listeners/HandleOrderDispatchFailed.php +12 -2
  207. package/server/src/Listeners/HandleOrderDispatched.php +61 -26
  208. package/server/src/Listeners/HandleOrderDriverAssigned.php +12 -2
  209. package/server/src/Listeners/HandleUserRemovedFromCompany.php +19 -6
  210. package/server/src/Listeners/NotifyDriverOnShiftChange.php +24 -4
  211. package/server/src/Listeners/NotifyOrderEvent.php +11 -6
  212. package/server/src/Models/Contact.php +6 -7
  213. package/server/src/Models/Device.php +26 -3
  214. package/server/src/Models/Driver.php +0 -6
  215. package/server/src/Models/Entity.php +4 -4
  216. package/server/src/Models/Equipment.php +1 -1
  217. package/server/src/Models/FuelReport.php +26 -1
  218. package/server/src/Models/IntegratedVendor.php +2 -2
  219. package/server/src/Models/Issue.php +5 -1
  220. package/server/src/Models/MaintenanceSchedule.php +25 -10
  221. package/server/src/Models/Order.php +18 -15
  222. package/server/src/Models/OrderConfig.php +1 -1
  223. package/server/src/Models/Payload.php +9 -16
  224. package/server/src/Models/Place.php +20 -13
  225. package/server/src/Models/Sensor.php +24 -0
  226. package/server/src/Models/ServiceArea.php +3 -6
  227. package/server/src/Models/ServiceQuote.php +5 -0
  228. package/server/src/Models/ServiceRate.php +13 -6
  229. package/server/src/Models/Telematic.php +2 -2
  230. package/server/src/Models/TrackingNumber.php +55 -10
  231. package/server/src/Models/TrackingStatus.php +24 -4
  232. package/server/src/Models/Waypoint.php +44 -10
  233. package/server/src/Models/Zone.php +3 -6
  234. package/server/src/Notifications/DriverArrivedAtGeofence.php +1 -1
  235. package/server/src/Notifications/OrderDispatched.php +3 -9
  236. package/server/src/Notifications/OrderPing.php +3 -9
  237. package/server/src/Observers/CategoryObserver.php +6 -1
  238. package/server/src/Observers/CompanyObserver.php +5 -0
  239. package/server/src/Observers/CompanyUserObserver.php +12 -2
  240. package/server/src/Observers/DriverObserver.php +19 -4
  241. package/server/src/Observers/FleetObserver.php +6 -1
  242. package/server/src/Observers/PurchaseRateObserver.php +66 -11
  243. package/server/src/Observers/ServiceAreaObserver.php +12 -2
  244. package/server/src/Observers/ServiceRateObserver.php +21 -6
  245. package/server/src/Observers/TrackingNumberObserver.php +19 -4
  246. package/server/src/Observers/UserObserver.php +6 -1
  247. package/server/src/Observers/VehicleObserver.php +27 -7
  248. package/server/src/Observers/WorkOrderObserver.php +27 -7
  249. package/server/src/Orchestration/Engines/DriverAssignmentEngine.php +8 -3
  250. package/server/src/Orchestration/Support/OrchestrationPayloadBuilder.php +2 -2
  251. package/server/src/Providers/FleetOpsServiceProvider.php +12 -0
  252. package/server/src/Providers/NotificationServiceProvider.php +6 -0
  253. package/server/src/Support/Ai/Capabilities/AssetStatusCapability.php +36 -26
  254. package/server/src/Support/Ai/Capabilities/CreateOrderPreviewCapability.php +18 -5
  255. package/server/src/Support/Ai/Capabilities/OrderInsightsCapability.php +6 -1
  256. package/server/src/Support/Ai/Capabilities/SearchResourcesCapability.php +26 -6
  257. package/server/src/Support/Analytics/FuelEfficiency.php +9 -3
  258. package/server/src/Support/Analytics/FuelProviderSummary.php +14 -5
  259. package/server/src/Support/Analytics/GeofenceViolations.php +40 -20
  260. package/server/src/Support/Analytics/IssuesInsights.php +38 -18
  261. package/server/src/Support/Analytics/MaintenanceOverview.php +51 -21
  262. package/server/src/Support/Analytics/OnTimeDelivery.php +3 -2
  263. package/server/src/Support/Analytics/OperationsPulse.php +59 -35
  264. package/server/src/Support/Analytics/OrdersByStatus.php +12 -7
  265. package/server/src/Support/Analytics/RevenueTrend.php +16 -6
  266. package/server/src/Support/Analytics/TopDrivers.php +3 -2
  267. package/server/src/Support/FleetOps.php +109 -104
  268. package/server/src/Support/FuelProviders/FuelProviderService.php +10 -1
  269. package/server/src/Support/Geocoding.php +52 -14
  270. package/server/src/Support/GeofenceIntersectionService.php +47 -27
  271. package/server/src/Support/LiveOrderQuery.php +6 -1
  272. package/server/src/Support/Metrics/ActiveRevenueQuery.php +3 -2
  273. package/server/src/Support/Metrics/FuelCostsMetric.php +6 -1
  274. package/server/src/Support/ResolvesOrderServiceStops.php +10 -0
  275. package/server/src/Support/Telematics/Providers/GeotabProvider.php +9 -4
  276. package/server/src/Support/Utils.php +122 -16
  277. package/server/src/Tracking/Providers/GoogleRoutesTrackingProvider.php +6 -1
  278. package/server/src/Tracking/Providers/OsrmTrackingProvider.php +11 -6
  279. package/server/src/Tracking/TrackingProviderManager.php +2 -3
  280. package/server/src/Tracking/TrackingProviderRegistry.php +8 -4
  281. package/server/src/Traits/PayloadAccessors.php +6 -1
  282. package/server/src/routes.php +24 -17
  283. package/server/tests/AiOperationalQueryCapabilityTest.php +457 -1
  284. package/server/tests/AiPreviewCapabilityContractsTest.php +180 -0
  285. package/server/tests/AnalyticsRoutesTest.php +380 -1
  286. package/server/tests/AnalyticsWidgetContractsTest.php +740 -0
  287. package/server/tests/ApiContactControllerContractsTest.php +284 -0
  288. package/server/tests/ApiCustomerControllerContractsTest.php +1130 -0
  289. package/server/tests/ApiDriverControllerContractsTest.php +603 -0
  290. package/server/tests/ApiEntityControllerContractsTest.php +329 -0
  291. package/server/tests/ApiEquipmentControllerContractsTest.php +239 -0
  292. package/server/tests/ApiFleetControllerContractsTest.php +176 -0
  293. package/server/tests/ApiFuelReportControllerContractsTest.php +217 -0
  294. package/server/tests/ApiIssueControllerContractsTest.php +217 -0
  295. package/server/tests/ApiOrderControllerContractsTest.php +1030 -0
  296. package/server/tests/ApiPartControllerContractsTest.php +251 -0
  297. package/server/tests/ApiPayloadControllerContractsTest.php +314 -0
  298. package/server/tests/ApiPlaceControllerContractsTest.php +541 -0
  299. package/server/tests/ApiPurchaseRateControllerContractsTest.php +283 -0
  300. package/server/tests/ApiServiceAreaZoneControllerContractsTest.php +442 -0
  301. package/server/tests/ApiServiceQuoteControllerContractsTest.php +322 -0
  302. package/server/tests/ApiTrackingNumberControllerContractsTest.php +225 -0
  303. package/server/tests/ApiTrackingStatusControllerContractsTest.php +254 -0
  304. package/server/tests/ApiVehicleControllerContractsTest.php +368 -0
  305. package/server/tests/ApiVendorControllerContractsTest.php +199 -0
  306. package/server/tests/AssetAndPlaceSearchContractsTest.php +220 -0
  307. package/server/tests/BackendSmallModelContractsTest.php +354 -0
  308. package/server/tests/CapacityAllocationEngineTest.php +56 -0
  309. package/server/tests/CommandContractsTest.php +2695 -0
  310. package/server/tests/CompactResourceSerializationTest.php +2838 -0
  311. package/server/tests/ControllerFilterContractsTest.php +1273 -0
  312. package/server/tests/ControllerHelperContractsTest.php +1740 -0
  313. package/server/tests/ControllerSerializationContractsTest.php +141 -0
  314. package/server/tests/CoverageSummaryScriptTest.php +93 -0
  315. package/server/tests/CustomerContactIdentitySafetyTest.php +1 -6
  316. package/server/tests/CustomerEndpointTest.php +169 -2
  317. package/server/tests/DriverFilterExecutionTest.php +263 -0
  318. package/server/tests/DriverGeofenceProcessingTest.php +153 -0
  319. package/server/tests/DriverResourceSerializationTest.php +245 -0
  320. package/server/tests/DriverSchedulingTraitContractsTest.php +259 -0
  321. package/server/tests/EventContractsTest.php +1259 -0
  322. package/server/tests/ExceptionContractsTest.php +81 -0
  323. package/server/tests/ExpansionContractsTest.php +242 -0
  324. package/server/tests/ExportCoverageTest.php +116 -0
  325. package/server/tests/Feature/Http/Api/ContactControllerPhotoAndDeleteTest.php +199 -0
  326. package/server/tests/Feature/Http/Api/CustomerControllerHelperSeamsTest.php +831 -0
  327. package/server/tests/Feature/Http/Api/DeviceControllerContractsTest.php +347 -0
  328. package/server/tests/Feature/Http/Api/DeviceControllerCrudTest.php +176 -0
  329. package/server/tests/Feature/Http/Api/DriverControllerAuthFlowsTest.php +530 -0
  330. package/server/tests/Feature/Http/Api/DriverControllerHelpersTest.php +240 -0
  331. package/server/tests/Feature/Http/Api/DriverControllerLookupTest.php +186 -0
  332. package/server/tests/Feature/Http/Api/DriverControllerOrganizationsTest.php +112 -0
  333. package/server/tests/Feature/Http/Api/DriverControllerSessionHelpersTest.php +207 -0
  334. package/server/tests/Feature/Http/Api/DriverControllerSwitchOrgAndGeofenceTest.php +329 -0
  335. package/server/tests/Feature/Http/Api/DriverControllerTrackTest.php +383 -0
  336. package/server/tests/Feature/Http/Api/EntityControllerHelpersTest.php +150 -0
  337. package/server/tests/Feature/Http/Api/FuelTransactionControllerContractsTest.php +352 -0
  338. package/server/tests/Feature/Http/Api/GeofenceControllerContractsTest.php +461 -0
  339. package/server/tests/Feature/Http/Api/LabelControllerSubjectsTest.php +148 -0
  340. package/server/tests/Feature/Http/Api/OrderConfigControllerTest.php +112 -0
  341. package/server/tests/Feature/Http/Api/OrderControllerCreateTest.php +279 -0
  342. package/server/tests/Feature/Http/Api/OrderControllerIntegratedVendorTest.php +295 -0
  343. package/server/tests/Feature/Http/Api/OrderControllerLifecycleActionsTest.php +279 -0
  344. package/server/tests/Feature/Http/Api/OrderControllerLookupHelpersTest.php +207 -0
  345. package/server/tests/Feature/Http/Api/OrderControllerNearbyFiltersTest.php +282 -0
  346. package/server/tests/Feature/Http/Api/OrderControllerPersistenceHelpersTest.php +216 -0
  347. package/server/tests/Feature/Http/Api/OrderControllerQueryFiltersTest.php +184 -0
  348. package/server/tests/Feature/Http/Api/OrderControllerStartActivityTest.php +663 -0
  349. package/server/tests/Feature/Http/Api/PayloadControllerHelpersTest.php +134 -0
  350. package/server/tests/Feature/Http/Api/PlaceAndServiceQuoteSeamsTest.php +479 -0
  351. package/server/tests/Feature/Http/Api/PurchaseRateControllerHelpersTest.php +176 -0
  352. package/server/tests/Feature/Http/Api/PurchaseRateControllerOrderCreationTest.php +268 -0
  353. package/server/tests/Feature/Http/Api/SensorControllerCrudTest.php +239 -0
  354. package/server/tests/Feature/Http/Api/ServiceAreaControllerBorderAndSeamsTest.php +187 -0
  355. package/server/tests/Feature/Http/Api/ServiceQuoteControllerQueryTest.php +405 -0
  356. package/server/tests/Feature/Http/Api/ServiceQuoteControllerVendorBridgeTest.php +212 -0
  357. package/server/tests/Feature/Http/Api/ServiceRateControllerContractsTest.php +263 -0
  358. package/server/tests/Feature/Http/Api/SmallApiControllerHelpersTest.php +272 -0
  359. package/server/tests/Feature/Http/Api/TrackingNumberControllerHelpersTest.php +160 -0
  360. package/server/tests/Feature/Http/Api/TrackingStatusControllerHelpersTest.php +165 -0
  361. package/server/tests/Feature/Http/Api/VehicleControllerTrackingTest.php +483 -0
  362. package/server/tests/Feature/Http/Api/WorkOrderControllerContractsTest.php +326 -0
  363. package/server/tests/Feature/Http/Api/WorkOrderControllerHelpersTest.php +130 -0
  364. package/server/tests/Feature/Http/Api/ZoneControllerBordersAndSeamsTest.php +294 -0
  365. package/server/tests/Feature/Http/FleetOpsRouteRegistrationTest.php +189 -0
  366. package/server/tests/Feature/Http/Internal/AnalyticsControllerSeamsTest.php +53 -0
  367. package/server/tests/Feature/Http/Internal/ContactControllerContractsTest.php +240 -0
  368. package/server/tests/Feature/Http/Internal/ContactControllerCustomerGuardsTest.php +128 -0
  369. package/server/tests/Feature/Http/Internal/ContactControllerEndpointsTest.php +294 -0
  370. package/server/tests/Feature/Http/Internal/CustomerControllerCredentialsTest.php +196 -0
  371. package/server/tests/Feature/Http/Internal/DeviceControllerContractsTest.php +370 -0
  372. package/server/tests/Feature/Http/Internal/DeviceControllerExportAndFiltersTest.php +151 -0
  373. package/server/tests/Feature/Http/Internal/DriverControllerContractsTest.php +1167 -0
  374. package/server/tests/Feature/Http/Internal/DriverControllerCreateRecordTest.php +233 -0
  375. package/server/tests/Feature/Http/Internal/DriverControllerExistingUserAdoptionTest.php +710 -0
  376. package/server/tests/Feature/Http/Internal/DriverControllerHelpersTest.php +218 -0
  377. package/server/tests/Feature/Http/Internal/FleetAssignmentsAndTriggersTest.php +198 -0
  378. package/server/tests/Feature/Http/Internal/FleetOpsLookupControllerDbTest.php +90 -0
  379. package/server/tests/Feature/Http/Internal/HubControllerTest.php +206 -0
  380. package/server/tests/Feature/Http/Internal/IssueControllerContractsTest.php +404 -0
  381. package/server/tests/Feature/Http/Internal/IssueControllerExportImportTest.php +118 -0
  382. package/server/tests/Feature/Http/Internal/LiveControllerEndpointsTest.php +246 -0
  383. package/server/tests/Feature/Http/Internal/LiveControllerTest.php +778 -0
  384. package/server/tests/Feature/Http/Internal/MaintenanceScheduleControllerContractsTest.php +263 -0
  385. package/server/tests/Feature/Http/Internal/MaintenanceScheduleControllerSurfacesTest.php +214 -0
  386. package/server/tests/Feature/Http/Internal/MetricsControllerContractsTest.php +249 -0
  387. package/server/tests/Feature/Http/Internal/MetricsControllerHelpersTest.php +66 -0
  388. package/server/tests/Feature/Http/Internal/MorphControllerHelpersTest.php +125 -0
  389. package/server/tests/Feature/Http/Internal/NavigatorControllerHelpersTest.php +201 -0
  390. package/server/tests/Feature/Http/Internal/OrchestrationControllerContractsTest.php +928 -0
  391. package/server/tests/Feature/Http/Internal/OrchestrationControllerHelpersTest.php +214 -0
  392. package/server/tests/Feature/Http/Internal/OrchestrationControllerImportOrdersTest.php +340 -0
  393. package/server/tests/Feature/Http/Internal/OrderConfigControllerContractsTest.php +314 -0
  394. package/server/tests/Feature/Http/Internal/OrderControllerActivityFlowsTest.php +663 -0
  395. package/server/tests/Feature/Http/Internal/OrderControllerCapturePhotoTest.php +507 -0
  396. package/server/tests/Feature/Http/Internal/OrderControllerContractsTest.php +1742 -0
  397. package/server/tests/Feature/Http/Internal/OrderControllerCreateRecordTest.php +488 -0
  398. package/server/tests/Feature/Http/Internal/OrderControllerHelperSeamsTest.php +298 -0
  399. package/server/tests/Feature/Http/Internal/OrderControllerHelpersTest.php +267 -0
  400. package/server/tests/Feature/Http/Internal/OrderControllerImportFromFilesTest.php +179 -0
  401. package/server/tests/Feature/Http/Internal/OrderControllerUpstreamNotFoundTest.php +80 -0
  402. package/server/tests/Feature/Http/Internal/PaymentControllerPaymentsTest.php +154 -0
  403. package/server/tests/Feature/Http/Internal/PlaceControllerTest.php +252 -0
  404. package/server/tests/Feature/Http/Internal/PositionControllerReplayMetricsTest.php +163 -0
  405. package/server/tests/Feature/Http/Internal/ProofControllerContractsTest.php +219 -0
  406. package/server/tests/Feature/Http/Internal/SearchControllerEndpointTest.php +151 -0
  407. package/server/tests/Feature/Http/Internal/ServiceQuoteControllerHelpersTest.php +208 -0
  408. package/server/tests/Feature/Http/Internal/ServiceQuoteControllerQueryRecordTest.php +217 -0
  409. package/server/tests/Feature/Http/Internal/ServiceQuoteControllerVendorBridgeTest.php +239 -0
  410. package/server/tests/Feature/Http/Internal/ServiceQuotePreliminaryQueryTest.php +271 -0
  411. package/server/tests/Feature/Http/Internal/SettingControllerHelpersTest.php +133 -0
  412. package/server/tests/Feature/Http/Internal/SmallInternalControllersTest.php +180 -0
  413. package/server/tests/Feature/Http/Internal/TelematicControllerEndpointsTest.php +417 -0
  414. package/server/tests/Feature/Http/Internal/VehicleControllerEndpointsTest.php +361 -0
  415. package/server/tests/Feature/Http/Internal/VendorControllerEndpointsTest.php +203 -0
  416. package/server/tests/Feature/Http/Internal/WorkOrderAndConfigSurfacesTest.php +289 -0
  417. package/server/tests/Feature/Http/Internal/WorkOrderControllerContractsTest.php +199 -0
  418. package/server/tests/Feature/Http/TelematicWebhookIngestTest.php +343 -0
  419. package/server/tests/Feature/Http/Telematics/TelematicWebhookControllerProcessingTest.php +254 -0
  420. package/server/tests/FleetOpsSupportContractsTest.php +187 -0
  421. package/server/tests/FlespiProviderContractsTest.php +244 -0
  422. package/server/tests/FlowResourceTest.php +295 -0
  423. package/server/tests/FuelProviderConnectionControllerContractsTest.php +303 -0
  424. package/server/tests/FuelProviderImplementationTest.php +753 -0
  425. package/server/tests/FuelProviderResourceContractsTest.php +171 -0
  426. package/server/tests/FuelProviderTransactionControllerContractsTest.php +198 -0
  427. package/server/tests/GeofenceIntersectionServiceContractsTest.php +164 -0
  428. package/server/tests/HOSConstraintContractsTest.php +128 -0
  429. package/server/tests/HubAndGettingStartedContractsTest.php +158 -0
  430. package/server/tests/ImportContractsTest.php +191 -0
  431. package/server/tests/InternalCustomerControllerContractsTest.php +238 -0
  432. package/server/tests/InternalFleetControllerContractsTest.php +389 -0
  433. package/server/tests/InternalImportExportControllerContractsTest.php +289 -0
  434. package/server/tests/InternalMetricsControllerContractsTest.php +69 -0
  435. package/server/tests/InternalPlaceWorkOrderControllerContractsTest.php +207 -0
  436. package/server/tests/InternalServiceQuoteCheckoutControllerContractsTest.php +470 -0
  437. package/server/tests/InternalVendorControllerContractsTest.php +331 -0
  438. package/server/tests/InventoryTelemetryModelContractsTest.php +516 -0
  439. package/server/tests/IssueTimelineContractsTest.php +191 -0
  440. package/server/tests/LabelPdfRenderingTest.php +57 -0
  441. package/server/tests/LalamoveIntegrationContractsTest.php +167 -0
  442. package/server/tests/LiveOrderQueryTest.php +145 -18
  443. package/server/tests/LookupAndGeocoderControllerContractsTest.php +357 -0
  444. package/server/tests/MaintainableTraitContractsTest.php +245 -0
  445. package/server/tests/MaintenanceScheduleControllerContractsTest.php +101 -0
  446. package/server/tests/MaintenanceWarrantyContractsTest.php +237 -0
  447. package/server/tests/ManifestControllerContractsTest.php +310 -0
  448. package/server/tests/MetricsRegistryTest.php +596 -10
  449. package/server/tests/MiddlewareContractsTest.php +150 -0
  450. package/server/tests/ModelAccessorContractsTest.php +3201 -0
  451. package/server/tests/MorphControllerContractsTest.php +410 -0
  452. package/server/tests/NavigatorControllerContractsTest.php +206 -0
  453. package/server/tests/NotificationAndMailContractsTest.php +677 -0
  454. package/server/tests/ObserverContractsTest.php +1414 -0
  455. package/server/tests/OperationalAlgorithmContractsTest.php +852 -0
  456. package/server/tests/OperationsMonitorEndpointTest.php +2 -1
  457. package/server/tests/OrchestrationPayloadBuilderTest.php +87 -0
  458. package/server/tests/OrderActivityFlowRegressionTest.php +354 -7
  459. package/server/tests/OrderDispatchListenerContractsTest.php +577 -0
  460. package/server/tests/OrderFilterExecutionTest.php +244 -0
  461. package/server/tests/OrderQueuedClosureRegressionTest.php +2 -1
  462. package/server/tests/PaymentControllerContractsTest.php +199 -0
  463. package/server/tests/PointResolutionTest.php +54 -0
  464. package/server/tests/ProcessAllocationJobContractsTest.php +224 -0
  465. package/server/tests/ProofAndSensorControllerContractsTest.php +44 -0
  466. package/server/tests/ProviderContractsTest.php +409 -0
  467. package/server/tests/PublicApiControllerInputContractsTest.php +294 -0
  468. package/server/tests/QueueJobContractsTest.php +764 -0
  469. package/server/tests/ReportSchemaContractsTest.php +271 -0
  470. package/server/tests/RequestContractsTest.php +1058 -0
  471. package/server/tests/RouteRegistrationExecutionTest.php +178 -0
  472. package/server/tests/RouteSequencingEngineTest.php +179 -0
  473. package/server/tests/RulesAndCastsTest.php +246 -0
  474. package/server/tests/SamsaraProviderContractsTest.php +272 -0
  475. package/server/tests/SearchControllerContractsTest.php +311 -0
  476. package/server/tests/ServiceRateRouteValidationTest.php +76 -0
  477. package/server/tests/SettingControllerContractsTest.php +410 -0
  478. package/server/tests/SmallControllerContractsTest.php +480 -0
  479. package/server/tests/SupportAnalyticsCustomerAuthContractsTest.php +221 -0
  480. package/server/tests/SupportJobAndAiCoverageTest.php +812 -0
  481. package/server/tests/SupportServiceContractsTest.php +287 -0
  482. package/server/tests/SupportValueObjectsTest.php +155 -0
  483. package/server/tests/TelematicControllerContractsTest.php +185 -0
  484. package/server/tests/TelematicProviderRegistryTest.php +167 -0
  485. package/server/tests/TelematicServiceHelperContractsTest.php +315 -0
  486. package/server/tests/TelematicWebhookControllerContractsTest.php +253 -0
  487. package/server/tests/TelematicsHardeningTest.php +642 -86
  488. package/server/tests/TrackingIntelligenceTest.php +644 -1
  489. package/server/tests/TrackingNumberTraitContractsTest.php +106 -0
  490. package/server/tests/Unit/Casts/SpatialCastBranchesTest.php +75 -0
  491. package/server/tests/Unit/Console/AssignCustomerRolesCommandTest.php +102 -0
  492. package/server/tests/Unit/Console/AssignDriverRolesCommandTest.php +113 -0
  493. package/server/tests/Unit/Console/Commands/FixLegacyOrderConfigsHelpersTest.php +108 -0
  494. package/server/tests/Unit/Console/Commands/ProcessOperationalAlertsCommandTest.php +265 -0
  495. package/server/tests/Unit/Console/Commands/ReplayVehicleLocationsCommandTest.php +320 -0
  496. package/server/tests/Unit/Console/Commands/SimulateGeofenceEventsCommandTest.php +207 -0
  497. package/server/tests/Unit/Console/FixCommandsAndMaintenanceControllerTest.php +256 -0
  498. package/server/tests/Unit/Console/FleetOpsCommandsTest.php +198 -0
  499. package/server/tests/Unit/Console/ReplayVehicleLocationsCommandTest.php +252 -0
  500. package/server/tests/Unit/Console/SendMaintenanceRemindersTest.php +212 -0
  501. package/server/tests/Unit/Console/SimulateGeofenceEventsCommandTest.php +232 -0
  502. package/server/tests/Unit/Console/SyncTelematicsCommandTest.php +136 -0
  503. package/server/tests/Unit/Constraints/HOSConstraintTest.php +158 -0
  504. package/server/tests/Unit/Events/EventModelRecordTest.php +93 -0
  505. package/server/tests/Unit/Events/OrderDispatchFailedTest.php +34 -0
  506. package/server/tests/Unit/Exports/ExportCollectionsTest.php +142 -0
  507. package/server/tests/Unit/Exports/OperationalExportMappingTest.php +651 -0
  508. package/server/tests/Unit/Exports/VehicleExportTest.php +284 -0
  509. package/server/tests/Unit/Flow/EventFiringTest.php +95 -0
  510. package/server/tests/Unit/Http/Api/QueryWithRequestSeamsTest.php +198 -0
  511. package/server/tests/Unit/Http/Api/SensorInputTest.php +95 -0
  512. package/server/tests/Unit/Http/Filter/DeviceFilterTest.php +199 -0
  513. package/server/tests/Unit/Http/Filter/EmptyValueGuardsTest.php +66 -0
  514. package/server/tests/Unit/Http/Filter/FilterDateRangesTest.php +72 -0
  515. package/server/tests/Unit/Http/Filter/FuelProviderTransactionFilterTest.php +172 -0
  516. package/server/tests/Unit/Http/Filter/InternalRelationUuidLookupTest.php +135 -0
  517. package/server/tests/Unit/Http/Filter/IssueFilterTest.php +129 -0
  518. package/server/tests/Unit/Http/Filter/MaintenanceScheduleFilterTest.php +69 -0
  519. package/server/tests/Unit/Http/Filter/PartFilterTest.php +173 -0
  520. package/server/tests/Unit/Http/Internal/ControllerDelegationSeamsTest.php +136 -0
  521. package/server/tests/Unit/Http/Internal/ControllerExcelSeamsTest.php +89 -0
  522. package/server/tests/Unit/Http/Middleware/SetupDriverSessionTest.php +32 -0
  523. package/server/tests/Unit/Http/Requests/CreateFuelReportRequestTest.php +68 -0
  524. package/server/tests/Unit/Http/Requests/FleetActionRequestTest.php +80 -0
  525. package/server/tests/Unit/Http/Requests/RequestAuthorizationTest.php +50 -0
  526. package/server/tests/Unit/Http/Resources/CustomerResourceTest.php +108 -0
  527. package/server/tests/Unit/Http/Resources/EntityCustomerTypeTest.php +35 -0
  528. package/server/tests/Unit/Http/Resources/FleetResourceTest.php +102 -0
  529. package/server/tests/Unit/Http/Resources/IndexOrderResourceTest.php +210 -0
  530. package/server/tests/Unit/Http/Resources/IndexVehicleResourceTest.php +194 -0
  531. package/server/tests/Unit/Http/Resources/MaintenanceResourcesTest.php +182 -0
  532. package/server/tests/Unit/Http/Resources/OrderAndDriverResourceTest.php +121 -0
  533. package/server/tests/Unit/Http/Resources/OrderConfigResourceTest.php +47 -0
  534. package/server/tests/Unit/Http/Resources/PayloadResourceTest.php +111 -0
  535. package/server/tests/Unit/Http/Resources/RelationResourceFallbacksTest.php +32 -0
  536. package/server/tests/Unit/Http/Resources/ResourceRelationResolutionTest.php +55 -0
  537. package/server/tests/Unit/Http/Resources/TrackingNumberResourceTest.php +174 -0
  538. package/server/tests/Unit/Http/Resources/WaypointResourceTest.php +295 -0
  539. package/server/tests/Unit/Http/Resources/WorkOrderResourceTest.php +164 -0
  540. package/server/tests/Unit/Imports/ImportDelegationTest.php +151 -0
  541. package/server/tests/Unit/Integrations/Lalamove/LalamoveDeliveryStopTest.php +79 -0
  542. package/server/tests/Unit/Integrations/Lalamove/LalamoveLifecycleTest.php +211 -0
  543. package/server/tests/Unit/Integrations/Lalamove/LalamoveQuoteAndServiceOrderTest.php +381 -0
  544. package/server/tests/Unit/Integrations/Lalamove/LalamoveServiceTypeTest.php +51 -0
  545. package/server/tests/Unit/Integrations/LalamoveQuotationTest.php +194 -0
  546. package/server/tests/Unit/Integrations/LalamoveResponseHandlingTest.php +58 -0
  547. package/server/tests/Unit/Jobs/GeofenceDwellAndBulkNotifyTest.php +190 -0
  548. package/server/tests/Unit/Jobs/ReplayPositionsTest.php +57 -0
  549. package/server/tests/Unit/Jobs/SimulateWaypointReachedTest.php +92 -0
  550. package/server/tests/Unit/Listeners/HandleGeofenceEnteredTest.php +256 -0
  551. package/server/tests/Unit/Listeners/HandleOrderDispatchFailedListenerTest.php +101 -0
  552. package/server/tests/Unit/Listeners/HandleOrderDispatchedConcreteTest.php +203 -0
  553. package/server/tests/Unit/Listeners/HandleOrderDispatchedHelpersTest.php +186 -0
  554. package/server/tests/Unit/Listeners/HandleUserRemovedFromCompanyTest.php +65 -0
  555. package/server/tests/Unit/Listeners/ListenerDelegationSeamsTest.php +83 -0
  556. package/server/tests/Unit/Models/AssetTest.php +469 -0
  557. package/server/tests/Unit/Models/ContactCustomerUserTest.php +370 -0
  558. package/server/tests/Unit/Models/ContactTest.php +577 -0
  559. package/server/tests/Unit/Models/DeviceEventTest.php +481 -0
  560. package/server/tests/Unit/Models/DeviceLastPositionDefaultTest.php +128 -0
  561. package/server/tests/Unit/Models/DriverModelHelpersTest.php +268 -0
  562. package/server/tests/Unit/Models/DriverTest.php +401 -0
  563. package/server/tests/Unit/Models/EntityTest.php +171 -0
  564. package/server/tests/Unit/Models/EquipmentTest.php +434 -0
  565. package/server/tests/Unit/Models/FleetTest.php +153 -0
  566. package/server/tests/Unit/Models/FuelProviderTransactionTest.php +82 -0
  567. package/server/tests/Unit/Models/FuelReportImportAndLabelsTest.php +193 -0
  568. package/server/tests/Unit/Models/ImportRowFallbackSeamsTest.php +100 -0
  569. package/server/tests/Unit/Models/IntegratedVendorLifecycleTest.php +203 -0
  570. package/server/tests/Unit/Models/IntegratedVendorTest.php +135 -0
  571. package/server/tests/Unit/Models/IssueTest.php +160 -0
  572. package/server/tests/Unit/Models/MaintenanceScheduleTest.php +319 -0
  573. package/server/tests/Unit/Models/MaintenanceTest.php +277 -0
  574. package/server/tests/Unit/Models/MaintenanceWorkOrderImportTest.php +161 -0
  575. package/server/tests/Unit/Models/ManifestStopTest.php +121 -0
  576. package/server/tests/Unit/Models/ManifestTest.php +163 -0
  577. package/server/tests/Unit/Models/ModelLookupFallbackSeamsTest.php +149 -0
  578. package/server/tests/Unit/Models/OrderConfigTest.php +341 -0
  579. package/server/tests/Unit/Models/OrderDynamicContractsTest.php +190 -0
  580. package/server/tests/Unit/Models/OrderPayloadConfigAndDistanceTest.php +426 -0
  581. package/server/tests/Unit/Models/OrderPayloadRouteTest.php +227 -0
  582. package/server/tests/Unit/Models/OrderPurchaseAndDriversTest.php +188 -0
  583. package/server/tests/Unit/Models/OrderTest.php +917 -0
  584. package/server/tests/Unit/Models/PartTest.php +399 -0
  585. package/server/tests/Unit/Models/PayloadTest.php +710 -0
  586. package/server/tests/Unit/Models/PayloadWaypointsAndEntitiesTest.php +317 -0
  587. package/server/tests/Unit/Models/PlaceCreationAndImportTest.php +384 -0
  588. package/server/tests/Unit/Models/PlaceGeocodingResultsTest.php +253 -0
  589. package/server/tests/Unit/Models/PlaceSharedAndGeocodingTest.php +271 -0
  590. package/server/tests/Unit/Models/PlaceTest.php +589 -0
  591. package/server/tests/Unit/Models/PositionTest.php +49 -0
  592. package/server/tests/Unit/Models/RouteTest.php +55 -0
  593. package/server/tests/Unit/Models/SensorLastPositionDefaultTest.php +130 -0
  594. package/server/tests/Unit/Models/SensorReadingsTest.php +168 -0
  595. package/server/tests/Unit/Models/SensorTest.php +284 -0
  596. package/server/tests/Unit/Models/ServiceAreaGeometryTest.php +129 -0
  597. package/server/tests/Unit/Models/ServiceAreaTest.php +125 -0
  598. package/server/tests/Unit/Models/ServiceQuoteTest.php +617 -0
  599. package/server/tests/Unit/Models/ServiceRateFeeTest.php +73 -0
  600. package/server/tests/Unit/Models/ServiceRatePersistenceAndServicabilityTest.php +428 -0
  601. package/server/tests/Unit/Models/ServiceRateTest.php +533 -0
  602. package/server/tests/Unit/Models/SmallModelSurfacesTest.php +294 -0
  603. package/server/tests/Unit/Models/TrackingNumberTest.php +199 -0
  604. package/server/tests/Unit/Models/TrackingStatusTest.php +131 -0
  605. package/server/tests/Unit/Models/VehicleDeviceTest.php +115 -0
  606. package/server/tests/Unit/Models/VehiclePositionsAndImportTest.php +220 -0
  607. package/server/tests/Unit/Models/VehicleTest.php +266 -0
  608. package/server/tests/Unit/Models/VendorRelationsAndImportTest.php +107 -0
  609. package/server/tests/Unit/Models/WarrantyTest.php +187 -0
  610. package/server/tests/Unit/Models/WaypointTest.php +366 -0
  611. package/server/tests/Unit/Models/WorkOrderTest.php +272 -0
  612. package/server/tests/Unit/Models/ZoneTest.php +83 -0
  613. package/server/tests/Unit/Notifications/MonitoringNotificationsTest.php +136 -0
  614. package/server/tests/Unit/Notifications/OrderAssignedChannelsTest.php +53 -0
  615. package/server/tests/Unit/Notifications/OrderDispatchedChannelsTest.php +81 -0
  616. package/server/tests/Unit/Notifications/OrderFailedChannelsTest.php +73 -0
  617. package/server/tests/Unit/Notifications/OrderPingChannelsTest.php +69 -0
  618. package/server/tests/Unit/Notifications/TerminalOrderNotificationsTest.php +185 -0
  619. package/server/tests/Unit/Observers/FleetOpsCompanyUserAndServiceAreaObserversTest.php +133 -0
  620. package/server/tests/Unit/Observers/ObserverHelperExecutionTest.php +135 -0
  621. package/server/tests/Unit/Observers/ObserverListenerSeamsTest.php +267 -0
  622. package/server/tests/Unit/Observers/PurchaseRateObserverAndOrderJobsTest.php +169 -0
  623. package/server/tests/Unit/Orchestration/Engines/DriverAssignmentEngineTest.php +393 -0
  624. package/server/tests/Unit/Orchestration/Engines/VroomOrchestrationEngineLifecycleTest.php +397 -0
  625. package/server/tests/Unit/Orchestration/OrchestrationEngineRegistryTest.php +45 -0
  626. package/server/tests/Unit/Orchestration/VroomEngineTransportAndSettingsTest.php +253 -0
  627. package/server/tests/Unit/Providers/FleetOpsAiRegistrationTest.php +59 -0
  628. package/server/tests/Unit/Rules/ResolvableVehicleTest.php +85 -0
  629. package/server/tests/Unit/Support/Ai/AssetStatusCapabilityQueriesTest.php +120 -0
  630. package/server/tests/Unit/Support/Ai/Capabilities/CreateOrderPreviewCapabilityTest.php +385 -0
  631. package/server/tests/Unit/Support/Ai/Capabilities/OrderInsightsCapabilityTest.php +226 -0
  632. package/server/tests/Unit/Support/Ai/Capabilities/SearchResourcesCapabilityTest.php +237 -0
  633. package/server/tests/Unit/Support/AiCapabilityPermissionsTest.php +124 -0
  634. package/server/tests/Unit/Support/AlgoEdgeBranchesTest.php +24 -0
  635. package/server/tests/Unit/Support/Analytics/GeofenceViolationsTest.php +109 -0
  636. package/server/tests/Unit/Support/Analytics/LiveFleetTest.php +205 -0
  637. package/server/tests/Unit/Support/Analytics/MaintenanceOverviewTest.php +236 -0
  638. package/server/tests/Unit/Support/Analytics/OnTimeDeliveryAndTopDriversTest.php +250 -0
  639. package/server/tests/Unit/Support/CreateOrderPreviewDraftingTest.php +235 -0
  640. package/server/tests/Unit/Support/CustomerAuthTest.php +132 -0
  641. package/server/tests/Unit/Support/FuelEfficiencyAndCanceledHandlersTest.php +164 -0
  642. package/server/tests/Unit/Support/FuelProviderRegistryResolutionTest.php +51 -0
  643. package/server/tests/Unit/Support/FuelProviderServiceMatchingTest.php +280 -0
  644. package/server/tests/Unit/Support/GeocodingInjectedClientTest.php +268 -0
  645. package/server/tests/Unit/Support/GeocodingLocateTest.php +32 -0
  646. package/server/tests/Unit/Support/GeofenceIntersectionServiceTest.php +188 -0
  647. package/server/tests/Unit/Support/GettingStartedTest.php +164 -0
  648. package/server/tests/Unit/Support/GuardReturnSeamsTest.php +172 -0
  649. package/server/tests/Unit/Support/IntegratedVendorsResolverTest.php +250 -0
  650. package/server/tests/Unit/Support/IssuesInsightsAndAllocationJobTest.php +156 -0
  651. package/server/tests/Unit/Support/MagicAccessorSeamsTest.php +54 -0
  652. package/server/tests/Unit/Support/MaintainableAndOperationalAlertsTest.php +215 -0
  653. package/server/tests/Unit/Support/Metrics/AvgOrderValueMetricTest.php +308 -0
  654. package/server/tests/Unit/Support/Metrics/CountMetricsTest.php +280 -0
  655. package/server/tests/Unit/Support/Metrics/FuelCostsMetricTest.php +102 -0
  656. package/server/tests/Unit/Support/Metrics/MetricQuerySeamsTest.php +129 -0
  657. package/server/tests/Unit/Support/Metrics/OrdersScheduledMetricTest.php +146 -0
  658. package/server/tests/Unit/Support/MoreQuerySeamsTest.php +410 -0
  659. package/server/tests/Unit/Support/OperationalQueryDistributionTest.php +152 -0
  660. package/server/tests/Unit/Support/OperationsPulseTest.php +130 -0
  661. package/server/tests/Unit/Support/OptimizeOrderRouteCapabilityTest.php +147 -0
  662. package/server/tests/Unit/Support/OrchestrationPayloadBuilderTest.php +135 -0
  663. package/server/tests/Unit/Support/OrchestrationRouteAndVehicleTasksTest.php +254 -0
  664. package/server/tests/Unit/Support/OsrmProofAndIssueFilterTest.php +247 -0
  665. package/server/tests/Unit/Support/PetroAppFuelProviderTest.php +165 -0
  666. package/server/tests/Unit/Support/PlaceSearchRankingTest.php +191 -0
  667. package/server/tests/Unit/Support/QuerySeamsTest.php +309 -0
  668. package/server/tests/Unit/Support/ResolvesOrderServiceStopsDbTest.php +303 -0
  669. package/server/tests/Unit/Support/ResolvesOrderServiceStopsTest.php +382 -0
  670. package/server/tests/Unit/Support/SkipAndFallbackGuardsTest.php +70 -0
  671. package/server/tests/Unit/Support/TelematicCredentialResolutionTest.php +31 -0
  672. package/server/tests/Unit/Support/TelematicServiceTest.php +256 -0
  673. package/server/tests/Unit/Support/Telematics/Providers/AbstractProviderTest.php +154 -0
  674. package/server/tests/Unit/Support/Telematics/Providers/AfaqyProviderTest.php +191 -0
  675. package/server/tests/Unit/Support/Telematics/Providers/AfaqyProviderTransportTest.php +138 -0
  676. package/server/tests/Unit/Support/Telematics/Providers/GeotabProviderTest.php +329 -0
  677. package/server/tests/Unit/Support/Telematics/Providers/ProviderTransportSeamsTest.php +133 -0
  678. package/server/tests/Unit/Support/Telematics/Providers/SafeeProviderTest.php +487 -0
  679. package/server/tests/Unit/Support/Telematics/TelematicProviderRegistryTest.php +39 -0
  680. package/server/tests/Unit/Support/Telematics/TelematicServiceLifecycleTest.php +711 -0
  681. package/server/tests/Unit/Support/Telematics/TelematicServiceTelemetryTest.php +364 -0
  682. package/server/tests/Unit/Support/UtilsAdditionalTest.php +256 -0
  683. package/server/tests/Unit/Support/UtilsGeoMatrixAndVendorTest.php +382 -0
  684. package/server/tests/Unit/Tracking/TrackingContextTest.php +114 -0
  685. package/server/tests/Unit/Tracking/TrackingIntelligenceAndContextTest.php +477 -0
  686. package/server/tests/Unit/Tracking/TrackingProviderManagerFallbacksTest.php +208 -0
  687. package/server/tests/Unit/Traits/HasTrackingNumberTest.php +303 -0
  688. package/server/tests/Unit/Traits/PayloadAccessorsTest.php +150 -0
  689. package/server/tests/UtilsContractsTest.php +244 -0
  690. package/server/tests/VehicleControllerHelperContractsTest.php +488 -0
  691. package/server/tests/VehicleDataParsingTest.php +45 -0
  692. package/server/tests/VehicleResourceContractsTest.php +313 -0
  693. package/translations/bg-bg.yaml +2 -0
  694. package/translations/en-us.yaml +2 -0
  695. package/translations/es-pa.yaml +2 -0
  696. package/translations/fr-fr.yaml +2 -0
  697. package/translations/mn-mn.yaml +2 -0
  698. package/translations/pt-br.yaml +2 -0
  699. package/translations/ru-ru.yaml +2 -0
  700. package/server/tests/DeviceFilterTest.php +0 -44
  701. package/server/tests/LiveControllerViewportTest.php +0 -52
@@ -0,0 +1,2695 @@
1
+ <?php
2
+
3
+ use Carbon\Carbon;
4
+ use Fleetbase\FleetOps\Console\Commands\AssignCustomerRoles;
5
+ use Fleetbase\FleetOps\Console\Commands\AssignDriverRoles;
6
+ use Fleetbase\FleetOps\Console\Commands\AuditCustomerUserConflicts;
7
+ use Fleetbase\FleetOps\Console\Commands\DebugOrderTracker;
8
+ use Fleetbase\FleetOps\Console\Commands\DispatchAdhocOrders;
9
+ use Fleetbase\FleetOps\Console\Commands\DispatchOrders;
10
+ use Fleetbase\FleetOps\Console\Commands\FixCustomerCompanies;
11
+ use Fleetbase\FleetOps\Console\Commands\FixDriverCompanies;
12
+ use Fleetbase\FleetOps\Console\Commands\FixLegacyOrderConfigs;
13
+ use Fleetbase\FleetOps\Console\Commands\ProcessMaintenanceTriggers;
14
+ use Fleetbase\FleetOps\Console\Commands\PurgeUnpurchasedServiceQuotes;
15
+ use Fleetbase\FleetOps\Console\Commands\SendDriverNotification;
16
+ use Fleetbase\FleetOps\Console\Commands\SendMaintenanceReminders;
17
+ use Fleetbase\FleetOps\Console\Commands\SimulateOrderRouteNavigation;
18
+ use Fleetbase\FleetOps\Console\Commands\SyncTelematics;
19
+ use Fleetbase\FleetOps\Console\Commands\TestEmail;
20
+ use Fleetbase\FleetOps\Console\Commands\TrackOrderDistanceAndTime;
21
+ use Fleetbase\FleetOps\Contracts\TelematicProviderDescriptor;
22
+ use Fleetbase\FleetOps\Models\Contact;
23
+ use Fleetbase\FleetOps\Models\Driver;
24
+ use Fleetbase\FleetOps\Models\MaintenanceSchedule;
25
+ use Fleetbase\FleetOps\Models\Order;
26
+ use Fleetbase\FleetOps\Models\OrderConfig;
27
+ use Fleetbase\FleetOps\Models\Vehicle;
28
+ use Fleetbase\FleetOps\Models\WorkOrder;
29
+ use Fleetbase\FleetOps\Notifications\OrderAssigned;
30
+ use Fleetbase\FleetOps\Notifications\OrderPing;
31
+ use Fleetbase\FleetOps\Support\Telematics\TelematicProviderRegistry;
32
+ use Fleetbase\LaravelMysqlSpatial\Types\Point;
33
+ use Fleetbase\Models\Company;
34
+ use Illuminate\Console\Command;
35
+ use Illuminate\Database\Eloquent\Collection as EloquentCollection;
36
+ use Illuminate\Database\Eloquent\Model;
37
+ use Illuminate\Support\Facades\Cache;
38
+
39
+ if (!function_exists('Fleetbase\Traits\config')) {
40
+ eval('namespace Fleetbase\Traits; function config($key = null, $default = null) { return false; }');
41
+ }
42
+
43
+ if (!function_exists('Fleetbase\Models\config')) {
44
+ eval('namespace Fleetbase\Models; function config($key = null, $default = null) { return $default; }');
45
+ }
46
+
47
+ if (!function_exists('Fleetbase\FleetOps\Console\Commands\config')) {
48
+ eval('namespace Fleetbase\FleetOps\Console\Commands; function config($key = null, $default = null) { return $default; }');
49
+ }
50
+
51
+ if (!function_exists('Fleetbase\FleetOps\Console\Commands\now')) {
52
+ eval('namespace Fleetbase\FleetOps\Console\Commands; function now($timezone = null) { return \Carbon\Carbon::now($timezone); }');
53
+ }
54
+
55
+ class FleetOpsCommandCacheFake
56
+ {
57
+ public function __construct(private FleetOpsCommandLockFake $lock)
58
+ {
59
+ }
60
+
61
+ public function lock($key, $seconds)
62
+ {
63
+ return $this->lock;
64
+ }
65
+
66
+ public function forget($key): bool
67
+ {
68
+ return true;
69
+ }
70
+ }
71
+
72
+ class FleetOpsCommandLockFake
73
+ {
74
+ public bool $released = false;
75
+
76
+ public function __construct(private bool $locked)
77
+ {
78
+ }
79
+
80
+ public function get(): bool
81
+ {
82
+ return $this->locked;
83
+ }
84
+
85
+ public function release(): void
86
+ {
87
+ $this->released = true;
88
+ }
89
+ }
90
+
91
+ class FleetOpsTelematicProviderRegistryFake extends TelematicProviderRegistry
92
+ {
93
+ public function __construct(Illuminate\Support\Collection $providers)
94
+ {
95
+ $this->providers = $providers;
96
+ }
97
+
98
+ public function all(): Illuminate\Support\Collection
99
+ {
100
+ return $this->providers;
101
+ }
102
+ }
103
+
104
+ class FleetOpsProcessMaintenanceTriggersProbe extends ProcessMaintenanceTriggers
105
+ {
106
+ public function callHelper(string $method, mixed ...$arguments): mixed
107
+ {
108
+ $reflection = new ReflectionMethod(ProcessMaintenanceTriggers::class, $method);
109
+ $reflection->setAccessible(true);
110
+
111
+ return $reflection->invoke($this, ...$arguments);
112
+ }
113
+ }
114
+
115
+ class FleetOpsProcessMaintenanceTriggersHandleFake extends ProcessMaintenanceTriggers
116
+ {
117
+ public array $createdWorkOrders = [];
118
+ public array $dispatchedEvents = [];
119
+ public array $messages = [];
120
+ public array $operations = [];
121
+ public Illuminate\Support\Collection $testSchedules;
122
+ public bool $existingOpen = false;
123
+ public int $workOrderCount = 0;
124
+
125
+ public function __construct(private array $testOptions)
126
+ {
127
+ parent::__construct();
128
+ $this->testSchedules = collect();
129
+ }
130
+
131
+ public function option($key = null)
132
+ {
133
+ return $key === null ? $this->testOptions : ($this->testOptions[$key] ?? null);
134
+ }
135
+
136
+ public function info($string, $verbosity = null)
137
+ {
138
+ $this->messages[] = ['info', $string];
139
+ }
140
+
141
+ public function line($string, $style = null, $verbosity = null)
142
+ {
143
+ $this->messages[] = ['line', $string];
144
+ }
145
+
146
+ protected function schedules(string $conn)
147
+ {
148
+ $this->operations[] = ['schedules', $conn];
149
+
150
+ return $this->testSchedules;
151
+ }
152
+
153
+ protected function openWorkOrderExists(string $conn, MaintenanceSchedule $schedule): bool
154
+ {
155
+ $this->operations[] = ['openWorkOrderExists', $conn, $schedule->uuid];
156
+
157
+ return $this->existingOpen;
158
+ }
159
+
160
+ protected function workOrderCount(string $conn): int
161
+ {
162
+ $this->operations[] = ['workOrderCount', $conn];
163
+
164
+ return $this->workOrderCount;
165
+ }
166
+
167
+ protected function createWorkOrder(string $conn, array $attributes): WorkOrder
168
+ {
169
+ $this->operations[] = ['createWorkOrder', $conn];
170
+ $this->createdWorkOrders[] = $attributes;
171
+
172
+ $workOrder = new WorkOrder();
173
+ $workOrder->setRawAttributes(array_merge(['public_id' => 'work_order_created'], $attributes), true);
174
+
175
+ return $workOrder;
176
+ }
177
+
178
+ protected function dispatchTriggeredEvent(MaintenanceSchedule $schedule, WorkOrder $workOrder): void
179
+ {
180
+ $this->dispatchedEvents[] = [$schedule->uuid, $workOrder->public_id];
181
+ }
182
+ }
183
+
184
+ class FleetOpsSendMaintenanceRemindersCommandFake extends SendMaintenanceReminders
185
+ {
186
+ public array $messages = [];
187
+ public array $sent = [];
188
+ public array $records = [];
189
+ public Illuminate\Support\Collection $testSchedules;
190
+
191
+ public function __construct(private array $testOptions, private array $alreadySent = [])
192
+ {
193
+ parent::__construct();
194
+ $this->testSchedules = collect();
195
+ }
196
+
197
+ public function option($key = null)
198
+ {
199
+ return $key === null ? $this->testOptions : ($this->testOptions[$key] ?? null);
200
+ }
201
+
202
+ public function info($string, $verbosity = null)
203
+ {
204
+ $this->messages[] = ['info', $string];
205
+ }
206
+
207
+ public function line($string, $style = null, $verbosity = null)
208
+ {
209
+ $this->messages[] = ['line', $string];
210
+ }
211
+
212
+ protected function schedules(string $conn): Illuminate\Support\Collection
213
+ {
214
+ $this->messages[] = ['schedules', $conn];
215
+
216
+ return $this->testSchedules;
217
+ }
218
+
219
+ protected function reminderAlreadySent(string $conn, MaintenanceSchedule $schedule, int $offsetDays, string $dueDateSnapshot): bool
220
+ {
221
+ $key = implode('|', [$conn, $schedule->uuid, $offsetDays, $dueDateSnapshot]);
222
+
223
+ return in_array($key, $this->alreadySent, true);
224
+ }
225
+
226
+ protected function sendReminder(string $email, MaintenanceSchedule $schedule, int $offsetDays): void
227
+ {
228
+ $this->sent[] = [$email, $schedule->uuid, $offsetDays];
229
+ }
230
+
231
+ protected function recordReminder(string $conn, MaintenanceSchedule $schedule, int $offsetDays, string $dueDateSnapshot): void
232
+ {
233
+ $this->records[] = [$conn, $schedule->uuid, $offsetDays, $dueDateSnapshot];
234
+ }
235
+ }
236
+
237
+ class FleetOpsSimulateOrderFake extends Order
238
+ {
239
+ public ?Driver $assignedDriver = null;
240
+
241
+ public function loadAssignedDriver(): Order
242
+ {
243
+ $this->setRelation('driverAssigned', $this->assignedDriver);
244
+
245
+ return $this;
246
+ }
247
+ }
248
+
249
+ class FleetOpsSimulateOrderRouteNavigationCommandFake extends SimulateOrderRouteNavigation
250
+ {
251
+ public array $messages = [];
252
+ public array $events = [];
253
+ public int $pauses = 0;
254
+ public ?Order $order = null;
255
+ public ?Driver $driver = null;
256
+ public array $route = [];
257
+ public array $waypoints = [];
258
+
259
+ public function __construct(private array $testArguments)
260
+ {
261
+ parent::__construct();
262
+ }
263
+
264
+ public function argument($key = null)
265
+ {
266
+ return $key === null ? $this->testArguments : ($this->testArguments[$key] ?? null);
267
+ }
268
+
269
+ public function info($string, $verbosity = null)
270
+ {
271
+ $this->messages[] = ['info', $string];
272
+ }
273
+
274
+ public function error($string, $verbosity = null)
275
+ {
276
+ $this->messages[] = ['error', $string];
277
+
278
+ return Command::FAILURE;
279
+ }
280
+
281
+ public function promptQuestions(): array
282
+ {
283
+ return $this->promptForMissingArgumentsUsing();
284
+ }
285
+
286
+ protected function findOrder(string $orderId): ?Order
287
+ {
288
+ $this->messages[] = ['findOrder', $orderId];
289
+
290
+ return $this->order;
291
+ }
292
+
293
+ protected function findDriver(string $driverId): ?Driver
294
+ {
295
+ $this->messages[] = ['findDriver', $driverId];
296
+
297
+ return $this->driver;
298
+ }
299
+
300
+ protected function getRoute(mixed $start, mixed $end): array
301
+ {
302
+ $this->messages[] = ['getRoute', (string) $start, (string) $end];
303
+
304
+ return $this->route;
305
+ }
306
+
307
+ protected function decodePolyline(string $routeGeometry): array
308
+ {
309
+ $this->messages[] = ['decodePolyline', $routeGeometry];
310
+
311
+ return $this->waypoints;
312
+ }
313
+
314
+ protected function dispatchLocationChanged(Driver $driver, mixed $waypoint, array $additionalData): void
315
+ {
316
+ $this->events[] = [$driver->public_id, (string) $waypoint, $additionalData];
317
+ }
318
+
319
+ protected function pauseBetweenWaypoints(): void
320
+ {
321
+ $this->pauses++;
322
+ }
323
+ }
324
+
325
+ class FleetOpsDispatchAdhocOrdersCommandFake extends DispatchAdhocOrders
326
+ {
327
+ public array $messages = [];
328
+ public array $tables = [];
329
+ public EloquentCollection $orders;
330
+ public EloquentCollection $drivers;
331
+
332
+ public function __construct(private array $testOptions)
333
+ {
334
+ parent::__construct();
335
+ $this->orders = new EloquentCollection();
336
+ $this->drivers = new EloquentCollection();
337
+ }
338
+
339
+ public function option($key = null)
340
+ {
341
+ return $key === null ? $this->testOptions : ($this->testOptions[$key] ?? null);
342
+ }
343
+
344
+ public function getDispatchableOrders(int $days = 2, int $intervalMinutes = 4, int $expiryHours = 72): EloquentCollection
345
+ {
346
+ $this->messages[] = ['getDispatchableOrders', $days, $intervalMinutes, $expiryHours];
347
+
348
+ return $this->orders;
349
+ }
350
+
351
+ public function getNearbyDriversForOrder(Order $order, Point $pickup, int $distance, bool $testing = false): EloquentCollection
352
+ {
353
+ $this->messages[] = ['getNearbyDriversForOrder', $order->public_id, $distance, $testing];
354
+
355
+ return $this->drivers;
356
+ }
357
+
358
+ public function info($string, $verbosity = null)
359
+ {
360
+ $this->messages[] = ['info', $string];
361
+ }
362
+
363
+ public function alert($string, $verbosity = null)
364
+ {
365
+ $this->messages[] = ['alert', $string];
366
+ }
367
+
368
+ public function table($headers, $rows, $tableStyle = 'default', array $columnStyles = [])
369
+ {
370
+ $this->tables[] = [$headers, $rows];
371
+ }
372
+
373
+ public function error($string, $verbosity = null)
374
+ {
375
+ $this->messages[] = ['error', $string];
376
+ }
377
+
378
+ public function line($string, $style = null, $verbosity = null)
379
+ {
380
+ $this->messages[] = ['line', $string];
381
+ }
382
+
383
+ public function warn($string, $verbosity = null)
384
+ {
385
+ $this->messages[] = ['warn', $string];
386
+ }
387
+ }
388
+
389
+ class FleetOpsDispatchAdhocOrdersQueryProbe extends DispatchAdhocOrders
390
+ {
391
+ public FleetOpsDispatchAdhocOrdersBuilderFake $orderQuery;
392
+ public FleetOpsDispatchAdhocOrdersBuilderFake $driverQuery;
393
+
394
+ public function __construct(private array $testOptions)
395
+ {
396
+ parent::__construct();
397
+ $this->orderQuery = new FleetOpsDispatchAdhocOrdersBuilderFake();
398
+ $this->driverQuery = new FleetOpsDispatchAdhocOrdersBuilderFake();
399
+ }
400
+
401
+ public function option($key = null)
402
+ {
403
+ return $key === null ? $this->testOptions : ($this->testOptions[$key] ?? null);
404
+ }
405
+
406
+ protected function newOrderQuery(string $connection)
407
+ {
408
+ $this->orderQuery->calls[] = ['connection', $connection];
409
+
410
+ return $this->orderQuery;
411
+ }
412
+
413
+ protected function newDriverQuery()
414
+ {
415
+ return $this->driverQuery;
416
+ }
417
+ }
418
+
419
+ class FleetOpsDispatchAdhocOrdersBuilderFake
420
+ {
421
+ public array $calls = [];
422
+ public EloquentCollection $results;
423
+
424
+ public function __construct()
425
+ {
426
+ $this->results = new EloquentCollection();
427
+ }
428
+
429
+ public function withoutGlobalScopes(): self
430
+ {
431
+ $this->calls[] = ['withoutGlobalScopes'];
432
+
433
+ return $this;
434
+ }
435
+
436
+ public function where(...$arguments): self
437
+ {
438
+ $this->calls[] = ['where', $arguments];
439
+
440
+ return $this;
441
+ }
442
+
443
+ public function whereBetween(string $column, array $range): self
444
+ {
445
+ $this->calls[] = ['whereBetween', $column, $range];
446
+
447
+ return $this;
448
+ }
449
+
450
+ public function whereNull(string $column): self
451
+ {
452
+ $this->calls[] = ['whereNull', $column];
453
+
454
+ return $this;
455
+ }
456
+
457
+ public function whereHas(string $relation, ?Closure $callback = null): self
458
+ {
459
+ $this->calls[] = ['whereHas', $relation];
460
+
461
+ if ($callback) {
462
+ $callback($this);
463
+ }
464
+
465
+ return $this;
466
+ }
467
+
468
+ public function with(array $relations): self
469
+ {
470
+ $this->calls[] = ['with', $relations];
471
+
472
+ return $this;
473
+ }
474
+
475
+ public function whereNotNull(string $column): self
476
+ {
477
+ $this->calls[] = ['whereNotNull', $column];
478
+
479
+ return $this;
480
+ }
481
+
482
+ public function whereRaw(string $sql): self
483
+ {
484
+ $this->calls[] = ['whereRaw', trim($sql)];
485
+
486
+ return $this;
487
+ }
488
+
489
+ public function distanceSphere(string $column, Point $point, int $distance): self
490
+ {
491
+ $this->calls[] = ['distanceSphere', $column, $point, $distance];
492
+
493
+ return $this;
494
+ }
495
+
496
+ public function distanceSphereValue(string $column, Point $point): self
497
+ {
498
+ $this->calls[] = ['distanceSphereValue', $column, $point];
499
+
500
+ return $this;
501
+ }
502
+
503
+ public function orWhereHas(string $relation, ?Closure $callback = null): self
504
+ {
505
+ $this->calls[] = ['orWhereHas', $relation];
506
+
507
+ if ($callback) {
508
+ $callback($this);
509
+ }
510
+
511
+ return $this;
512
+ }
513
+
514
+ public function get(): EloquentCollection
515
+ {
516
+ return $this->results;
517
+ }
518
+ }
519
+
520
+ class FleetOpsDispatchAdhocOrdersOrderFake extends Order
521
+ {
522
+ public ?string $public_id = null;
523
+ public mixed $dispatched_at = null;
524
+ public ?string $company_uuid = null;
525
+ public bool $dispatchedForPing = false;
526
+ public mixed $pickupLocation;
527
+ public int $adhocDistance = 6000;
528
+
529
+ public function getPickupLocation(): mixed
530
+ {
531
+ return $this->pickupLocation;
532
+ }
533
+
534
+ public function getAdhocPingDistance(): int
535
+ {
536
+ return $this->adhocDistance;
537
+ }
538
+
539
+ public function dispatch(bool $save = true): self
540
+ {
541
+ $this->dispatchedForPing = $save;
542
+
543
+ return $this;
544
+ }
545
+ }
546
+
547
+ class FleetOpsDispatchAdhocOrdersDriverFake extends Driver
548
+ {
549
+ public ?string $public_id = null;
550
+ public ?string $name = null;
551
+ public array $notifications = [];
552
+
553
+ public function notify($instance): void
554
+ {
555
+ $this->notifications[] = $instance::class;
556
+ }
557
+ }
558
+
559
+ class FleetOpsTrackOrderDistanceAndTimeProbe extends TrackOrderDistanceAndTime
560
+ {
561
+ public array $messages = [];
562
+ public FleetOpsTrackOrderQueryFake $query;
563
+ public FleetOpsTrackProgressBarFake $progressBar;
564
+
565
+ public function __construct(private array $testOptions, array $orders = [])
566
+ {
567
+ parent::__construct();
568
+ $this->query = new FleetOpsTrackOrderQueryFake($orders);
569
+ $this->progressBar = new FleetOpsTrackProgressBarFake();
570
+ }
571
+
572
+ public function option($key = null)
573
+ {
574
+ return $key === null ? $this->testOptions : ($this->testOptions[$key] ?? null);
575
+ }
576
+
577
+ public function info($string, $verbosity = null)
578
+ {
579
+ $this->messages[] = ['info', $string];
580
+ }
581
+
582
+ public function warn($string, $verbosity = null)
583
+ {
584
+ $this->messages[] = ['warn', $string];
585
+ }
586
+
587
+ public function error($string, $verbosity = null)
588
+ {
589
+ $this->messages[] = ['error', $string];
590
+ }
591
+
592
+ public function alert($string, $verbosity = null)
593
+ {
594
+ $this->messages[] = ['alert', $string];
595
+ }
596
+
597
+ public function newLine($count = 1)
598
+ {
599
+ $this->messages[] = ['newLine', $count];
600
+ }
601
+
602
+ protected function activeOrdersQuery(Carbon $cutoff)
603
+ {
604
+ $this->messages[] = ['activeOrdersQuery', $cutoff->toDateTimeString()];
605
+
606
+ return $this->query;
607
+ }
608
+
609
+ protected function createProgressBar(int $total)
610
+ {
611
+ $this->messages[] = ['createProgressBar', $total];
612
+
613
+ return $this->progressBar;
614
+ }
615
+ }
616
+
617
+ class FleetOpsTrackProgressBarFake
618
+ {
619
+ public int $started = 0;
620
+ public int $advanced = 0;
621
+ public int $finished = 0;
622
+
623
+ public function start(): void
624
+ {
625
+ $this->started++;
626
+ }
627
+
628
+ public function advance(): void
629
+ {
630
+ $this->advanced++;
631
+ }
632
+
633
+ public function finish(): void
634
+ {
635
+ $this->finished++;
636
+ }
637
+ }
638
+
639
+ class FleetOpsTrackOrderQueryFake
640
+ {
641
+ public array $calls = [];
642
+
643
+ public function __construct(private array $orders)
644
+ {
645
+ }
646
+
647
+ public function __clone()
648
+ {
649
+ $this->calls[] = ['clone'];
650
+ }
651
+
652
+ public function count($columns = '*'): int
653
+ {
654
+ $this->calls[] = ['count', $columns];
655
+
656
+ return count($this->orders);
657
+ }
658
+
659
+ public function orderBy(string $column): self
660
+ {
661
+ $this->calls[] = ['orderBy', $column];
662
+
663
+ return $this;
664
+ }
665
+
666
+ public function chunkById(int $perChunk, Closure $callback): void
667
+ {
668
+ $this->calls[] = ['chunkById', $perChunk];
669
+
670
+ $callback(new FleetOpsTrackOrderChunkFake($this->orders));
671
+ }
672
+ }
673
+
674
+ class FleetOpsFixLegacyOrderConfigsCommandFake extends FixLegacyOrderConfigs
675
+ {
676
+ public array $messages = [];
677
+ public array $created = [];
678
+ public array $configs = [];
679
+ public Illuminate\Support\Collection $testCompanies;
680
+ public Illuminate\Support\Collection $testOrders;
681
+ public FleetOpsTrackProgressBarFake $progressBar;
682
+
683
+ public function __construct(private bool $createConfigs)
684
+ {
685
+ parent::__construct();
686
+ $this->testCompanies = collect();
687
+ $this->testOrders = collect();
688
+ $this->progressBar = new FleetOpsTrackProgressBarFake();
689
+ }
690
+
691
+ public function option($key = null)
692
+ {
693
+ $options = ['create-configs' => $this->createConfigs];
694
+
695
+ return $key === null ? $options : ($options[$key] ?? null);
696
+ }
697
+
698
+ public function info($string, $verbosity = null)
699
+ {
700
+ $this->messages[] = ['info', $string];
701
+ }
702
+
703
+ public function line($string, $style = null, $verbosity = null)
704
+ {
705
+ $this->messages[] = ['line', $string];
706
+ }
707
+
708
+ public function error($string, $verbosity = null)
709
+ {
710
+ $this->messages[] = ['error', $string];
711
+ }
712
+
713
+ protected function companies()
714
+ {
715
+ return $this->testCompanies;
716
+ }
717
+
718
+ protected function createTransportConfig(Company $company): void
719
+ {
720
+ $this->created[] = $company->uuid;
721
+ }
722
+
723
+ protected function ordersWithoutConfig()
724
+ {
725
+ return $this->testOrders;
726
+ }
727
+
728
+ protected function transportConfigForCompany(string $companyUuid): ?OrderConfig
729
+ {
730
+ return $this->configs[$companyUuid] ?? null;
731
+ }
732
+
733
+ protected function createProgressBar(int $total)
734
+ {
735
+ $this->messages[] = ['createProgressBar', $total];
736
+
737
+ return $this->progressBar;
738
+ }
739
+ }
740
+
741
+ class FleetOpsLegacyOrderFake extends Order
742
+ {
743
+ public array $updates = [];
744
+ public bool $failUpdate = false;
745
+
746
+ public function update(array $attributes = [], array $options = [])
747
+ {
748
+ if ($this->failUpdate) {
749
+ throw new RuntimeException('order update failed');
750
+ }
751
+
752
+ $this->updates[] = $attributes;
753
+
754
+ return true;
755
+ }
756
+ }
757
+
758
+ class FleetOpsFixCustomerCompaniesCommandFake extends FixCustomerCompanies
759
+ {
760
+ public array $messages = [];
761
+ public array $users = [];
762
+ public array $missing = [];
763
+ public array $companies = [];
764
+ public array $customerUsers = [];
765
+ public Illuminate\Support\Collection $testCustomers;
766
+
767
+ public function __construct()
768
+ {
769
+ parent::__construct();
770
+ $this->testCustomers = collect();
771
+ }
772
+
773
+ public function info($string, $verbosity = null)
774
+ {
775
+ $this->messages[] = ['info', $string];
776
+ }
777
+
778
+ public function line($string, $style = null, $verbosity = null)
779
+ {
780
+ $this->messages[] = ['line', $string];
781
+ }
782
+
783
+ public function error($string, $verbosity = null)
784
+ {
785
+ $this->messages[] = ['error', $string];
786
+ }
787
+
788
+ protected function customers()
789
+ {
790
+ return $this->testCustomers;
791
+ }
792
+
793
+ protected function customerUser(Contact $customer)
794
+ {
795
+ return $this->customerUsers[spl_object_id($customer)] ?? null;
796
+ }
797
+
798
+ protected function createUserForCustomer(Contact $customer): mixed
799
+ {
800
+ if ($customer->failCreate ?? false) {
801
+ throw new RuntimeException('existing user');
802
+ }
803
+
804
+ $this->customerUsers[spl_object_id($customer)] = $customer->createdUser;
805
+
806
+ return $customer->createdUser;
807
+ }
808
+
809
+ protected function assignExistingUserToCustomer(Contact $customer, $existingUser): void
810
+ {
811
+ $customer->updateQuietly(['user_uuid' => $existingUser->uuid]);
812
+ $this->customerUsers[spl_object_id($customer)] = $existingUser;
813
+ }
814
+
815
+ protected function userByEmail(string $email)
816
+ {
817
+ return $this->users[$email] ?? null;
818
+ }
819
+
820
+ protected function missingCompanyUser(string $userUuid, string $companyUuid): bool
821
+ {
822
+ return $this->missing[$userUuid . ':' . $companyUuid] ?? false;
823
+ }
824
+
825
+ protected function companyByUuid(string $companyUuid): ?Company
826
+ {
827
+ return $this->companies[$companyUuid] ?? null;
828
+ }
829
+ }
830
+
831
+ class FleetOpsFixDriverCompaniesCommandFake extends FixDriverCompanies
832
+ {
833
+ public array $messages = [];
834
+ public array $missing = [];
835
+ public array $companies = [];
836
+ public Illuminate\Support\Collection $testDrivers;
837
+
838
+ public function __construct()
839
+ {
840
+ parent::__construct();
841
+ $this->testDrivers = collect();
842
+ }
843
+
844
+ public function line($string, $style = null, $verbosity = null)
845
+ {
846
+ $this->messages[] = ['line', $string];
847
+ }
848
+
849
+ protected function drivers()
850
+ {
851
+ return $this->testDrivers;
852
+ }
853
+
854
+ protected function missingCompanyUser(string $userUuid, string $companyUuid): bool
855
+ {
856
+ return $this->missing[$userUuid . ':' . $companyUuid] ?? false;
857
+ }
858
+
859
+ protected function companyByUuid(string $companyUuid): ?Company
860
+ {
861
+ return $this->companies[$companyUuid] ?? null;
862
+ }
863
+ }
864
+
865
+ class FleetOpsUserCommandFake
866
+ {
867
+ public string $uuid;
868
+ public string $name;
869
+ public string $email;
870
+ public array $synced = [];
871
+ public array $assigned = [];
872
+
873
+ public function __construct(string $uuid, string $name, string $email)
874
+ {
875
+ $this->uuid = $uuid;
876
+ $this->name = $name;
877
+ $this->email = $email;
878
+ }
879
+
880
+ public function syncProperty(string $property, Model $model): bool
881
+ {
882
+ $this->synced[] = [$property, $model::class];
883
+
884
+ return true;
885
+ }
886
+
887
+ public function assignCompany(Company $company, string $role = 'Administrator'): self
888
+ {
889
+ $this->assigned[] = [$company->uuid, $role];
890
+
891
+ return $this;
892
+ }
893
+ }
894
+
895
+ class FleetOpsCustomerCommandFake extends Contact
896
+ {
897
+ public mixed $createdUser = null;
898
+ public bool $failCreate = false;
899
+ public array $quietUpdates = [];
900
+
901
+ public function loadMissing($relations)
902
+ {
903
+ return $this;
904
+ }
905
+
906
+ public function updateQuietly(array $attributes = [], array $options = [])
907
+ {
908
+ $this->quietUpdates[] = $attributes;
909
+
910
+ return true;
911
+ }
912
+ }
913
+
914
+ class FleetOpsPurgeServiceQuotesCommandFake extends PurgeUnpurchasedServiceQuotes
915
+ {
916
+ public array $messages = [];
917
+ public array $events = [];
918
+ public int $deletedCount = 0;
919
+ public ?Throwable $failure = null;
920
+
921
+ public function info($string, $verbosity = null)
922
+ {
923
+ $this->messages[] = ['info', $string];
924
+ }
925
+
926
+ public function error($string, $verbosity = null)
927
+ {
928
+ $this->messages[] = ['error', $string];
929
+ }
930
+
931
+ protected function disableForeignKeyConstraints(): void
932
+ {
933
+ $this->events[] = 'disable';
934
+ }
935
+
936
+ protected function enableForeignKeyConstraints(): void
937
+ {
938
+ $this->events[] = 'enable';
939
+ }
940
+
941
+ protected function beginTransaction(): void
942
+ {
943
+ $this->events[] = 'begin';
944
+ }
945
+
946
+ protected function commit(): void
947
+ {
948
+ $this->events[] = 'commit';
949
+ }
950
+
951
+ protected function rollBack(): void
952
+ {
953
+ $this->events[] = 'rollback';
954
+ }
955
+
956
+ protected function purgeServiceQuotes($thresholdDate): int
957
+ {
958
+ $this->events[] = ['purge', $thresholdDate->copy()];
959
+
960
+ if ($this->failure) {
961
+ throw $this->failure;
962
+ }
963
+
964
+ return $this->deletedCount;
965
+ }
966
+ }
967
+
968
+ class FleetOpsSendDriverNotificationCommandFake extends SendDriverNotification
969
+ {
970
+ public array $messages = [];
971
+ public array $questions = [];
972
+ public array $choices = [];
973
+ public array $sent = [];
974
+ public ?Order $order = null;
975
+ public object $matrix;
976
+
977
+ public function __construct(private array $testOptions)
978
+ {
979
+ parent::__construct();
980
+ $this->matrix = (object) ['distance' => 1234];
981
+ }
982
+
983
+ public function option($key = null)
984
+ {
985
+ return $key === null ? $this->testOptions : ($this->testOptions[$key] ?? null);
986
+ }
987
+
988
+ public function ask($question, $default = null)
989
+ {
990
+ $this->questions[] = [$question, $default];
991
+
992
+ return 'asked_order';
993
+ }
994
+
995
+ public function choice($question, array $choices, $default = null, $attempts = null, $multiple = false)
996
+ {
997
+ $this->choices[] = [$question, $choices, $default, $attempts, $multiple];
998
+
999
+ return 'assigned';
1000
+ }
1001
+
1002
+ public function info($string, $verbosity = null)
1003
+ {
1004
+ $this->messages[] = ['info', $string];
1005
+ }
1006
+
1007
+ public function error($string, $verbosity = null)
1008
+ {
1009
+ $this->messages[] = ['error', $string];
1010
+ }
1011
+
1012
+ protected function findOrder(string $orderId): ?Order
1013
+ {
1014
+ $this->messages[] = ['findOrder', $orderId];
1015
+
1016
+ return $this->order;
1017
+ }
1018
+
1019
+ protected function calculateDrivingDistanceAndTime(mixed $origin, mixed $destination): object
1020
+ {
1021
+ $this->messages[] = ['distance', $origin, $destination];
1022
+
1023
+ return $this->matrix;
1024
+ }
1025
+
1026
+ protected function notifyDriver($driver, string $notificationClass, Order $order, mixed $distance = null): void
1027
+ {
1028
+ $this->sent[] = [$driver, $notificationClass, $order->public_id, $distance];
1029
+ }
1030
+ }
1031
+
1032
+ class FleetOpsNotificationDriverCommandFake extends Driver
1033
+ {
1034
+ public array $notifications = [];
1035
+ public mixed $locationForTest = null;
1036
+
1037
+ public function getAttribute($key)
1038
+ {
1039
+ if ($key === 'location') {
1040
+ return $this->locationForTest;
1041
+ }
1042
+
1043
+ return parent::getAttribute($key);
1044
+ }
1045
+
1046
+ public function notify($instance): void
1047
+ {
1048
+ $this->notifications[] = $instance;
1049
+ }
1050
+ }
1051
+
1052
+ class FleetOpsNotificationOrderCommandFake extends Order
1053
+ {
1054
+ public string $publicIdForTest = 'order_public';
1055
+ public mixed $driverAssignedForTest = null;
1056
+ public mixed $payloadForTest = null;
1057
+
1058
+ public function loadMissing($relations)
1059
+ {
1060
+ return $this;
1061
+ }
1062
+
1063
+ public function getAttribute($key)
1064
+ {
1065
+ return match ($key) {
1066
+ 'public_id' => $this->publicIdForTest,
1067
+ 'driverAssigned' => $this->driverAssignedForTest,
1068
+ 'payload' => $this->payloadForTest,
1069
+ default => parent::getAttribute($key),
1070
+ };
1071
+ }
1072
+ }
1073
+
1074
+ class FleetOpsNotificationPayloadCommandFake
1075
+ {
1076
+ public function __construct(private mixed $pickup)
1077
+ {
1078
+ }
1079
+
1080
+ public function getPickupOrFirstWaypoint(): mixed
1081
+ {
1082
+ return $this->pickup;
1083
+ }
1084
+ }
1085
+
1086
+ class FleetOpsDispatchOrdersCommandFake extends DispatchOrders
1087
+ {
1088
+ public array $messages = [];
1089
+ public EloquentCollection $orders;
1090
+
1091
+ public function __construct(private array $testOptions)
1092
+ {
1093
+ parent::__construct();
1094
+ $this->orders = new EloquentCollection();
1095
+ }
1096
+
1097
+ public function option($key = null)
1098
+ {
1099
+ return $key === null ? $this->testOptions : ($this->testOptions[$key] ?? null);
1100
+ }
1101
+
1102
+ public function info($string, $verbosity = null)
1103
+ {
1104
+ $this->messages[] = ['info', $string];
1105
+ }
1106
+
1107
+ public function alert($string, $verbosity = null)
1108
+ {
1109
+ $this->messages[] = ['alert', $string];
1110
+ }
1111
+
1112
+ public function warn($string, $verbosity = null)
1113
+ {
1114
+ $this->messages[] = ['warn', $string];
1115
+ }
1116
+
1117
+ protected function getScheduledOrders(bool $sandboxMode): EloquentCollection
1118
+ {
1119
+ $this->messages[] = ['getScheduledOrders', $sandboxMode];
1120
+
1121
+ return $this->orders;
1122
+ }
1123
+ }
1124
+
1125
+ class FleetOpsScheduledOrderCommandFake extends Order
1126
+ {
1127
+ public string $publicIdForTest = '';
1128
+ public string $scheduledAtForTest = '';
1129
+ public bool $ready = false;
1130
+ public bool $dispatchedForTest = false;
1131
+
1132
+ public function getAttribute($key)
1133
+ {
1134
+ return match ($key) {
1135
+ 'public_id' => $this->publicIdForTest,
1136
+ 'scheduled_at' => $this->scheduledAtForTest,
1137
+ default => parent::getAttribute($key),
1138
+ };
1139
+ }
1140
+
1141
+ public function shouldDispatch($precision = 1)
1142
+ {
1143
+ return $this->ready;
1144
+ }
1145
+
1146
+ public function dispatch(bool $save = true): self
1147
+ {
1148
+ $this->dispatchedForTest = true;
1149
+
1150
+ return $this;
1151
+ }
1152
+ }
1153
+
1154
+ class FleetOpsAssignDriverRolesCommandFake extends AssignDriverRoles
1155
+ {
1156
+ public array $messages = [];
1157
+ public Illuminate\Support\Collection $testCompanies;
1158
+
1159
+ public function __construct()
1160
+ {
1161
+ parent::__construct();
1162
+ $this->testCompanies = collect();
1163
+ }
1164
+
1165
+ public function info($string, $verbosity = null)
1166
+ {
1167
+ $this->messages[] = ['info', $string];
1168
+ }
1169
+
1170
+ public function error($string, $verbosity = null)
1171
+ {
1172
+ $this->messages[] = ['error', $string];
1173
+ }
1174
+
1175
+ protected function companies()
1176
+ {
1177
+ return $this->testCompanies;
1178
+ }
1179
+
1180
+ protected function isUser($user): bool
1181
+ {
1182
+ return $user instanceof FleetOpsRoleUserCommandFake;
1183
+ }
1184
+
1185
+ protected function setCompanyUserRelation($user, Company $company): void
1186
+ {
1187
+ $user->companies[] = $company->uuid;
1188
+ }
1189
+
1190
+ protected function driverForCompany($user, string $companyUuid)
1191
+ {
1192
+ return $user->drivers[$companyUuid] ?? null;
1193
+ }
1194
+
1195
+ protected function isNotAdmin($user): bool
1196
+ {
1197
+ return !$user->admin;
1198
+ }
1199
+
1200
+ protected function assignDriverRole($user): void
1201
+ {
1202
+ $user->assignSingleRole('Driver');
1203
+ }
1204
+ }
1205
+
1206
+ class FleetOpsAssignCustomerRolesCommandFake extends AssignCustomerRoles
1207
+ {
1208
+ public array $messages = [];
1209
+ public array $customerUsers = [];
1210
+ public Illuminate\Support\Collection $testCustomers;
1211
+
1212
+ public function __construct()
1213
+ {
1214
+ parent::__construct();
1215
+ $this->testCustomers = collect();
1216
+ }
1217
+
1218
+ public function info($string, $verbosity = null)
1219
+ {
1220
+ $this->messages[] = ['info', $string];
1221
+ }
1222
+
1223
+ public function error($string, $verbosity = null)
1224
+ {
1225
+ $this->messages[] = ['error', $string];
1226
+ }
1227
+
1228
+ protected function customers()
1229
+ {
1230
+ return $this->testCustomers;
1231
+ }
1232
+
1233
+ protected function customerUser(Contact $customer)
1234
+ {
1235
+ return $this->customerUsers[spl_object_id($customer)] ?? null;
1236
+ }
1237
+
1238
+ protected function createUserForCustomer(Contact $customer)
1239
+ {
1240
+ $this->customerUsers[spl_object_id($customer)] = $customer->createdUser;
1241
+
1242
+ return $customer->createdUser;
1243
+ }
1244
+
1245
+ protected function assignCustomerRole($user): void
1246
+ {
1247
+ $user->assignSingleRole('Fleet-Ops Customer');
1248
+ }
1249
+ }
1250
+
1251
+ class FleetOpsRoleUserCommandFake
1252
+ {
1253
+ public string $email;
1254
+ public bool $admin = false;
1255
+ public array $roles = [];
1256
+ public array $companies = [];
1257
+ public array $drivers = [];
1258
+ public ?Throwable $failure = null;
1259
+
1260
+ public function __construct(string $email)
1261
+ {
1262
+ $this->email = $email;
1263
+ }
1264
+
1265
+ public function assignSingleRole(string $role): void
1266
+ {
1267
+ if ($this->failure) {
1268
+ throw $this->failure;
1269
+ }
1270
+
1271
+ $this->roles[] = $role;
1272
+ }
1273
+ }
1274
+
1275
+ class FleetOpsCustomerRoleCommandFake extends Contact
1276
+ {
1277
+ public mixed $createdUser = null;
1278
+ }
1279
+
1280
+ class FleetOpsTrackOrderChunkFake implements IteratorAggregate
1281
+ {
1282
+ public array $loaded = [];
1283
+
1284
+ public function __construct(private array $orders)
1285
+ {
1286
+ }
1287
+
1288
+ public function load(array $relations): self
1289
+ {
1290
+ $this->loaded = $relations;
1291
+
1292
+ return $this;
1293
+ }
1294
+
1295
+ public function getIterator(): Traversable
1296
+ {
1297
+ return new ArrayIterator($this->orders);
1298
+ }
1299
+ }
1300
+
1301
+ class FleetOpsTrackOrderFake extends Order
1302
+ {
1303
+ public int $id;
1304
+ public array $distanceCalls = [];
1305
+
1306
+ public function __construct(int $id = 0, private bool $shouldFail = false)
1307
+ {
1308
+ parent::__construct();
1309
+ $this->id = $id;
1310
+ }
1311
+
1312
+ public function setDistanceAndTime(array $options = []): Order
1313
+ {
1314
+ if ($this->shouldFail) {
1315
+ throw new RuntimeException('distance provider failed');
1316
+ }
1317
+
1318
+ $this->distanceCalls[] = $options;
1319
+
1320
+ return $this;
1321
+ }
1322
+ }
1323
+
1324
+ class FleetOpsAuditCustomerUserConflictsProbe extends AuditCustomerUserConflicts
1325
+ {
1326
+ public array $messages = [];
1327
+ public array $tables = [];
1328
+ public FleetOpsAuditCustomerQueryFake $query;
1329
+
1330
+ public function __construct(private array $testOptions, array $contacts)
1331
+ {
1332
+ parent::__construct();
1333
+ $this->query = new FleetOpsAuditCustomerQueryFake($contacts);
1334
+ }
1335
+
1336
+ public function option($key = null)
1337
+ {
1338
+ return $key === null ? $this->testOptions : ($this->testOptions[$key] ?? null);
1339
+ }
1340
+
1341
+ public function line($string, $style = null, $verbosity = null)
1342
+ {
1343
+ $this->messages[] = ['line', $string];
1344
+ }
1345
+
1346
+ public function info($string, $verbosity = null)
1347
+ {
1348
+ $this->messages[] = ['info', $string];
1349
+ }
1350
+
1351
+ public function table($headers, $rows, $tableStyle = 'default', array $columnStyles = [])
1352
+ {
1353
+ $this->tables[] = [$headers, $rows];
1354
+ }
1355
+
1356
+ protected function customerContactsQuery()
1357
+ {
1358
+ return $this->query;
1359
+ }
1360
+ }
1361
+
1362
+ class FleetOpsAuditCustomerQueryFake
1363
+ {
1364
+ public array $calls = [];
1365
+
1366
+ public function __construct(private array $contacts)
1367
+ {
1368
+ }
1369
+
1370
+ public function where(string $column, mixed $value): self
1371
+ {
1372
+ $this->calls[] = ['where', $column, $value];
1373
+
1374
+ return $this;
1375
+ }
1376
+
1377
+ public function get(): Illuminate\Support\Collection
1378
+ {
1379
+ $this->calls[] = ['get'];
1380
+
1381
+ return collect($this->contacts);
1382
+ }
1383
+ }
1384
+
1385
+ function fleetOpsAuditContact(array $attributes, mixed $user, ?object $company = null): Contact
1386
+ {
1387
+ $contact = new Contact();
1388
+ $contact->setRawAttributes(array_merge([
1389
+ 'uuid' => 'contact-uuid',
1390
+ 'public_id' => 'contact_public',
1391
+ 'name' => 'Jane Customer',
1392
+ 'email' => 'jane@example.test',
1393
+ 'company_uuid' => 'company-uuid',
1394
+ 'updated_at' => Carbon::parse('2026-07-26 12:00:00'),
1395
+ ], $attributes), true);
1396
+ $contact->setRelation('anyUser', $user);
1397
+ $contact->setRelation('company', $company ?? (object) ['name' => 'Acme Logistics']);
1398
+
1399
+ return $contact;
1400
+ }
1401
+
1402
+ function fleetOpsSyncTelematicsCommandWithOptions(array $options = []): SyncTelematics
1403
+ {
1404
+ return new class($options) extends SyncTelematics {
1405
+ public array $messages = [];
1406
+
1407
+ public function __construct(private array $testOptions)
1408
+ {
1409
+ parent::__construct();
1410
+ }
1411
+
1412
+ public function option($key = null)
1413
+ {
1414
+ return $key === null ? $this->testOptions : ($this->testOptions[$key] ?? null);
1415
+ }
1416
+
1417
+ public function info($string, $verbosity = null)
1418
+ {
1419
+ $this->messages[] = ['info', $string];
1420
+ }
1421
+
1422
+ public function warn($string, $verbosity = null)
1423
+ {
1424
+ $this->messages[] = ['warn', $string];
1425
+ }
1426
+ };
1427
+ }
1428
+
1429
+ function fleetOpsReminderSchedule(string $uuid, string $publicId, string $name, string $nextDueDate, array $offsets, ?string $email): MaintenanceSchedule
1430
+ {
1431
+ $schedule = (new ReflectionClass(MaintenanceSchedule::class))->newInstanceWithoutConstructor();
1432
+ $schedule->setRawAttributes([
1433
+ 'uuid' => $uuid,
1434
+ 'public_id' => $publicId,
1435
+ 'name' => $name,
1436
+ 'next_due_date' => Carbon::parse($nextDueDate),
1437
+ 'reminder_offsets' => json_encode($offsets),
1438
+ 'default_assignee_uuid' => $email ? $uuid . '-assignee' : null,
1439
+ ], true);
1440
+
1441
+ $schedule->setRelation('defaultAssignee', $email ? (object) ['email' => $email] : null);
1442
+
1443
+ return $schedule;
1444
+ }
1445
+
1446
+ function fleetOpsMaintenanceTriggerSchedule(string $uuid, string $publicId, string $name, string $nextDueDate, int $nextOdometer, int $nextEngineHours, Vehicle $subject): MaintenanceSchedule
1447
+ {
1448
+ $schedule = (new ReflectionClass(MaintenanceSchedule::class))->newInstanceWithoutConstructor();
1449
+ $schedule->setRawAttributes([
1450
+ 'uuid' => $uuid,
1451
+ 'public_id' => $publicId,
1452
+ 'company_uuid' => 'company-uuid',
1453
+ 'subject_type' => Vehicle::class,
1454
+ 'subject_uuid' => $subject->uuid,
1455
+ 'name' => $name,
1456
+ 'status' => 'active',
1457
+ 'next_due_date' => Carbon::parse($nextDueDate),
1458
+ 'next_due_odometer' => $nextOdometer,
1459
+ 'next_due_engine_hours' => $nextEngineHours,
1460
+ 'default_priority' => null,
1461
+ 'default_assignee_type' => 'user',
1462
+ 'default_assignee_uuid' => 'assignee-uuid',
1463
+ 'instructions' => 'Inspect brakes and tires.',
1464
+ ], true);
1465
+ $schedule->setRelation('subject', $subject);
1466
+
1467
+ return $schedule;
1468
+ }
1469
+
1470
+ function fleetOpsSimulationDriver(string $uuid = 'driver-uuid', string $publicId = 'driver_public', string $name = 'Jane Driver'): Driver
1471
+ {
1472
+ $driver = new FleetOpsDispatchAdhocOrdersDriverFake();
1473
+ $driver->uuid = $uuid;
1474
+ $driver->public_id = $publicId;
1475
+ $driver->name = $name;
1476
+
1477
+ return $driver;
1478
+ }
1479
+
1480
+ function fleetOpsSimulationOrder(?Driver $assignedDriver = null): FleetOpsSimulateOrderFake
1481
+ {
1482
+ $order = (new ReflectionClass(FleetOpsSimulateOrderFake::class))->newInstanceWithoutConstructor();
1483
+ $order->setRawAttributes([
1484
+ 'uuid' => 'order-uuid',
1485
+ 'public_id' => 'order_public',
1486
+ ], true);
1487
+ $order->assignedDriver = $assignedDriver;
1488
+ $order->setRelation('payload', new class {
1489
+ public function getPickupOrFirstWaypoint(): object
1490
+ {
1491
+ return (object) [
1492
+ 'address' => '1 Pickup Street',
1493
+ 'location' => new Point(1.30, 103.80),
1494
+ ];
1495
+ }
1496
+
1497
+ public function getDropoffOrLastWaypoint(): object
1498
+ {
1499
+ return (object) [
1500
+ 'address' => '99 Dropoff Avenue',
1501
+ 'location' => new Point(1.32, 103.82),
1502
+ ];
1503
+ }
1504
+ });
1505
+
1506
+ return $order;
1507
+ }
1508
+
1509
+ test('sync telematics exits cleanly when another process holds the lock', function () {
1510
+ $lock = new FleetOpsCommandLockFake(false);
1511
+ Cache::swap(new FleetOpsCommandCacheFake($lock));
1512
+
1513
+ $registry = new FleetOpsTelematicProviderRegistryFake(collect());
1514
+ $command = fleetOpsSyncTelematicsCommandWithOptions(['no-lock' => false]);
1515
+
1516
+ expect($command->handle($registry))->toBe(Command::SUCCESS)
1517
+ ->and($command->messages)->toContain(['warn', 'Another telematics sync run appears to be in progress.'])
1518
+ ->and($lock->released)->toBeFalse();
1519
+ });
1520
+
1521
+ test('sync telematics reports no pollable providers after provider filtering', function () {
1522
+ $registry = new FleetOpsTelematicProviderRegistryFake(collect([
1523
+ 'webhook' => new TelematicProviderDescriptor([
1524
+ 'key' => 'webhook',
1525
+ 'label' => 'Webhook Provider',
1526
+ 'supports_webhooks' => true,
1527
+ 'supports_discovery' => true,
1528
+ ]),
1529
+ 'manual' => new TelematicProviderDescriptor([
1530
+ 'key' => 'manual',
1531
+ 'label' => 'Manual Provider',
1532
+ 'supports_discovery' => false,
1533
+ ]),
1534
+ ]));
1535
+
1536
+ $command = fleetOpsSyncTelematicsCommandWithOptions([
1537
+ 'no-lock' => true,
1538
+ 'provider' => [],
1539
+ 'exclude-webhook-providers' => true,
1540
+ ]);
1541
+
1542
+ expect($command->handle($registry))->toBe(Command::SUCCESS)
1543
+ ->and($command->messages)->toContain(['info', 'No pollable telematics providers found.']);
1544
+ });
1545
+
1546
+ test('sync telematics filters requested pollable providers', function () {
1547
+ $registry = new FleetOpsTelematicProviderRegistryFake(collect([
1548
+ 'afaqy' => new TelematicProviderDescriptor([
1549
+ 'key' => 'afaqy',
1550
+ 'label' => 'Afaqy',
1551
+ 'supports_discovery' => true,
1552
+ ]),
1553
+ 'samsara' => new TelematicProviderDescriptor([
1554
+ 'key' => 'samsara',
1555
+ 'label' => 'Samsara',
1556
+ 'supports_discovery' => true,
1557
+ ]),
1558
+ 'webhook_only' => new TelematicProviderDescriptor([
1559
+ 'key' => 'webhook_only',
1560
+ 'label' => 'Webhook Only',
1561
+ 'supports_webhooks' => true,
1562
+ 'supports_discovery' => true,
1563
+ ]),
1564
+ ]));
1565
+
1566
+ $command = fleetOpsSyncTelematicsCommandWithOptions([
1567
+ 'provider' => ['samsara', 'webhook_only'],
1568
+ 'exclude-webhook-providers' => true,
1569
+ ]);
1570
+
1571
+ $method = new ReflectionMethod($command, 'pollableProviderKeys');
1572
+ $method->setAccessible(true);
1573
+
1574
+ expect($method->invoke($command, $registry))->toBe(['samsara']);
1575
+ });
1576
+
1577
+ test('debug order tracker command exits successfully', function () {
1578
+ expect((new DebugOrderTracker())->handle())->toBe(Command::SUCCESS);
1579
+ });
1580
+
1581
+ test('process maintenance triggers exposes deterministic command helpers', function () {
1582
+ Carbon::setTestNow(Carbon::parse('2026-02-03 04:05:06'));
1583
+
1584
+ $command = new FleetOpsProcessMaintenanceTriggersProbe();
1585
+ $schedule = (object) [
1586
+ 'next_due_date' => Carbon::parse('2026-02-01'),
1587
+ 'next_due_odometer' => 12000,
1588
+ 'next_due_engine_hours' => 300,
1589
+ ];
1590
+
1591
+ $vehicle = new Vehicle();
1592
+ $vehicle->odometer = 12500;
1593
+ $vehicle->engine_hours = 450;
1594
+
1595
+ expect($command->callHelper('connectionName', true))->toBe('sandbox')
1596
+ ->and($command->callHelper('connectionName', false))->toBe('mysql')
1597
+ ->and($command->callHelper('currentReadingsFromSubject', $vehicle))->toBe([12500, 450])
1598
+ ->and($command->callHelper('currentReadingsFromSubject', new stdClass()))->toBe([null, null])
1599
+ ->and($command->callHelper('triggerReasons', $schedule, 12500, 450))->toBe([
1600
+ 'date due 2026-02-01',
1601
+ 'odometer 12500 >= 12000',
1602
+ 'engine hours 450 >= 300',
1603
+ ])
1604
+ ->and($command->callHelper('triggerReasons', $schedule, 11000, 250))->toBe([
1605
+ 'date due 2026-02-01',
1606
+ ])
1607
+ ->and($command->callHelper('workOrderCode', 7, Carbon::parse('2026-02-03')))->toBe('WO-20260203-0007')
1608
+ ->and($command->callHelper('processedSummary', 2, false))->toBe('Processed 2 schedule trigger(s).')
1609
+ ->and($command->callHelper('processedSummary', 2, true))->toBe('Processed 2 schedule trigger(s) (dry run — no work orders created)');
1610
+
1611
+ Carbon::setTestNow();
1612
+ });
1613
+
1614
+ test('process maintenance triggers handles dry run create and duplicate branches', function () {
1615
+ Carbon::setTestNow(Carbon::parse('2026-02-03 04:05:06'));
1616
+
1617
+ $vehicle = new Vehicle();
1618
+ $vehicle->uuid = 'vehicle-uuid';
1619
+ $vehicle->odometer = 12500;
1620
+ $vehicle->engine_hours = 450;
1621
+
1622
+ $due = fleetOpsMaintenanceTriggerSchedule(
1623
+ 'schedule-due',
1624
+ 'schedule_public',
1625
+ 'Brake Service',
1626
+ '2026-02-01',
1627
+ 12000,
1628
+ 300,
1629
+ $vehicle
1630
+ );
1631
+ $notDue = fleetOpsMaintenanceTriggerSchedule(
1632
+ 'schedule-not-due',
1633
+ 'schedule_future',
1634
+ 'Future Service',
1635
+ '2026-03-01',
1636
+ 13000,
1637
+ 600,
1638
+ $vehicle
1639
+ );
1640
+
1641
+ $dryRun = new FleetOpsProcessMaintenanceTriggersHandleFake(['sandbox' => true, 'dry-run' => true]);
1642
+ $dryRun->testSchedules = collect([$due, $notDue]);
1643
+ $dryRun->handle();
1644
+
1645
+ $create = new FleetOpsProcessMaintenanceTriggersHandleFake(['sandbox' => false, 'dry-run' => false]);
1646
+ $create->testSchedules = collect([$due, $notDue]);
1647
+ $create->workOrderCount = 6;
1648
+ $create->handle();
1649
+
1650
+ $skipOpen = new FleetOpsProcessMaintenanceTriggersHandleFake(['sandbox' => false, 'dry-run' => false]);
1651
+ $skipOpen->testSchedules = collect([$due]);
1652
+ $skipOpen->existingOpen = true;
1653
+ $skipOpen->handle();
1654
+
1655
+ expect($dryRun->operations)->toBe([['schedules', 'sandbox']])
1656
+ ->and($dryRun->createdWorkOrders)->toBe([])
1657
+ ->and($dryRun->dispatchedEvents)->toBe([])
1658
+ ->and($dryRun->messages)->toContain(
1659
+ ['info', 'Processing maintenance schedule triggers [DRY RUN] at 2026-02-03 04:05:06'],
1660
+ ['info', 'Processed 1 schedule trigger(s) (dry run — no work orders created)']
1661
+ )
1662
+ ->and($dryRun->messages[1][1])->toContain('Triggered: schedule schedule_public (Brake Service)')
1663
+ ->and($create->operations)->toBe([
1664
+ ['schedules', 'mysql'],
1665
+ ['openWorkOrderExists', 'mysql', 'schedule-due'],
1666
+ ['workOrderCount', 'mysql'],
1667
+ ['createWorkOrder', 'mysql'],
1668
+ ])
1669
+ ->and($create->createdWorkOrders)->toHaveCount(1)
1670
+ ->and($create->createdWorkOrders[0])->toMatchArray([
1671
+ 'company_uuid' => 'company-uuid',
1672
+ 'schedule_uuid' => 'schedule-due',
1673
+ 'subject' => 'Brake Service',
1674
+ 'category' => 'preventive_maintenance',
1675
+ 'code' => 'WO-20260203-0007',
1676
+ 'status' => 'open',
1677
+ 'priority' => 'normal',
1678
+ 'target_type' => Vehicle::class,
1679
+ 'target_uuid' => 'vehicle-uuid',
1680
+ 'assignee_type' => 'user',
1681
+ 'assignee_uuid' => 'assignee-uuid',
1682
+ 'instructions' => 'Inspect brakes and tires.',
1683
+ 'created_by_uuid' => null,
1684
+ ])
1685
+ ->and($create->createdWorkOrders[0]['due_at']->toDateString())->toBe('2026-02-01')
1686
+ ->and($create->createdWorkOrders[0]['opened_at']->toDateTimeString())->toBe('2026-02-03 04:05:06')
1687
+ ->and($create->dispatchedEvents)->toBe([['schedule-due', 'work_order_created']])
1688
+ ->and($create->messages)->toContain(
1689
+ ['line', ' → Created work order work_order_created'],
1690
+ ['info', 'Processed 1 schedule trigger(s).']
1691
+ )
1692
+ ->and($skipOpen->operations)->toBe([
1693
+ ['schedules', 'mysql'],
1694
+ ['openWorkOrderExists', 'mysql', 'schedule-due'],
1695
+ ])
1696
+ ->and($skipOpen->createdWorkOrders)->toBe([])
1697
+ ->and($skipOpen->dispatchedEvents)->toBe([])
1698
+ ->and($skipOpen->messages)->toContain(
1699
+ ['line', ' → Skipped: open work order already exists for this schedule.'],
1700
+ ['info', 'Processed 0 schedule trigger(s).']
1701
+ );
1702
+
1703
+ Carbon::setTestNow();
1704
+ });
1705
+
1706
+ test('track order distance command exits when another estimation run is locked', function () {
1707
+ $lock = new FleetOpsCommandLockFake(false);
1708
+ Cache::swap(new FleetOpsCommandCacheFake($lock));
1709
+
1710
+ $command = new FleetOpsTrackOrderDistanceAndTimeProbe([
1711
+ 'provider' => null,
1712
+ 'days' => 2,
1713
+ 'chunk' => 250,
1714
+ 'dry' => false,
1715
+ 'no-lock' => false,
1716
+ ]);
1717
+
1718
+ expect($command->handle())->toBe(Command::SUCCESS)
1719
+ ->and($command->messages)->toContain(['info', 'Using provider: '])
1720
+ ->and($command->messages)->toContain(['info', 'Looking back: last 2 day(s)'])
1721
+ ->and($command->messages)->toContain(['info', 'Chunk size: 250'])
1722
+ ->and($command->messages)->toContain(['warn', 'Another run appears to be in progress (lock active). Use --no-lock to bypass.'])
1723
+ ->and($lock->released)->toBeFalse();
1724
+ });
1725
+
1726
+ test('track order distance command processes chunks and records handled order errors', function () {
1727
+ Carbon::setTestNow(Carbon::parse('2026-07-26 12:00:00'));
1728
+
1729
+ $first = new FleetOpsTrackOrderFake(101);
1730
+ $second = new FleetOpsTrackOrderFake(102, shouldFail: true);
1731
+ $third = new FleetOpsTrackOrderFake(103);
1732
+
1733
+ $command = new FleetOpsTrackOrderDistanceAndTimeProbe([
1734
+ 'provider' => 'osrm',
1735
+ 'days' => 0,
1736
+ 'chunk' => 10,
1737
+ 'dry' => false,
1738
+ 'no-lock' => true,
1739
+ ], [$first, $second, $third]);
1740
+
1741
+ expect($command->handle())->toBe(Command::SUCCESS)
1742
+ ->and($command->messages)->toContain(['info', 'Using provider: osrm'])
1743
+ ->and($command->messages)->toContain(['info', 'Looking back: last 1 day(s)'])
1744
+ ->and($command->messages)->toContain(['info', 'Chunk size: 50'])
1745
+ ->and($command->messages)->toContain(['activeOrdersQuery', '2026-07-25 12:00:00'])
1746
+ ->and($command->messages)->toContain(['createProgressBar', 3])
1747
+ ->and($command->messages)->toContain(['error', 'Order 102 failed: distance provider failed'])
1748
+ ->and($command->messages)->toContain(['newLine', 2])
1749
+ ->and($command->messages)->toContain(['info', 'Updated 2/3 orders.'])
1750
+ ->and($command->messages)->toContain(['warn', 'Encountered 1 error(s). Check logs for details.'])
1751
+ ->and($command->query->calls)->toContain(['orderBy', 'id'])
1752
+ ->and($command->query->calls)->toContain(['chunkById', 50])
1753
+ ->and($command->progressBar->started)->toBe(1)
1754
+ ->and($command->progressBar->advanced)->toBe(3)
1755
+ ->and($command->progressBar->finished)->toBe(1)
1756
+ ->and($first->distanceCalls)->toBe([['provider' => 'osrm']])
1757
+ ->and($second->distanceCalls)->toBe([])
1758
+ ->and($third->distanceCalls)->toBe([['provider' => 'osrm']]);
1759
+
1760
+ Carbon::setTestNow();
1761
+ });
1762
+
1763
+ test('audit customer user conflicts outputs suspicious rows as json and applies company filters', function () {
1764
+ $adminUser = (object) [
1765
+ 'uuid' => 'user-uuid',
1766
+ 'email' => 'admin@example.test',
1767
+ 'type' => 'admin',
1768
+ 'companyUsers' => collect([
1769
+ (object) [
1770
+ 'company_uuid' => 'company-uuid',
1771
+ 'roles' => collect([
1772
+ (object) ['name' => 'Admin'],
1773
+ (object) ['name' => 'Fleet-Ops Customer'],
1774
+ ]),
1775
+ ],
1776
+ ]),
1777
+ ];
1778
+ $contact = fleetOpsAuditContact([], $adminUser);
1779
+
1780
+ $command = new FleetOpsAuditCustomerUserConflictsProbe([
1781
+ 'company' => 'company-uuid',
1782
+ 'json' => true,
1783
+ ], [$contact]);
1784
+
1785
+ expect($command->handle())->toBe(Command::SUCCESS)
1786
+ ->and($command->query->calls)->toContain(['where', 'company_uuid', 'company-uuid'])
1787
+ ->and($command->messages)->toHaveCount(1)
1788
+ ->and(json_decode($command->messages[0][1], true))->toMatchArray([
1789
+ [
1790
+ 'contact_id' => 'contact_public',
1791
+ 'contact_uuid' => 'contact-uuid',
1792
+ 'contact_name' => 'Jane Customer',
1793
+ 'contact_email' => 'jane@example.test',
1794
+ 'user_uuid' => 'user-uuid',
1795
+ 'user_email' => 'admin@example.test',
1796
+ 'user_type' => 'admin',
1797
+ 'roles' => 'Admin, Fleet-Ops Customer',
1798
+ 'company_uuid' => 'company-uuid',
1799
+ 'company_name' => 'Acme Logistics',
1800
+ 'updated_at' => '2026-07-26 12:00:00',
1801
+ 'reason' => 'linked user type is admin; linked user has non-customer roles: Admin',
1802
+ ],
1803
+ ]);
1804
+ });
1805
+
1806
+ test('audit customer user conflicts handles missing users tables and clean audits', function () {
1807
+ $missingUserContact = fleetOpsAuditContact([
1808
+ 'uuid' => 'missing-contact-uuid',
1809
+ 'public_id' => 'missing_contact',
1810
+ 'name' => 'Missing User',
1811
+ ], null);
1812
+ $customerUser = (object) [
1813
+ 'uuid' => 'customer-user-uuid',
1814
+ 'email' => 'customer@example.test',
1815
+ 'type' => 'customer',
1816
+ 'companyUsers' => collect([
1817
+ (object) [
1818
+ 'company_uuid' => 'company-uuid',
1819
+ 'roles' => collect([
1820
+ (object) ['name' => 'Fleet-Ops Customer'],
1821
+ ]),
1822
+ ],
1823
+ ]),
1824
+ ];
1825
+ $cleanContact = fleetOpsAuditContact([
1826
+ 'uuid' => 'clean-contact-uuid',
1827
+ 'public_id' => 'clean_contact',
1828
+ ], $customerUser);
1829
+
1830
+ $tableCommand = new FleetOpsAuditCustomerUserConflictsProbe([
1831
+ 'company' => null,
1832
+ 'json' => false,
1833
+ ], [$missingUserContact, $cleanContact]);
1834
+ $cleanCommand = new FleetOpsAuditCustomerUserConflictsProbe([
1835
+ 'company' => null,
1836
+ 'json' => false,
1837
+ ], [$cleanContact]);
1838
+
1839
+ expect($tableCommand->handle())->toBe(Command::SUCCESS)
1840
+ ->and($tableCommand->tables)->toHaveCount(1)
1841
+ ->and($tableCommand->tables[0][1][0])->toMatchArray([
1842
+ 'contact_id' => 'missing_contact',
1843
+ 'user_uuid' => null,
1844
+ 'user_type' => null,
1845
+ 'roles' => '',
1846
+ 'reason' => 'missing linked user',
1847
+ ])
1848
+ ->and($cleanCommand->handle())->toBe(Command::SUCCESS)
1849
+ ->and($cleanCommand->messages)->toContain(['info', 'No suspicious customer user conflicts found.']);
1850
+ });
1851
+
1852
+ test('send maintenance reminders sends eligible reminders and skips ineligible schedules', function () {
1853
+ Carbon::setTestNow(Carbon::parse('2026-07-26 09:30:00'));
1854
+
1855
+ $eligible = fleetOpsReminderSchedule(
1856
+ 'schedule-eligible',
1857
+ 'schedule_public',
1858
+ 'Quarterly inspection',
1859
+ '2026-07-28',
1860
+ [7, 1],
1861
+ 'mechanic@example.test',
1862
+ );
1863
+ $alreadySent = fleetOpsReminderSchedule(
1864
+ 'schedule-already',
1865
+ 'schedule_already',
1866
+ 'Already sent',
1867
+ '2026-07-26',
1868
+ [0],
1869
+ 'lead@example.test',
1870
+ );
1871
+ $withoutEmail = fleetOpsReminderSchedule(
1872
+ 'schedule-no-email',
1873
+ 'schedule_no_email',
1874
+ 'No assignee email',
1875
+ '2026-07-27',
1876
+ [7],
1877
+ null,
1878
+ );
1879
+
1880
+ $alreadySentKey = implode('|', ['sandbox', 'schedule-already', 0, '2026-07-26']);
1881
+ $command = new FleetOpsSendMaintenanceRemindersCommandFake([
1882
+ 'sandbox' => true,
1883
+ 'dry-run' => false,
1884
+ ], [$alreadySentKey]);
1885
+ $command->testSchedules = collect([$eligible, $alreadySent, $withoutEmail]);
1886
+
1887
+ $command->handle();
1888
+
1889
+ expect($command->messages)->toContain(['schedules', 'sandbox'])
1890
+ ->and($command->sent)->toBe([
1891
+ ['mechanic@example.test', 'schedule-eligible', 7],
1892
+ ])
1893
+ ->and($command->records)->toBe([
1894
+ ['sandbox', 'schedule-eligible', 7, '2026-07-28'],
1895
+ ])
1896
+ ->and($command->messages)->toContain(['info', 'Sent 1 reminder(s).'])
1897
+ ->and(collect($command->messages)->pluck(1)->filter()->contains(fn ($message) => str_contains($message, 'no email on default assignee')))->toBeTrue();
1898
+
1899
+ Carbon::setTestNow();
1900
+ });
1901
+
1902
+ test('send maintenance reminders dry run counts reminders without sending or recording', function () {
1903
+ Carbon::setTestNow(Carbon::parse('2026-07-26 09:30:00'));
1904
+
1905
+ $command = new FleetOpsSendMaintenanceRemindersCommandFake([
1906
+ 'sandbox' => false,
1907
+ 'dry-run' => true,
1908
+ ]);
1909
+ $command->testSchedules = collect([
1910
+ fleetOpsReminderSchedule(
1911
+ 'schedule-dry-run',
1912
+ 'schedule_dry_run',
1913
+ 'Dry run service',
1914
+ '2026-07-30',
1915
+ [7],
1916
+ 'dry@example.test',
1917
+ ),
1918
+ ]);
1919
+
1920
+ $command->handle();
1921
+
1922
+ expect($command->messages)->toContain(['schedules', 'mysql'])
1923
+ ->and($command->sent)->toBe([])
1924
+ ->and($command->records)->toBe([])
1925
+ ->and($command->messages)->toContain(['info', 'Sent 1 reminder(s) (dry run — no emails sent)']);
1926
+
1927
+ Carbon::setTestNow();
1928
+ });
1929
+
1930
+ test('simulate order route navigation dispatches waypoint events with headings', function () {
1931
+ $driver = fleetOpsSimulationDriver();
1932
+ $command = new FleetOpsSimulateOrderRouteNavigationCommandFake([
1933
+ 'order' => 'order_public',
1934
+ 'driver' => 'driver_public',
1935
+ ]);
1936
+ $command->order = fleetOpsSimulationOrder();
1937
+ $command->driver = $driver;
1938
+ $command->route = ['code' => 'Ok', 'routes' => [['geometry' => 'encoded-route']]];
1939
+ $command->waypoints = [
1940
+ new Point(1.30, 103.80),
1941
+ new Point(1.31, 103.81),
1942
+ new Point(1.32, 103.82),
1943
+ ];
1944
+
1945
+ expect($command->handle())->toBe(Command::SUCCESS)
1946
+ ->and($command->messages)->toContain(['findOrder', 'order_public'])
1947
+ ->and($command->messages)->toContain(['findDriver', 'driver_public'])
1948
+ ->and($command->messages)->toContain(['decodePolyline', 'encoded-route'])
1949
+ ->and($command->messages)->toContain(['info', 'Order pickup point located at: 1 Pickup Street'])
1950
+ ->and($command->messages)->toContain(['info', 'Order dropoff point located at: 99 Dropoff Avenue'])
1951
+ ->and($command->events)->toHaveCount(3)
1952
+ ->and($command->events[0][0])->toBe('driver_public')
1953
+ ->and($command->events[0][2])->toHaveKey('heading')
1954
+ ->and($command->events[0][2]['index'])->toBe(0)
1955
+ ->and($command->events[1][2])->toHaveKey('heading')
1956
+ ->and($command->events[2][2])->toBe(['index' => 2])
1957
+ ->and($command->pauses)->toBe(3)
1958
+ ->and($command->promptQuestions())->toBe([
1959
+ 'order' => 'Which order ID should be used to simulate driving the route for?',
1960
+ ]);
1961
+ });
1962
+
1963
+ test('simulate order route navigation handles missing order driver fallback and unroutable responses', function () {
1964
+ $missingOrder = new FleetOpsSimulateOrderRouteNavigationCommandFake([
1965
+ 'order' => 'missing-order',
1966
+ 'driver' => null,
1967
+ ]);
1968
+
1969
+ expect($missingOrder->handle())->toBe(Command::FAILURE)
1970
+ ->and($missingOrder->messages)->toContain(['error', 'Order not found to simulate driving for.']);
1971
+
1972
+ $fallbackDriver = fleetOpsSimulationDriver('assigned-driver-uuid', 'assigned_driver', 'Assigned Driver');
1973
+ $fallback = new FleetOpsSimulateOrderRouteNavigationCommandFake([
1974
+ 'order' => 'order_public',
1975
+ 'driver' => 'missing-driver',
1976
+ ]);
1977
+ $fallback->order = fleetOpsSimulationOrder($fallbackDriver);
1978
+ $fallback->route = ['code' => 'NoRoute'];
1979
+ $fallback->driver = null;
1980
+
1981
+ expect($fallback->handle())->toBe(Command::SUCCESS)
1982
+ ->and($fallback->messages)->toContain(['findDriver', 'missing-driver'])
1983
+ ->and($fallback->messages)->toContain(['error', 'The driver specified was not found, defaulting to driver assigned to order.'])
1984
+ ->and($fallback->messages)->toContain(['info', 'Route navigation simulation completed.'])
1985
+ ->and($fallback->events)->toBe([])
1986
+ ->and($fallback->pauses)->toBe(0);
1987
+
1988
+ $withoutDriver = new FleetOpsSimulateOrderRouteNavigationCommandFake([
1989
+ 'order' => 'order_public',
1990
+ 'driver' => null,
1991
+ ]);
1992
+ $withoutDriver->order = fleetOpsSimulationOrder();
1993
+
1994
+ expect($withoutDriver->handle())->toBe(Command::FAILURE)
1995
+ ->and($withoutDriver->messages)->toContain(['error', 'No driver found to simulate the order.']);
1996
+ });
1997
+
1998
+ test('dispatch adhoc command exits when no orders are dispatchable', function () {
1999
+ $command = new FleetOpsDispatchAdhocOrdersCommandFake([
2000
+ 'sandbox' => false,
2001
+ 'testing' => false,
2002
+ 'days' => 0,
2003
+ ]);
2004
+
2005
+ $command->handle();
2006
+
2007
+ expect($command->messages)->toContain(['info', 'Running in production mode.'])
2008
+ ->and($command->messages)->toContain(['info', 'Looking back 1 day(s) for dispatchable orders...'])
2009
+ ->and($command->messages)->toContain(['getDispatchableOrders', 1, 4, 72])
2010
+ ->and($command->messages)->toContain(['info', 'No dispatchable orders found in the given timeframe.']);
2011
+ });
2012
+
2013
+ test('dispatch adhoc command handles invalid pickups empty drivers and successful pings', function () {
2014
+ Carbon::setTestNow(Carbon::parse('2026-05-06 07:08:09'));
2015
+
2016
+ $invalidOrder = new FleetOpsDispatchAdhocOrdersOrderFake();
2017
+ $invalidOrder->public_id = 'order_invalid';
2018
+ $invalidOrder->dispatched_at = '2026-05-06 06:00:00';
2019
+ $invalidOrder->pickupLocation = null;
2020
+
2021
+ $emptyDriverOrder = new FleetOpsDispatchAdhocOrdersOrderFake();
2022
+ $emptyDriverOrder->public_id = 'order_empty';
2023
+ $emptyDriverOrder->dispatched_at = '2026-05-06 06:05:00';
2024
+ $emptyDriverOrder->pickupLocation = new Point(1.3, 103.8);
2025
+ $emptyDriverOrder->adhocDistance = 1200;
2026
+
2027
+ $pingOrder = new FleetOpsDispatchAdhocOrdersOrderFake();
2028
+ $pingOrder->public_id = 'order_ping';
2029
+ $pingOrder->dispatched_at = '2026-05-06 06:10:00';
2030
+ $pingOrder->pickupLocation = new Point(1.31, 103.81);
2031
+ $pingOrder->adhocDistance = 2400;
2032
+
2033
+ $driver = new FleetOpsDispatchAdhocOrdersDriverFake();
2034
+ $driver->public_id = 'driver_public';
2035
+ $driver->name = 'Jane Driver';
2036
+
2037
+ $command = new class(['sandbox' => true, 'testing' => true, 'days' => 3], $pingOrder, $driver) extends FleetOpsDispatchAdhocOrdersCommandFake {
2038
+ public function __construct(array $options, private Order $pingOrder, private Driver $driver)
2039
+ {
2040
+ parent::__construct($options);
2041
+ }
2042
+
2043
+ public function getNearbyDriversForOrder(Order $order, Point $pickup, int $distance, bool $testing = false): EloquentCollection
2044
+ {
2045
+ parent::getNearbyDriversForOrder($order, $pickup, $distance, $testing);
2046
+
2047
+ return $order === $this->pingOrder ? new EloquentCollection([$this->driver]) : new EloquentCollection();
2048
+ }
2049
+ };
2050
+ $command->orders = new EloquentCollection([$invalidOrder, $emptyDriverOrder, $pingOrder]);
2051
+
2052
+ $command->handle();
2053
+
2054
+ expect($command->messages)->toContain(['info', 'Running in sandbox mode.'])
2055
+ ->and($command->messages)->toContain(['alert', '3 orders found for ad-hoc dispatch. Current Time: 2026-05-06 07:08:09'])
2056
+ ->and($command->messages)->toContain(['error', 'Invalid pickup location for order order_invalid'])
2057
+ ->and($command->messages)->toContain(['warn', 'No available drivers found for order order_empty'])
2058
+ ->and($command->messages)->toContain(['line', 'Checking order order_ping for nearby drivers within 2400 meters.'])
2059
+ ->and($command->messages)->toContain(['info', 'Order order_ping dispatched successfully to 1 nearby drivers.'])
2060
+ ->and($command->messages)->toContain(['info', 'Pinging driver Jane Driver (driver_public) ...'])
2061
+ ->and($pingOrder->dispatchedForPing)->toBeTrue()
2062
+ ->and($driver->notifications)->toBe([OrderPing::class])
2063
+ ->and($command->tables)->toHaveCount(1);
2064
+
2065
+ Carbon::setTestNow();
2066
+ });
2067
+
2068
+ test('dispatch adhoc command builds dispatchable order and nearby driver queries', function () {
2069
+ Carbon::setTestNow(Carbon::parse('2026-05-06 07:08:09'));
2070
+
2071
+ $command = new FleetOpsDispatchAdhocOrdersQueryProbe([
2072
+ 'sandbox' => true,
2073
+ ]);
2074
+
2075
+ expect($command->getDispatchableOrders(3))->toBe($command->orderQuery->results)
2076
+ ->and($command->orderQuery->calls[0])->toBe(['connection', 'sandbox'])
2077
+ ->and($command->orderQuery->calls)->toContain(['where', [['adhoc' => 1, 'dispatched' => 1, 'started' => 0]]])
2078
+ ->and($command->orderQuery->calls)->toContain(['whereNull', 'driver_assigned_uuid'])
2079
+ ->and($command->orderQuery->calls)->toContain(['whereNull', 'deleted_at'])
2080
+ ->and($command->orderQuery->calls)->toContain(['where', ['status', '!=', 'canceled']])
2081
+ ->and($command->orderQuery->calls)->toContain(['whereHas', 'company'])
2082
+ ->and($command->orderQuery->calls)->toContain(['whereHas', 'payload'])
2083
+ ->and($command->orderQuery->calls)->toContain(['with', ['company', 'payload']]);
2084
+
2085
+ $order = new Order();
2086
+ $order->setRawAttributes(['company_uuid' => 'company-uuid']);
2087
+ $point = new Point(1.3, 103.8);
2088
+
2089
+ expect($command->getNearbyDriversForOrder($order, $point, 500, true))->toBe($command->driverQuery->results)
2090
+ ->and($command->driverQuery->calls)->toContain(['where', [['online' => 1]]])
2091
+ ->and($command->driverQuery->calls)->toContain(['whereNull', 'deleted_at'])
2092
+ ->and($command->driverQuery->calls)->not->toContain(['whereNotNull', 'location']);
2093
+
2094
+ $command = new FleetOpsDispatchAdhocOrdersQueryProbe([
2095
+ 'sandbox' => false,
2096
+ ]);
2097
+
2098
+ $command->getNearbyDriversForOrder($order, $point, 750, false);
2099
+
2100
+ expect($command->driverQuery->calls)->toContain(['whereNotNull', 'location'])
2101
+ ->and($command->driverQuery->calls)->toContain(['distanceSphere', 'location', $point, 750])
2102
+ ->and($command->driverQuery->calls)->toContain(['distanceSphereValue', 'location', $point]);
2103
+
2104
+ Carbon::setTestNow();
2105
+ });
2106
+
2107
+ test('legacy order config command creates configs and updates legacy orders', function () {
2108
+ $company = new Company();
2109
+ $company->setRawAttributes(['uuid' => 'company-uuid'], true);
2110
+
2111
+ $order = new FleetOpsLegacyOrderFake();
2112
+ $order->setRawAttributes(['uuid' => 'order-uuid', 'company_uuid' => 'company-uuid'], true);
2113
+
2114
+ $config = new OrderConfig();
2115
+ $config->setRawAttributes(['uuid' => 'config-uuid'], true);
2116
+
2117
+ $command = new FleetOpsFixLegacyOrderConfigsCommandFake(true);
2118
+ $command->testCompanies = collect([$company]);
2119
+ $command->testOrders = collect([$order]);
2120
+ $command->configs = ['company-uuid' => $config];
2121
+
2122
+ $command->handle();
2123
+
2124
+ expect($command->created)->toBe(['company-uuid'])
2125
+ ->and($order->updates)->toBe([['order_config_uuid' => 'config-uuid']])
2126
+ ->and($command->messages)->toContain(['info', 'Initializing transport config for 1 companies.'])
2127
+ ->and($command->messages)->toContain(['info', '1 orders found for updating.'])
2128
+ ->and($command->messages)->toContain(['info', 'All orders have been processed.'])
2129
+ ->and($command->progressBar->started)->toBe(2)
2130
+ ->and($command->progressBar->advanced)->toBe(2)
2131
+ ->and($command->progressBar->finished)->toBe(2);
2132
+ });
2133
+
2134
+ test('legacy order config command reports order update errors and continues', function () {
2135
+ $order = new FleetOpsLegacyOrderFake();
2136
+ $order->setRawAttributes(['uuid' => 'order-error', 'company_uuid' => 'company-uuid'], true);
2137
+ $order->failUpdate = true;
2138
+
2139
+ $config = new OrderConfig();
2140
+ $config->setRawAttributes(['uuid' => 'config-uuid'], true);
2141
+
2142
+ $command = new FleetOpsFixLegacyOrderConfigsCommandFake(false);
2143
+ $command->testOrders = collect([$order]);
2144
+ $command->configs = ['company-uuid' => $config];
2145
+
2146
+ $command->handle();
2147
+
2148
+ expect($command->created)->toBe([])
2149
+ ->and($command->messages)->toContain(['error', 'order update failed'])
2150
+ ->and($command->messages)->toContain(['error', 'Order ID: order-error'])
2151
+ ->and($command->messages)->toContain(['info', 'All orders have been processed.'])
2152
+ ->and($command->progressBar->advanced)->toBe(0)
2153
+ ->and($command->progressBar->finished)->toBe(1);
2154
+ });
2155
+
2156
+ test('fix customer companies command creates or links users and assigns missing company records', function () {
2157
+ $createdUser = new FleetOpsUserCommandFake('created-user', 'Created User', 'created@example.test');
2158
+
2159
+ $customerWithoutUser = new FleetOpsCustomerCommandFake();
2160
+ $customerWithoutUser->setRawAttributes([
2161
+ 'name' => 'Created Customer',
2162
+ 'email' => 'created@example.test',
2163
+ 'phone' => '100',
2164
+ 'company_uuid' => 'company-created',
2165
+ ], true);
2166
+ $customerWithoutUser->createdUser = $createdUser;
2167
+
2168
+ $existingUser = new FleetOpsUserCommandFake('existing-user', 'Existing User', 'existing@example.test');
2169
+
2170
+ $customerWithExistingEmail = new FleetOpsCustomerCommandFake();
2171
+ $customerWithExistingEmail->setRawAttributes([
2172
+ 'name' => 'Existing Customer',
2173
+ 'email' => 'existing@example.test',
2174
+ 'phone' => '200',
2175
+ 'company_uuid' => 'company-existing',
2176
+ ], true);
2177
+ $customerWithExistingEmail->failCreate = true;
2178
+
2179
+ $createdCompany = new Company();
2180
+ $createdCompany->setRawAttributes(['uuid' => 'company-created', 'name' => 'Created Company'], true);
2181
+
2182
+ $existingCompany = new Company();
2183
+ $existingCompany->setRawAttributes(['uuid' => 'company-existing', 'name' => 'Existing Company'], true);
2184
+
2185
+ $command = new FleetOpsFixCustomerCompaniesCommandFake();
2186
+ $command->testCustomers = collect([$customerWithoutUser, $customerWithExistingEmail]);
2187
+ $command->users = ['existing@example.test' => $existingUser];
2188
+ $command->missing = [
2189
+ 'created-user:company-created' => true,
2190
+ 'existing-user:company-existing' => true,
2191
+ ];
2192
+ $command->companies = [
2193
+ 'company-created' => $createdCompany,
2194
+ 'company-existing' => $existingCompany,
2195
+ ];
2196
+
2197
+ expect($command->handle())->toBe(Command::SUCCESS)
2198
+ ->and($command->messages)->toContain(['info', 'User created for customer (Created Customer - created@example.test)'])
2199
+ ->and($command->messages)->toContain(['error', 'existing user'])
2200
+ ->and($command->messages)->toContain(['error', 'Existing user: existing@example.test'])
2201
+ ->and($command->messages)->toContain(['info', 'Update customer user to existing user of the same email address.'])
2202
+ ->and($customerWithExistingEmail->quietUpdates)->toBe([['user_uuid' => 'existing-user']])
2203
+ ->and($createdUser->synced)->toBe([
2204
+ ['email', FleetOpsCustomerCommandFake::class],
2205
+ ['phone', FleetOpsCustomerCommandFake::class],
2206
+ ])
2207
+ ->and($existingUser->synced)->toBe([
2208
+ ['email', FleetOpsCustomerCommandFake::class],
2209
+ ['phone', FleetOpsCustomerCommandFake::class],
2210
+ ])
2211
+ ->and($createdUser->assigned)->toBe([['company-created', 'Administrator']])
2212
+ ->and($existingUser->assigned)->toBe([['company-existing', 'Administrator']]);
2213
+ });
2214
+
2215
+ test('fix driver companies command syncs assigned users and missing company assignments', function () {
2216
+ $user = new FleetOpsUserCommandFake('driver-user', 'Driver User', 'driver@example.test');
2217
+
2218
+ $driver = new Driver();
2219
+ $driver->setRawAttributes([
2220
+ 'email' => 'driver@example.test',
2221
+ 'phone' => '300',
2222
+ 'company_uuid' => 'driver-company',
2223
+ ], true);
2224
+ $driver->setRelation('user', $user);
2225
+
2226
+ $company = new Company();
2227
+ $company->setRawAttributes(['uuid' => 'driver-company', 'name' => 'Driver Company'], true);
2228
+
2229
+ $command = new FleetOpsFixDriverCompaniesCommandFake();
2230
+ $command->testDrivers = collect([$driver]);
2231
+ $command->missing = ['driver-user:driver-company' => true];
2232
+ $command->companies = ['driver-company' => $company];
2233
+
2234
+ expect($command->handle())->toBe(Command::SUCCESS)
2235
+ ->and($user->synced)->toBe([
2236
+ ['email', Driver::class],
2237
+ ['phone', Driver::class],
2238
+ ])
2239
+ ->and($user->assigned)->toBe([['driver-company', 'Administrator']])
2240
+ ->and($command->messages)->toContain(['line', 'Found driver Driver User (driver@example.test) which doesnt have correct company assignment.'])
2241
+ ->and($command->messages)->toContain(['line', 'Driver driver@example.test was assigned to company: Driver Company']);
2242
+ });
2243
+
2244
+ test('purge unpurchased service quotes command commits deletes and rolls back failures', function () {
2245
+ Carbon::setTestNow(Carbon::parse('2026-05-06 12:00:00'));
2246
+
2247
+ $deleted = new FleetOpsPurgeServiceQuotesCommandFake();
2248
+ $deleted->deletedCount = 3;
2249
+
2250
+ expect($deleted->handle())->toBe(Command::SUCCESS)
2251
+ ->and($deleted->events[0])->toBe('disable')
2252
+ ->and($deleted->events[1])->toBe('begin')
2253
+ ->and($deleted->events[2][0])->toBe('purge')
2254
+ ->and($deleted->events[2][1]->toDateTimeString())->toBe('2026-05-04 12:00:00')
2255
+ ->and($deleted->events)->toContain('commit')
2256
+ ->and($deleted->events)->toContain('enable')
2257
+ ->and($deleted->messages)->toContain(['info', 'Successfully deleted 3 unpurchased service quotes.']);
2258
+
2259
+ $empty = new FleetOpsPurgeServiceQuotesCommandFake();
2260
+
2261
+ expect($empty->handle())->toBe(Command::SUCCESS)
2262
+ ->and($empty->messages)->toContain(['info', 'No unpurchased service quotes found for deletion.']);
2263
+
2264
+ $failed = new FleetOpsPurgeServiceQuotesCommandFake();
2265
+ $failed->failure = new RuntimeException('delete failed');
2266
+
2267
+ expect($failed->handle())->toBe(Command::FAILURE)
2268
+ ->and($failed->events)->toContain('rollback')
2269
+ ->and($failed->events)->toContain('enable')
2270
+ ->and($failed->messages)->toContain(['error', 'Error deleting unpurchased service quotes: delete failed']);
2271
+
2272
+ Carbon::setTestNow();
2273
+ });
2274
+
2275
+ test('send driver notification command handles lookup validation choices and ping notifications', function () {
2276
+ $missing = new FleetOpsSendDriverNotificationCommandFake([
2277
+ 'id' => null,
2278
+ 'event' => 'assigned',
2279
+ ]);
2280
+
2281
+ expect($missing->handle())->toBe(1)
2282
+ ->and($missing->questions)->toBe([['Enter the order ID to trigger the notification', null]])
2283
+ ->and($missing->messages)->toContain(['findOrder', 'asked_order'])
2284
+ ->and($missing->messages)->toContain(['error', 'Order not found!']);
2285
+
2286
+ $withoutDriver = new FleetOpsSendDriverNotificationCommandFake([
2287
+ 'id' => 'order_public',
2288
+ 'event' => 'assigned',
2289
+ ]);
2290
+ $withoutDriver->order = new FleetOpsNotificationOrderCommandFake();
2291
+ $withoutDriver->order->driverAssignedForTest = null;
2292
+
2293
+ expect($withoutDriver->handle())->toBe(1)
2294
+ ->and($withoutDriver->messages)->toContain(['error', 'Order does not have a driver assigned!']);
2295
+
2296
+ $driver = new FleetOpsNotificationDriverCommandFake();
2297
+ $driver->locationForTest = 'driver-location';
2298
+
2299
+ $order = new FleetOpsNotificationOrderCommandFake();
2300
+ $order->driverAssignedForTest = $driver;
2301
+ $order->payloadForTest = new FleetOpsNotificationPayloadCommandFake('pickup-location');
2302
+
2303
+ $ping = new FleetOpsSendDriverNotificationCommandFake([
2304
+ 'id' => 'order_public',
2305
+ 'event' => 'ping',
2306
+ ]);
2307
+ $ping->order = $order;
2308
+
2309
+ expect($ping->handle())->toBe(0)
2310
+ ->and($ping->messages)->toContain(['distance', 'driver-location', 'pickup-location'])
2311
+ ->and($ping->messages)->toContain(['info', "Notification 'ping' has been triggered for order ID 'order_public'."])
2312
+ ->and($ping->sent)->toBe([[$driver, OrderPing::class, 'order_public', 1234]]);
2313
+
2314
+ $choice = new FleetOpsSendDriverNotificationCommandFake([
2315
+ 'id' => 'order_public',
2316
+ 'event' => null,
2317
+ ]);
2318
+ $choice->order = $order;
2319
+
2320
+ expect($choice->handle())->toBe(0)
2321
+ ->and($choice->choices[0][0])->toBe('Select the event to trigger')
2322
+ ->and($choice->choices[0][1])->toBe(['assigned', 'canceled', 'dispatched', 'ping'])
2323
+ ->and($choice->sent)->toBe([[$driver, OrderAssigned::class, 'order_public', null]]);
2324
+
2325
+ $invalid = new FleetOpsSendDriverNotificationCommandFake([
2326
+ 'id' => 'order_public',
2327
+ 'event' => 'unknown',
2328
+ ]);
2329
+ $invalid->order = $order;
2330
+
2331
+ expect($invalid->handle())->toBe(1)
2332
+ ->and($invalid->messages)->toContain(['error', 'Invalid event selected!']);
2333
+ });
2334
+
2335
+ test('dispatch orders command dispatches ready scheduled orders and warns for unready ones', function () {
2336
+ Carbon::setTestNow(Carbon::parse('2026-05-06 07:08:09'));
2337
+
2338
+ $ready = new FleetOpsScheduledOrderCommandFake();
2339
+ $ready->publicIdForTest = 'order_ready';
2340
+ $ready->scheduledAtForTest = '2026-05-06 07:00:00';
2341
+ $ready->ready = true;
2342
+
2343
+ $waiting = new FleetOpsScheduledOrderCommandFake();
2344
+ $waiting->publicIdForTest = 'order_waiting';
2345
+ $waiting->scheduledAtForTest = '2026-05-06 08:00:00';
2346
+
2347
+ $command = new FleetOpsDispatchOrdersCommandFake(['sandbox' => 'true']);
2348
+ $command->orders = new EloquentCollection([$ready, $waiting]);
2349
+
2350
+ $command->handle();
2351
+
2352
+ expect($command->messages)->toContain(['info', 'Running in sandbox mode.'])
2353
+ ->and($command->messages)->toContain(['getScheduledOrders', true])
2354
+ ->and($command->messages)->toContain(['alert', 'Found 2 orders scheduled for dispatch. Current Time: 2026-05-06 07:08:09'])
2355
+ ->and($command->messages)->toContain(['info', 'Order order_ready dispatched successfully (2026-05-06 07:00:00).'])
2356
+ ->and($command->messages)->toContain(['warn', 'Order order_waiting is not ready for dispatch (2026-05-06 08:00:00).'])
2357
+ ->and($ready->dispatchedForTest)->toBeTrue()
2358
+ ->and($waiting->dispatchedForTest)->toBeFalse();
2359
+
2360
+ Carbon::setTestNow();
2361
+ });
2362
+
2363
+ test('assign driver roles command assigns non admin driver users and reports failures', function () {
2364
+ $company = new Company();
2365
+ $company->setRawAttributes(['uuid' => 'company-uuid', 'name' => 'Acme Logistics'], true);
2366
+
2367
+ $driverUser = new FleetOpsRoleUserCommandFake('driver@example.test');
2368
+ $driverUser->drivers = ['company-uuid' => (object) ['uuid' => 'driver-uuid']];
2369
+
2370
+ $adminUser = new FleetOpsRoleUserCommandFake('admin@example.test');
2371
+ $adminUser->admin = true;
2372
+ $adminUser->drivers = ['company-uuid' => (object) ['uuid' => 'admin-driver']];
2373
+
2374
+ $failingUser = new FleetOpsRoleUserCommandFake('fail@example.test');
2375
+ $failingUser->drivers = ['company-uuid' => (object) ['uuid' => 'fail-driver']];
2376
+ $failingUser->failure = new RuntimeException('role store failed');
2377
+
2378
+ $company->setRelation('users', collect([$driverUser, $adminUser, $failingUser, (object) ['email' => 'ignored@example.test']]));
2379
+
2380
+ $command = new FleetOpsAssignDriverRolesCommandFake();
2381
+ $command->testCompanies = collect([$company]);
2382
+
2383
+ expect($command->handle())->toBe(Command::SUCCESS)
2384
+ ->and($driverUser->roles)->toBe(['Driver'])
2385
+ ->and($adminUser->roles)->toBe([])
2386
+ ->and($failingUser->roles)->toBe([])
2387
+ ->and($driverUser->companies)->toBe(['company-uuid'])
2388
+ ->and($command->messages)->toContain(['info', 'Acme Logistics - Driver: driver@example.test has been made Driver.'])
2389
+ ->and($command->messages)->toContain(['error', 'role store failed']);
2390
+ });
2391
+
2392
+ test('assign customer roles command creates missing users and reports assignment errors', function () {
2393
+ $createdUser = new FleetOpsRoleUserCommandFake('created@example.test');
2394
+ $existingUser = new FleetOpsRoleUserCommandFake('existing@example.test');
2395
+ $failingUser = new FleetOpsRoleUserCommandFake('fail@example.test');
2396
+ $failingUser->failure = new RuntimeException('customer role failed');
2397
+
2398
+ $needsUser = new FleetOpsCustomerRoleCommandFake();
2399
+ $needsUser->setRawAttributes(['name' => 'Created Customer', 'email' => 'created@example.test'], true);
2400
+ $needsUser->createdUser = $createdUser;
2401
+
2402
+ $existing = new FleetOpsCustomerRoleCommandFake();
2403
+ $existing->setRawAttributes(['name' => 'Existing Customer', 'email' => 'existing@example.test'], true);
2404
+
2405
+ $failing = new FleetOpsCustomerRoleCommandFake();
2406
+ $failing->setRawAttributes(['name' => 'Fail Customer', 'email' => 'fail@example.test'], true);
2407
+
2408
+ $command = new FleetOpsAssignCustomerRolesCommandFake();
2409
+ $command->testCustomers = collect([$needsUser, $existing, $failing]);
2410
+ $command->customerUsers = [
2411
+ spl_object_id($existing) => $existingUser,
2412
+ spl_object_id($failing) => $failingUser,
2413
+ ];
2414
+
2415
+ expect($command->handle())->toBe(Command::SUCCESS)
2416
+ ->and($createdUser->roles)->toBe(['Fleet-Ops Customer'])
2417
+ ->and($existingUser->roles)->toBe(['Fleet-Ops Customer'])
2418
+ ->and($failingUser->roles)->toBe([])
2419
+ ->and($command->messages)->toContain(['info', 'Created Customer - Customer: created@example.test has been assigned the Customer role.'])
2420
+ ->and($command->messages)->toContain(['info', 'Existing Customer - Customer: existing@example.test has been assigned the Customer role.'])
2421
+ ->and($command->messages)->toContain(['error', 'customer role failed']);
2422
+ });
2423
+
2424
+ test('test email command rejects unsupported email types before sending mail', function () {
2425
+ $command = new class extends TestEmail {
2426
+ public array $messages = [];
2427
+
2428
+ public function argument($key = null)
2429
+ {
2430
+ $arguments = ['email' => 'customer@example.test'];
2431
+
2432
+ return $key === null ? $arguments : ($arguments[$key] ?? null);
2433
+ }
2434
+
2435
+ public function option($key = null)
2436
+ {
2437
+ $options = ['type' => 'unknown'];
2438
+
2439
+ return $key === null ? $options : ($options[$key] ?? null);
2440
+ }
2441
+
2442
+ public function info($string, $verbosity = null)
2443
+ {
2444
+ $this->messages[] = ['info', $string];
2445
+ }
2446
+
2447
+ public function error($string, $verbosity = null)
2448
+ {
2449
+ $this->messages[] = ['error', $string];
2450
+ }
2451
+ };
2452
+
2453
+ expect($command->handle())->toBe(Command::FAILURE)
2454
+ ->and($command->messages)->toContain(['error', 'Unknown email type: unknown']);
2455
+ });
2456
+
2457
+ test('test email command reports send failures as a command failure', function () {
2458
+ $command = new class extends TestEmail {
2459
+ public array $messages = [];
2460
+
2461
+ public function argument($key = null)
2462
+ {
2463
+ $arguments = ['email' => 'customer@example.test'];
2464
+
2465
+ return $key === null ? $arguments : ($arguments[$key] ?? null);
2466
+ }
2467
+
2468
+ public function option($key = null)
2469
+ {
2470
+ $options = ['type' => 'customer_credentials'];
2471
+
2472
+ return $key === null ? $options : ($options[$key] ?? null);
2473
+ }
2474
+
2475
+ public function info($string, $verbosity = null)
2476
+ {
2477
+ $this->messages[] = ['info', $string];
2478
+ }
2479
+
2480
+ public function error($string, $verbosity = null)
2481
+ {
2482
+ $this->messages[] = ['error', $string];
2483
+ }
2484
+ };
2485
+
2486
+ // The mail transport is unavailable in the harness, so the send throws and
2487
+ // the command reports the failure rather than surfacing the exception
2488
+ expect($command->handle())->toBe(Command::FAILURE)
2489
+ ->and(collect($command->messages)->pluck(0))->toContain('error');
2490
+ });
2491
+
2492
+ test('track order distance command exits early when no qualifying orders exist', function () {
2493
+ Carbon::setTestNow(Carbon::parse('2026-07-26 12:00:00'));
2494
+
2495
+ $command = new FleetOpsTrackOrderDistanceAndTimeProbe([
2496
+ 'provider' => null,
2497
+ 'days' => 2,
2498
+ 'chunk' => 100,
2499
+ 'dry' => false,
2500
+ 'no-lock' => true,
2501
+ ], []);
2502
+
2503
+ expect($command->handle())->toBe(Command::SUCCESS)
2504
+ ->and($command->messages)->toContain(['info', 'No qualifying orders found. Exiting.']);
2505
+
2506
+ Carbon::setTestNow();
2507
+ });
2508
+
2509
+ test('track order distance real query and progress bar builders execute', function () {
2510
+ $connection = new Illuminate\Database\SQLiteConnection(new PDO('sqlite::memory:'));
2511
+ $resolver = new Illuminate\Database\ConnectionResolver(['default' => $connection, 'mysql' => $connection]);
2512
+ $resolver->setDefaultConnection('mysql');
2513
+ Model::setConnectionResolver($resolver);
2514
+ $schema = $connection->getSchemaBuilder();
2515
+ foreach (['orders' => ['uuid', 'public_id', 'company_uuid', 'payload_uuid', 'status', 'started_at', '_key'], 'payloads' => ['uuid', 'public_id', 'company_uuid', '_key']] as $table => $columns) {
2516
+ $schema->create($table, function ($blueprint) use ($columns) {
2517
+ $blueprint->increments('id');
2518
+ foreach ($columns as $column) {
2519
+ $blueprint->string($column)->nullable();
2520
+ }
2521
+ $blueprint->timestamps();
2522
+ $blueprint->timestamp('deleted_at')->nullable();
2523
+ });
2524
+ }
2525
+ $connection->table('payloads')->insert(['uuid' => 'payload-track-1', 'company_uuid' => 'company-1']);
2526
+ $connection->table('orders')->insert([
2527
+ ['uuid' => 'order-track-active', 'company_uuid' => 'company-1', 'payload_uuid' => 'payload-track-1', 'status' => 'started', 'started_at' => '2026-07-25 10:00:00', 'created_at' => '2026-07-25 10:00:00', 'updated_at' => '2026-07-25 10:00:00'],
2528
+ ['uuid' => 'order-track-done', 'company_uuid' => 'company-1', 'payload_uuid' => 'payload-track-1', 'status' => 'completed', 'started_at' => '2026-07-25 10:00:00', 'created_at' => '2026-07-25 10:00:00', 'updated_at' => '2026-07-25 10:00:00'],
2529
+ ]);
2530
+
2531
+ $command = new TrackOrderDistanceAndTime();
2532
+ $reflection = new ReflectionMethod(TrackOrderDistanceAndTime::class, 'activeOrdersQuery');
2533
+ $reflection->setAccessible(true);
2534
+ $query = $reflection->invoke($command, Illuminate\Support\Carbon::parse('2026-07-20 00:00:00'));
2535
+ expect($query->count('id'))->toBe(1);
2536
+
2537
+ $output = new class {
2538
+ public array $bars = [];
2539
+
2540
+ public function createProgressBar(int $total)
2541
+ {
2542
+ $this->bars[] = $total;
2543
+
2544
+ return new FleetOpsTrackProgressBarFake();
2545
+ }
2546
+ };
2547
+ $outputProperty = new ReflectionProperty(Command::class, 'output');
2548
+ $outputProperty->setAccessible(true);
2549
+ $outputProperty->setValue($command, $output);
2550
+
2551
+ $barMethod = new ReflectionMethod(TrackOrderDistanceAndTime::class, 'createProgressBar');
2552
+ $barMethod->setAccessible(true);
2553
+ expect($barMethod->invoke($command, 7))->toBeInstanceOf(FleetOpsTrackProgressBarFake::class)
2554
+ ->and($output->bars)->toBe([7]);
2555
+ });
2556
+
2557
+ class FleetOpsRecordedDriverNotification
2558
+ {
2559
+ public static array $constructed = [];
2560
+
2561
+ public function __construct(...$args)
2562
+ {
2563
+ static::$constructed[] = $args;
2564
+ }
2565
+ }
2566
+
2567
+ class FleetOpsThrowingDriverNotificationCommandFake extends FleetOpsSendDriverNotificationCommandFake
2568
+ {
2569
+ protected function notifyDriver($driver, string $notificationClass, Order $order, mixed $distance = null): void
2570
+ {
2571
+ throw new Exception('notification channel offline');
2572
+ }
2573
+ }
2574
+
2575
+ test('send driver notification real helpers resolve orders distances and notify branches', function () {
2576
+ // Real order lookup against sqlite
2577
+ $connection = new Illuminate\Database\SQLiteConnection(new PDO('sqlite::memory:'));
2578
+ $resolver = new Illuminate\Database\ConnectionResolver(['default' => $connection, 'mysql' => $connection]);
2579
+ $resolver->setDefaultConnection('mysql');
2580
+ Model::setConnectionResolver($resolver);
2581
+ $schema = $connection->getSchemaBuilder();
2582
+ $schema->create('orders', function ($blueprint) {
2583
+ $blueprint->increments('id');
2584
+ foreach (['uuid', 'public_id', 'company_uuid', 'status', 'meta', '_key'] as $column) {
2585
+ $blueprint->string($column)->nullable();
2586
+ }
2587
+ $blueprint->timestamps();
2588
+ $blueprint->timestamp('deleted_at')->nullable();
2589
+ });
2590
+ $connection->table('orders')->insert(['uuid' => 'order-sdn-1', 'public_id' => 'order_sdnreal1', 'company_uuid' => 'company-1', 'status' => 'created']);
2591
+
2592
+ $command = new SendDriverNotification();
2593
+ $helper = function (string $method, ...$arguments) use ($command) {
2594
+ $reflection = new ReflectionMethod(SendDriverNotification::class, $method);
2595
+ $reflection->setAccessible(true);
2596
+
2597
+ return $reflection->invoke($command, ...$arguments);
2598
+ };
2599
+
2600
+ expect($helper('findOrder', 'order_sdnreal1')?->uuid)->toBe('order-sdn-1')
2601
+ ->and($helper('findOrder', 'order_missing0'))->toBeNull();
2602
+
2603
+ // Real driving matrix from two points
2604
+ $matrix = $helper('calculateDrivingDistanceAndTime', new Point(1.30, 103.80), new Point(1.31, 103.81));
2605
+ expect($matrix->distance)->toBeGreaterThan(0);
2606
+
2607
+ // Both notify branches construct the notification with and without distance
2608
+ $order = new Order();
2609
+ $order->setRawAttributes(['uuid' => 'order-sdn-1', 'public_id' => 'order_sdnreal1'], true);
2610
+ $driver = new class {
2611
+ public array $notified = [];
2612
+
2613
+ public function notify($notification)
2614
+ {
2615
+ $this->notified[] = $notification;
2616
+ }
2617
+ };
2618
+ FleetOpsRecordedDriverNotification::$constructed = [];
2619
+ $helper('notifyDriver', $driver, FleetOpsRecordedDriverNotification::class, $order, 4321);
2620
+ $helper('notifyDriver', $driver, FleetOpsRecordedDriverNotification::class, $order);
2621
+ expect($driver->notified)->toHaveCount(2)
2622
+ ->and(FleetOpsRecordedDriverNotification::$constructed[0])->toHaveCount(2)
2623
+ ->and(FleetOpsRecordedDriverNotification::$constructed[1])->toHaveCount(1);
2624
+
2625
+ // Notification failures surface as command errors
2626
+ $driverFake = new FleetOpsNotificationDriverCommandFake();
2627
+ $orderFake = new FleetOpsNotificationOrderCommandFake();
2628
+ $orderFake->driverAssignedForTest = $driverFake;
2629
+
2630
+ $failing = new FleetOpsThrowingDriverNotificationCommandFake([
2631
+ 'id' => 'order_public',
2632
+ 'event' => 'assigned',
2633
+ ]);
2634
+ $failing->order = $orderFake;
2635
+
2636
+ expect($failing->handle())->toBe(0)
2637
+ ->and($failing->messages)->toContain(['error', 'notification channel offline']);
2638
+ });
2639
+
2640
+ if (!function_exists('Fleetbase\FleetOps\Console\Commands\event')) {
2641
+ eval('namespace Fleetbase\FleetOps\Console\Commands; function event($event = null) { $GLOBALS["fleetopsSimulatedEvents"][] = $event; return []; }');
2642
+ }
2643
+
2644
+ test('simulate order route navigation real helpers resolve lookups routes and events', function () {
2645
+ $connection = new Illuminate\Database\SQLiteConnection(new PDO('sqlite::memory:'));
2646
+ $resolver = new Illuminate\Database\ConnectionResolver(['default' => $connection, 'mysql' => $connection]);
2647
+ $resolver->setDefaultConnection('mysql');
2648
+ Model::setConnectionResolver($resolver);
2649
+ $schema = $connection->getSchemaBuilder();
2650
+ foreach (['orders' => ['uuid', 'public_id', 'company_uuid', 'status', '_key'], 'drivers' => ['uuid', 'public_id', 'company_uuid', 'user_uuid', '_key'], 'users' => ['uuid', 'public_id', 'company_uuid', '_key']] as $table => $columns) {
2651
+ $schema->create($table, function ($blueprint) use ($columns) {
2652
+ $blueprint->increments('id');
2653
+ foreach ($columns as $column) {
2654
+ $blueprint->string($column)->nullable();
2655
+ }
2656
+ $blueprint->timestamps();
2657
+ $blueprint->timestamp('deleted_at')->nullable();
2658
+ });
2659
+ }
2660
+ $connection->table('orders')->insert(['uuid' => 'order-simnav-1', 'public_id' => 'order_simnav1', 'company_uuid' => 'company-1', 'status' => 'created']);
2661
+ $connection->table('users')->insert(['uuid' => 'user-simnav-1', 'company_uuid' => 'company-1']);
2662
+ $connection->table('drivers')->insert(['uuid' => 'driver-simnav-1', 'public_id' => 'driver_simnav1', 'company_uuid' => 'company-1', 'user_uuid' => 'user-simnav-1']);
2663
+
2664
+ $command = new SimulateOrderRouteNavigation();
2665
+ $helper = function (string $method, ...$arguments) use ($command) {
2666
+ $reflection = new ReflectionMethod(SimulateOrderRouteNavigation::class, $method);
2667
+ $reflection->setAccessible(true);
2668
+
2669
+ return $reflection->invoke($command, ...$arguments);
2670
+ };
2671
+
2672
+ expect($helper('findOrder', 'order_simnav1')?->uuid)->toBe('order-simnav-1')
2673
+ ->and($helper('findDriver', 'driver_simnav1')?->uuid)->toBe('driver-simnav-1');
2674
+
2675
+ // Route retrieval flows through the faked OSRM client and polyline decode
2676
+ Cache::swap(new Illuminate\Cache\Repository(new Illuminate\Cache\ArrayStore()));
2677
+ Illuminate\Support\Facades\Http::swap(new Illuminate\Http\Client\Factory());
2678
+ Illuminate\Support\Facades\Http::fake(fn () => Illuminate\Support\Facades\Http::response(['code' => 'Ok', 'routes' => [['geometry' => '_p~iF~ps|U_ulLnnqC_mqNvxq`@']]]));
2679
+ $route = $helper('getRoute', new Point(1.30, 103.80), new Point(1.31, 103.81));
2680
+ expect($route['code'])->toBe('Ok');
2681
+
2682
+ $points = $helper('decodePolyline', '_p~iF~ps|U_ulLnnqC_mqNvxq`@');
2683
+ expect($points)->toHaveCount(3);
2684
+
2685
+ // Location events dispatch through the namespaced event shim
2686
+ $GLOBALS['fleetopsSimulatedEvents'] = [];
2687
+ $driver = Driver::where('uuid', 'driver-simnav-1')->first();
2688
+ $helper('dispatchLocationChanged', $driver, $points[0], ['progress' => 10]);
2689
+ expect($GLOBALS['fleetopsSimulatedEvents'])->toHaveCount(1);
2690
+
2691
+ // The waypoint pacing pause simply sleeps between broadcasts
2692
+ $paused = microtime(true);
2693
+ $helper('pauseBetweenWaypoints');
2694
+ expect(microtime(true) - $paused)->toBeGreaterThan(2.5);
2695
+ });