@fleetbase/fleetops-engine 0.6.48 → 0.6.50

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 (266) hide show
  1. package/addon/components/customer/create-order-form.hbs +2 -2
  2. package/addon/components/customer/form.hbs +2 -2
  3. package/addon/components/customer/order-form.hbs +2 -2
  4. package/addon/components/customer/orders.hbs +2 -2
  5. package/addon/components/display-place.hbs +2 -2
  6. package/addon/components/driver/details.hbs +5 -1
  7. package/addon/components/driver/form.hbs +11 -1
  8. package/addon/components/fleet-listing-panel.hbs +2 -2
  9. package/addon/components/fleet-listing-panel.js +10 -0
  10. package/addon/components/map/google-live-map.js +18 -75
  11. package/addon/components/map/leaflet-live-map.hbs +187 -37
  12. package/addon/components/map/leaflet-live-map.js +184 -20
  13. package/addon/components/modals/order-change-destination.hbs +46 -0
  14. package/addon/components/modals/order-change-destination.js +106 -0
  15. package/addon/components/modals/stop-activity.hbs +28 -0
  16. package/addon/components/modals/stop-activity.js +52 -0
  17. package/addon/components/modals/update-order-activity.hbs +45 -3
  18. package/addon/components/modals/update-order-activity.js +143 -5
  19. package/addon/components/orchestrator/phase-builder.hbs +21 -0
  20. package/addon/components/orchestrator/phase-builder.js +13 -0
  21. package/addon/components/orchestrator/resource-panel.hbs +0 -10
  22. package/addon/components/orchestrator/resource-panel.js +2 -4
  23. package/addon/components/orchestrator-workbench.js +1 -0
  24. package/addon/components/order/activity-list.hbs +2 -2
  25. package/addon/components/order/activity-timeline.hbs +31 -29
  26. package/addon/components/order/details/activity.hbs +3 -3
  27. package/addon/components/order/details/activity.js +11 -0
  28. package/addon/components/order/details/documents.js +2 -0
  29. package/addon/components/order/details/purchase-rate.hbs +2 -2
  30. package/addon/components/order/details/route.hbs +11 -3
  31. package/addon/components/order/details/route.js +91 -14
  32. package/addon/components/order/details/tracking.hbs +101 -72
  33. package/addon/components/order/details/tracking.js +149 -2
  34. package/addon/components/order/form/details.hbs +3 -3
  35. package/addon/components/order/form/documents.js +11 -4
  36. package/addon/components/order/form/route.hbs +108 -104
  37. package/addon/components/order/form/route.js +37 -1
  38. package/addon/components/order/form/service-rate.hbs +2 -2
  39. package/addon/components/order/form.hbs +2 -2
  40. package/addon/components/order-config-manager/activity-flow.hbs +4 -1
  41. package/addon/components/order-config-manager/activity-flow.js +178 -12
  42. package/addon/components/place/address.js +1 -1
  43. package/addon/components/route-list.hbs +17 -17
  44. package/addon/components/route-list.js +42 -10
  45. package/addon/components/service-rate/details.hbs +41 -0
  46. package/addon/components/service-rate/form.hbs +118 -0
  47. package/addon/components/service-rate/form.js +55 -0
  48. package/addon/components/vehicle/form.js +1 -1
  49. package/addon/components/vehicle/pill.hbs +1 -1
  50. package/addon/components/widget/fleet-ops-key-metrics.hbs +19 -17
  51. package/addon/components/widget/fleet-ops-key-metrics.js +26 -0
  52. package/addon/components/widget/fuel-efficiency.hbs +26 -0
  53. package/addon/components/widget/fuel-efficiency.js +43 -0
  54. package/addon/components/widget/geofence-violations.hbs +44 -0
  55. package/addon/components/widget/geofence-violations.js +55 -0
  56. package/addon/components/widget/issues-insights.hbs +43 -0
  57. package/addon/components/widget/issues-insights.js +62 -0
  58. package/addon/components/widget/kpi-active-orders.hbs +10 -0
  59. package/addon/components/widget/kpi-active-orders.js +5 -0
  60. package/addon/components/widget/kpi-aov.hbs +10 -0
  61. package/addon/components/widget/kpi-aov.js +5 -0
  62. package/addon/components/widget/kpi-distance.hbs +10 -0
  63. package/addon/components/widget/kpi-distance.js +5 -0
  64. package/addon/components/widget/kpi-drivers-online.hbs +10 -0
  65. package/addon/components/widget/kpi-drivers-online.js +5 -0
  66. package/addon/components/widget/kpi-earnings.hbs +10 -0
  67. package/addon/components/widget/kpi-earnings.js +5 -0
  68. package/addon/components/widget/kpi-open-issues.hbs +11 -0
  69. package/addon/components/widget/kpi-open-issues.js +5 -0
  70. package/addon/components/widget/kpi-tile.hbs +59 -0
  71. package/addon/components/widget/kpi-tile.js +168 -0
  72. package/addon/components/widget/live-fleet.hbs +122 -0
  73. package/addon/components/widget/live-fleet.js +365 -0
  74. package/addon/components/widget/maintenance-overview.hbs +45 -0
  75. package/addon/components/widget/maintenance-overview.js +40 -0
  76. package/addon/components/widget/on-time-delivery.hbs +27 -0
  77. package/addon/components/widget/on-time-delivery.js +73 -0
  78. package/addon/components/widget/operations-pulse.hbs +51 -0
  79. package/addon/components/widget/operations-pulse.js +173 -0
  80. package/addon/components/widget/orders-by-status.hbs +26 -0
  81. package/addon/components/widget/orders-by-status.js +54 -0
  82. package/addon/components/widget/revenue-trend.hbs +34 -0
  83. package/addon/components/widget/revenue-trend.js +67 -0
  84. package/addon/components/widget/top-drivers.hbs +44 -0
  85. package/addon/components/widget/top-drivers.js +54 -0
  86. package/addon/controllers/management/vehicles/index/new.js +1 -1
  87. package/addon/controllers/operations/orders/index/details.js +4 -4
  88. package/addon/controllers/operations/orders/index.js +14 -0
  89. package/addon/controllers/settings/routing.js +26 -0
  90. package/addon/extension.js +172 -3
  91. package/addon/helpers/place-address-html.js +2 -2
  92. package/addon/routes/operations/service-rates/index.js +1 -1
  93. package/addon/services/geofence.js +163 -19
  94. package/addon/services/leaflet-contextmenu-manager.js +27 -0
  95. package/addon/services/map-adapter/google.js +13 -2
  96. package/addon/services/vehicle-actions.js +1 -1
  97. package/addon/styles/fleetops-engine.css +205 -88
  98. package/addon/templates/maintenance/equipment/index.hbs +1 -1
  99. package/addon/templates/maintenance/parts/index.hbs +1 -1
  100. package/addon/templates/maintenance/schedules/index.hbs +1 -1
  101. package/addon/templates/management/vehicles/index.hbs +1 -1
  102. package/addon/templates/settings/avatars.hbs +1 -1
  103. package/addon/templates/settings/custom-fields.hbs +1 -1
  104. package/addon/templates/settings/map.hbs +1 -1
  105. package/addon/templates/settings/navigator-app.hbs +1 -1
  106. package/addon/templates/settings/notifications.hbs +1 -1
  107. package/addon/templates/settings/orchestrator.hbs +1 -1
  108. package/addon/templates/settings/payments/index.hbs +1 -1
  109. package/addon/templates/settings/payments/onboard.hbs +1 -1
  110. package/addon/templates/settings/routing.hbs +22 -2
  111. package/addon/templates/settings/scheduling.hbs +1 -1
  112. package/addon/utils/fleet-ops-options.js +2 -1
  113. package/addon/utils/live-map-card-content.js +171 -0
  114. package/addon/utils/normalize-order-config-flow.js +41 -0
  115. package/addon/utils/place-address-html.js +3 -2
  116. package/addon/utils/route-visualization.js +54 -7
  117. package/app/components/modals/order-change-destination.js +1 -0
  118. package/app/components/modals/stop-activity.js +1 -0
  119. package/app/components/widget/fuel-efficiency.js +1 -0
  120. package/app/components/widget/geofence-violations.js +1 -0
  121. package/app/components/widget/issues-insights.js +1 -0
  122. package/app/components/widget/kpi-active-orders.js +1 -0
  123. package/app/components/widget/kpi-aov.js +1 -0
  124. package/app/components/widget/kpi-distance.js +1 -0
  125. package/app/components/widget/kpi-drivers-online.js +1 -0
  126. package/app/components/widget/kpi-earnings.js +1 -0
  127. package/app/components/widget/kpi-open-issues.js +1 -0
  128. package/app/components/widget/kpi-tile.js +1 -0
  129. package/app/components/widget/live-fleet.js +1 -0
  130. package/app/components/widget/maintenance-overview.js +1 -0
  131. package/app/components/widget/on-time-delivery.js +1 -0
  132. package/app/components/widget/operations-pulse.js +1 -0
  133. package/app/components/widget/orders-by-status.js +1 -0
  134. package/app/components/widget/revenue-trend.js +1 -0
  135. package/app/components/widget/top-drivers.js +1 -0
  136. package/app/utils/live-map-card-content.js +1 -0
  137. package/composer.json +1 -1
  138. package/extension.json +1 -1
  139. package/package.json +3 -3
  140. package/pnpm-workspace.yaml +1 -0
  141. package/server/config/telematics.php +111 -1
  142. package/server/migrations/2026_05_18_000001_add_license_expiry_to_drivers_table.php +25 -0
  143. package/server/migrations/2026_05_21_000001_add_geographic_rules_to_service_rate_fees_table.php +37 -0
  144. package/server/migrations/2026_05_25_000001_add_endpoint_tracking_refs_to_payloads_table.php +50 -0
  145. package/server/migrations/2026_05_27_000001_default_driver_vehicle_statuses_to_available.php +48 -0
  146. package/server/resources/views/labels/default.php +57 -49
  147. package/server/resources/views/labels/waypoint-label.php +27 -20
  148. package/server/seeders/Testing/Concerns/SeedsTestingData.php +183 -0
  149. package/server/seeders/Testing/ConnectivitySeeder.php +213 -0
  150. package/server/seeders/Testing/FleetSeeder.php +341 -0
  151. package/server/seeders/Testing/MaintenanceSeeder.php +238 -0
  152. package/server/seeders/Testing/NetworkSeeder.php +250 -0
  153. package/server/seeders/Testing/OrdersSeeder.php +198 -0
  154. package/server/seeders/Testing/TestingSeeder.php +40 -0
  155. package/server/seeders/{VroomOrchestrationSeeder.php → Testing/VroomOrchestrationSeeder.php} +29 -22
  156. package/server/src/Console/Commands/AuditCustomerUserConflicts.php +75 -0
  157. package/server/src/Console/Commands/ProcessOperationalAlerts.php +279 -0
  158. package/server/src/Exceptions/CustomerUserConflictException.php +13 -0
  159. package/server/src/Http/Controllers/Api/v1/ContactController.php +17 -2
  160. package/server/src/Http/Controllers/Api/v1/CustomerController.php +876 -0
  161. package/server/src/Http/Controllers/Api/v1/DriverController.php +1 -1
  162. package/server/src/Http/Controllers/Api/v1/OrderConfigController.php +54 -0
  163. package/server/src/Http/Controllers/Api/v1/OrderController.php +98 -70
  164. package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -2
  165. package/server/src/Http/Controllers/Api/v1/VehicleController.php +20 -3
  166. package/server/src/Http/Controllers/Internal/v1/AnalyticsController.php +119 -0
  167. package/server/src/Http/Controllers/Internal/v1/ContactController.php +19 -0
  168. package/server/src/Http/Controllers/Internal/v1/LiveController.php +101 -66
  169. package/server/src/Http/Controllers/Internal/v1/MetricsController.php +82 -3
  170. package/server/src/Http/Controllers/Internal/v1/OrderController.php +441 -32
  171. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +1 -1
  172. package/server/src/Http/Controllers/Internal/v1/SettingController.php +46 -1
  173. package/server/src/Http/Controllers/Internal/v1/VehicleController.php +54 -1
  174. package/server/src/Http/Middleware/AuthenticateCustomerToken.php +34 -0
  175. package/server/src/Http/Requests/CreateCustomerOrderRequest.php +63 -0
  176. package/server/src/Http/Requests/CreateCustomerRequest.php +48 -0
  177. package/server/src/Http/Requests/CreateDriverRequest.php +1 -1
  178. package/server/src/Http/Requests/CreateVehicleRequest.php +25 -1
  179. package/server/src/Http/Requests/Internal/CreateDriverRequest.php +1 -1
  180. package/server/src/Http/Requests/VerifyCreateCustomerRequest.php +29 -0
  181. package/server/src/Http/Resources/Internal/v1/OrderConfig.php +41 -0
  182. package/server/src/Http/Resources/v1/Customer.php +79 -0
  183. package/server/src/Http/Resources/v1/Driver.php +2 -0
  184. package/server/src/Http/Resources/v1/Fleet.php +4 -0
  185. package/server/src/Http/Resources/v1/Index/Driver.php +48 -0
  186. package/server/src/Http/Resources/v1/Index/Vehicle.php +74 -1
  187. package/server/src/Http/Resources/v1/OrderConfig.php +74 -0
  188. package/server/src/Http/Resources/v1/Payload.php +23 -20
  189. package/server/src/Http/Resources/v1/ServiceArea.php +33 -19
  190. package/server/src/Http/Resources/v1/ServiceRateFee.php +7 -0
  191. package/server/src/Listeners/HandleOrderDispatched.php +2 -2
  192. package/server/src/Models/Contact.php +92 -15
  193. package/server/src/Models/Customer.php +64 -0
  194. package/server/src/Models/Driver.php +14 -3
  195. package/server/src/Models/Entity.php +2 -2
  196. package/server/src/Models/Order.php +29 -4
  197. package/server/src/Models/OrderConfig.php +14 -11
  198. package/server/src/Models/Payload.php +17 -1
  199. package/server/src/Models/ServiceRate.php +273 -1
  200. package/server/src/Models/ServiceRateFee.php +39 -9
  201. package/server/src/Models/TrackingNumber.php +8 -2
  202. package/server/src/Models/Vehicle.php +18 -2
  203. package/server/src/Models/Waypoint.php +2 -2
  204. package/server/src/Notifications/LateDeparture.php +48 -0
  205. package/server/src/Notifications/ProlongedStoppage.php +48 -0
  206. package/server/src/Notifications/RouteDeviation.php +48 -0
  207. package/server/src/Observers/ContactObserver.php +2 -0
  208. package/server/src/Observers/ServiceRateObserver.php +6 -6
  209. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +29 -2
  210. package/server/src/Providers/FleetOpsServiceProvider.php +6 -0
  211. package/server/src/Providers/NotificationServiceProvider.php +3 -0
  212. package/server/src/Support/Analytics/AbstractAnalytics.php +77 -0
  213. package/server/src/Support/Analytics/FuelEfficiency.php +83 -0
  214. package/server/src/Support/Analytics/GeofenceViolations.php +63 -0
  215. package/server/src/Support/Analytics/IssuesInsights.php +67 -0
  216. package/server/src/Support/Analytics/LiveFleet.php +127 -0
  217. package/server/src/Support/Analytics/MaintenanceOverview.php +70 -0
  218. package/server/src/Support/Analytics/OnTimeDelivery.php +75 -0
  219. package/server/src/Support/Analytics/OperationsPulse.php +86 -0
  220. package/server/src/Support/Analytics/OrdersByStatus.php +64 -0
  221. package/server/src/Support/Analytics/RevenueTrend.php +78 -0
  222. package/server/src/Support/Analytics/TopDrivers.php +92 -0
  223. package/server/src/Support/CustomerAuth.php +98 -0
  224. package/server/src/Support/LabelPdf.php +52 -0
  225. package/server/src/Support/Metrics/AbstractMetric.php +170 -0
  226. package/server/src/Support/Metrics/ActiveLiveOrdersMetric.php +28 -0
  227. package/server/src/Support/Metrics/AvgOrderValueMetric.php +53 -0
  228. package/server/src/Support/Metrics/DriversOnlineMetric.php +30 -0
  229. package/server/src/Support/Metrics/EarningsMetric.php +30 -0
  230. package/server/src/Support/Metrics/FuelCostsMetric.php +30 -0
  231. package/server/src/Support/Metrics/MoneyMetric.php +30 -0
  232. package/server/src/Support/Metrics/OpenIssuesMetric.php +35 -0
  233. package/server/src/Support/Metrics/OrdersCanceledMetric.php +35 -0
  234. package/server/src/Support/Metrics/OrdersCompletedMetric.php +35 -0
  235. package/server/src/Support/Metrics/OrdersInProgressMetric.php +51 -0
  236. package/server/src/Support/Metrics/OrdersScheduledMetric.php +37 -0
  237. package/server/src/Support/Metrics/Registry.php +46 -0
  238. package/server/src/Support/Metrics/ResolvedIssuesMetric.php +35 -0
  239. package/server/src/Support/Metrics/TotalCustomersMetric.php +29 -0
  240. package/server/src/Support/Metrics/TotalDistanceTraveledMetric.php +35 -0
  241. package/server/src/Support/Metrics/TotalDriversMetric.php +28 -0
  242. package/server/src/Support/Metrics/TotalTimeTraveledMetric.php +35 -0
  243. package/server/src/Support/Metrics.php +46 -249
  244. package/server/src/Support/ResolvesOrderServiceStops.php +512 -0
  245. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +290 -0
  246. package/server/src/Support/Telematics/Providers/SafeeProvider.php +333 -0
  247. package/server/src/Tracking/TrackingContextBuilder.php +41 -38
  248. package/server/src/Tracking/TrackingIntelligenceService.php +109 -9
  249. package/server/src/Tracking/TrackingStop.php +13 -11
  250. package/server/src/routes.php +50 -0
  251. package/server/tests/AnalyticsRoutesTest.php +41 -0
  252. package/server/tests/CapacityAllocationEngineTest.php +10 -10
  253. package/server/tests/CustomerContactIdentitySafetyTest.php +54 -0
  254. package/server/tests/CustomerEndpointTest.php +199 -0
  255. package/server/tests/DriverVehicleStatusDefaultTest.php +39 -0
  256. package/server/tests/LabelPdfRenderingTest.php +23 -0
  257. package/server/tests/LiveControllerViewportTest.php +60 -0
  258. package/server/tests/MetricsRegistryTest.php +84 -0
  259. package/server/tests/OrchestrationPayloadBuilderTest.php +2 -2
  260. package/server/tests/OrderConfigEndpointTest.php +86 -0
  261. package/server/tests/OrderFileAttachmentTest.php +20 -0
  262. package/server/tests/ServiceAreaZoneApiShapeTest.php +34 -0
  263. package/server/tests/VehicleDriverAssignmentTest.php +18 -0
  264. package/server/tests/VroomOrchestrationEngineTest.php +91 -0
  265. package/translations/en-us.yaml +38 -0
  266. package/server/migrations/2026_05_13_000001_repair_customer_contact_user_types.php +0 -86
