@fleetbase/fleetops-engine 0.6.49 → 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 (142) 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/map/google-live-map.js +6 -2
  7. package/addon/components/map/leaflet-live-map.hbs +48 -14
  8. package/addon/components/map/leaflet-live-map.js +184 -20
  9. package/addon/components/modals/order-change-destination.hbs +46 -0
  10. package/addon/components/modals/order-change-destination.js +106 -0
  11. package/addon/components/modals/stop-activity.hbs +28 -0
  12. package/addon/components/modals/stop-activity.js +52 -0
  13. package/addon/components/modals/update-order-activity.hbs +45 -3
  14. package/addon/components/modals/update-order-activity.js +143 -5
  15. package/addon/components/orchestrator/resource-panel.hbs +0 -10
  16. package/addon/components/orchestrator/resource-panel.js +2 -4
  17. package/addon/components/order/activity-list.hbs +2 -2
  18. package/addon/components/order/activity-timeline.hbs +31 -29
  19. package/addon/components/order/details/activity.hbs +3 -3
  20. package/addon/components/order/details/activity.js +11 -0
  21. package/addon/components/order/details/documents.js +2 -0
  22. package/addon/components/order/details/purchase-rate.hbs +2 -2
  23. package/addon/components/order/details/route.hbs +11 -3
  24. package/addon/components/order/details/route.js +91 -14
  25. package/addon/components/order/details/tracking.hbs +101 -72
  26. package/addon/components/order/details/tracking.js +149 -2
  27. package/addon/components/order/form/details.hbs +3 -3
  28. package/addon/components/order/form/documents.js +11 -4
  29. package/addon/components/order/form/route.hbs +108 -104
  30. package/addon/components/order/form/route.js +37 -1
  31. package/addon/components/order/form/service-rate.hbs +2 -2
  32. package/addon/components/order-config-manager/activity-flow.hbs +4 -1
  33. package/addon/components/order-config-manager/activity-flow.js +178 -12
  34. package/addon/components/place/address.js +1 -1
  35. package/addon/components/route-list.hbs +17 -17
  36. package/addon/components/route-list.js +42 -10
  37. package/addon/components/service-rate/details.hbs +41 -0
  38. package/addon/components/service-rate/form.hbs +118 -0
  39. package/addon/components/service-rate/form.js +55 -0
  40. package/addon/components/vehicle/form.js +1 -1
  41. package/addon/components/vehicle/pill.hbs +1 -1
  42. package/addon/controllers/management/vehicles/index/new.js +1 -1
  43. package/addon/controllers/operations/orders/index/details.js +4 -4
  44. package/addon/controllers/operations/orders/index.js +14 -0
  45. package/addon/helpers/place-address-html.js +2 -2
  46. package/addon/routes/operations/service-rates/index.js +1 -1
  47. package/addon/services/geofence.js +163 -19
  48. package/addon/services/leaflet-contextmenu-manager.js +27 -0
  49. package/addon/services/vehicle-actions.js +1 -1
  50. package/addon/styles/fleetops-engine.css +91 -1
  51. package/addon/utils/fleet-ops-options.js +2 -1
  52. package/addon/utils/normalize-order-config-flow.js +41 -0
  53. package/addon/utils/place-address-html.js +3 -2
  54. package/addon/utils/route-visualization.js +54 -7
  55. package/app/components/modals/order-change-destination.js +1 -0
  56. package/app/components/modals/stop-activity.js +1 -0
  57. package/composer.json +1 -1
  58. package/extension.json +1 -1
  59. package/package.json +3 -3
  60. package/server/migrations/2026_05_21_000001_add_geographic_rules_to_service_rate_fees_table.php +37 -0
  61. package/server/migrations/2026_05_25_000001_add_endpoint_tracking_refs_to_payloads_table.php +50 -0
  62. package/server/migrations/2026_05_27_000001_default_driver_vehicle_statuses_to_available.php +48 -0
  63. package/server/resources/views/labels/default.php +57 -49
  64. package/server/resources/views/labels/waypoint-label.php +27 -20
  65. package/server/seeders/Testing/ConnectivitySeeder.php +1 -0
  66. package/server/seeders/Testing/FleetSeeder.php +22 -22
  67. package/server/seeders/Testing/OrdersSeeder.php +6 -22
  68. package/server/seeders/Testing/VroomOrchestrationSeeder.php +21 -21
  69. package/server/src/Console/Commands/AuditCustomerUserConflicts.php +75 -0
  70. package/server/src/Exceptions/CustomerUserConflictException.php +13 -0
  71. package/server/src/Http/Controllers/Api/v1/ContactController.php +17 -2
  72. package/server/src/Http/Controllers/Api/v1/CustomerController.php +876 -0
  73. package/server/src/Http/Controllers/Api/v1/DriverController.php +1 -1
  74. package/server/src/Http/Controllers/Api/v1/OrderConfigController.php +54 -0
  75. package/server/src/Http/Controllers/Api/v1/OrderController.php +98 -70
  76. package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -2
  77. package/server/src/Http/Controllers/Api/v1/VehicleController.php +20 -3
  78. package/server/src/Http/Controllers/Internal/v1/ContactController.php +19 -0
  79. package/server/src/Http/Controllers/Internal/v1/LiveController.php +101 -66
  80. package/server/src/Http/Controllers/Internal/v1/OrderController.php +429 -26
  81. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +1 -1
  82. package/server/src/Http/Controllers/Internal/v1/SettingController.php +2 -2
  83. package/server/src/Http/Controllers/Internal/v1/VehicleController.php +54 -1
  84. package/server/src/Http/Middleware/AuthenticateCustomerToken.php +34 -0
  85. package/server/src/Http/Requests/CreateCustomerOrderRequest.php +63 -0
  86. package/server/src/Http/Requests/CreateCustomerRequest.php +48 -0
  87. package/server/src/Http/Requests/CreateDriverRequest.php +1 -1
  88. package/server/src/Http/Requests/CreateVehicleRequest.php +25 -1
  89. package/server/src/Http/Requests/Internal/CreateDriverRequest.php +1 -1
  90. package/server/src/Http/Requests/VerifyCreateCustomerRequest.php +29 -0
  91. package/server/src/Http/Resources/Internal/v1/OrderConfig.php +41 -0
  92. package/server/src/Http/Resources/v1/Customer.php +79 -0
  93. package/server/src/Http/Resources/v1/OrderConfig.php +74 -0
  94. package/server/src/Http/Resources/v1/Payload.php +23 -20
  95. package/server/src/Http/Resources/v1/ServiceArea.php +33 -19
  96. package/server/src/Http/Resources/v1/ServiceRateFee.php +7 -0
  97. package/server/src/Listeners/HandleOrderDispatched.php +2 -2
  98. package/server/src/Models/Contact.php +92 -15
  99. package/server/src/Models/Customer.php +64 -0
  100. package/server/src/Models/Driver.php +12 -3
  101. package/server/src/Models/Entity.php +2 -2
  102. package/server/src/Models/Order.php +29 -4
  103. package/server/src/Models/Payload.php +17 -1
  104. package/server/src/Models/ServiceRate.php +273 -1
  105. package/server/src/Models/ServiceRateFee.php +39 -9
  106. package/server/src/Models/TrackingNumber.php +8 -2
  107. package/server/src/Models/Vehicle.php +18 -2
  108. package/server/src/Models/Waypoint.php +2 -2
  109. package/server/src/Notifications/LateDeparture.php +2 -2
  110. package/server/src/Notifications/ProlongedStoppage.php +2 -2
  111. package/server/src/Notifications/RouteDeviation.php +2 -2
  112. package/server/src/Observers/ContactObserver.php +2 -0
  113. package/server/src/Observers/ServiceRateObserver.php +6 -6
  114. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +1 -1
  115. package/server/src/Providers/FleetOpsServiceProvider.php +1 -0
  116. package/server/src/Support/Analytics/AbstractAnalytics.php +6 -6
  117. package/server/src/Support/Analytics/FuelEfficiency.php +2 -2
  118. package/server/src/Support/Analytics/OrdersByStatus.php +1 -1
  119. package/server/src/Support/Analytics/RevenueTrend.php +0 -1
  120. package/server/src/Support/Analytics/TopDrivers.php +1 -1
  121. package/server/src/Support/CustomerAuth.php +98 -0
  122. package/server/src/Support/LabelPdf.php +52 -0
  123. package/server/src/Support/Metrics/AbstractMetric.php +4 -4
  124. package/server/src/Support/ResolvesOrderServiceStops.php +512 -0
  125. package/server/src/Tracking/TrackingContextBuilder.php +41 -38
  126. package/server/src/Tracking/TrackingIntelligenceService.php +109 -9
  127. package/server/src/Tracking/TrackingStop.php +13 -11
  128. package/server/src/routes.php +43 -9
  129. package/server/tests/CapacityAllocationEngineTest.php +10 -10
  130. package/server/tests/CustomerContactIdentitySafetyTest.php +54 -0
  131. package/server/tests/CustomerEndpointTest.php +199 -0
  132. package/server/tests/DriverVehicleStatusDefaultTest.php +39 -0
  133. package/server/tests/LabelPdfRenderingTest.php +23 -0
  134. package/server/tests/LiveControllerViewportTest.php +60 -0
  135. package/server/tests/MetricsRegistryTest.php +4 -8
  136. package/server/tests/OrchestrationPayloadBuilderTest.php +2 -2
  137. package/server/tests/OrderConfigEndpointTest.php +86 -0
  138. package/server/tests/OrderFileAttachmentTest.php +20 -0
  139. package/server/tests/ServiceAreaZoneApiShapeTest.php +34 -0
  140. package/server/tests/VehicleDriverAssignmentTest.php +18 -0
  141. package/translations/en-us.yaml +1 -0
  142. 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
+ }
@@ -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');