@fleetbase/fleetops-engine 0.6.46 → 0.6.48

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 (95) hide show
  1. package/addon/components/admin/routing-settings.hbs +71 -0
  2. package/addon/components/admin/routing-settings.js +124 -0
  3. package/addon/components/customer/orders.js +1 -1
  4. package/addon/components/display-place.hbs +1 -1
  5. package/addon/components/map/order-list-overlay/order.hbs +18 -3
  6. package/addon/components/map/toolbar.hbs +2 -2
  7. package/addon/components/map/toolbar.js +10 -2
  8. package/addon/components/orchestrator/phase-builder.hbs +28 -5
  9. package/addon/components/orchestrator/phase-builder.js +21 -1
  10. package/addon/components/orchestrator/resource-panel.hbs +19 -1
  11. package/addon/components/orchestrator/resource-panel.js +4 -0
  12. package/addon/components/orchestrator-workbench.js +3 -1
  13. package/addon/components/order/details/purchase-rate.hbs +19 -22
  14. package/addon/components/order/details/route.hbs +26 -1
  15. package/addon/components/order/details/route.js +50 -0
  16. package/addon/components/order/details/tracking.hbs +171 -27
  17. package/addon/components/order/details/tracking.js +457 -0
  18. package/addon/components/order/form/details.hbs +1 -2
  19. package/addon/components/order/form/details.js +6 -0
  20. package/addon/components/order/form/route.js +9 -1
  21. package/addon/components/order/kanban-card.hbs +18 -3
  22. package/addon/components/order-list-overlay/order.hbs +18 -3
  23. package/addon/components/order-progress-card.hbs +21 -6
  24. package/addon/components/order-tracking-lookup.hbs +7 -7
  25. package/addon/components/order-tracking-lookup.js +1 -1
  26. package/addon/components/route-list.hbs +78 -8
  27. package/addon/components/route-list.js +48 -6
  28. package/addon/components/tracking-stop-progress.hbs +45 -0
  29. package/addon/components/tracking-stop-progress.js +79 -0
  30. package/addon/controllers/settings/payments/index.js +1 -1
  31. package/addon/controllers/settings/routing.js +98 -1
  32. package/addon/helpers/format-duration.js +38 -0
  33. package/addon/routes/application.js +6 -0
  34. package/addon/services/order-list-overlay.js +2 -2
  35. package/addon/styles/fleetops-engine.css +733 -0
  36. package/addon/templates/settings/routing.hbs +60 -2
  37. package/app/components/tracking-stop-progress.js +1 -0
  38. package/app/helpers/format-duration.js +1 -0
  39. package/composer.json +1 -1
  40. package/extension.json +1 -1
  41. package/package.json +3 -3
  42. package/pnpm-workspace.yaml +7 -0
  43. package/server/config/fleetops.php +32 -11
  44. package/server/migrations/2026_05_13_000001_repair_customer_contact_user_types.php +86 -0
  45. package/server/seeders/VroomOrchestrationSeeder.php +402 -0
  46. package/server/src/Http/Controllers/Api/v1/OrchestrationController.php +64 -0
  47. package/server/src/Http/Controllers/Api/v1/OrderController.php +26 -26
  48. package/server/src/Http/Controllers/Api/v1/PayloadController.php +10 -10
  49. package/server/src/Http/Controllers/Api/v1/ServiceQuoteController.php +1 -1
  50. package/server/src/Http/Controllers/Internal/v1/ContactController.php +41 -0
  51. package/server/src/Http/Controllers/Internal/v1/LiveController.php +6 -42
  52. package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +1 -1
  53. package/server/src/Http/Controllers/Internal/v1/OrderController.php +69 -15
  54. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +1 -1
  55. package/server/src/Http/Controllers/Internal/v1/SettingController.php +119 -12
  56. package/server/src/Http/Resources/v1/Index/Payload.php +2 -2
  57. package/server/src/Models/Contact.php +122 -13
  58. package/server/src/Models/Order.php +1 -1
  59. package/server/src/Models/Payload.php +8 -8
  60. package/server/src/Models/Place.php +0 -3
  61. package/server/src/Models/ServiceRate.php +1 -1
  62. package/server/src/Observers/ContactObserver.php +8 -0
  63. package/server/src/Orchestration/Engines/CapacityAllocationEngine.php +220 -0
  64. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +330 -38
  65. package/server/src/Orchestration/Support/OrchestrationPayloadBuilder.php +259 -51
  66. package/server/src/Providers/FleetOpsServiceProvider.php +27 -0
  67. package/server/src/Support/LiveOrderQuery.php +75 -0
  68. package/server/src/Support/Metrics.php +15 -0
  69. package/server/src/Support/OSRM.php +11 -6
  70. package/server/src/Support/OrderTracker.php +7 -582
  71. package/server/src/Support/Utils.php +1 -1
  72. package/server/src/Tracking/Contracts/TrackingProviderInterface.php +19 -0
  73. package/server/src/Tracking/Providers/CalculatedTrackingProvider.php +65 -0
  74. package/server/src/Tracking/Providers/GoogleRoutesTrackingProvider.php +118 -0
  75. package/server/src/Tracking/Providers/OsrmTrackingProvider.php +71 -0
  76. package/server/src/Tracking/README.md +25 -0
  77. package/server/src/Tracking/Support/FakeTrackingProvider.php +42 -0
  78. package/server/src/Tracking/TrackingContext.php +63 -0
  79. package/server/src/Tracking/TrackingContextBuilder.php +204 -0
  80. package/server/src/Tracking/TrackingIntelligenceService.php +180 -0
  81. package/server/src/Tracking/TrackingOptions.php +49 -0
  82. package/server/src/Tracking/TrackingProviderCapabilities.php +30 -0
  83. package/server/src/Tracking/TrackingProviderManager.php +89 -0
  84. package/server/src/Tracking/TrackingProviderRegistry.php +35 -0
  85. package/server/src/Tracking/TrackingProviderResult.php +20 -0
  86. package/server/src/Tracking/TrackingStop.php +59 -0
  87. package/server/src/routes.php +11 -0
  88. package/server/tests/CapacityAllocationEngineTest.php +108 -0
  89. package/server/tests/LiveOrderQueryTest.php +34 -0
  90. package/server/tests/OrchestrationPayloadBuilderTest.php +132 -0
  91. package/server/tests/OrchestratorConsumableApiTest.php +74 -0
  92. package/server/tests/PingDriverEndpointTest.php +19 -0
  93. package/server/tests/TrackingIntelligenceTest.php +176 -0
  94. package/server/tests/VroomOrchestrationEngineTest.php +159 -0
  95. package/translations/en-us.yaml +4 -0
