@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
@@ -0,0 +1,522 @@
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 payloadHasWaypointMarkers(?Payload $payload): bool
36
+ {
37
+ if (!$payload) {
38
+ return false;
39
+ }
40
+
41
+ $payload->loadMissing('waypointMarkers');
42
+
43
+ return $payload->waypointMarkers->isNotEmpty();
44
+ }
45
+
46
+ protected function payloadUsesServiceStopActivity(?Payload $payload): bool
47
+ {
48
+ if (!$payload) {
49
+ return false;
50
+ }
51
+
52
+ return $this->payloadHasWaypointMarkers($payload)
53
+ || $this->payloadHasWaypoints($payload)
54
+ || $this->payloadServiceStops($payload)->count() > 2;
55
+ }
56
+
57
+ protected function resolveProof($proof): ?Proof
58
+ {
59
+ if ($proof instanceof Proof) {
60
+ return $proof;
61
+ }
62
+
63
+ if (is_string($proof)) {
64
+ return Proof::where('public_id', $proof)->orWhere('uuid', $proof)->first();
65
+ }
66
+
67
+ return null;
68
+ }
69
+
70
+ protected function payloadServiceStops(?Payload $payload): Collection
71
+ {
72
+ if (!$payload) {
73
+ return collect();
74
+ }
75
+
76
+ $payload->loadMissing([
77
+ 'pickup',
78
+ 'dropoff',
79
+ 'waypoints',
80
+ 'waypointMarkers.place',
81
+ 'waypointMarkers.trackingNumber.status',
82
+ ]);
83
+
84
+ $stops = collect();
85
+ $sequence = 0;
86
+
87
+ if ($payload->pickup instanceof Place) {
88
+ $stops->push([
89
+ 'type' => 'pickup',
90
+ 'place' => $payload->pickup,
91
+ 'waypoint' => null,
92
+ 'tracking_number_uuid' => $payload->pickup_tracking_number_uuid,
93
+ 'sequence' => ++$sequence,
94
+ ]);
95
+ }
96
+
97
+ $markers = $payload->waypointMarkers instanceof Collection ? $payload->waypointMarkers : collect();
98
+ if ($markers->isNotEmpty()) {
99
+ foreach ($markers->sortBy('order')->values() as $waypoint) {
100
+ $waypoint->loadMissing('place', 'trackingNumber.status');
101
+
102
+ if ($waypoint->place instanceof Place) {
103
+ $stops->push([
104
+ 'type' => 'waypoint',
105
+ 'place' => $waypoint->place,
106
+ 'waypoint' => $waypoint,
107
+ 'tracking_number_uuid' => $waypoint->tracking_number_uuid,
108
+ 'sequence' => ++$sequence,
109
+ ]);
110
+ }
111
+ }
112
+ } elseif ($payload->waypoints instanceof Collection) {
113
+ foreach ($payload->waypoints as $place) {
114
+ if ($place instanceof Place) {
115
+ $stops->push([
116
+ 'type' => 'waypoint',
117
+ 'place' => $place,
118
+ 'waypoint' => null,
119
+ 'tracking_number_uuid' => null,
120
+ 'sequence' => ++$sequence,
121
+ ]);
122
+ }
123
+ }
124
+ }
125
+
126
+ if ($payload->dropoff instanceof Place) {
127
+ $stops->push([
128
+ 'type' => 'dropoff',
129
+ 'place' => $payload->dropoff,
130
+ 'waypoint' => null,
131
+ 'tracking_number_uuid' => $payload->dropoff_tracking_number_uuid,
132
+ 'sequence' => ++$sequence,
133
+ ]);
134
+ }
135
+
136
+ return $stops->values();
137
+ }
138
+
139
+ protected function payloadCurrentServiceStop(?Payload $payload): ?array
140
+ {
141
+ $stops = $this->payloadServiceStops($payload);
142
+ if ($stops->isEmpty()) {
143
+ return null;
144
+ }
145
+
146
+ $currentStopId = $payload?->current_waypoint_uuid;
147
+ if ($currentStopId) {
148
+ $current = $stops->first(function (array $stop) use ($currentStopId) {
149
+ $place = $stop['place'] ?? null;
150
+ $waypoint = $stop['waypoint'] ?? null;
151
+
152
+ return $place instanceof Place && in_array($currentStopId, [$place->uuid, $place->public_id, $place->id], true)
153
+ || $waypoint instanceof Waypoint && in_array($currentStopId, [$waypoint->uuid, $waypoint->public_id, $waypoint->place_uuid], true);
154
+ });
155
+
156
+ if ($current) {
157
+ return $current;
158
+ }
159
+ }
160
+
161
+ return $stops->first();
162
+ }
163
+
164
+ protected function ensurePayloadCurrentServiceStop(?Payload $payload): ?array
165
+ {
166
+ $stop = $this->payloadCurrentServiceStop($payload);
167
+ if (!$payload || !$stop) {
168
+ return null;
169
+ }
170
+
171
+ $place = $stop['place'] ?? null;
172
+ if ($place instanceof Place && $payload->current_waypoint_uuid !== $place->uuid) {
173
+ $this->setPayloadCurrentServiceStop($payload, $stop);
174
+ }
175
+
176
+ return $stop;
177
+ }
178
+
179
+ protected function setPayloadCurrentServiceStop(Payload $payload, array $stop): ?Place
180
+ {
181
+ $place = $stop['place'] ?? null;
182
+ if (!$place instanceof Place) {
183
+ return null;
184
+ }
185
+
186
+ $payload->current_waypoint_uuid = $place->uuid;
187
+ $payload->saveQuietly();
188
+ $payload->setRelation('currentWaypoint', $place);
189
+
190
+ if (($stop['waypoint'] ?? null) instanceof Waypoint) {
191
+ $payload->setRelation('currentWaypointMarker', $stop['waypoint']);
192
+ }
193
+
194
+ return $place;
195
+ }
196
+
197
+ protected function resolveServiceStopFromKey(?Payload $payload, ?string $key = null): ?array
198
+ {
199
+ if (!$payload || !$key) {
200
+ return null;
201
+ }
202
+
203
+ return $this->payloadServiceStops($payload)->first(function (array $stop) use ($key) {
204
+ $place = $stop['place'] ?? null;
205
+ $waypoint = $stop['waypoint'] ?? null;
206
+
207
+ return $place instanceof Place && in_array($key, [$place->uuid, $place->public_id, $place->id], true)
208
+ || $waypoint instanceof Waypoint && in_array($key, [$waypoint->uuid, $waypoint->public_id, $waypoint->place_uuid], true);
209
+ });
210
+ }
211
+
212
+ protected function nextIncompleteServiceStop(Order $order, Payload $payload): ?array
213
+ {
214
+ $current = $this->payloadCurrentServiceStop($payload);
215
+ $currentSequence = (int) data_get($current, 'sequence', 0);
216
+
217
+ return $this->payloadServiceStops($payload)
218
+ ->filter(fn (array $stop) => (int) data_get($stop, 'sequence', 0) > $currentSequence)
219
+ ->first(fn (array $stop) => !$this->serviceStopIsComplete($order, $payload, $stop));
220
+ }
221
+
222
+ protected function serviceStopIsComplete(Order $order, Payload $payload, array $stop): bool
223
+ {
224
+ if (in_array($order->status, ['completed', 'canceled'], true)) {
225
+ return true;
226
+ }
227
+
228
+ $type = $stop['type'] ?? null;
229
+ if ($type === 'waypoint') {
230
+ $waypoint = $stop['waypoint'] ?? null;
231
+
232
+ return $waypoint instanceof Waypoint && $this->waypointMarkerIsComplete($waypoint);
233
+ }
234
+
235
+ $trackingNumberUuid = $this->serviceStopTrackingNumberUuid($payload, $stop);
236
+ if ($trackingNumberUuid) {
237
+ return (bool) data_get($this->trackingNumberStatus($trackingNumberUuid), 'complete', false);
238
+ }
239
+
240
+ return false;
241
+ }
242
+
243
+ protected function updateCurrentServiceStopActivity(Order $order, Activity $activity, $location = null, $proof = null, bool $skipEndpointOrderActivity = false, bool $updateEndpointOrderStatus = true): ?array
244
+ {
245
+ $payload = $order->payload;
246
+ $stop = $this->ensurePayloadCurrentServiceStop($payload);
247
+ if (!$payload || !$stop || !Utils::isActivity($activity) || !$location) {
248
+ return $stop;
249
+ }
250
+
251
+ $waypoint = $stop['waypoint'] ?? null;
252
+ if ($waypoint instanceof Waypoint) {
253
+ $payload->loadMissing(['entities']);
254
+ $activityId = $waypoint->insertActivity($activity, $location, $proof);
255
+ if ($activityId && $waypoint->tracking_number_uuid) {
256
+ DB::table((new TrackingNumber())->getTable())->where('uuid', $waypoint->tracking_number_uuid)->update(['status_uuid' => $activityId]);
257
+ if ($waypoint->relationLoaded('trackingNumber')) {
258
+ $waypoint->trackingNumber->status_uuid = $activityId;
259
+ $waypoint->trackingNumber->unsetRelation('status');
260
+ }
261
+ }
262
+ $activity->fireEvents($order, $waypoint);
263
+
264
+ $entities = $payload->entities->where('destination_uuid', $waypoint->place_uuid);
265
+ foreach ($entities as $entity) {
266
+ $entity->insertActivity($activity, $location, $proof);
267
+ if ($activity->complete()) {
268
+ event(new EntityCompleted($entity, $activity));
269
+ } else {
270
+ event(new EntityActivityChanged($entity, $activity));
271
+ }
272
+ }
273
+
274
+ if ($activity->complete()) {
275
+ event(new WaypointCompleted($waypoint, $activity));
276
+ } else {
277
+ event(new WaypointActivityChanged($waypoint, $activity));
278
+ }
279
+
280
+ return $stop;
281
+ }
282
+
283
+ if (!$skipEndpointOrderActivity) {
284
+ $this->insertEndpointServiceStopActivity($order, $payload, $stop, $activity, $location, $proof);
285
+ }
286
+
287
+ return $stop;
288
+ }
289
+
290
+ protected function advanceCurrentServiceStopDestination(Order $order, Payload $payload): ?array
291
+ {
292
+ $nextStop = $this->nextIncompleteServiceStop($order, $payload);
293
+ if (!$nextStop) {
294
+ return null;
295
+ }
296
+
297
+ $this->setPayloadCurrentServiceStop($payload, $nextStop);
298
+
299
+ return $nextStop;
300
+ }
301
+
302
+ protected function payloadHasCurrentServiceStopActivity(?Payload $payload, Activity $activity): bool
303
+ {
304
+ if (!$payload || !Utils::isActivity($activity)) {
305
+ return false;
306
+ }
307
+
308
+ $stop = $this->payloadCurrentServiceStop($payload);
309
+ if (!$stop) {
310
+ return false;
311
+ }
312
+
313
+ $waypoint = $stop['waypoint'] ?? null;
314
+ if ($waypoint instanceof Waypoint) {
315
+ if (!$waypoint->tracking_number_uuid) {
316
+ return false;
317
+ }
318
+
319
+ return TrackingStatus::where('tracking_number_uuid', $waypoint->tracking_number_uuid)
320
+ ->where('code', TrackingStatus::prepareCode($activity->code))
321
+ ->exists();
322
+ }
323
+
324
+ $trackingNumberUuid = $this->serviceStopTrackingNumberUuid($payload, $stop);
325
+ if (!$trackingNumberUuid) {
326
+ return false;
327
+ }
328
+
329
+ return TrackingStatus::where('tracking_number_uuid', $trackingNumberUuid)
330
+ ->where('code', TrackingStatus::prepareCode($activity->code))
331
+ ->exists();
332
+ }
333
+
334
+ protected function serviceStopTrackingNumberUuid(Payload $payload, array $stop): ?string
335
+ {
336
+ $waypoint = $stop['waypoint'] ?? null;
337
+ if ($waypoint instanceof Waypoint) {
338
+ return $waypoint->tracking_number_uuid;
339
+ }
340
+
341
+ $column = $this->endpointTrackingNumberColumn($stop['type'] ?? null);
342
+ if (!$column) {
343
+ return null;
344
+ }
345
+
346
+ return $payload->{$column} ?: null;
347
+ }
348
+
349
+ protected function endpointTrackingNumberColumn(?string $type): ?string
350
+ {
351
+ return match ($type) {
352
+ 'pickup' => 'pickup_tracking_number_uuid',
353
+ 'dropoff' => 'dropoff_tracking_number_uuid',
354
+ default => null,
355
+ };
356
+ }
357
+
358
+ protected function endpointServiceStopTrackingNumber(Order $order, Payload $payload, array $stop, bool $create = false): ?TrackingNumber
359
+ {
360
+ $column = $this->endpointTrackingNumberColumn($stop['type'] ?? null);
361
+ $place = $stop['place'] ?? null;
362
+ if (!$column || !$place instanceof Place) {
363
+ return null;
364
+ }
365
+
366
+ $trackingNumberUuid = $payload->{$column};
367
+ if ($trackingNumberUuid) {
368
+ $trackingNumber = TrackingNumber::where('uuid', $trackingNumberUuid)->first();
369
+ if ($trackingNumber) {
370
+ return $trackingNumber;
371
+ }
372
+ }
373
+
374
+ if (!$create) {
375
+ return null;
376
+ }
377
+
378
+ $trackingNumberUuid = TrackingNumber::insertGetUuid([
379
+ 'company_uuid' => $payload->company_uuid ?? $order->company_uuid,
380
+ 'region' => strtoupper($place->country ?: $place->province ?: 'SG'),
381
+ 'location' => Utils::parsePointToWkt($this->serviceStopLocationPoint($place)),
382
+ ], $place);
383
+
384
+ if (!$trackingNumberUuid) {
385
+ return null;
386
+ }
387
+
388
+ DB::table($payload->getTable())->where('uuid', $payload->uuid)->update([$column => $trackingNumberUuid]);
389
+ $payload->{$column} = $trackingNumberUuid;
390
+
391
+ return TrackingNumber::where('uuid', $trackingNumberUuid)->first();
392
+ }
393
+
394
+ protected function insertEndpointServiceStopActivity(Order $order, Payload $payload, array $stop, Activity $activity, $location = null, $proof = null): ?string
395
+ {
396
+ $trackingNumber = $this->endpointServiceStopTrackingNumber($order, $payload, $stop, true);
397
+ if (!$trackingNumber) {
398
+ return null;
399
+ }
400
+
401
+ $proof = $this->resolveProof($proof);
402
+ $activityId = TrackingStatus::insertGetUuid([
403
+ 'company_uuid' => $payload->company_uuid ?? $order->company_uuid,
404
+ 'tracking_number_uuid' => $trackingNumber->uuid,
405
+ 'proof_uuid' => data_get($proof, 'uuid'),
406
+ 'status' => TemplateString::resolve($activity->get('status', ''), $order),
407
+ 'details' => TemplateString::resolve($activity->get('details', ''), $order),
408
+ 'location' => Utils::parsePointToWkt($this->activityLocationPoint($location)),
409
+ 'code' => TrackingStatus::prepareCode($activity->get('code')),
410
+ 'complete' => $activity->complete(),
411
+ ]);
412
+
413
+ if ($activityId) {
414
+ DB::table($trackingNumber->getTable())->where('uuid', $trackingNumber->uuid)->update(['status_uuid' => $activityId]);
415
+ $trackingNumber->status_uuid = $activityId;
416
+ }
417
+
418
+ if ($trackingNumber->relationLoaded('status')) {
419
+ $trackingNumber->unsetRelation('status');
420
+ }
421
+
422
+ return $activityId ?: null;
423
+ }
424
+
425
+ protected function serviceStopActivityContext(Order $order, Payload $payload, array $stop): Order|Waypoint
426
+ {
427
+ $waypoint = $stop['waypoint'] ?? null;
428
+ if ($waypoint instanceof Waypoint) {
429
+ return $waypoint;
430
+ }
431
+
432
+ $trackingNumber = $this->endpointServiceStopTrackingNumber($order, $payload, $stop);
433
+ $context = new Waypoint([
434
+ 'company_uuid' => $payload->company_uuid ?? $order->company_uuid,
435
+ 'payload_uuid' => $payload->uuid,
436
+ 'place_uuid' => data_get($stop, 'place.uuid'),
437
+ 'tracking_number_uuid' => data_get($trackingNumber, 'uuid'),
438
+ 'type' => $stop['type'] ?? null,
439
+ 'order' => $stop['sequence'] ?? null,
440
+ ]);
441
+
442
+ if ($trackingNumber) {
443
+ $trackingNumber->loadMissing('status');
444
+ $context->setRelation('trackingNumber', $trackingNumber);
445
+ }
446
+
447
+ return $context;
448
+ }
449
+
450
+ protected function nextActivitiesForServiceStop(Order $order, Payload $payload, array $stop): Collection
451
+ {
452
+ $orderConfig = $order->ensureOrderConfig();
453
+ if (!$orderConfig) {
454
+ return collect();
455
+ }
456
+
457
+ $waypoint = $stop['waypoint'] ?? null;
458
+ if ($waypoint instanceof Waypoint) {
459
+ return $orderConfig->nextActivity($waypoint);
460
+ }
461
+
462
+ $trackingNumberUuid = $this->serviceStopTrackingNumberUuid($payload, $stop);
463
+ $currentStatus = $trackingNumberUuid ? $this->trackingNumberStatus($trackingNumberUuid) : null;
464
+ $currentCode = $currentStatus
465
+ ? $currentStatus->code
466
+ : 'created';
467
+
468
+ $currentActivity = $orderConfig->activities()->firstWhere('code', strtolower((string) $currentCode));
469
+ if (!$currentActivity) {
470
+ return collect();
471
+ }
472
+
473
+ return $currentActivity->getNext($this->serviceStopActivityContext($order, $payload, $stop));
474
+ }
475
+
476
+ protected function trackingNumberStatus(string $trackingNumberUuid): ?TrackingStatus
477
+ {
478
+ $trackingNumber = TrackingNumber::where('uuid', $trackingNumberUuid)->first();
479
+ if (!$trackingNumber) {
480
+ return null;
481
+ }
482
+
483
+ if ($trackingNumber->status_uuid) {
484
+ return TrackingStatus::where('uuid', $trackingNumber->status_uuid)->first();
485
+ }
486
+
487
+ return TrackingStatus::where('tracking_number_uuid', $trackingNumberUuid)->latest()->first();
488
+ }
489
+
490
+ protected function serviceStopLocationPoint(Place $place): Point
491
+ {
492
+ try {
493
+ $point = Utils::getPointFromMixed($place);
494
+
495
+ return $point instanceof Point ? $point : new Point(0, 0);
496
+ } catch (\Throwable) {
497
+ return new Point(0, 0);
498
+ }
499
+ }
500
+
501
+ protected function activityLocationPoint($location): Point
502
+ {
503
+ if ($location instanceof Point) {
504
+ return $location;
505
+ }
506
+
507
+ if (is_array($location)) {
508
+ return new Point(...$location);
509
+ }
510
+
511
+ return new Point(0, 0);
512
+ }
513
+
514
+ protected function waypointMarkerIsComplete(Waypoint $waypoint): bool
515
+ {
516
+ if (!$waypoint->tracking_number_uuid) {
517
+ return false;
518
+ }
519
+
520
+ return (bool) data_get($this->trackingNumberStatus($waypoint->tracking_number_uuid), 'complete', false);
521
+ }
522
+ }
@@ -5,12 +5,15 @@ namespace Fleetbase\FleetOps\Tracking;
5
5
  use Fleetbase\FleetOps\Models\Order;
