@fleetbase/fleetops-engine 0.6.49 → 0.6.51

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 (271) hide show
  1. package/README.md +185 -67
  2. package/addon/components/contact/panel-header.hbs +11 -48
  3. package/addon/components/customer/create-order-form.hbs +2 -2
  4. package/addon/components/customer/details.hbs +1 -2
  5. package/addon/components/customer/form.hbs +62 -50
  6. package/addon/components/customer/form.js +27 -1
  7. package/addon/components/customer/order-form.hbs +2 -2
  8. package/addon/components/customer/orders.hbs +50 -2
  9. package/addon/components/customer/orders.js +67 -5
  10. package/addon/components/display-place.hbs +2 -2
  11. package/addon/components/driver/form.hbs +1 -1
  12. package/addon/components/home/getting-started-guidance.hbs +113 -0
  13. package/addon/components/home/getting-started-guidance.js +107 -0
  14. package/addon/components/issue/details/comments.hbs +3 -0
  15. package/addon/components/issue/details/comments.js +3 -0
  16. package/addon/components/issue/details/correspondence.hbs +9 -0
  17. package/addon/components/issue/details/correspondence.js +83 -0
  18. package/addon/components/issue/details/documents.hbs +69 -0
  19. package/addon/components/issue/details/documents.js +85 -0
  20. package/addon/components/issue/details.hbs +220 -59
  21. package/addon/components/issue/details.js +245 -0
  22. package/addon/components/issue/form.hbs +19 -1
  23. package/addon/components/issue/panel-header.hbs +43 -0
  24. package/addon/components/issue/panel-header.js +22 -0
  25. package/addon/components/issue/timeline.hbs +40 -0
  26. package/addon/components/issue/timeline.js +45 -0
  27. package/addon/components/leaflet-tracking-marker.js +3 -0
  28. package/addon/components/map/drawer/position-listing.js +3 -0
  29. package/addon/components/map/google-live-map.js +6 -2
  30. package/addon/components/map/leaflet-live-map.hbs +48 -14
  31. package/addon/components/map/leaflet-live-map.js +201 -20
  32. package/addon/components/map/order-list-overlay.hbs +4 -4
  33. package/addon/components/modals/convert-customer-to-vendor.hbs +30 -0
  34. package/addon/components/modals/convert-customer-to-vendor.js +55 -0
  35. package/addon/components/modals/issue-assign.hbs +20 -0
  36. package/addon/components/modals/issue-assign.js +49 -0
  37. package/addon/components/modals/issue-close.hbs +30 -0
  38. package/addon/components/modals/issue-close.js +73 -0
  39. package/addon/components/modals/issue-status.hbs +20 -0
  40. package/addon/components/modals/issue-status.js +43 -0
  41. package/addon/components/modals/order-change-destination.hbs +46 -0
  42. package/addon/components/modals/order-change-destination.js +106 -0
  43. package/addon/components/modals/reset-customer-credentials.js +12 -6
  44. package/addon/components/modals/stop-activity.hbs +28 -0
  45. package/addon/components/modals/stop-activity.js +52 -0
  46. package/addon/components/modals/update-order-activity.hbs +78 -2
  47. package/addon/components/modals/update-order-activity.js +197 -5
  48. package/addon/components/orchestrator/resource-panel.hbs +0 -10
  49. package/addon/components/orchestrator/resource-panel.js +2 -4
  50. package/addon/components/order/activity-list.hbs +2 -2
  51. package/addon/components/order/activity-timeline.hbs +31 -29
  52. package/addon/components/order/details/activity.hbs +3 -3
  53. package/addon/components/order/details/activity.js +13 -2
  54. package/addon/components/order/details/documents.js +2 -0
  55. package/addon/components/order/details/proof.hbs +40 -38
  56. package/addon/components/order/details/proof.js +31 -4
  57. package/addon/components/order/details/purchase-rate.hbs +2 -2
  58. package/addon/components/order/details/route.hbs +11 -3
  59. package/addon/components/order/details/route.js +91 -14
  60. package/addon/components/order/details/tracking.hbs +101 -72
  61. package/addon/components/order/details/tracking.js +149 -2
  62. package/addon/components/order/details.hbs +5 -5
  63. package/addon/components/order/details.js +19 -1
  64. package/addon/components/order/form/details.hbs +3 -3
  65. package/addon/components/order/form/documents.js +11 -4
  66. package/addon/components/order/form/route.hbs +108 -104
  67. package/addon/components/order/form/route.js +37 -1
  68. package/addon/components/order/form/service-rate.hbs +2 -2
  69. package/addon/components/order/header.hbs +1 -1
  70. package/addon/components/order-config-manager/activity-flow.hbs +4 -1
  71. package/addon/components/order-config-manager/activity-flow.js +178 -12
  72. package/addon/components/place/address.js +1 -1
  73. package/addon/components/route-list.hbs +17 -29
  74. package/addon/components/route-list.js +42 -10
  75. package/addon/components/service-rate/details.hbs +42 -1
  76. package/addon/components/service-rate/form.hbs +119 -1
  77. package/addon/components/service-rate/form.js +59 -0
  78. package/addon/components/vehicle/form.js +1 -1
  79. package/addon/components/vehicle/pill.hbs +1 -1
  80. package/addon/components/vendor/personnel-manager.hbs +166 -0
  81. package/addon/components/vendor/personnel-manager.js +143 -0
  82. package/addon/components/widget/live-fleet.hbs +1 -0
  83. package/addon/components/widget/live-fleet.js +2 -7
  84. package/addon/components/widget/revenue-trend.hbs +1 -1
  85. package/addon/components/widget/revenue-trend.js +75 -2
  86. package/addon/controllers/management/contacts/customers/details.js +31 -13
  87. package/addon/controllers/management/contacts/customers.js +16 -5
  88. package/addon/controllers/management/contacts/index/details.js +31 -13
  89. package/addon/controllers/management/contacts/index.js +7 -0
  90. package/addon/controllers/management/issues/index/details.js +52 -6
  91. package/addon/controllers/management/vehicles/index/new.js +1 -1
  92. package/addon/controllers/management/vendors/index/details.js +4 -0
  93. package/addon/controllers/operations/orders/index/details.js +15 -7
  94. package/addon/controllers/operations/orders/index.js +15 -1
  95. package/addon/extension.js +19 -12
  96. package/addon/helpers/format-duration.js +1 -38
  97. package/addon/helpers/place-address-html.js +2 -2
  98. package/addon/initializers/load-leaflet-assets.js +5 -6
  99. package/addon/initializers/patch-ember-leaflet-tooltip-layer.js +51 -0
  100. package/addon/routes/management/issues/index/details.js +1 -1
  101. package/addon/routes/operations/service-rates/index.js +1 -1
  102. package/addon/routes.js +1 -0
  103. package/addon/services/contact-actions.js +231 -1
  104. package/addon/services/geofence.js +163 -19
  105. package/addon/services/getting-started.js +44 -0
  106. package/addon/services/issue-actions.js +71 -0
  107. package/addon/services/leaflet-contextmenu-manager.js +27 -0
  108. package/addon/services/leaflet-map-manager.js +2 -0
  109. package/addon/services/map-adapter/leaflet.js +19 -0
  110. package/addon/services/service-areas.js +3 -0
  111. package/addon/services/vehicle-actions.js +1 -1
  112. package/addon/styles/fleetops-engine.css +1127 -1
  113. package/addon/templates/management/contacts/customers/details.hbs +1 -1
  114. package/addon/templates/management/contacts/index/details.hbs +1 -1
  115. package/addon/templates/management/contacts.hbs +2 -2
  116. package/addon/templates/management/issues/index/details/index.hbs +3 -1
  117. package/addon/templates/management/issues/index/details.hbs +3 -2
  118. package/addon/templates/management/vendors/index/details/personnel.hbs +1 -2
  119. package/addon/templates/operations/orders/index/details/index.hbs +2 -2
  120. package/addon/templates/operations/orders/index.hbs +1 -1
  121. package/addon/templates/operations/service-rates/index/edit.hbs +1 -0
  122. package/addon/templates/operations/service-rates/index/new.hbs +1 -0
  123. package/addon/templates/settings/payments/onboard.hbs +8 -8
  124. package/addon/utils/fleet-ops-options.js +2 -1
  125. package/addon/utils/leaflet-draw-namespace-guard.js +22 -0
  126. package/addon/utils/leaflet-plugin-loader.js +219 -0
  127. package/addon/utils/normalize-order-config-flow.js +41 -0
  128. package/addon/utils/place-address-html.js +3 -2
  129. package/addon/utils/route-visualization.js +54 -7
  130. package/addon/utils/setup-customer-portal.js +4 -64
  131. package/app/components/home/getting-started-guidance.js +1 -0
  132. package/app/components/issue/details/comments.js +1 -0
  133. package/app/components/issue/details/correspondence.js +1 -0
  134. package/app/components/issue/details/documents.js +1 -0
  135. package/app/components/issue/panel-header.js +1 -0
  136. package/app/components/modals/convert-customer-to-vendor.js +1 -0
  137. package/app/components/modals/issue-assign.js +1 -0
  138. package/app/components/modals/issue-close.js +1 -0
  139. package/app/components/modals/issue-status.js +1 -0
  140. package/app/components/modals/order-change-destination.js +1 -0
  141. package/app/components/modals/stop-activity.js +1 -0
  142. package/app/components/vendor/personnel-manager.js +1 -0
  143. package/app/helpers/format-duration.js +1 -1
  144. package/app/services/getting-started.js +1 -0
  145. package/app/utils/leaflet-draw-namespace-guard.js +1 -0
  146. package/app/utils/leaflet-plugin-loader.js +1 -0
  147. package/composer.json +1 -1
  148. package/extension.json +1 -1
  149. package/package.json +5 -5
  150. package/server/migrations/2026_05_21_000001_add_geographic_rules_to_service_rate_fees_table.php +37 -0
  151. package/server/migrations/2026_05_25_000001_add_endpoint_tracking_refs_to_payloads_table.php +50 -0
  152. package/server/migrations/2026_05_27_000001_default_driver_vehicle_statuses_to_available.php +48 -0
  153. package/server/migrations/2026_05_31_000001_add_portal_fields_to_vendor_personnels_table.php +30 -0
  154. package/server/migrations/2026_06_03_000000_ensure_tags_column_on_issues_table.php +31 -0
  155. package/server/migrations/2026_06_03_000001_add_order_uuid_to_issues_table.php +25 -0
  156. package/server/migrations/2026_06_04_000001_backfill_order_transaction_subjects.php +39 -0
  157. package/server/migrations/2026_06_04_000002_backfill_order_transaction_directions.php +24 -0
  158. package/server/resources/views/labels/default.php +57 -49
  159. package/server/resources/views/labels/waypoint-label.php +27 -20
  160. package/server/resources/views/mail/customer-credentials.blade.php +16 -13
  161. package/server/seeders/Testing/ConnectivitySeeder.php +1 -0
  162. package/server/seeders/Testing/FleetSeeder.php +22 -22
  163. package/server/seeders/Testing/OrdersSeeder.php +6 -22
  164. package/server/seeders/Testing/VroomOrchestrationSeeder.php +21 -21
  165. package/server/src/Console/Commands/AuditCustomerUserConflicts.php +75 -0
  166. package/server/src/Exceptions/CustomerUserConflictException.php +13 -0
  167. package/server/src/Expansions/UserFilterExpansion.php +28 -0
  168. package/server/src/Http/Controllers/Api/v1/ContactController.php +17 -2
  169. package/server/src/Http/Controllers/Api/v1/CustomerController.php +876 -0
  170. package/server/src/Http/Controllers/Api/v1/DriverController.php +1 -1
  171. package/server/src/Http/Controllers/Api/v1/IssueController.php +2 -0
  172. package/server/src/Http/Controllers/Api/v1/OrderConfigController.php +54 -0
  173. package/server/src/Http/Controllers/Api/v1/OrderController.php +109 -80
  174. package/server/src/Http/Controllers/Api/v1/PlaceController.php +10 -58
  175. package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -2
  176. package/server/src/Http/Controllers/Api/v1/VehicleController.php +20 -3
  177. package/server/src/Http/Controllers/Internal/v1/ContactController.php +154 -0
  178. package/server/src/Http/Controllers/Internal/v1/CustomerController.php +116 -6
  179. package/server/src/Http/Controllers/Internal/v1/GettingStartedController.php +17 -0
  180. package/server/src/Http/Controllers/Internal/v1/IssueController.php +251 -0
  181. package/server/src/Http/Controllers/Internal/v1/LiveController.php +101 -66
  182. package/server/src/Http/Controllers/Internal/v1/OrderController.php +530 -52
  183. package/server/src/Http/Controllers/Internal/v1/PlaceController.php +10 -68
  184. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +1 -1
  185. package/server/src/Http/Controllers/Internal/v1/SettingController.php +2 -2
  186. package/server/src/Http/Controllers/Internal/v1/VehicleController.php +54 -1
  187. package/server/src/Http/Controllers/Internal/v1/VendorController.php +89 -0
  188. package/server/src/Http/Middleware/AuthenticateCustomerToken.php +34 -0
  189. package/server/src/Http/Requests/CreateCustomerOrderRequest.php +63 -0
  190. package/server/src/Http/Requests/CreateCustomerRequest.php +48 -0
  191. package/server/src/Http/Requests/CreateDriverRequest.php +1 -1
  192. package/server/src/Http/Requests/CreateIssueRequest.php +4 -0
  193. package/server/src/Http/Requests/CreateVehicleRequest.php +25 -1
  194. package/server/src/Http/Requests/Internal/CreateDriverRequest.php +1 -1
  195. package/server/src/Http/Requests/UpdateIssueRequest.php +4 -0
  196. package/server/src/Http/Requests/VerifyCreateCustomerRequest.php +29 -0
  197. package/server/src/Http/Resources/Internal/v1/OrderConfig.php +41 -0
  198. package/server/src/Http/Resources/v1/Customer.php +79 -0
  199. package/server/src/Http/Resources/v1/Index/Order.php +12 -9
  200. package/server/src/Http/Resources/v1/Issue.php +25 -0
  201. package/server/src/Http/Resources/v1/Order.php +20 -18
  202. package/server/src/Http/Resources/v1/OrderConfig.php +74 -0
  203. package/server/src/Http/Resources/v1/Payload.php +23 -20
  204. package/server/src/Http/Resources/v1/ServiceArea.php +33 -19
  205. package/server/src/Http/Resources/v1/ServiceRateFee.php +7 -0
  206. package/server/src/Listeners/HandleOrderDispatched.php +2 -2
  207. package/server/src/Mail/CustomerCredentialsMail.php +5 -6
  208. package/server/src/Models/Contact.php +122 -66
  209. package/server/src/Models/Customer.php +64 -0
  210. package/server/src/Models/Driver.php +12 -3
  211. package/server/src/Models/Entity.php +2 -2
  212. package/server/src/Models/Issue.php +47 -6
  213. package/server/src/Models/Order.php +62 -10
  214. package/server/src/Models/Payload.php +17 -1
  215. package/server/src/Models/ServiceArea.php +1 -1
  216. package/server/src/Models/ServiceRate.php +273 -1
  217. package/server/src/Models/ServiceRateFee.php +39 -9
  218. package/server/src/Models/TrackingNumber.php +8 -2
  219. package/server/src/Models/Vehicle.php +18 -2
  220. package/server/src/Models/VendorPersonnel.php +10 -1
  221. package/server/src/Models/Waypoint.php +2 -2
  222. package/server/src/Models/Zone.php +1 -1
  223. package/server/src/Notifications/LateDeparture.php +2 -2
  224. package/server/src/Notifications/ProlongedStoppage.php +2 -2
  225. package/server/src/Notifications/RouteDeviation.php +2 -2
  226. package/server/src/Observers/ContactObserver.php +2 -0
  227. package/server/src/Observers/PurchaseRateObserver.php +20 -0
  228. package/server/src/Observers/ServiceRateObserver.php +6 -6
  229. package/server/src/Observers/ZoneObserver.php +56 -0
  230. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +1 -1
  231. package/server/src/Providers/FleetOpsServiceProvider.php +2 -0
  232. package/server/src/Support/Analytics/AbstractAnalytics.php +6 -6
  233. package/server/src/Support/Analytics/FuelEfficiency.php +2 -2
  234. package/server/src/Support/Analytics/OrdersByStatus.php +1 -1
  235. package/server/src/Support/Analytics/RevenueTrend.php +0 -1
  236. package/server/src/Support/Analytics/TopDrivers.php +10 -2
  237. package/server/src/Support/CustomerAuth.php +98 -0
  238. package/server/src/Support/GettingStarted.php +144 -0
  239. package/server/src/Support/LabelPdf.php +52 -0
  240. package/server/src/Support/Metrics/AbstractMetric.php +4 -4
  241. package/server/src/Support/PlaceSearch.php +282 -0
  242. package/server/src/Support/ResolvesOrderServiceStops.php +522 -0
  243. package/server/src/Tracking/TrackingContextBuilder.php +41 -38
  244. package/server/src/Tracking/TrackingIntelligenceService.php +109 -9
  245. package/server/src/Tracking/TrackingStop.php +13 -11
  246. package/server/src/routes.php +76 -10
  247. package/server/tests/AnalyticsRoutesTest.php +8 -0
  248. package/server/tests/CapacityAllocationEngineTest.php +10 -10
  249. package/server/tests/CustomerContactIdentitySafetyTest.php +95 -0
  250. package/server/tests/CustomerEndpointTest.php +199 -0
  251. package/server/tests/DriverVehicleStatusDefaultTest.php +39 -0
  252. package/server/tests/GettingStartedStatusTest.php +21 -0
  253. package/server/tests/LabelPdfRenderingTest.php +23 -0
  254. package/server/tests/LiveControllerViewportTest.php +60 -0
  255. package/server/tests/MetricsRegistryTest.php +4 -8
  256. package/server/tests/OrchestrationPayloadBuilderTest.php +2 -2
  257. package/server/tests/OrderActivityFlowRegressionTest.php +116 -0
  258. package/server/tests/OrderConfigEndpointTest.php +86 -0
  259. package/server/tests/OrderFileAttachmentTest.php +20 -0
  260. package/server/tests/ServiceAreaZoneApiShapeTest.php +78 -0
  261. package/server/tests/VehicleDriverAssignmentTest.php +18 -0
  262. package/translations/ar-ae.yml +9 -0
  263. package/translations/bg-bg.yaml +9 -0
  264. package/translations/en-us.yaml +12 -0
  265. package/translations/es-pa.yaml +9 -0
  266. package/translations/fr-fr.yaml +9 -0
  267. package/translations/mn-mn.yaml +9 -0
  268. package/translations/pt-br.yaml +9 -0
  269. package/translations/ru-ru.yaml +9 -0
  270. package/translations/vi-vn.yaml +9 -0
  271. package/server/migrations/2026_05_13_000001_repair_customer_contact_user_types.php +0 -86