@@ -0,0 +1,118 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Tracking\Providers;
4
+
5
+ use Fleetbase\FleetOps\Tracking\Contracts\TrackingProviderInterface;
6
+ use Fleetbase\FleetOps\Tracking\TrackingContext;
7
+ use Fleetbase\FleetOps\Tracking\TrackingOptions;
8
+ use Fleetbase\FleetOps\Tracking\TrackingProviderCapabilities;
9
+ use Fleetbase\FleetOps\Tracking\TrackingProviderResult;
10
+ use Illuminate\Support\Facades\Http;
11
+ use Illuminate\Support\Str;
12
+
13
+ class GoogleRoutesTrackingProvider implements TrackingProviderInterface
14
+ {
15
+ public function key(): string
16
+ {
17
+ return 'google_routes';
18
+ }
19
+
20
+ public function capabilities(): TrackingProviderCapabilities
21
+ {
22
+ return new TrackingProviderCapabilities(traffic: true, perLegEta: true, routeGeometry: true);
23
+ }
24
+
25
+ public function canTrack(TrackingContext $context): bool
26
+ {
27
+ return $context->canRoute() && filled($this->apiKey());
28
+ }
29
+
30
+ public function track(TrackingContext $context, TrackingOptions $options): TrackingProviderResult
31
+ {
32
+ $points = $context->routePoints();
33
+ $origin = array_shift($points);
34
+ $destination = array_pop($points);
35
+ $body = [
36
+ 'origin' => ['location' => ['latLng' => $this->latLng($origin)]],
37
+ 'destination' => ['location' => ['latLng' => $this->latLng($destination)]],
38
+ 'travelMode' => 'DRIVE',
39
+ 'routingPreference' => $options->trafficEnabled ? 'TRAFFIC_AWARE_OPTIMAL' : 'TRAFFIC_UNAWARE',
40
+ 'computeAlternativeRoutes' => false,
41
+ 'languageCode' => 'en-US',
42
+ 'units' => 'METRIC',
43
+ ];
44
+
45
+ if (!empty($points)) {
46
+ $body['intermediates'] = array_map(fn ($point) => ['location' => ['latLng' => $this->latLng($point)]], $points);
47
+ }
48
+
49
+ $response = Http::timeout(5)
50
+ ->withHeaders([
51
+ 'X-Goog-Api-Key' => $this->apiKey(),
52
+ 'X-Goog-FieldMask' => 'routes.distanceMeters,routes.duration,routes.staticDuration,routes.polyline.encodedPolyline,routes.legs.distanceMeters,routes.legs.duration,routes.legs.staticDuration',
53
+ ])
54
+ ->post('https://routes.googleapis.com/directions/v2:computeRoutes', $body);
55
+
56
+ if (!$response->successful()) {
57
+ throw new \RuntimeException('Google Routes request failed with status ' . $response->status());
58
+ }
59
+
60
+ $route = data_get($response->json(), 'routes.0');
61
+ if (!$route) {
62
+ throw new \RuntimeException('Google Routes returned no route.');
63
+ }
64
+
65
+ $duration = $this->durationToSeconds(data_get($route, 'staticDuration')) ?? $this->durationToSeconds(data_get($route, 'duration'));
66
+ $trafficDuration = $this->durationToSeconds(data_get($route, 'duration'));
67
+
68
+ return new TrackingProviderResult(
69
+ provider: $this->key(),
70
+ distanceMeters: data_get($route, 'distanceMeters'),
71
+ durationSeconds: $duration,
72
+ durationInTrafficSeconds: $options->trafficEnabled ? $trafficDuration : null,
73
+ polyline: data_get($route, 'polyline.encodedPolyline'),
74
+ legs: $this->legs(data_get($route, 'legs', []), $options),
75
+ warnings: [],
76
+ confidence: $options->trafficEnabled ? 'high' : 'medium',
77
+ raw: $route
78
+ );
79
+ }
80
+
81
+ protected function legs(array $legs, TrackingOptions $options): array
82
+ {
83
+ return collect($legs)->map(function ($leg, $index) use ($options) {
84
+ $duration = $this->durationToSeconds(data_get($leg, 'staticDuration')) ?? $this->durationToSeconds(data_get($leg, 'duration'));
85
+ $trafficDuration = $this->durationToSeconds(data_get($leg, 'duration'));
86
+
87
+ return [
88
+ 'index' => $index,
89
+ 'distance_m' => data_get($leg, 'distanceMeters'),
90
+ 'duration_s' => $duration,
91
+ 'duration_in_traffic_s' => $options->trafficEnabled ? $trafficDuration : null,
92
+ 'provider' => $this->key(),
93
+ ];
94
+ })->all();
95
+ }
96
+
97
+ protected function latLng($point): array
98
+ {
99
+ return [
100
+ 'latitude' => $point->getLat(),
101
+ 'longitude' => $point->getLng(),
102
+ ];
103
+ }
104
+
105
+ protected function durationToSeconds(?string $duration): ?float
106
+ {
107
+ if (!$duration) {
108
+ return null;
109
+ }
110
+
111
+ return (float) Str::replaceLast('s', '', $duration);
112
+ }
113
+
114
+ protected function apiKey(): ?string
115
+ {
116
+ return config('services.google_maps.api_key') ?: env('GOOGLE_MAPS_API_KEY');
117
+ }
118
+ }
@@ -0,0 +1,71 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Tracking\Providers;
4
+
5
+ use Fleetbase\FleetOps\Support\OSRM;
6
+ use Fleetbase\FleetOps\Tracking\Contracts\TrackingProviderInterface;
7
+ use Fleetbase\FleetOps\Tracking\TrackingContext;
8
+ use Fleetbase\FleetOps\Tracking\TrackingOptions;
9
+ use Fleetbase\FleetOps\Tracking\TrackingProviderCapabilities;
10
+ use Fleetbase\FleetOps\Tracking\TrackingProviderResult;
11
+ use Illuminate\Support\Arr;
12
+
13
+ class OsrmTrackingProvider implements TrackingProviderInterface
14
+ {
15
+ public function key(): string
16
+ {
17
+ return 'osrm';
18
+ }
19
+
20
+ public function capabilities(): TrackingProviderCapabilities
21
+ {
22
+ return new TrackingProviderCapabilities(perLegEta: true, routeGeometry: true);
23
+ }
24
+
25
+ public function canTrack(TrackingContext $context): bool
26
+ {
27
+ return $context->canRoute();
28
+ }
29
+
30
+ public function track(TrackingContext $context, TrackingOptions $options): TrackingProviderResult
31
+ {
32
+ $response = OSRM::getRouteFromPoints($context->routePoints(), [
33
+ 'overview' => 'full',
34
+ 'geometries' => 'polyline',
35
+ 'steps' => 'false',
36
+ 'annotations' => 'false',
37
+ ]);
38
+
39
+ if (data_get($response, 'code') !== 'Ok') {
40
+ throw new \RuntimeException('OSRM did not return a routable response.');
41
+ }
42
+
43
+ $route = Arr::first(data_get($response, 'routes', []));
44
+ if (!$route) {
45
+ throw new \RuntimeException('OSRM returned no route.');
46
+ }
47
+
48
+ $legs = collect(data_get($route, 'legs', []))->map(function ($leg, $index) {
49
+ return [
50
+ 'index' => $index,
51
+ 'distance_m' => data_get($leg, 'distance'),
52
+ 'duration_s' => data_get($leg, 'duration'),
53
+ 'duration_in_traffic_s' => null,
54
+ 'provider' => $this->key(),
55
+ ];
56
+ })->all();
57
+
58
+ return new TrackingProviderResult(
59
+ provider: $this->key(),
60
+ distanceMeters: data_get($route, 'distance'),
61
+ durationSeconds: data_get($route, 'duration'),
62
+ durationInTrafficSeconds: null,
63
+ polyline: data_get($route, 'geometry'),
64
+ coordinates: data_get($route, 'waypoints', []),
65
+ legs: $legs,
66
+ warnings: ['no_live_traffic'],
67
+ confidence: 'medium',
68
+ raw: $route
69
+ );
70
+ }
71
+ }
@@ -0,0 +1,25 @@
1
+ # Order Tracking Providers
2
+
3
+ FleetOps tracking providers adapt third-party routing or tracking systems into the canonical tracking intelligence response.
4
+
5
+ Providers must implement `Fleetbase\FleetOps\Tracking\Contracts\TrackingProviderInterface`:
6
+
7
+ ```php
8
+ public function key(): string;
9
+ public function capabilities(): TrackingProviderCapabilities;
10
+ public function canTrack(TrackingContext $context): bool;
11
+ public function track(TrackingContext $context, TrackingOptions $options): TrackingProviderResult;
12
+ ```
13
+
14
+ Register providers from a service provider:
15
+
16
+ ```php
17
+ use Fleetbase\FleetOps\Tracking\TrackingProviderRegistry;
18
+
19
+ public function boot(): void
20
+ {
21
+ app(TrackingProviderRegistry::class)->register(new TomTomTrackingProvider());
22
+ }
23
+ ```
24
+
25
+ Provider adapters should never expose vendor response shapes directly. Map provider responses into `TrackingProviderResult` and declare capabilities such as traffic-aware ETA, per-leg ETA, route geometry, or map matching.
@@ -0,0 +1,42 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Tracking\Support;
4
+
5
+ use Fleetbase\FleetOps\Tracking\Contracts\TrackingProviderInterface;
6
+ use Fleetbase\FleetOps\Tracking\Providers\CalculatedTrackingProvider;
7
+ use Fleetbase\FleetOps\Tracking\TrackingContext;
8
+ use Fleetbase\FleetOps\Tracking\TrackingOptions;
9
+ use Fleetbase\FleetOps\Tracking\TrackingProviderCapabilities;
10
+ use Fleetbase\FleetOps\Tracking\TrackingProviderResult;
11
+
12
+ class FakeTrackingProvider implements TrackingProviderInterface
13
+ {
14
+ public function __construct(protected string $providerKey = 'fake')
15
+ {
16
+ }
17
+
18
+ public function key(): string
19
+ {
20
+ return $this->providerKey;
21
+ }
22
+
23
+ public function capabilities(): TrackingProviderCapabilities
24
+ {
25
+ return new TrackingProviderCapabilities(traffic: true, perLegEta: true);
26
+ }
27
+
28
+ public function canTrack(TrackingContext $context): bool
29
+ {
30
+ return $context->canRoute();
31
+ }
32
+
33
+ public function track(TrackingContext $context, TrackingOptions $options): TrackingProviderResult
34
+ {
35
+ $result = (new CalculatedTrackingProvider())->track($context, $options);
36
+ $result->provider = $this->providerKey;
37
+ $result->confidence = 'high';
38
+ $result->warnings = [];
39
+
40
+ return $result;
41
+ }
42
+ }
@@ -0,0 +1,63 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Tracking;
4
+
5
+ use Fleetbase\FleetOps\Models\Driver;
6
+ use Fleetbase\FleetOps\Models\Order;
7
+ use Fleetbase\FleetOps\Models\Payload;
8
+ use Fleetbase\LaravelMysqlSpatial\Types\Point;
9
+ use Illuminate\Support\Collection;
10
+
11
+ class TrackingContext
12
+ {
13
+ public function __construct(
14
+ public Order $order,
15
+ public ?Payload $payload,
16
+ public ?Driver $driver,
17
+ public ?Point $origin,
18
+ public ?Point $driverLocation,
19
+ public Collection $stops,
20
+ public Collection $completedStops,
21
+ public Collection $remainingStops,
22
+ public ?TrackingStop $activeStop,
23
+ public ?TrackingStop $nextStop,
24
+ public ?int $driverLocationAgeSeconds,
25
+ public array $warnings = [],
26
+ ) {
27
+ }
28
+
29
+ public function routePoints(): array
30
+ {
31
+ $points = [];
32
+ if ($this->origin) {
33
+ $points[] = $this->origin;
34
+ }
35
+
36
+ foreach ($this->remainingStops as $stop) {
37
+ $point = $stop instanceof TrackingStop ? $stop->point() : null;
38
+ if ($point) {
39
+ $points[] = $point;
40
+ }
41
+ }
42
+
43
+ return $points;
44
+ }
45
+
46
+ public function canRoute(): bool
47
+ {
48
+ return count($this->routePoints()) >= 2;
49
+ }
50
+
51
+ public function stateSignature(): string
52
+ {
53
+ return md5(json_encode($this->stops->map(function (TrackingStop $stop) {
54
+ return [
55
+ 'uuid' => $stop->uuid,
56
+ 'type' => $stop->type,
57
+ 'status' => $stop->status,
58
+ 'completed' => $stop->completed,
59
+ 'sequence' => $stop->sequence,
60
+ ];
61
+ })->values()->all()));
62
+ }
63
+ }
@@ -0,0 +1,204 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Tracking;
4
+
5
+ use Fleetbase\FleetOps\Models\Order;
6
+ use Fleetbase\FleetOps\Models\Place;
7
+ use Fleetbase\FleetOps\Models\Waypoint;
8
+ use Fleetbase\FleetOps\Support\Utils;
9
+ use Illuminate\Support\Collection;
10
+ use Illuminate\Support\Str;
11
+
12
+ class TrackingContextBuilder
13
+ {
14
+ public function build(Order $order, TrackingOptions $options): TrackingContext
15
+ {
16
+ $order->loadAssignedDriver();
17
+ $order->loadMissing([
18
+ 'payload',
19
+ 'payload.pickup',
20
+ 'payload.dropoff',
21
+ 'payload.waypoints',
22
+ 'payload.waypointMarkers.place',
23
+ ]);
24
+
25
+ $payload = $order->payload;
26
+ $driver = $order->driverAssigned;
27
+ $warnings = [];
28
+ $driverLocationAge = $driver?->updated_at ? now()->diffInSeconds($driver->updated_at) : null;
29
+
30
+ $origin = null;
31
+ $driverLocation = null;
32
+ if ($driver && $driver->location) {
33
+ try {
34
+ $driverLocation = Utils::getPointFromMixed($driver);
35
+ if (!$this->isValidPoint($driverLocation)) {
36
+ $driverLocation = null;
37
+ }
38
+ } catch (\Throwable) {
39
+ $driverLocation = null;
40
+ }
41
+ }
42
+
43
+ if ($driverLocation) {
44
+ $origin = $driverLocation;
45
+ } else {
46
+ $warnings[] = 'missing_driver_location';
47
+ $origin = $this->fallbackOrigin($payload);
48
+ }
49
+
50
+ if ($driverLocationAge !== null && $driverLocationAge > $options->staleLocationThresholdSeconds) {
51
+ $warnings[] = 'stale_driver_location';
52
+ }
53
+
54
+ $stops = $this->stops($payload, $order);
55
+ $completedStops = $stops->filter(fn (TrackingStop $stop) => $stop->completed)->values();
56
+ $remainingStops = $stops->reject(fn (TrackingStop $stop) => $stop->completed)->values();
57
+ $activeStop = $this->activeStop($payload, $remainingStops);
58
+ $nextStop = $remainingStops->first(fn (TrackingStop $stop) => !$activeStop || $stop->uuid !== $activeStop->uuid);
59
+
60
+ if (!$activeStop && $remainingStops->isNotEmpty()) {
61
+ $activeStop = $remainingStops->first();
62
+ }
63
+
64
+ $missingStopLocations = $remainingStops->filter(fn (TrackingStop $stop) => !$stop->point())->count();
65
+ if ($missingStopLocations > 0) {
66
+ $warnings[] = 'missing_stop_location';
67
+ }
68
+
69
+ return new TrackingContext(
70
+ order: $order,
71
+ payload: $payload,
72
+ driver: $driver,
73
+ origin: $origin,
74
+ driverLocation: $driverLocation,
75
+ stops: $stops,
76
+ completedStops: $completedStops,
77
+ remainingStops: $remainingStops,
78
+ activeStop: $activeStop,
79
+ nextStop: $nextStop,
80
+ driverLocationAgeSeconds: $driverLocationAge,
81
+ warnings: array_values(array_unique($warnings))
82
+ );
83
+ }
84
+
85
+ protected function stops($payload, Order $order): Collection
86
+ {
87
+ if (!$payload) {
88
+ return collect();
89
+ }
90
+
91
+ $stops = collect();
92
+ $sequence = 0;
93
+
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
+ }
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));
110
+ }
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
+
127
+ if ($payload->dropoff instanceof Place) {
128
+ $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
136
+ ));
137
+ }
138
+
139
+ return $stops->values();
140
+ }
141
+
142
+ protected function stopFromWaypoint(Waypoint $waypoint, int $sequence): TrackingStop
143
+ {
144
+ $waypoint->loadMissing('place');
145
+ $status = strtolower((string) $waypoint->status_code);
146
+
147
+ return new TrackingStop(
148
+ uuid: $waypoint->place_uuid,
149
+ publicId: data_get($waypoint, 'place.public_id'),
150
+ type: 'waypoint',
151
+ status: $status ?: null,
152
+ place: $waypoint->place,
153
+ waypoint: $waypoint,
154
+ completed: in_array($status, ['completed', 'canceled']),
155
+ sequence: $sequence
156
+ );
157
+ }
158
+
159
+ protected function activeStop($payload, Collection $remainingStops): ?TrackingStop
160
+ {
161
+ if (!$payload || !$payload->current_waypoint_uuid || !Str::isUuid($payload->current_waypoint_uuid)) {
162
+ return $remainingStops->first();
163
+ }
164
+
165
+ return $remainingStops->first(function (TrackingStop $stop) use ($payload) {
166
+ return $stop->uuid === $payload->current_waypoint_uuid
167
+ || data_get($stop->waypoint, 'uuid') === $payload->current_waypoint_uuid
168
+ || data_get($stop->waypoint, 'place_uuid') === $payload->current_waypoint_uuid;
169
+ });
170
+ }
171
+
172
+ protected function fallbackOrigin($payload)
173
+ {
174
+ if (!$payload) {
175
+ return null;
176
+ }
177
+
178
+ try {
179
+ $point = Utils::getPointFromMixed($payload->getPickupOrCurrentWaypoint());
180
+
181
+ return $this->isValidPoint($point) ? $point : null;
182
+ } catch (\Throwable) {
183
+ return null;
184
+ }
185
+ }
186
+
187
+ protected function isValidPoint($point): bool
188
+ {
189
+ if (!$point || !method_exists($point, 'getLat') || !method_exists($point, 'getLng')) {
190
+ return false;
191
+ }
192
+
193
+ $lat = (float) $point->getLat();
194
+ $lng = (float) $point->getLng();
195
+
196
+ return is_finite($lat)
197
+ && is_finite($lng)
198
+ && $lat >= -90
199
+ && $lat <= 90
200
+ && $lng >= -180
201
+ && $lng <= 180
202
+ && !(abs($lat) < 0.000001 && abs($lng) < 0.000001);
203
+ }
204
+ }