6
6
  use Fleetbase\FleetOps\Models\Place;
7
7
  use Fleetbase\FleetOps\Models\Waypoint;
8
+ use Fleetbase\FleetOps\Support\ResolvesOrderServiceStops;
8
9
  use Fleetbase\FleetOps\Support\Utils;
9
10
  use Illuminate\Support\Collection;
10
11
  use Illuminate\Support\Str;
11
12
 
12
13
  class TrackingContextBuilder
13
14
  {
15
+ use ResolvesOrderServiceStops;
16
+
14
17
  public function build(Order $order, TrackingOptions $options): TrackingContext
15
18
  {
16
19
  $order->loadAssignedDriver();
@@ -18,8 +21,10 @@ class TrackingContextBuilder
18
21
  'payload',
19
22
  'payload.pickup',
20
23
  'payload.dropoff',
24
+ 'payload.return',
21
25
  'payload.waypoints',
22
26
  'payload.waypointMarkers.place',
27
+ 'payload.waypointMarkers.trackingNumber.status',
23
28
  ]);
24
29
 
25
30
  $payload = $order->payload;
@@ -91,54 +96,52 @@ class TrackingContextBuilder
91
96
  $stops = collect();
92
97
  $sequence = 0;
93
98
 
94
- if ($payload->pickup instanceof Place) {
95
- $stops->push(new TrackingStop(
96
- uuid: $payload->pickup->uuid,
97
- publicId: $payload->pickup->public_id,
98
- type: 'pickup',
99
- status: null,
100
- place: $payload->pickup,
101
- completed: !in_array($order->status, ['created', 'dispatched', 'pending']),
102
- sequence: ++$sequence
103
- ));
104
- }
99
+ foreach ($this->payloadServiceStops($payload) as $serviceStop) {
100
+ $place = $serviceStop['place'] ?? null;
101
+ $waypoint = $serviceStop['waypoint'] ?? null;
102
+ $status = $waypoint instanceof Waypoint
103
+ ? $this->waypointServiceStopStatus($waypoint)
104
+ : $this->endpointServiceStopStatus($payload, $serviceStop);
105
105
 
106
- $markers = $payload->waypointMarkers instanceof Collection ? $payload->waypointMarkers : collect();
107
- if ($markers->isNotEmpty()) {
108
- foreach ($markers->sortBy('order')->values() as $waypoint) {
109
- $stops->push($this->stopFromWaypoint($waypoint, ++$sequence));
106
+ if (!$place instanceof Place) {
107
+ continue;
110
108
  }
111
- } elseif ($payload->waypoints instanceof Collection) {
112
- foreach ($payload->waypoints as $place) {
113
- if ($place instanceof Place) {
114
- $stops->push(new TrackingStop(
115
- uuid: $place->uuid,
116
- publicId: $place->public_id,
117
- type: 'waypoint',
118
- status: null,
119
- place: $place,
120
- completed: false,
121
- sequence: ++$sequence
122
- ));
123
- }
124
- }
125
- }
126
109
 
127
- if ($payload->dropoff instanceof Place) {
128
110
  $stops->push(new TrackingStop(
129
- uuid: $payload->dropoff->uuid,
130
- publicId: $payload->dropoff->public_id,
131
- type: 'dropoff',
132
- status: null,
133
- place: $payload->dropoff,
134
- completed: in_array($order->status, ['completed', 'canceled']),
135
- sequence: ++$sequence
111
+ uuid: $place->uuid,
112
+ publicId: $place->public_id,
113
+ type: $serviceStop['type'],
114
+ status: $status ?: null,
115
+ place: $place,
116
+ waypoint: $waypoint,
117
+ completed: $this->serviceStopIsComplete($order, $payload, $serviceStop),
118
+ sequence: ++$sequence,
119
+ trackingNumberUuid: $serviceStop['tracking_number_uuid'] ?? null
136
120
  ));
137
121
  }
138
122
 
139
123
  return $stops->values();
140
124
  }
141
125
 
126
+ protected function endpointServiceStopStatus($payload, array $serviceStop): ?string
127
+ {
128
+ $trackingNumberUuid = $this->serviceStopTrackingNumberUuid($payload, $serviceStop);
129
+ if (!$trackingNumberUuid) {
130
+ return null;
131
+ }
132
+
133
+ return strtolower((string) data_get($this->trackingNumberStatus($trackingNumberUuid), 'code')) ?: null;
134
+ }
135
+
136
+ protected function waypointServiceStopStatus(Waypoint $waypoint): ?string
137
+ {
138
+ if (!$waypoint->tracking_number_uuid) {
139
+ return strtolower((string) $waypoint->status_code) ?: null;
140
+ }
141
+
142
+ return strtolower((string) data_get($this->trackingNumberStatus($waypoint->tracking_number_uuid), 'code')) ?: null;
143
+ }
144
+
142
145
  protected function stopFromWaypoint(Waypoint $waypoint, int $sequence): TrackingStop
143
146
  {
144
147
  $waypoint->loadMissing('place');