@@ -8,6 +8,8 @@ use Illuminate\Support\Facades\Cache;
8
8
 
9
9
  class TrackingIntelligenceService
10
10
  {
11
+ protected const TERMINAL_STATUSES = ['completed', 'canceled'];
12
+
11
13
  public function __construct(
12
14
  protected TrackingContextBuilder $contextBuilder,
13
15
  protected TrackingProviderManager $providerManager,
@@ -34,19 +36,28 @@ class TrackingIntelligenceService
34
36
  'completion_seconds' => data_get($tracker, 'eta.completion_seconds'),
35
37
  'active_stop_at' => data_get($tracker, 'eta.active_stop_at'),
36
38
  'completion_at' => data_get($tracker, 'eta.completion_at'),
39
+ 'start_seconds' => data_get($tracker, 'eta.start_seconds'),
40
+ 'start_at' => data_get($tracker, 'eta.start_at'),
37
41
  'provider' => data_get($tracker, 'provider'),
38
42
  'confidence' => data_get($tracker, 'confidence'),
43
+ 'lifecycle' => data_get($tracker, 'lifecycle'),
39
44
  'warnings' => data_get($tracker, 'warnings', []),
40
45
  ];
41
46
  }
42
47
 
43
48
  protected function buildResult(TrackingContext $context, TrackingProviderResult $providerResult, TrackingOptions $options): array
