@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,180 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Tracking;
4
+
5
+ use Fleetbase\FleetOps\Models\Order;
6
+ use Illuminate\Support\Carbon;
7
+ use Illuminate\Support\Facades\Cache;
8
+
9
+ class TrackingIntelligenceService
10
+ {
11
+ public function __construct(
12
+ protected TrackingContextBuilder $contextBuilder,
13
+ protected TrackingProviderManager $providerManager,
14
+ ) {
15
+ }
16
+
17
+ public function track(Order $order, array|TrackingOptions $options = []): array
18
+ {
19
+ $options = $options instanceof TrackingOptions ? $options : TrackingOptions::fromArray($options);
20
+ $context = $this->contextBuilder->build($order, $options);
21
+ $cacheKey = $this->cacheKey($context, $options);
22
+
23
+ return Cache::remember($cacheKey, $options->cacheTtlSeconds, function () use ($context, $options) {
24
+ return $this->buildResult($context, $this->providerManager->track($context, $options), $options);
25
+ });
26
+ }
27
+
28
+ public function eta(Order $order, array|TrackingOptions $options = []): array
29
+ {
30
+ $tracker = $this->track($order, $options);
31
+
32
+ return [
33
+ 'active_stop_seconds' => data_get($tracker, 'eta.active_stop_seconds'),
34
+ 'completion_seconds' => data_get($tracker, 'eta.completion_seconds'),
35
+ 'active_stop_at' => data_get($tracker, 'eta.active_stop_at'),
36
+ 'completion_at' => data_get($tracker, 'eta.completion_at'),
37
+ 'provider' => data_get($tracker, 'provider'),
38
+ 'confidence' => data_get($tracker, 'confidence'),
39
+ 'warnings' => data_get($tracker, 'warnings', []),
40
+ ];
41
+ }
42
+
43
+ protected function buildResult(TrackingContext $context, TrackingProviderResult $providerResult, TrackingOptions $options): array
44
+ {
45
+ $now = now();
46
+ $completionSeconds = $providerResult->durationInTrafficSeconds ?? $providerResult->durationSeconds;
47
+ $activeStopSeconds = data_get($providerResult->legs, '0.duration_in_traffic_s', data_get($providerResult->legs, '0.duration_s', $completionSeconds));
48
+ $progress = $this->progress($context, $providerResult);
49
+ $warnings = array_values(array_unique([...$context->warnings, ...$providerResult->warnings]));
50
+
51
+ if ($providerResult->confidence === 'low') {
52
+ $warnings[] = 'low_confidence_eta';
53
+ }
54
+
55
+ return [
56
+ 'provider' => $providerResult->provider,
57
+ 'fallback_provider' => in_array('fallback_used', $warnings) ? $providerResult->provider : null,
58
+ 'generated_at' => $now->toISOString(),
59
+ 'confidence' => $providerResult->confidence,
60
+ 'warnings' => array_values(array_unique($warnings)),
61
+ 'driver' => [
62
+ 'location' => $this->pointToGeoJson($context->driverLocation),
63
+ 'location_age_seconds' => $context->driverLocationAgeSeconds,
64
+ 'online' => (bool) data_get($context->driver, 'online', false),
65
+ ],
66
+ 'progress' => $progress,
67
+ 'stops' => $context->stops->map(fn (TrackingStop $stop) => $stop->toArray())->values()->all(),
68
+ 'active_stop' => $context->activeStop?->toArray(),
69
+ 'next_stop' => $context->nextStop?->toArray(),
70
+ 'route' => [
71
+ 'distance_m' => $providerResult->distanceMeters,
72
+ 'duration_s' => $providerResult->durationSeconds,
73
+ 'duration_in_traffic_s' => $providerResult->durationInTrafficSeconds,
74
+ 'polyline' => $providerResult->polyline,
75
+ 'coordinates' => $providerResult->coordinates,
76
+ 'legs' => $this->legs($context, $providerResult, $now),
77
+ ],
78
+ 'eta' => [
79
+ 'active_stop_seconds' => $activeStopSeconds,
80
+ 'completion_seconds' => $completionSeconds,
81
+ 'active_stop_at' => $this->addSeconds($now, $activeStopSeconds),
82
+ 'completion_at' => $this->addSeconds($now, $completionSeconds),
83
+ ],
84
+ 'insights' => [
85
+ 'is_delayed' => false,
86
+ 'delay_seconds' => 0,
87
+ 'is_location_stale' => in_array('stale_driver_location', $warnings),
88
+ 'is_off_route' => in_array('off_route', $warnings),
89
+ ],
90
+ 'capabilities' => $this->capabilitiesFor($providerResult->provider),
91
+ ];
92
+ }
93
+
94
+ protected function progress(TrackingContext $context, TrackingProviderResult $providerResult): array
95
+ {
96
+ $totalStops = max($context->stops->count(), 1);
97
+ $completedStops = $context->completedStops->count();
98
+ $percentage = $context->order->status === 'completed' ? 100 : round(($completedStops / $totalStops) * 100, 2);
99
+
100
+ return [
101
+ 'percentage' => $percentage,
102
+ 'completed_stops' => $completedStops,
103
+ 'remaining_stops' => $context->remainingStops->count(),
104
+ 'total_stops' => $context->stops->count(),
105
+ 'completed_distance_m' => null,
106
+ 'remaining_distance_m' => $providerResult->distanceMeters,
107
+ ];
108
+ }
109
+
110
+ protected function legs(TrackingContext $context, TrackingProviderResult $providerResult, Carbon $now): array
111
+ {
112
+ $elapsedSeconds = 0;
113
+
114
+ return collect($providerResult->legs)->map(function ($leg, $index) use ($context, $now, &$elapsedSeconds) {
115
+ $stop = $context->remainingStops->values()->get($index);
116
+ $legSeconds = data_get($leg, 'duration_in_traffic_s', data_get($leg, 'duration_s'));
117
+ $etaSeconds = null;
118
+ $etaAt = null;
119
+
120
+ if ($legSeconds !== null) {
121
+ $elapsedSeconds += (float) $legSeconds;
122
+ $etaSeconds = $elapsedSeconds;
123
+ $etaAt = $this->addSeconds($now, $etaSeconds);
124
+ }
125
+
126
+ return array_merge($leg, [
127
+ 'stop' => $stop instanceof TrackingStop ? $stop->toArray() : null,
128
+ 'eta_seconds' => $etaSeconds,
129
+ 'eta_at' => $etaAt,
130
+ ]);
131
+ })->all();
132
+ }
133
+
134
+ protected function capabilitiesFor(string $provider): array
135
+ {
136
+ $registry = app(TrackingProviderRegistry::class);
137
+ $registered = $registry->get($provider);
138
+
139
+ return $registered ? $registered->capabilities()->toArray() : (new TrackingProviderCapabilities())->toArray();
140
+ }
141
+
142
+ protected function cacheKey(TrackingContext $context, TrackingOptions $options): string
143
+ {
144
+ return 'order_tracking_intelligence:' . md5(json_encode([
145
+ 'order' => $context->order->uuid,
146
+ 'order_ts' => optional($context->order->updated_at)->timestamp,
147
+ 'payload' => optional($context->payload)->uuid,
148
+ 'payload_ts' => optional($context->payload?->updated_at)->timestamp,
149
+ 'driver' => optional($context->driver)->uuid,
150
+ 'driver_ts' => optional($context->driver?->updated_at)->timestamp,
151
+ 'stops' => $context->stateSignature(),
152
+ 'provider' => $options->provider,
153
+ 'fallbacks' => $options->fallbacks,
154
+ 'traffic' => $options->trafficEnabled,
155
+ 'settings' => [
156
+ 'summary_ttl' => $options->cacheTtlSeconds,
157
+ 'route_ttl' => $options->routeCacheTtlSeconds,
158
+ 'stale_threshold' => $options->staleLocationThresholdSeconds,
159
+ 'fallback_speed' => $options->defaultVehicleSpeedKph,
160
+ ],
161
+ ]));
162
+ }
163
+
164
+ protected function pointToGeoJson($point): ?array
165
+ {
166
+ if (!$point) {
167
+ return null;
168
+ }
169
+
170
+ return [
171
+ 'type' => 'Point',
172
+ 'coordinates' => [$point->getLng(), $point->getLat()],
173
+ ];
174
+ }
175
+
176
+ protected function addSeconds(Carbon $now, ?float $seconds): ?string
177
+ {
178
+ return $seconds === null ? null : $now->copy()->addSeconds((int) round($seconds))->toISOString();
179
+ }
180
+ }
@@ -0,0 +1,49 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Tracking;
4
+
5
+ use Fleetbase\Models\Setting;
6
+ use Illuminate\Support\Arr;
7
+
8
+ class TrackingOptions
9
+ {
10
+ public function __construct(
11
+ public ?string $provider = null,
12
+ public array $fallbacks = [],
13
+ public bool $trafficEnabled = true,
14
+ public int $cacheTtlSeconds = 60,
15
+ public int $routeCacheTtlSeconds = 600,
16
+ public int $staleLocationThresholdSeconds = 300,
17
+ public float $defaultVehicleSpeedKph = 35,
18
+ public array $raw = [],
19
+ ) {
20
+ }
21
+
22
+ public static function fromArray(array $options = []): self
23
+ {
24
+ $systemSettings = Setting::lookup('fleet-ops.tracking-settings', []);
25
+ $config = array_merge(config('fleetops.tracking', []), is_array($systemSettings) ? $systemSettings : []);
26
+ $companySettings = [];
27
+ try {
28
+ $companySettings = Setting::lookupCompany('tracking', []);
29
+ } catch (\Throwable) {
30
+ $companySettings = [];
31
+ }
32
+
33
+ $fallbacks = Arr::get($options, 'fallbacks', Arr::get($companySettings, 'fallbacks', Arr::get($config, 'fallbacks', [])));
34
+ if (is_string($fallbacks)) {
35
+ $fallbacks = array_values(array_filter(array_map('trim', explode(',', $fallbacks))));
36
+ }
37
+
38
+ return new self(
39
+ provider: Arr::get($options, 'provider', Arr::get($companySettings, 'provider', Arr::get($config, 'provider'))),
40
+ fallbacks: Arr::wrap($fallbacks),
41
+ trafficEnabled: (bool) Arr::get($options, 'traffic_enabled', Arr::get($companySettings, 'traffic_enabled', Arr::get($config, 'traffic_enabled', true))),
42
+ cacheTtlSeconds: (int) Arr::get($options, 'cache_ttl_seconds', Arr::get($companySettings, 'cache_ttl_seconds', Arr::get($config, 'cache_ttl_seconds', 60))),
43
+ routeCacheTtlSeconds: (int) Arr::get($options, 'route_cache_ttl_seconds', Arr::get($companySettings, 'route_cache_ttl_seconds', Arr::get($config, 'route_cache_ttl_seconds', 600))),
44
+ staleLocationThresholdSeconds: (int) Arr::get($options, 'stale_location_threshold_seconds', Arr::get($companySettings, 'stale_location_threshold_seconds', Arr::get($config, 'stale_location_threshold_seconds', 300))),
45
+ defaultVehicleSpeedKph: (float) Arr::get($options, 'default_vehicle_speed_kph', Arr::get($companySettings, 'default_vehicle_speed_kph', Arr::get($config, 'default_vehicle_speed_kph', 35))),
46
+ raw: $options
47
+ );
48
+ }
49
+ }
@@ -0,0 +1,30 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Tracking;
4
+
5
+ class TrackingProviderCapabilities implements \JsonSerializable
6
+ {
7
+ public function __construct(
8
+ public bool $traffic = false,
9
+ public bool $perLegEta = false,
10
+ public bool $mapMatching = false,
11
+ public bool $routeGeometry = false,
12
+ public array $extras = [],
13
+ ) {
14
+ }
15
+
16
+ public function toArray(): array
17
+ {
18
+ return array_merge([
19
+ 'traffic' => $this->traffic,
20
+ 'per_leg_eta' => $this->perLegEta,
21
+ 'map_matching' => $this->mapMatching,
22
+ 'route_geometry' => $this->routeGeometry,
23
+ ], $this->extras);
24
+ }
25
+
26
+ public function jsonSerialize(): mixed
27
+ {
28
+ return $this->toArray();
29
+ }
30
+ }
@@ -0,0 +1,89 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Tracking;
4
+
5
+ use Fleetbase\FleetOps\Tracking\Contracts\TrackingProviderInterface;
6
+ use Illuminate\Support\Arr;
7
+ use Illuminate\Support\Facades\Cache;
8
+ use Illuminate\Support\Facades\Log;
9
+ use Illuminate\Support\Str;
10
+
11
+ class TrackingProviderManager
12
+ {
13
+ public function __construct(protected TrackingProviderRegistry $registry)
14
+ {
15
+ }
16
+
17
+ public function track(TrackingContext $context, TrackingOptions $options): TrackingProviderResult
18
+ {
19
+ $warnings = [];
20
+ foreach ($this->providerOrder($options) as $providerKey) {
21
+ $provider = $this->registry->get($providerKey);
22
+ if (!$provider instanceof TrackingProviderInterface) {
23
+ $warnings[] = 'provider_not_registered:' . $providerKey;
24
+ continue;
25
+ }
26
+
27
+ if (!$provider->canTrack($context)) {
28
+ $warnings[] = 'provider_unavailable:' . $providerKey;
29
+ continue;
30
+ }
31
+
32
+ try {
33
+ $result = $this->trackWithProviderCache($provider, $context, $options);
34
+ $result->warnings = array_values(array_unique([...$warnings, ...$result->warnings]));
35
+
36
+ if (Str::snake($result->provider) !== Str::snake((string) $options->provider)) {
37
+ $result->warnings[] = 'fallback_used';
38
+ }
39
+
40
+ return $result;
41
+ } catch (\Throwable $e) {
42
+ $warnings[] = 'provider_failed:' . $providerKey;
43
+ Log::warning('Tracking provider failed.', [
44
+ 'provider' => $providerKey,
45
+ 'order' => $context->order->public_id ?? $context->order->uuid,
46
+ 'error' => $e->getMessage(),
47
+ ]);
48
+ }
49
+ }
50
+
51
+ return new TrackingProviderResult(
52
+ provider: 'none',
53
+ warnings: array_values(array_unique([...$warnings, 'no_tracking_provider_available'])),
54
+ confidence: 'none'
55
+ );
56
+ }
57
+
58
+ protected function providerOrder(TrackingOptions $options): array
59
+ {
60
+ $provider = $options->provider ?: Arr::get(config('fleetops.tracking', []), 'provider', 'google_routes');
61
+ $fallbacks = $options->fallbacks ?: Arr::get(config('fleetops.tracking', []), 'fallbacks', ['osrm', 'calculated']);
62
+
63
+ return collect([$provider, ...$fallbacks])
64
+ ->filter()
65
+ ->map(fn ($key) => Str::snake($key))
66
+ ->unique()
67
+ ->values()
68
+ ->all();
69
+ }
70
+
71
+ protected function trackWithProviderCache(TrackingProviderInterface $provider, TrackingContext $context, TrackingOptions $options): TrackingProviderResult
72
+ {
73
+ return Cache::remember($this->providerCacheKey($provider, $context, $options), $options->routeCacheTtlSeconds, function () use ($provider, $context, $options) {
74
+ return $provider->track($context, $options);
75
+ });
76
+ }
77
+
78
+ protected function providerCacheKey(TrackingProviderInterface $provider, TrackingContext $context, TrackingOptions $options): string
79
+ {
80
+ return 'order_tracking_provider_route:' . md5(json_encode([
81
+ 'provider' => $provider->key(),
82
+ 'points' => array_map(function ($point) {
83
+ return [$point->getLat(), $point->getLng()];
84
+ }, $context->routePoints()),
85
+ 'traffic' => $options->trafficEnabled,
86
+ 'speed' => $options->defaultVehicleSpeedKph,
87
+ ]));
88
+ }
89
+ }
@@ -0,0 +1,35 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Tracking;
4
+
5
+ use Fleetbase\FleetOps\Tracking\Contracts\TrackingProviderInterface;
6
+ use Illuminate\Support\Str;
7
+
8
+ class TrackingProviderRegistry
9
+ {
10
+ protected array $providers = [];
11
+
12
+ public function register(TrackingProviderInterface|string $provider, ?string $key = null): self
13
+ {
14
+ $instance = is_string($provider) ? app($provider) : $provider;
15
+ $providerKey = Str::snake($key ?? $instance->key());
16
+ $this->providers[$providerKey] = $instance;
17
+
18
+ return $this;
19
+ }
20
+
21
+ public function has(string $key): bool
22
+ {
23
+ return isset($this->providers[Str::snake($key)]);
24
+ }
25
+
26
+ public function get(string $key): ?TrackingProviderInterface
27
+ {
28
+ return $this->providers[Str::snake($key)] ?? null;
29
+ }
30
+
31
+ public function all(): array
32
+ {
33
+ return $this->providers;
34
+ }
35
+ }
@@ -0,0 +1,20 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Tracking;
4
+
5
+ class TrackingProviderResult
6
+ {
7
+ public function __construct(
8
+ public string $provider,
9
+ public ?float $distanceMeters = null,
10
+ public ?float $durationSeconds = null,
11
+ public ?float $durationInTrafficSeconds = null,
12
+ public ?string $polyline = null,
13
+ public array $coordinates = [],
14
+ public array $legs = [],
15
+ public array $warnings = [],
16
+ public string $confidence = 'medium',
17
+ public ?array $raw = null,
18
+ ) {
19
+ }
20
+ }
@@ -0,0 +1,59 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Tracking;
4
+
5
+ use Fleetbase\FleetOps\Models\Place;
6
+ use Fleetbase\FleetOps\Models\Waypoint;
7
+ use Fleetbase\FleetOps\Support\Utils;
8
+ use Fleetbase\LaravelMysqlSpatial\Types\Point;
9
+
10
+ class TrackingStop implements \JsonSerializable
11
+ {
12
+ public function __construct(
13
+ public ?string $uuid,
14
+ public ?string $publicId,
15
+ public string $type,
16
+ public ?string $status,
17
+ public ?Place $place,
18
+ public ?Waypoint $waypoint = null,
19
+ public bool $completed = false,
20
+ public ?int $sequence = null,
21
+ ) {
22
+ }
23
+
24
+ public function point(): ?Point
25
+ {
26
+ try {
27
+ return Utils::getPointFromMixed($this->place);
28
+ } catch (\Throwable) {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ public function toArray(): array
34
+ {
35
+ $point = $this->point();
36
+
37
+ return [
38
+ 'uuid' => $this->uuid,
39
+ 'public_id' => $this->publicId,
40
+ 'type' => $this->type,
41
+ 'status' => $this->status,
42
+ 'completed' => $this->completed,
43
+ 'sequence' => $this->sequence,
44
+ 'address' => data_get($this->place, 'address'),
45
+ 'name' => data_get($this->place, 'name'),
46
+ 'location' => $point ? [
47
+ 'type' => 'Point',
48
+ 'coordinates' => [$point->getLng(), $point->getLat()],
49
+ ] : null,
50
+ 'latitude' => $point ? $point->getLat() : null,
51
+ 'longitude' => $point ? $point->getLng() : null,
52
+ ];
53
+ }
54
+
55
+ public function jsonSerialize(): mixed
56
+ {
57
+ return $this->toArray();
58
+ }
59
+ }
@@ -203,6 +203,12 @@ Route::prefix(config('fleetops.api.routing.prefix', null))->namespace('Fleetbase
203
203
  $router->get('{id}', 'LabelController@getLabel');
204
204
  });
205
205
 
206
+ // orchestrator routes
207
+ $router->group(['prefix' => 'orchestrator'], function () use ($router) {
208
+ $router->post('run', 'OrchestrationController@run');
209
+ $router->post('commit', 'OrchestrationController@commit');
210
+ });
211
+
206
212
  // navigator routes
207
213
  $router->group(['prefix' => 'onboard'], function () use ($router) {
208
214
  $router->get('driver-onboard-settings/{companyId}', 'NavigatorController@getDriverOnboardSettings');
@@ -339,6 +345,7 @@ Route::prefix(config('fleetops.api.routing.prefix', null))->namespace('Fleetbase
339
345
  $router->get('next-activity/{id}', $controller('nextActivity'));
340
346
  $router->get('{id}/tracker', 'OrderController@trackerInfo');
341
347
  $router->get('{id}/eta', 'OrderController@waypointEtas');
348
+ $router->post('{id}/ping-driver', $controller('pingDriver'));
342
349
  $router->post('process-imports', $controller('importFromFiles'));
343
350
  $router->patch('route/{id}', $controller('editOrderRoute'));
344
351
  $router->patch('update-activity/{id}', $controller('updateActivity'));
@@ -546,6 +553,10 @@ Route::prefix(config('fleetops.api.routing.prefix', null))->namespace('Fleetbase
546
553
  $router->post('notification-settings', 'SettingController@saveNotificationSettings');
547
554
  $router->get('routing-settings', 'SettingController@getRoutingSettings');
548
555
  $router->post('routing-settings', 'SettingController@saveRoutingSettings');
556
+ $router->get('tracking-settings', 'SettingController@getTrackingSettings');
557
+ $router->post('tracking-settings', 'SettingController@saveTrackingSettings');
558
+ $router->get('admin-tracking-settings', 'SettingController@getAdminTrackingSettings');
559
+ $router->post('admin-tracking-settings', 'SettingController@saveAdminTrackingSettings');
549
560
  $router->get('map', 'SettingController@getMapSettings');
550
561
  $router->post('map', 'SettingController@saveMapSettings');
551
562
  $router->get('admin-map', 'SettingController@getAdminMapSettings');
@@ -0,0 +1,108 @@
1
+ <?php
2
+
3
+ use Fleetbase\FleetOps\Models\Order;
4
+ use Fleetbase\FleetOps\Models\Vehicle;
5
+ use Fleetbase\FleetOps\Orchestration\Engines\CapacityAllocationEngine;
6
+
7
+ function capacityTestOrder(string $publicId, float $weightKg, float $volumeLitres = 0, array $skills = []): Order
8
+ {
9
+ $payload = new class($weightKg, $volumeLitres) {
10
+ public $entities;
11
+
12
+ public function __construct(float $weightKg, float $volumeLitres)
13
+ {
14
+ $this->entities = collect([
15
+ new class($weightKg, $volumeLitres) {
16
+ public float $weight;
17
+ public string $weight_unit = 'kg';
18
+ public float $length;
19
+ public float $width = 1;
20
+ public float $height = 1;
21
+ public string $dimensions_unit = 'm';
22
+
23
+ public function __construct(float $weightKg, float $volumeLitres)
24
+ {
25
+ $this->weight = $weightKg;
26
+ $this->length = $volumeLitres / 1000;
27
+ }
28
+ },
29
+ ]);
30
+ }
31
+ };
32
+
33
+ $order = new Order();
34
+ $order->public_id = $publicId;
35
+ $order->required_skills = $skills;
36
+ $order->setRelation('payload', $payload);
37
+
38
+ return $order;
39
+ }
40
+
41
+ function capacityTestVehicle(string $publicId, float $weightKg, float $volumeM3 = 0, int $maxTasks = 0, array $skills = []): Vehicle
42
+ {
43
+ $vehicle = new Vehicle();
44
+ $vehicle->public_id = $publicId;
45
+ $vehicle->payload_capacity = $weightKg;
46
+ $vehicle->payload_capacity_volume = $volumeM3;
47
+ $vehicle->payload_capacity_pallets = 0;
48
+ $vehicle->payload_capacity_parcels = 100;
49
+ $vehicle->max_tasks = $maxTasks;
50
+ $vehicle->skills = $skills;
51
+ $vehicle->setRelation('driver', null);
52
+
53
+ return $vehicle;
54
+ }
55
+
56
+ test('capacity engine assigns orders without vehicle locations', function () {
57
+ $engine = new CapacityAllocationEngine();
58
+
59
+ $result = $engine->allocate(collect([
60
+ capacityTestOrder('order_light', 10, 250),
61
+ capacityTestOrder('order_medium', 20, 500),
62
+ ]), collect([
63
+ capacityTestVehicle('vehicle_van', 100, 2),
64
+ ]), [
65
+ 'balance_workload' => false,
66
+ ]);
67
+
68
+ expect($result['assignments'])->toHaveCount(2);
69
+ expect(array_column($result['assignments'], 'vehicle_id'))->toBe(['vehicle_van', 'vehicle_van']);
70
+ expect($result['unassigned'])->toBe([]);
71
+ expect($result['summary']['allocation_strategy'])->toBe('capacity_only');
72
+ });
73
+
74
+ test('capacity engine rejects over-capacity orders with useful reasons', function () {
75
+ $engine = new CapacityAllocationEngine();
76
+
77
+ $result = $engine->allocate(collect([
78
+ capacityTestOrder('order_heavy', 250),
79
+ ]), collect([
80
+ capacityTestVehicle('vehicle_small', 100),
81
+ ]));
82
+
83
+ expect($result['assignments'])->toBe([]);
84
+ expect($result['unassigned'])->toBe(['order_heavy']);
85
+ expect($result['summary']['unassigned_reasons'][0])->toMatchArray([
86
+ 'order_id' => 'order_heavy',
87
+ 'reason' => 'capacity_exceeded',
88
+ ]);
89
+ });
90
+
91
+ test('capacity engine respects skills max tasks and balanced workload', function () {
92
+ $engine = new CapacityAllocationEngine();
93
+
94
+ $result = $engine->allocate(collect([
95
+ capacityTestOrder('order_cold_1', 10, 0, ['cold_chain']),
96
+ capacityTestOrder('order_cold_2', 10, 0, ['cold_chain']),
97
+ capacityTestOrder('order_fragile', 10, 0, ['fragile']),
98
+ ]), collect([
99
+ capacityTestVehicle('vehicle_cold_a', 100, 0, 1, ['cold_chain']),
100
+ capacityTestVehicle('vehicle_cold_b', 100, 0, 1, ['cold_chain']),
101
+ ]), [
102
+ 'balance_workload' => true,
103
+ ]);
104
+
105
+ expect(array_column($result['assignments'], 'vehicle_id'))->toBe(['vehicle_cold_a', 'vehicle_cold_b']);
106
+ expect($result['unassigned'])->toBe(['order_fragile']);
107
+ expect($result['summary']['unassigned_reasons'][0]['reason'])->toBe('missing_required_skills');
108
+ });
@@ -0,0 +1,34 @@
1
+ <?php
2
+
3
+ use Fleetbase\FleetOps\Support\LiveOrderQuery;
4
+
5
+ test('active live order query uses the same active status rules as the map overlay', function () {
6
+ $query = LiveOrderQuery::make('company_test', [
7
+ 'active' => true,
8
+ 'apply_permissions' => false,
9
+ ]);
10
+
11
+ $bindings = $query->getBindings();
12
+
13
+ expect($bindings)->toContain('company_test')
14
+ ->and($bindings)->toContain('created')
15
+ ->and($bindings)->toContain('pending')
16
+ ->and($bindings)->toContain('completed')
17
+ ->and($bindings)->toContain('canceled')
18
+ ->and($bindings)->toContain('expired')
19
+ ->and($bindings)->toContain('order_canceled');
20
+ });
21
+
22
+ test('live order query requires renderable payload and tracking data', function () {
23
+ $query = LiveOrderQuery::make('company_test', [
24
+ 'active' => true,
25
+ 'apply_permissions' => false,
26
+ ]);
27
+
28
+ $sql = $query->toSql();
29
+
30
+ expect($sql)->toContain('exists')
31
+ ->and($sql)->toContain('payload')
32
+ ->and($sql)->toContain('tracking')
33
+ ->and($sql)->toContain('driver');
34
+ });