@@ -0,0 +1,512 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support;
4
+
5
+ use Fleetbase\FleetOps\Events\EntityActivityChanged;
6
+ use Fleetbase\FleetOps\Events\EntityCompleted;
7
+ use Fleetbase\FleetOps\Events\WaypointActivityChanged;
8
+ use Fleetbase\FleetOps\Events\WaypointCompleted;
9
+ use Fleetbase\FleetOps\Flow\Activity;
10
+ use Fleetbase\FleetOps\Models\Order;
11
+ use Fleetbase\FleetOps\Models\Payload;
12
+ use Fleetbase\FleetOps\Models\Place;
13
+ use Fleetbase\FleetOps\Models\Proof;
14
+ use Fleetbase\FleetOps\Models\TrackingNumber;
15
+ use Fleetbase\FleetOps\Models\TrackingStatus;
16
+ use Fleetbase\FleetOps\Models\Waypoint;
17
+ use Fleetbase\LaravelMysqlSpatial\Types\Point;
18
+ use Fleetbase\Support\TemplateString;
19
+ use Illuminate\Support\Collection;
20
+ use Illuminate\Support\Facades\DB;
21
+
22
+ trait ResolvesOrderServiceStops
23
+ {
24
+ protected function payloadHasWaypoints(?Payload $payload): bool
25
+ {
26
+ if (!$payload) {
27
+ return false;
28
+ }
29
+
30
+ $payload->loadMissing('waypoints');
31
+
32
+ return $payload->waypoints->isNotEmpty();
33
+ }
34
+
35
+ protected function resolveProof($proof): ?Proof
36
+ {
37
+ if ($proof instanceof Proof) {
38
+ return $proof;
39
+ }
40
+
41
+ if (is_string($proof)) {
42
+ return Proof::where('public_id', $proof)->orWhere('uuid', $proof)->first();
43
+ }
44
+
45
+ return null;
46
+ }
47
+
48
+ protected function payloadServiceStops(?Payload $payload): Collection
49
+ {
50
+ if (!$payload) {
51
+ return collect();
52
+ }
53
+
54
+ $payload->loadMissing([
55
+ 'pickup',
56
+ 'dropoff',
57
+ 'return',
58
+ 'waypoints',
59
+ 'waypointMarkers.place',
60
+ 'waypointMarkers.trackingNumber.status',
61
+ ]);
62
+
63
+ $stops = collect();
64
+ $sequence = 0;
65
+
66
+ if ($payload->pickup instanceof Place) {
67
+ $stops->push([
68
+ 'type' => 'pickup',
69
+ 'place' => $payload->pickup,
70
+ 'waypoint' => null,
71
+ 'tracking_number_uuid' => $payload->pickup_tracking_number_uuid,
72
+ 'sequence' => ++$sequence,
73
+ ]);
74
+ }
75
+
76
+ $markers = $payload->waypointMarkers instanceof Collection ? $payload->waypointMarkers : collect();
77
+ if ($markers->isNotEmpty()) {
78
+ foreach ($markers->sortBy('order')->values() as $waypoint) {
79
+ $waypoint->loadMissing('place', 'trackingNumber.status');
80
+
81
+ if ($waypoint->place instanceof Place) {
82
+ $stops->push([
83
+ 'type' => 'waypoint',
84
+ 'place' => $waypoint->place,
85
+ 'waypoint' => $waypoint,
86
+ 'tracking_number_uuid' => $waypoint->tracking_number_uuid,
87
+ 'sequence' => ++$sequence,
88
+ ]);
89
+ }
90
+ }
91
+ } elseif ($payload->waypoints instanceof Collection) {
92
+ foreach ($payload->waypoints as $place) {
93
+ if ($place instanceof Place) {
94
+ $stops->push([
95
+ 'type' => 'waypoint',
96
+ 'place' => $place,
97
+ 'waypoint' => null,
98
+ 'tracking_number_uuid' => null,
99
+ 'sequence' => ++$sequence,
100
+ ]);
101
+ }
102
+ }
103
+ }
104
+
105
+ if ($payload->dropoff instanceof Place) {
106
+ $stops->push([
107
+ 'type' => 'dropoff',
108
+ 'place' => $payload->dropoff,
109
+ 'waypoint' => null,
110
+ 'tracking_number_uuid' => $payload->dropoff_tracking_number_uuid,
111
+ 'sequence' => ++$sequence,
112
+ ]);
113
+ }
114
+
115
+ if ($payload->return instanceof Place) {
116
+ $stops->push([
117
+ 'type' => 'return',
118
+ 'place' => $payload->return,
119
+ 'waypoint' => null,
120
+ 'tracking_number_uuid' => $payload->return_tracking_number_uuid,
121
+ 'sequence' => ++$sequence,
122
+ ]);
123
+ }
124
+
125
+ return $stops->values();
126
+ }
127
+
128
+ protected function payloadCurrentServiceStop(?Payload $payload): ?array
129
+ {
130
+ $stops = $this->payloadServiceStops($payload);
131
+ if ($stops->isEmpty()) {
132
+ return null;
133
+ }
134
+
135
+ $currentStopId = $payload?->current_waypoint_uuid;
136
+ if ($currentStopId) {
137
+ $current = $stops->first(function (array $stop) use ($currentStopId) {
138
+ $place = $stop['place'] ?? null;
139
+ $waypoint = $stop['waypoint'] ?? null;
140
+
141
+ return $place instanceof Place && in_array($currentStopId, [$place->uuid, $place->public_id, $place->id], true)
142
+ || $waypoint instanceof Waypoint && in_array($currentStopId, [$waypoint->uuid, $waypoint->public_id, $waypoint->place_uuid], true);
143
+ });
144
+
145
+ if ($current) {
146
+ return $current;
147
+ }
148
+ }
149
+
150
+ return $stops->first();
151
+ }
152
+
153
+ protected function ensurePayloadCurrentServiceStop(?Payload $payload): ?array
154
+ {
155
+ $stop = $this->payloadCurrentServiceStop($payload);
156
+ if (!$payload || !$stop) {
157
+ return null;
158
+ }
159
+
160
+ $place = $stop['place'] ?? null;
161
+ if ($place instanceof Place && $payload->current_waypoint_uuid !== $place->uuid) {
162
+ $this->setPayloadCurrentServiceStop($payload, $stop);
163
+ }
164
+
165
+ return $stop;
166
+ }
167
+
168
+ protected function setPayloadCurrentServiceStop(Payload $payload, array $stop): ?Place
169
+ {
170
+ $place = $stop['place'] ?? null;
171
+ if (!$place instanceof Place) {
172
+ return null;
173
+ }
174
+
175
+ $payload->current_waypoint_uuid = $place->uuid;
176
+ $payload->saveQuietly();
177
+ $payload->setRelation('currentWaypoint', $place);
178
+
179
+ if (($stop['waypoint'] ?? null) instanceof Waypoint) {
180
+ $payload->setRelation('currentWaypointMarker', $stop['waypoint']);
181
+ }
182
+
183
+ return $place;
184
+ }
185
+
186
+ protected function resolveServiceStopFromKey(?Payload $payload, ?string $key = null): ?array
187
+ {
188
+ if (!$payload || !$key) {
189
+ return null;
190
+ }
191
+
192
+ return $this->payloadServiceStops($payload)->first(function (array $stop) use ($key) {
193
+ $place = $stop['place'] ?? null;
194
+ $waypoint = $stop['waypoint'] ?? null;
195
+
196
+ return $place instanceof Place && in_array($key, [$place->uuid, $place->public_id, $place->id], true)
197
+ || $waypoint instanceof Waypoint && in_array($key, [$waypoint->uuid, $waypoint->public_id, $waypoint->place_uuid], true);
198
+ });
199
+ }
200
+
201
+ protected function nextIncompleteServiceStop(Order $order, Payload $payload): ?array
202
+ {
203
+ $current = $this->payloadCurrentServiceStop($payload);
204
+ $currentSequence = (int) data_get($current, 'sequence', 0);
205
+
206
+ return $this->payloadServiceStops($payload)
207
+ ->filter(fn (array $stop) => (int) data_get($stop, 'sequence', 0) > $currentSequence)
208
+ ->first(fn (array $stop) => !$this->serviceStopIsComplete($order, $payload, $stop));
209
+ }
210
+
211
+ protected function serviceStopIsComplete(Order $order, Payload $payload, array $stop): bool
212
+ {
213
+ if (in_array($order->status, ['completed', 'canceled'], true)) {
214
+ return true;
215
+ }
216
+
217
+ $type = $stop['type'] ?? null;
218
+ if ($type === 'waypoint') {
219
+ $waypoint = $stop['waypoint'] ?? null;
220
+
221
+ return $waypoint instanceof Waypoint && $this->waypointMarkerIsComplete($waypoint);
222
+ }
223
+
224
+ $trackingNumberUuid = $this->serviceStopTrackingNumberUuid($payload, $stop);
225
+ if ($trackingNumberUuid) {
226
+ return (bool) data_get($this->trackingNumberStatus($trackingNumberUuid), 'complete', false);
227
+ }
228
+
229
+ return false;
230
+ }
231
+
232
+ protected function updateCurrentServiceStopActivity(Order $order, Activity $activity, $location = null, $proof = null, bool $skipEndpointOrderActivity = false, bool $updateEndpointOrderStatus = true): ?array
233
+ {
234
+ $payload = $order->payload;
235
+ $stop = $this->ensurePayloadCurrentServiceStop($payload);
236
+ if (!$payload || !$stop || !Utils::isActivity($activity) || !$location) {
237
+ return $stop;
238
+ }
239
+
240
+ $waypoint = $stop['waypoint'] ?? null;
241
+ if ($waypoint instanceof Waypoint) {
242
+ $payload->loadMissing(['entities']);
243
+ $activityId = $waypoint->insertActivity($activity, $location, $proof);
244
+ if ($activityId && $waypoint->tracking_number_uuid) {
245
+ DB::table((new TrackingNumber())->getTable())->where('uuid', $waypoint->tracking_number_uuid)->update(['status_uuid' => $activityId]);
246
+ if ($waypoint->relationLoaded('trackingNumber')) {
247
+ $waypoint->trackingNumber->status_uuid = $activityId;
248
+ $waypoint->trackingNumber->unsetRelation('status');
249
+ }
250
+ }
251
+ $activity->fireEvents($order, $waypoint);
252
+
253
+ $entities = $payload->entities->where('destination_uuid', $waypoint->place_uuid);
254
+ foreach ($entities as $entity) {
255
+ $entity->insertActivity($activity, $location, $proof);
256
+ if ($activity->complete()) {
257
+ event(new EntityCompleted($entity, $activity));
258
+ } else {
259
+ event(new EntityActivityChanged($entity, $activity));
260
+ }
261
+ }
262
+
263
+ if ($activity->complete()) {
264
+ event(new WaypointCompleted($waypoint, $activity));
265
+ } else {
266
+ event(new WaypointActivityChanged($waypoint, $activity));
267
+ }
268
+
269
+ return $stop;
270
+ }
271
+
272
+ if (!$skipEndpointOrderActivity) {
273
+ $this->insertEndpointServiceStopActivity($order, $payload, $stop, $activity, $location, $proof);
274
+ }
275
+
276
+ return $stop;
277
+ }
278
+
279
+ protected function advanceCurrentServiceStopDestination(Order $order, Payload $payload): ?array
280
+ {
281
+ $nextStop = $this->nextIncompleteServiceStop($order, $payload);
282
+ if (!$nextStop) {
283
+ return null;
284
+ }
285
+
286
+ $this->setPayloadCurrentServiceStop($payload, $nextStop);
287
+
288
+ return $nextStop;
289
+ }
290
+
291
+ protected function payloadHasCurrentServiceStopActivity(?Payload $payload, Activity $activity): bool
292
+ {
293
+ if (!$payload || !Utils::isActivity($activity)) {
294
+ return false;
295
+ }
296
+
297
+ $stop = $this->payloadCurrentServiceStop($payload);
298
+ if (!$stop) {
299
+ return false;
300
+ }
301
+
302
+ $waypoint = $stop['waypoint'] ?? null;
303
+ if ($waypoint instanceof Waypoint) {
304
+ if (!$waypoint->tracking_number_uuid) {
305
+ return false;
306
+ }
307
+
308
+ return TrackingStatus::where('tracking_number_uuid', $waypoint->tracking_number_uuid)
309
+ ->where('code', TrackingStatus::prepareCode($activity->code))
310
+ ->exists();
311
+ }
312
+
313
+ $trackingNumberUuid = $this->serviceStopTrackingNumberUuid($payload, $stop);
314
+ if (!$trackingNumberUuid) {
315
+ return false;
316
+ }
317
+
318
+ return TrackingStatus::where('tracking_number_uuid', $trackingNumberUuid)
319
+ ->where('code', TrackingStatus::prepareCode($activity->code))
320
+ ->exists();
321
+ }
322
+
323
+ protected function serviceStopTrackingNumberUuid(Payload $payload, array $stop): ?string
324
+ {
325
+ $waypoint = $stop['waypoint'] ?? null;
326
+ if ($waypoint instanceof Waypoint) {
327
+ return $waypoint->tracking_number_uuid;
328
+ }
329
+
330
+ $column = $this->endpointTrackingNumberColumn($stop['type'] ?? null);
331
+ if (!$column) {
332
+ return null;
333
+ }
334
+
335
+ return $payload->{$column} ?: null;
336
+ }
337
+
338
+ protected function endpointTrackingNumberColumn(?string $type): ?string
339
+ {
340
+ return match ($type) {
341
+ 'pickup' => 'pickup_tracking_number_uuid',
342
+ 'dropoff' => 'dropoff_tracking_number_uuid',
343
+ 'return' => 'return_tracking_number_uuid',
344
+ default => null,
345
+ };
346
+ }
347
+
348
+ protected function endpointServiceStopTrackingNumber(Order $order, Payload $payload, array $stop, bool $create = false): ?TrackingNumber
349
+ {
350
+ $column = $this->endpointTrackingNumberColumn($stop['type'] ?? null);
351
+ $place = $stop['place'] ?? null;
352
+ if (!$column || !$place instanceof Place) {
353
+ return null;
354
+ }
355
+
356
+ $trackingNumberUuid = $payload->{$column};
357
+ if ($trackingNumberUuid) {
358
+ $trackingNumber = TrackingNumber::where('uuid', $trackingNumberUuid)->first();
359
+ if ($trackingNumber) {
360
+ return $trackingNumber;
361
+ }
362
+ }
363
+
364
+ if (!$create) {
365
+ return null;
366
+ }
367
+
368
+ $trackingNumberUuid = TrackingNumber::insertGetUuid([
369
+ 'company_uuid' => $payload->company_uuid ?? $order->company_uuid,
370
+ 'region' => strtoupper($place->country ?: $place->province ?: 'SG'),
371
+ 'location' => Utils::parsePointToWkt($this->serviceStopLocationPoint($place)),
372
+ ], $place);
373
+
374
+ if (!$trackingNumberUuid) {
375
+ return null;
376
+ }
377
+
378
+ DB::table($payload->getTable())->where('uuid', $payload->uuid)->update([$column => $trackingNumberUuid]);
379
+ $payload->{$column} = $trackingNumberUuid;
380
+
381
+ return TrackingNumber::where('uuid', $trackingNumberUuid)->first();
382
+ }
383
+
384
+ protected function insertEndpointServiceStopActivity(Order $order, Payload $payload, array $stop, Activity $activity, $location = null, $proof = null): ?string
385
+ {
386
+ $trackingNumber = $this->endpointServiceStopTrackingNumber($order, $payload, $stop, true);
387
+ if (!$trackingNumber) {
388
+ return null;
389
+ }
390
+
391
+ $proof = $this->resolveProof($proof);
392
+ $activityId = TrackingStatus::insertGetUuid([
393
+ 'company_uuid' => $payload->company_uuid ?? $order->company_uuid,
394
+ 'tracking_number_uuid' => $trackingNumber->uuid,
395
+ 'proof_uuid' => data_get($proof, 'uuid'),
396
+ 'status' => TemplateString::resolve($activity->get('status', ''), $order),
397
+ 'details' => TemplateString::resolve($activity->get('details', ''), $order),
398
+ 'location' => Utils::parsePointToWkt($this->activityLocationPoint($location)),
399
+ 'code' => TrackingStatus::prepareCode($activity->get('code')),
400
+ 'complete' => $activity->complete(),
401
+ ]);
402
+
403
+ if ($activityId) {
404
+ DB::table($trackingNumber->getTable())->where('uuid', $trackingNumber->uuid)->update(['status_uuid' => $activityId]);
405
+ $trackingNumber->status_uuid = $activityId;
406
+ }
407
+
408
+ if ($trackingNumber->relationLoaded('status')) {
409
+ $trackingNumber->unsetRelation('status');
410
+ }
411
+
412
+ return $activityId ?: null;
413
+ }
414
+
415
+ protected function serviceStopActivityContext(Order $order, Payload $payload, array $stop): Order|Waypoint
416
+ {
417
+ $waypoint = $stop['waypoint'] ?? null;
418
+ if ($waypoint instanceof Waypoint) {
419
+ return $waypoint;
420
+ }
421
+
422
+ $trackingNumber = $this->endpointServiceStopTrackingNumber($order, $payload, $stop);
423
+ $context = new Waypoint([
424
+ 'company_uuid' => $payload->company_uuid ?? $order->company_uuid,
425
+ 'payload_uuid' => $payload->uuid,
426
+ 'place_uuid' => data_get($stop, 'place.uuid'),
427
+ 'tracking_number_uuid' => data_get($trackingNumber, 'uuid'),
428
+ 'type' => $stop['type'] ?? null,
429
+ 'order' => $stop['sequence'] ?? null,
430
+ ]);
431
+
432
+ if ($trackingNumber) {
433
+ $trackingNumber->loadMissing('status');
434
+ $context->setRelation('trackingNumber', $trackingNumber);
435
+ }
436
+
437
+ return $context;
438
+ }
439
+
440
+ protected function nextActivitiesForServiceStop(Order $order, Payload $payload, array $stop): Collection
441
+ {
442
+ $orderConfig = $order->ensureOrderConfig();
443
+ if (!$orderConfig) {
444
+ return collect();
445
+ }
446
+
447
+ $waypoint = $stop['waypoint'] ?? null;
448
+ if ($waypoint instanceof Waypoint) {
449
+ return $orderConfig->nextActivity($waypoint);
450
+ }
451
+
452
+ $trackingNumberUuid = $this->serviceStopTrackingNumberUuid($payload, $stop);
453
+ $currentStatus = $trackingNumberUuid ? $this->trackingNumberStatus($trackingNumberUuid) : null;
454
+ $currentCode = $currentStatus
455
+ ? $currentStatus->code
456
+ : 'created';
457
+
458
+ $currentActivity = $orderConfig->activities()->firstWhere('code', strtolower((string) $currentCode));
459
+ if (!$currentActivity) {
460
+ return collect();
461
+ }
462
+
463
+ return $currentActivity->getNext($this->serviceStopActivityContext($order, $payload, $stop));
464
+ }
465
+
466
+ protected function trackingNumberStatus(string $trackingNumberUuid): ?TrackingStatus
467
+ {
468
+ $trackingNumber = TrackingNumber::where('uuid', $trackingNumberUuid)->first();
469
+ if (!$trackingNumber) {
470
+ return null;
471
+ }
472
+
473
+ if ($trackingNumber->status_uuid) {
474
+ return TrackingStatus::where('uuid', $trackingNumber->status_uuid)->first();
475
+ }
476
+
477
+ return TrackingStatus::where('tracking_number_uuid', $trackingNumberUuid)->latest()->first();
478
+ }
479
+
480
+ protected function serviceStopLocationPoint(Place $place): Point
481
+ {
482
+ try {
483
+ $point = Utils::getPointFromMixed($place);
484
+
485
+ return $point instanceof Point ? $point : new Point(0, 0);
486
+ } catch (\Throwable) {
487
+ return new Point(0, 0);
488
+ }
489
+ }
490
+
491
+ protected function activityLocationPoint($location): Point
492
+ {
493
+ if ($location instanceof Point) {
494
+ return $location;
495
+ }
496
+
497
+ if (is_array($location)) {
498
+ return new Point(...$location);
499
+ }
500
+
501
+ return new Point(0, 0);
502
+ }
503
+
504
+ protected function waypointMarkerIsComplete(Waypoint $waypoint): bool
505
+ {
506
+ if (!$waypoint->tracking_number_uuid) {
507
+ return false;
508
+ }
509
+
510
+ return (bool) data_get($this->trackingNumberStatus($waypoint->tracking_number_uuid), 'complete', false);
511
+ }
512
+ }