44
49
  {
45
- $now = now();
46
- $completionSeconds = $providerResult->durationInTrafficSeconds ?? $providerResult->durationSeconds;
47
- $activeStopSeconds = data_get($providerResult->legs, '0.duration_in_traffic_s', data_get($providerResult->legs, '0.duration_s', $completionSeconds));
48
- $progress = $this->progress($context, $providerResult);
49
- $warnings = array_values(array_unique([...$context->warnings, ...$providerResult->warnings]));
50
+ $now = now();
51
+ $rawCompletionSeconds = $providerResult->durationInTrafficSeconds ?? $providerResult->durationSeconds;
52
+ $rawActiveStopSeconds = data_get($providerResult->legs, '0.duration_in_traffic_s', data_get($providerResult->legs, '0.duration_s', $rawCompletionSeconds));
53
+ $lifecycle = $this->lifecycle($context, $rawActiveStopSeconds, $now);
54
+ $showsLiveEta = (bool) data_get($lifecycle, 'show_live_eta');
55
+ $showsStartEta = (bool) data_get($lifecycle, 'show_start_eta');
56
+ $completionSeconds = $showsLiveEta ? $rawCompletionSeconds : null;
57
+ $activeStopSeconds = $showsLiveEta ? $rawActiveStopSeconds : null;
58
+ $startSeconds = $showsStartEta ? $rawActiveStopSeconds : null;
59
+ $progress = $this->progress($context, $providerResult);
60
+ $warnings = array_values(array_unique([...$context->warnings, ...$providerResult->warnings]));
50
61
 
51
62
  if ($providerResult->confidence === 'low') {
52
63
  $warnings[] = 'low_confidence_eta';
@@ -64,6 +75,7 @@ class TrackingIntelligenceService
64
75
  'online' => (bool) data_get($context->driver, 'online', false),
65
76
  ],
66
77
  'progress' => $progress,
78
+ 'lifecycle' => $lifecycle,
67
79
  'stops' => $context->stops->map(fn (TrackingStop $stop) => $stop->toArray())->values()->all(),
68
80
  'active_stop' => $context->activeStop?->toArray(),
69
81
  'next_stop' => $context->nextStop?->toArray(),
@@ -73,13 +85,15 @@ class TrackingIntelligenceService
73
85
  'duration_in_traffic_s' => $providerResult->durationInTrafficSeconds,
74
86
  'polyline' => $providerResult->polyline,
75
87
  'coordinates' => $providerResult->coordinates,
76
- 'legs' => $this->legs($context, $providerResult, $now),
88
+ 'legs' => $this->legs($context, $providerResult, $now, $showsLiveEta),
77
89
  ],
78
90
  'eta' => [
79
91
  'active_stop_seconds' => $activeStopSeconds,
80
92
  'completion_seconds' => $completionSeconds,
81
93
  'active_stop_at' => $this->addSeconds($now, $activeStopSeconds),
82
94
  'completion_at' => $this->addSeconds($now, $completionSeconds),
95
+ 'start_seconds' => $startSeconds,
96
+ 'start_at' => $this->addSeconds($now, $startSeconds),
83
97
  ],
84
98
  'insights' => [
85
99
  'is_delayed' => false,
@@ -107,17 +121,17 @@ class TrackingIntelligenceService
107
121
  ];
108
122
  }
109
123
 
110
- protected function legs(TrackingContext $context, TrackingProviderResult $providerResult, Carbon $now): array
124
+ protected function legs(TrackingContext $context, TrackingProviderResult $providerResult, Carbon $now, bool $showsLiveEta = true): array
111
125
  {
112
126
  $elapsedSeconds = 0;
113
127
 
114
- return collect($providerResult->legs)->map(function ($leg, $index) use ($context, $now, &$elapsedSeconds) {
128
+ return collect($providerResult->legs)->map(function ($leg, $index) use ($context, $now, $showsLiveEta, &$elapsedSeconds) {
115
129
  $stop = $context->remainingStops->values()->get($index);
116
130
  $legSeconds = data_get($leg, 'duration_in_traffic_s', data_get($leg, 'duration_s'));
117
131
  $etaSeconds = null;
118
132
  $etaAt = null;
119
133
 
120
- if ($legSeconds !== null) {
134
+ if ($showsLiveEta && $stop instanceof TrackingStop && $legSeconds !== null) {
121
135
  $elapsedSeconds += (float) $legSeconds;
122
136
  $etaSeconds = $elapsedSeconds;
123
137
  $etaAt = $this->addSeconds($now, $etaSeconds);
@@ -131,6 +145,92 @@ class TrackingIntelligenceService
131
145
  })->all();
132
146
  }
133
147
 
148
+ protected function lifecycle(TrackingContext $context, ?float $startSeconds, Carbon $now): array
149
+ {
150
+ $order = $context->order;
151
+ $status = strtolower((string) ($order->status ?? 'created'));
152
+ $hasAssignedDriver = !empty($order->driver_assigned_uuid);
153
+ $hasStarted = $this->hasOrderStarted($order);
154
+ $isTerminal = in_array($status, self::TERMINAL_STATUSES, true);
155
+ $isDispatched = $status === 'dispatched' || (bool) $order->dispatched;
156
+
157
+ if ($isTerminal) {
158
+ return [
159
+ 'status' => $status,
160
+ 'mode' => $status,
161
+ 'message' => $status === 'completed' ? 'Order has been completed.' : 'Order has been canceled.',
162
+ 'is_terminal' => true,
163
+ 'has_started' => $hasStarted,
164
+ 'show_live_eta' => false,
165
+ 'show_start_eta' => false,
166
+ ];
167
+ }
168
+
169
+ if (!$hasAssignedDriver) {
170
+ return [
171
+ 'status' => $status,
172
+ 'mode' => 'unassigned',
173
+ 'message' => 'Assign a driver to start live tracking and improve ETA accuracy.',
174
+ 'is_terminal' => false,
175
+ 'has_started' => $hasStarted,
176
+ 'show_live_eta' => false,
177
+ 'show_start_eta' => false,
178
+ ];
179
+ }
180
+
181
+ if (!$hasStarted && $isDispatched) {
182
+ return [
183
+ 'status' => $status,
184
+ 'mode' => 'dispatched',
185
+ 'message' => 'Order has been dispatched. Estimated start is based on the assigned driver route to the first stop.',
186
+ 'is_terminal' => false,
187
+ 'has_started' => false,
188
+ 'show_live_eta' => false,
189
+ 'show_start_eta' => $startSeconds !== null,
190
+ 'start_at' => $this->addSeconds($now, $startSeconds),
191
+ ];
192
+ }
193
+
194
+ if (!$hasStarted) {
195
+ return [
196
+ 'status' => $status,
197
+ 'mode' => 'pre_start',
198
+ 'message' => 'Live ETA will begin once the order is started.',
199
+ 'is_terminal' => false,
200
+ 'has_started' => false,
201
+ 'show_live_eta' => false,
202
+ 'show_start_eta' => false,
203
+ ];
204
+ }
205
+
206
+ return [
207
+ 'status' => $status,
208
+ 'mode' => 'active',
209
+ 'message' => null,
210
+ 'is_terminal' => false,
211
+ 'has_started' => true,
212
+ 'show_live_eta' => true,
213
+ 'show_start_eta' => false,
214
+ ];
215
+ }
216
+
217
+ protected function hasOrderStarted(Order $order): bool
218
+ {
219
+ if ((bool) $order->started || $order->started_at || strtolower((string) $order->status) === 'started') {
220
+ return true;
221
+ }
222
+
223
+ try {
224
+ if ($order->relationLoaded('trackingStatuses')) {
225
+ return $order->trackingStatuses->contains(fn ($status) => strtolower((string) data_get($status, 'code')) === 'started');
226
+ }
227
+
228
+ return $order->trackingStatuses()->whereRaw('LOWER(code) = ?', ['started'])->exists();
229
+ } catch (\Throwable) {
230
+ return false;
231
+ }
232
+ }
233
+
134
234
  protected function capabilitiesFor(string $provider): array
135
235
  {
136
236
  $registry = app(TrackingProviderRegistry::class);
@@ -18,6 +18,7 @@ class TrackingStop implements \JsonSerializable
18
18
  public ?Waypoint $waypoint = null,
19
19
  public bool $completed = false,
20
20
  public ?int $sequence = null,
21
+ public ?string $trackingNumberUuid = null,
21
22
  ) {
22
23
  }
23
24
 
@@ -35,20 +36,21 @@ class TrackingStop implements \JsonSerializable
35
36
  $point = $this->point();
36
37
 
37
38
  return [
38
- 'uuid' => $this->uuid,
39
- 'public_id' => $this->publicId,
40
- 'type' => $this->type,
41
- 'status' => $this->status,
42
- 'completed' => $this->completed,
43
- 'sequence' => $this->sequence,
44
- 'address' => data_get($this->place, 'address'),
45
- 'name' => data_get($this->place, 'name'),
46
- 'location' => $point ? [
39
+ 'uuid' => $this->uuid,
40
+ 'public_id' => $this->publicId,
41
+ 'type' => $this->type,
42
+ 'status' => $this->status,
43
+ 'completed' => $this->completed,
44
+ 'sequence' => $this->sequence,
45
+ 'tracking_number_uuid' => $this->trackingNumberUuid,
46
+ 'address' => data_get($this->place, 'address'),
47
+ 'name' => data_get($this->place, 'name'),
48
+ 'location' => $point ? [
47
49
  'type' => 'Point',
48
50
  'coordinates' => [$point->getLng(), $point->getLat()],
49
51
  ] : null,
50
- 'latitude' => $point ? $point->getLat() : null,
51
- 'longitude' => $point ? $point->getLng() : null,
52
+ 'latitude' => $point ? $point->getLat() : null,
53
+ 'longitude' => $point ? $point->getLng() : null,
52
54
  ];
53
55
  }
54
56
 
@@ -12,7 +12,7 @@ use Illuminate\Support\Facades\Route;
12
12
  | is assigned the "api" middleware group. Enjoy building your API!
13
13
  |
14
14
  */
15
- Route::prefix(config('fleetops.api.routing.prefix', null))->namespace('Fleetbase\FleetOps\Http\Controllers')->group(
15
+ Route::prefix(config('fleetops.api.routing.prefix'))->namespace('Fleetbase\FleetOps\Http\Controllers')->group(
16
16
  function ($router) {
17
17
  /*
18
18
  |--------------------------------------------------------------------------
@@ -49,6 +49,38 @@ Route::prefix(config('fleetops.api.routing.prefix', null))->namespace('Fleetbase
49
49
  $router->put('{id}', 'ContactController@update');
50
50
  $router->delete('{id}', 'ContactController@delete');
51
51
  });
52
+ // order-configs — read-only public projection of the OrderConfig flow.
53
+ $router->group(['prefix' => 'order-configs'], function () use ($router) {
54
+ $router->get('/', 'OrderConfigController@query');
55
+ $router->get('{id}', 'OrderConfigController@find');
56
+ });
57
+ // customers routes — public B2C customer auth + customer-scoped orders.
58
+ // - Public endpoints: API key only (resolves company from credential)
59
+ // - Authenticated endpoints: API key + `Customer-Token` header (Sanctum)
60
+ $router->group(['prefix' => 'customers', 'middleware' => []], function () use ($router) {
61
+ // Public auth flows
62
+ $router->post('request-creation-code', 'CustomerController@requestCreationCode');
63
+ $router->post('/', 'CustomerController@create');
64
+ $router->post('login', 'CustomerController@login');
65
+ $router->post('login-with-sms', 'CustomerController@loginWithPhone');
66
+ $router->post('verify-code', 'CustomerController@verifyCode');
67
+ $router->post('forgot-password', 'CustomerController@forgotPassword');
68
+ $router->post('reset-password', 'CustomerController@resetPassword');
69
+
70
+ // Authenticated (require Customer-Token)
71
+ $router->group(['middleware' => [Fleetbase\FleetOps\Http\Middleware\AuthenticateCustomerToken::class]], function () use ($router) {
72
+ $router->get('me', 'CustomerController@me');
73
+ $router->put('me', 'CustomerController@updateMe');
74
+ $router->match(['post', 'patch'], 'me', 'CustomerController@updateMe');
75
+ $router->post('logout', 'CustomerController@logout');
76
+ $router->post('logout-all', 'CustomerController@logoutAll');
77
+ $router->post('register-device', 'CustomerController@registerDevice');
78
+ $router->get('places', 'CustomerController@places');
79
+ $router->get('orders', 'CustomerController@orders');
80
+ $router->post('orders', 'CustomerController@createOrder');
81
+ $router->get('orders/{id}', 'CustomerController@findOrder');
82
+ });
83
+ });
52
84
  // vendors routes
53
85
  $router->group(['prefix' => 'vendors'], function () use ($router) {
54
86
  $router->post('/', 'VendorController@create');
@@ -280,6 +312,7 @@ Route::prefix(config('fleetops.api.routing.prefix', null))->namespace('Fleetbase
280
312
  $router->post('import', $controller('import'));
281
313
  $router->get('facilitators/{id}', $controller('getAsFacilitator'));
282
314
  $router->get('customers/{id}', $controller('getAsCustomer'));
315
+ $router->post('{id}/convert-to-vendor', $controller('convertToVendor'));
283
316
  $router->delete('bulk-delete', $controller('bulkDelete'));
284
317
  }
285
318
  );
@@ -319,6 +352,7 @@ Route::prefix(config('fleetops.api.routing.prefix', null))->namespace('Fleetbase
319
352
  $router->delete('bulk-delete', $controller('bulkDelete'));
320
353
  }
321
354
  );
355
+ $router->get('issues/{id}/timeline', 'IssueController@timeline');
322
356
  $router->fleetbaseRoutes(
323
357
  'issues',
324
358
  function ($router, $controller) {
@@ -346,8 +380,10 @@ Route::prefix(config('fleetops.api.routing.prefix', null))->namespace('Fleetbase
346
380
  $router->get('{id}/tracker', 'OrderController@trackerInfo');
347
381
  $router->get('{id}/eta', 'OrderController@waypointEtas');
348
382
  $router->post('{id}/ping-driver', $controller('pingDriver'));
383
+ $router->post('{id}/capture-photo/{subjectId?}', $controller('capturePhoto'));
349
384
  $router->post('process-imports', $controller('importFromFiles'));
350
385
  $router->patch('route/{id}', $controller('editOrderRoute'));
386
+ $router->patch('set-destination/{id}/{placeId}', $controller('setDestination'));
351
387
  $router->patch('update-activity/{id}', $controller('updateActivity'));
352
388
  $router->get('{id}/proofs/{subjectId?}', $controller('proofs'));
353
389
  $router->patch('bulk-assign-driver', $controller('bulkAssignDriver'));
@@ -436,6 +472,9 @@ Route::prefix(config('fleetops.api.routing.prefix', null))->namespace('Fleetbase
436
472
  $router->match(['get', 'post'], 'export', $controller('export'));
437
473
  $router->get('facilitators/{id}', $controller('getAsFacilitator'));
438
474
  $router->get('customers/{id}', $controller('getAsCustomer'));
475
+ $router->get('{id}/personnels', $controller('vendorPersonnels'));
476
+ $router->post('{id}/personnels', $controller('addVendorPersonnel'));
477
+ $router->delete('{id}/personnels/{contact}', $controller('removeVendorPersonnel'));
439
478
  $router->post('{id}/assign-driver', $controller('assignDriver'));
440
479
  $router->post('{id}/remove-driver', $controller('removeDriver'));
441
480
  $router->delete('bulk-delete', $controller('bulkDelete'));
@@ -489,7 +528,11 @@ Route::prefix(config('fleetops.api.routing.prefix', null))->namespace('Fleetbase
489
528
  ['prefix' => 'customers'],
490
529
  function () use ($router) {
491
530
  $router->get('/', 'MorphController@queryCustomers');
531
+ $router->post('create-portal-login', 'CustomerController@createPortalLogin');
492
532
  $router->post('reset-credentials', 'CustomerController@resetCredentials');
533
+ $router->post('send-credentials', 'CustomerController@sendCredentials');
534
+ $router->post('deactivate-portal-login', 'CustomerController@deactivatePortalLogin');
535
+ $router->post('reactivate-portal-login', 'CustomerController@reactivatePortalLogin');
493
536
  }
494
537
  );
495
538
  $router->group(
@@ -579,16 +622,22 @@ Route::prefix(config('fleetops.api.routing.prefix', null))->namespace('Fleetbase
579
622
  $router->group(
580
623
  ['prefix' => 'analytics'],
581
624
  function ($router) {
582
- $router->get('operations-pulse', 'AnalyticsController@operationsPulse');
583
- $router->get('revenue-trend', 'AnalyticsController@revenueTrend');
584
- $router->get('orders-by-status', 'AnalyticsController@ordersByStatus');
585
- $router->get('on-time-delivery', 'AnalyticsController@onTimeDelivery');
586
- $router->get('top-drivers', 'AnalyticsController@topDrivers');
587
- $router->get('fuel-efficiency', 'AnalyticsController@fuelEfficiency');
588
- $router->get('issues-insights', 'AnalyticsController@issuesInsights');
625
+ $router->get('operations-pulse', 'AnalyticsController@operationsPulse');
626
+ $router->get('revenue-trend', 'AnalyticsController@revenueTrend');
627
+ $router->get('orders-by-status', 'AnalyticsController@ordersByStatus');
628
+ $router->get('on-time-delivery', 'AnalyticsController@onTimeDelivery');
629
+ $router->get('top-drivers', 'AnalyticsController@topDrivers');
630
+ $router->get('fuel-efficiency', 'AnalyticsController@fuelEfficiency');
631
+ $router->get('issues-insights', 'AnalyticsController@issuesInsights');
589
632
  $router->get('maintenance-overview', 'AnalyticsController@maintenanceOverview');
590
- $router->get('geofence-violations', 'AnalyticsController@geofenceViolations');
591
- $router->get('live-fleet', 'AnalyticsController@liveFleet');
633
+ $router->get('geofence-violations', 'AnalyticsController@geofenceViolations');
634
+ $router->get('live-fleet', 'AnalyticsController@liveFleet');
635
+ }
636
+ );
637
+ $router->group(
638
+ ['prefix' => 'getting-started'],
639
+ function ($router) {
640
+ $router->get('status', 'GettingStartedController@status');
592
641
  }
593
642
  );
594
643
  $router->group(
@@ -627,3 +676,20 @@ Route::prefix(config('fleetops.api.routing.prefix', null))->namespace('Fleetbase
627
676
  );
628
677
  }
629
678
  );
679
+
680
+ if (filled(config('fleetops.api.routing.prefix'))) {
681
+ Route::prefix(config('fleetops.api.routing.internal_prefix', 'int') . '/v1')
682
+ ->namespace('Fleetbase\FleetOps\Http\Controllers\Internal\v1')
683
+ ->middleware([
684
+ 'fleetbase.protected',
685
+ Fleetbase\FleetOps\Http\Middleware\TransformLocationMiddleware::class,
686
+ Fleetbase\FleetOps\Http\Middleware\SetupDriverSession::class,
687
+ ])
688
+ ->group(function ($router) {
689
+ $router->get('issues/{id}/timeline', 'IssueController@timeline');
690
+ $router->get('vendors/{id}/personnels', 'VendorController@vendorPersonnels');
691
+ $router->post('vendors/{id}/personnels', 'VendorController@addVendorPersonnel');
692
+ $router->delete('vendors/{id}/personnels/{contact}', 'VendorController@removeVendorPersonnel');
693
+ $router->post('contacts/{id}/convert-to-vendor', 'ContactController@convertToVendor');
694
+ });
695
+ }
@@ -39,3 +39,11 @@ test('analytics controller has one method per widget', function () {
39
39
  expect($controller)->toContain("public function {$method}(");
40
40
  }
41
41
  });
42
+
43
+ test('top drivers on-time sorting uses a sql aggregate expression', function () {
44
+ $widget = file_get_contents(dirname(__DIR__) . '/src/Support/Analytics/TopDrivers.php');
45
+
46
+ expect($widget)->toContain("'on_time' => \"CASE");
47
+ expect($widget)->toContain('TIMESTAMPDIFF(SECOND, orders.scheduled_at, orders.updated_at) <= 1800');
48
+ expect($widget)->not->toContain("'on_time' => 'on_time_pct'");
49
+ });
@@ -16,8 +16,8 @@ function capacityTestOrder(string $publicId, float $weightKg, float $volumeLitre
16
16
  public float $weight;
17
17
  public string $weight_unit = 'kg';
18
18
  public float $length;
19
- public float $width = 1;
20
- public float $height = 1;
19
+ public float $width = 1;
20
+ public float $height = 1;
21
21
  public string $dimensions_unit = 'm';
22
22
 
23
23
  public function __construct(float $weightKg, float $volumeLitres)
@@ -30,8 +30,8 @@ function capacityTestOrder(string $publicId, float $weightKg, float $volumeLitre
30
30
  }
31
31
  };
32
32
 
33
- $order = new Order();
34
- $order->public_id = $publicId;
33
+ $order = new Order();
34
+ $order->public_id = $publicId;
35
35
  $order->required_skills = $skills;
36
36
  $order->setRelation('payload', $payload);
37
37
 
@@ -40,14 +40,14 @@ function capacityTestOrder(string $publicId, float $weightKg, float $volumeLitre
40
40
 
41
41
  function capacityTestVehicle(string $publicId, float $weightKg, float $volumeM3 = 0, int $maxTasks = 0, array $skills = []): Vehicle
42
42
  {
43
- $vehicle = new Vehicle();
44
- $vehicle->public_id = $publicId;
45
- $vehicle->payload_capacity = $weightKg;
46
- $vehicle->payload_capacity_volume = $volumeM3;
43
+ $vehicle = new Vehicle();
44
+ $vehicle->public_id = $publicId;
45
+ $vehicle->payload_capacity = $weightKg;
46
+ $vehicle->payload_capacity_volume = $volumeM3;
47
47
  $vehicle->payload_capacity_pallets = 0;
48
48
  $vehicle->payload_capacity_parcels = 100;
49
- $vehicle->max_tasks = $maxTasks;
50
- $vehicle->skills = $skills;
49
+ $vehicle->max_tasks = $maxTasks;
50
+ $vehicle->skills = $skills;
51
51
  $vehicle->setRelation('driver', null);
52
52
 
53
53
  return $vehicle;
@@ -0,0 +1,95 @@
1
+ <?php
2
+
3
+ test('customer contacts reject existing non-customer users by identity', function () {
4
+ $contactModel = file_get_contents(__DIR__ . '/../src/Models/Contact.php');
5
+ $conflictClass = file_get_contents(__DIR__ . '/../src/Exceptions/CustomerUserConflictException.php');
6
+ $observer = file_get_contents(__DIR__ . '/../src/Observers/ContactObserver.php');
7
+
8
+ expect($contactModel)
9
+ ->toContain('assertCustomerIdentityIsAvailable')
10
+ ->toContain('assertCustomerUserCanBeAssigned')
11
+ ->toContain("orWhereHas('companyUsers'")
12
+ ->toContain("\$user->type === 'customer'")
13
+ ->toContain('cannot be used for a customer account');
14
+
15
+ expect($conflictClass)->toContain('class CustomerUserConflictException extends UserAlreadyExistsException');
16
+
17
+ expect($observer)
18
+ ->toContain('$contact->assertCustomerIdentityIsAvailable();')
19
+ ->toContain('$contact->normalizeCustomerUser();');
20
+ });
21
+
22
+ test('customer user assignment no longer promotes staff users', function () {
23
+ $contactModel = file_get_contents(__DIR__ . '/../src/Models/Contact.php');
24
+
25
+ expect($contactModel)
26
+ ->toContain('public function assignUser(User $user, bool $sendInvite = false): self')
27
+ ->toContain('$this->assertCustomerUserCanBeAssigned($user);')
28
+ ->not->toContain("if (\$this->isCustomer() && \$user->type !== 'customer') {\n \$user->setType('customer');");
29
+ });
30
+
31
+ test('customer creation entrypoints run the customer identity guard', function () {
32
+ $apiContact = file_get_contents(__DIR__ . '/../src/Http/Controllers/Api/v1/ContactController.php');
33
+ $internalContact = file_get_contents(__DIR__ . '/../src/Http/Controllers/Internal/v1/ContactController.php');
34
+ $orderController = file_get_contents(__DIR__ . '/../src/Http/Controllers/Api/v1/OrderController.php');
35
+ $customerForm = file_get_contents(__DIR__ . '/../../addon/components/customer/form.hbs');
36
+
37
+ expect($apiContact)->toContain('$contactCandidate->assertCustomerIdentityIsAvailable();');
38
+ expect($internalContact)->toContain('$this->assertCustomerIdentityIsAvailable($input');
39
+ expect($orderController)->toContain('$customerCandidate->assertCustomerIdentityIsAvailable();');
40
+ expect($customerForm)->toContain('is_customer=true');
41
+ });
42
+
43
+ test('customer conflict audit command is registered and read only', function () {
44
+ $provider = file_get_contents(__DIR__ . '/../src/Providers/FleetOpsServiceProvider.php');
45
+ $command = file_get_contents(__DIR__ . '/../src/Console/Commands/AuditCustomerUserConflicts.php');
46
+
47
+ expect($provider)->toContain('AuditCustomerUserConflicts::class');
48
+ expect($command)
49
+ ->toContain('fleetops:audit-customer-user-conflicts')
50
+ ->toContain('Reports customer contacts linked to users')
51
+ ->not->toContain('->save(')
52
+ ->not->toContain('->update(')
53
+ ->not->toContain('forceFill');
54
+ });
55
+
56
+ test('customer portal welcome email is opt in and does not create organization invites', function () {
57
+ $internalContact = file_get_contents(__DIR__ . '/../src/Http/Controllers/Internal/v1/ContactController.php');
58
+ $internalCustomer = file_get_contents(__DIR__ . '/../src/Http/Controllers/Internal/v1/CustomerController.php');
59
+ $contactModel = file_get_contents(__DIR__ . '/../src/Models/Contact.php');
60
+ $customerCredentials = file_get_contents(__DIR__ . '/../src/Mail/CustomerCredentialsMail.php');
61
+ $customerEmail = file_get_contents(__DIR__ . '/../resources/views/mail/customer-credentials.blade.php');
62
+ $customerForm = file_get_contents(__DIR__ . '/../../addon/components/customer/form.hbs');
63
+
64
+ expect($internalContact)
65
+ ->toContain('meta.customer_portal.send_welcome_email')
66
+ ->toContain('assertCustomerPortalCanSendWelcomeEmail')
67
+ ->toContain('sendCustomerPortalWelcomeEmail')
68
+ ->toContain('Contact::createUserFromContact($contact, false, true)')
69
+ ->toContain('Mail::to($user)->send(new CustomerCredentialsMail($password, $contact))')
70
+ ->toContain("data_forget(\$meta, 'customer_portal.send_welcome_email')")
71
+ ->toContain('fleetbase/customer-portal-api')
72
+ ->not->toContain('UserInvited')
73
+ ->not->toContain('Invite::create');
74
+
75
+ expect($internalCustomer)
76
+ ->toContain('Contact::createUserFromContact($customer, false, true)')
77
+ ->not->toContain('send_invite');
78
+
79
+ expect($customerCredentials)->toContain('customer portal access is ready')
80
+ ->and($customerCredentials)->toContain("Utils::consoleUrl(\$accessUrlSlug ?: 'customer-portal')")
81
+ ->and($customerEmail)->toContain('Your customer portal access is ready')
82
+ ->and($customerEmail)->toContain('Sign in to customer portal')
83
+ ->and($customerEmail)->toContain('Temporary password:')
84
+ ->and($customerEmail)->toContain('You can change your password after signing in.')
85
+ ->and($customerForm)->toContain('this.showWelcomeEmailOption')
86
+ ->and($customerForm)->toContain('this.toggleWelcomeEmail');
87
+
88
+ expect($contactModel)
89
+ ->toContain('assignUserToContactCompany')
90
+ ->toContain('$contact->company->addUser($user, $role)')
91
+ ->toContain('Deprecated and ignored. Contact and customer users do not receive organization invitations.')
92
+ ->not->toContain('UserInvited')
93
+ ->not->toContain('Invite::create')
94
+ ->not->toContain('->assignCompany(');
95
+ });