@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,132 @@
1
+ <?php
2
+
3
+ use Fleetbase\FleetOps\Models\Order;
4
+ use Fleetbase\FleetOps\Orchestration\Support\OrchestrationPayloadBuilder;
5
+ use Illuminate\Support\Collection;
6
+
7
+ function orchestrationTestPlace($lng, $lat)
8
+ {
9
+ return new class($lng, $lat) {
10
+ public $location;
11
+
12
+ public function __construct($lng, $lat)
13
+ {
14
+ $this->location = new class($lng, $lat) {
15
+ public function __construct(private $lng, private $lat)
16
+ {
17
+ }
18
+
19
+ public function getLng()
20
+ {
21
+ return $this->lng;
22
+ }
23
+
24
+ public function getLat()
25
+ {
26
+ return $this->lat;
27
+ }
28
+ };
29
+ }
30
+ };
31
+ }
32
+
33
+ function orchestrationTestOrder($pickup = null, $dropoff = null, array $waypoints = []): Order
34
+ {
35
+ $payload = new class($pickup, $dropoff, $waypoints) {
36
+ public Collection $entities;
37
+ public Collection $waypointMarkers;
38
+
39
+ public function __construct(public $pickup, public $dropoff, array $waypoints)
40
+ {
41
+ $this->entities = collect();
42
+ $this->waypointMarkers = collect($waypoints)->map(function ($place, $index) {
43
+ return new class($place, $index) {
44
+ public string $public_id;
45
+ public string $uuid;
46
+ public int $order;
47
+
48
+ public function __construct(public $place, int $index)
49
+ {
50
+ $this->public_id = 'waypoint_' . $index;
51
+ $this->uuid = 'internal-waypoint-' . $index;
52
+ $this->order = $index;
53
+ }
54
+ };
55
+ });
56
+ }
57
+
58
+ public function relationLoaded(string $relation): bool
59
+ {
60
+ return $relation === 'waypointMarkers';
61
+ }
62
+ };
63
+
64
+ $order = new Order();
65
+ $order->public_id = 'order_test';
66
+ $order->setRelation('payload', $payload);
67
+
68
+ return $order;
69
+ }
70
+
71
+ test('route stops use pickup waypoint dropoff order for mixed payloads', function () {
72
+ $order = orchestrationTestOrder(
73
+ orchestrationTestPlace(103.85, 1.28),
74
+ orchestrationTestPlace(103.90, 1.31),
75
+ [
76
+ orchestrationTestPlace(103.86, 1.29),
77
+ orchestrationTestPlace(103.87, 1.30),
78
+ ]
79
+ );
80
+
81
+ $stops = OrchestrationPayloadBuilder::buildRouteStops($order);
82
+
83
+ expect(array_column($stops, 'role'))->toBe(['pickup', 'waypoint', 'waypoint', 'dropoff']);
84
+ expect(array_column($stops, 'location'))->toBe([
85
+ [103.85, 1.28],
86
+ [103.86, 1.29],
87
+ [103.87, 1.30],
88
+ [103.90, 1.31],
89
+ ]);
90
+ expect($stops[1])->not->toHaveKey('waypoint_uuid');
91
+ });
92
+
93
+ test('route stops support endpoint only and waypoint only payloads', function () {
94
+ $endpointOnly = OrchestrationPayloadBuilder::buildRouteStops(orchestrationTestOrder(
95
+ orchestrationTestPlace(103.85, 1.28),
96
+ orchestrationTestPlace(103.90, 1.31)
97
+ ));
98
+ $waypointOnly = OrchestrationPayloadBuilder::buildRouteStops(orchestrationTestOrder(
99
+ null,
100
+ null,
101
+ [orchestrationTestPlace(103.86, 1.29)]
102
+ ));
103
+
104
+ expect(array_column($endpointOnly, 'role'))->toBe(['pickup', 'dropoff']);
105
+ expect(array_column($waypointOnly, 'role'))->toBe(['waypoint']);
106
+ });
107
+
108
+ test('route stops drop invalid coordinates before vroom payload mapping', function () {
109
+ $stops = OrchestrationPayloadBuilder::buildRouteStops(orchestrationTestOrder(
110
+ orchestrationTestPlace('and', 1.28),
111
+ orchestrationTestPlace(103.90, 1.31)
112
+ ));
113
+
114
+ expect(array_column($stops, 'role'))->toBe(['dropoff']);
115
+ expect($stops[0]['location'])->toBe([103.90, 1.31]);
116
+ });
117
+
118
+ test('route tasks mark declared invalid-coordinate stops unassigned', function () {
119
+ $tasks = OrchestrationPayloadBuilder::buildRouteTasks(collect([
120
+ orchestrationTestOrder(
121
+ orchestrationTestPlace('and', 1.28),
122
+ orchestrationTestPlace(103.90, 1.31)
123
+ ),
124
+ ]));
125
+
126
+ expect($tasks[0])->toMatchArray([
127
+ 'id' => 'order_test',
128
+ 'invalid' => true,
129
+ 'stops' => [],
130
+ ]);
131
+ expect($tasks[0]['reason'])->toContain('pickup stop is missing valid coordinates');
132
+ });
@@ -0,0 +1,74 @@
1
+ <?php
2
+
3
+ use Fleetbase\FleetOps\Http\Controllers\Api\v1\OrchestrationController;
4
+ use Fleetbase\FleetOps\Orchestration\OrchestrationEngineRegistry;
5
+
6
+ function sanitize_orchestrator_payload(array $payload): array
7
+ {
8
+ $controller = new OrchestrationController(new OrchestrationEngineRegistry());
9
+ $method = new ReflectionMethod($controller, 'sanitizePublicPayload');
10
+ $method->setAccessible(true);
11
+
12
+ return $method->invoke($controller, $payload);
13
+ }
14
+
15
+ function assert_no_internal_orchestrator_identifiers(array $payload): void
16
+ {
17
+ foreach ($payload as $key => $value) {
18
+ expect($key)->not->toBe('uuid');
19
+ expect($key)->not->toBe('company_uuid');
20
+ expect($key)->not->toBe('internal_id');
21
+ expect(str_ends_with((string) $key, '_uuid'))->toBeFalse();
22
+
23
+ if (is_array($value)) {
24
+ assert_no_internal_orchestrator_identifiers($value);
25
+ }
26
+ }
27
+ }
28
+
29
+ test('consumable orchestrator responses remove internal identifiers recursively', function () {
30
+ $payload = [
31
+ 'assignments' => [
32
+ [
33
+ 'order_id' => 'order_123',
34
+ 'order_uuid' => '6a76d5ef-9d08-4eb9-a235-62406d6a0ed2',
35
+ 'vehicle_id' => 'vehicle_123',
36
+ 'vehicle_uuid' => '9a05d9d1-9ff7-460f-a6ee-509a0ed9b345',
37
+ 'driver_id' => 'driver_123',
38
+ 'internal_id' => 123,
39
+ 'nested' => [
40
+ 'uuid' => '793f2f9d-5225-49f7-ad22-d73a2c4ea2d1',
41
+ 'company_uuid' => 'company-internal',
42
+ 'public_id' => 'safe_public_id',
43
+ ],
44
+ ],
45
+ ],
46
+ 'unassigned' => ['order_456'],
47
+ 'summary' => [
48
+ 'routes' => 1,
49
+ 'meta' => [
50
+ 'request_uuid' => '524c5175-1df2-4e9a-a27b-3107f9e08545',
51
+ ],
52
+ ],
53
+ ];
54
+
55
+ $sanitized = sanitize_orchestrator_payload($payload);
56
+
57
+ expect($sanitized['assignments'][0])->toMatchArray([
58
+ 'order_id' => 'order_123',
59
+ 'vehicle_id' => 'vehicle_123',
60
+ 'driver_id' => 'driver_123',
61
+ ]);
62
+ expect($sanitized['assignments'][0]['nested']['public_id'])->toBe('safe_public_id');
63
+ expect($sanitized['summary']['routes'])->toBe(1);
64
+
65
+ assert_no_internal_orchestrator_identifiers($sanitized);
66
+ });
67
+
68
+ test('consumable orchestrator routes are registered under the public api group', function () {
69
+ $routes = file_get_contents(__DIR__ . '/../src/routes.php');
70
+
71
+ expect($routes)->toContain("['prefix' => 'orchestrator']");
72
+ expect($routes)->toContain("\$router->post('run', 'OrchestrationController@run');");
73
+ expect($routes)->toContain("\$router->post('commit', 'OrchestrationController@commit');");
74
+ });
@@ -0,0 +1,19 @@
1
+ <?php
2
+
3
+ test('ping driver is only exposed through the internal fleetops order route group', function () {
4
+ $routes = file_get_contents(dirname(__DIR__) . '/src/routes.php');
5
+
6
+ expect($routes)->not->toContain("\$router->post('{id}/ping-driver', 'OrderController@pingDriver');")
7
+ ->and($routes)->toContain("\$router->post('{id}/ping-driver', \$controller('pingDriver'));");
8
+ });
9
+
10
+ test('ping driver handler belongs to the internal order controller', function () {
11
+ $apiController = file_get_contents(dirname(__DIR__) . '/src/Http/Controllers/Api/v1/OrderController.php');
12
+ $internalController = file_get_contents(dirname(__DIR__) . '/src/Http/Controllers/Internal/v1/OrderController.php');
13
+
14
+ expect($apiController)->not->toContain('function pingDriver')
15
+ ->and($apiController)->not->toContain('Notifications\OrderPing')
16
+ ->and($internalController)->toContain('function pingDriver')
17
+ ->and($internalController)->toContain('Notifications\OrderPing')
18
+ ->and($internalController)->toContain('fleet-ops update order');
19
+ });
@@ -0,0 +1,176 @@
1
+ <?php
2
+
3
+ use Fleetbase\FleetOps\Models\Driver;
4
+ use Fleetbase\FleetOps\Models\Order;
5
+ use Fleetbase\FleetOps\Models\Payload;
6
+ use Fleetbase\FleetOps\Models\Place;
7
+ use Fleetbase\FleetOps\Tracking\Providers\CalculatedTrackingProvider;
8
+ use Fleetbase\FleetOps\Tracking\Support\FakeTrackingProvider;
9
+ use Fleetbase\FleetOps\Tracking\TrackingContextBuilder;
10
+ use Fleetbase\FleetOps\Tracking\TrackingOptions;
11
+ use Fleetbase\FleetOps\Tracking\TrackingProviderManager;
12
+ use Fleetbase\FleetOps\Tracking\TrackingProviderRegistry;
13
+ use Fleetbase\LaravelMysqlSpatial\Types\Point;
14
+ use Illuminate\Support\Carbon;
15
+
16
+ function trackingPlace(string $uuid, float $lat, float $lng): Place
17
+ {
18
+ $place = new Place();
19
+ $place->uuid = $uuid;
20
+ $place->public_id = 'place_' . substr($uuid, 0, 8);
21
+ $place->address = 'Test address ' . $uuid;
22
+ $place->location = new Point($lat, $lng);
23
+
24
+ return $place;
25
+ }
26
+
27
+ function trackingOrderWithStops(): Order
28
+ {
29
+ $pickup = trackingPlace('11111111-1111-1111-1111-111111111111', 1.30, 103.80);
30
+ $dropoff = trackingPlace('22222222-2222-2222-2222-222222222222', 1.35, 103.85);
31
+ $payload = new Payload();
32
+ $payload->uuid = '33333333-3333-3333-3333-333333333333';
33
+ $payload->current_waypoint_uuid = $pickup->uuid;
34
+ $payload->setRelation('pickup', $pickup);
35
+ $payload->setRelation('dropoff', $dropoff);
36
+ $payload->setRelation('waypoints', collect());
37
+ $payload->setRelation('waypointMarkers', collect());
38
+
39
+ $driver = new Driver();
40
+ $driver->uuid = '44444444-4444-4444-4444-444444444444';
41
+ $driver->location = new Point(1.29, 103.79);
42
+ $driver->online = true;
43
+ $driver->updated_at = Carbon::now();
44
+
45
+ $order = new Order();
46
+ $order->uuid = '55555555-5555-5555-5555-555555555555';
47
+ $order->public_id = 'order_test';
48
+ $order->status = 'started';
49
+ $order->updated_at = Carbon::now();
50
+ $order->setRelation('payload', $payload);
51
+ $order->setRelation('driverAssigned', $driver);
52
+
53
+ return $order;
54
+ }
55
+
56
+ test('tracking context builder normalizes order stops and driver telemetry', function () {
57
+ $context = (new TrackingContextBuilder())->build(trackingOrderWithStops(), TrackingOptions::fromArray([
58
+ 'provider' => 'calculated',
59
+ ]));
60
+
61
+ expect($context->stops)->toHaveCount(2)
62
+ ->and($context->activeStop?->type)->toBe('dropoff')
63
+ ->and($context->nextStop)->toBeNull()
64
+ ->and($context->driverLocationAgeSeconds)->toBeInt()
65
+ ->and($context->warnings)->toBe([]);
66
+ });
67
+
68
+ test('tracking context ignores zero coordinate driver location and falls back to pickup origin', function () {
69
+ $order = trackingOrderWithStops();
70
+ $order->driverAssigned->location = new Point(0, 0);
71
+
72
+ $context = (new TrackingContextBuilder())->build($order, TrackingOptions::fromArray([
73
+ 'provider' => 'calculated',
74
+ ]));
75
+
76
+ expect($context->driverLocation)->toBeNull()
77
+ ->and($context->origin?->getLat())->toBe(1.30)
78
+ ->and($context->origin?->getLng())->toBe(103.80)
79
+ ->and($context->routePoints()[0]->getLat())->toBe(1.30)
80
+ ->and($context->warnings)->toContain('missing_driver_location');
81
+ });
82
+
83
+ test('calculated provider returns normalized low confidence route data', function () {
84
+ $context = (new TrackingContextBuilder())->build(trackingOrderWithStops(), TrackingOptions::fromArray([
85
+ 'provider' => 'calculated',
86
+ ]));
87
+
88
+ $result = (new CalculatedTrackingProvider())->track($context, TrackingOptions::fromArray([
89
+ 'provider' => 'calculated',
90
+ 'default_vehicle_speed_kph' => 36,
91
+ ]));
92
+
93
+ expect($result->provider)->toBe('calculated')
94
+ ->and($result->distanceMeters)->toBeGreaterThan(0)
95
+ ->and($result->durationSeconds)->toBeGreaterThan(0)
96
+ ->and($result->confidence)->toBe('low')
97
+ ->and($result->warnings)->toContain('calculated_route_used');
98
+ });
99
+
100
+ test('tracking route legs include cumulative stop eta values', function () {
101
+ $context = (new TrackingContextBuilder())->build(trackingOrderWithStops(), TrackingOptions::fromArray([
102
+ 'provider' => 'calculated',
103
+ ]));
104
+
105
+ $result = (new CalculatedTrackingProvider())->track($context, TrackingOptions::fromArray([
106
+ 'provider' => 'calculated',
107
+ ]));
108
+ $service = app(Fleetbase\FleetOps\Tracking\TrackingIntelligenceService::class);
109
+ $method = new ReflectionMethod($service, 'legs');
110
+ $method->setAccessible(true);
111
+ $legs = $method->invoke($service, $context, $result, Carbon::parse('2026-05-12 00:00:00'));
112
+
113
+ expect($legs[0]['eta_seconds'])->toBeGreaterThan(0)
114
+ ->and($legs[0]['eta_at'])->not->toBeNull();
115
+ });
116
+
117
+ test('provider manager falls back to registered provider and records fallback warning', function () {
118
+ $registry = new TrackingProviderRegistry();
119
+ $registry->register(new FakeTrackingProvider('fake'));
120
+ $manager = new TrackingProviderManager($registry);
121
+ $context = (new TrackingContextBuilder())->build(trackingOrderWithStops(), TrackingOptions::fromArray([
122
+ 'provider' => 'missing',
123
+ 'fallbacks' => ['fake'],
124
+ ]));
125
+
126
+ $result = $manager->track($context, TrackingOptions::fromArray([
127
+ 'provider' => 'missing',
128
+ 'fallbacks' => ['fake'],
129
+ ]));
130
+
131
+ expect($result->provider)->toBe('fake')
132
+ ->and($result->warnings)->toContain('provider_not_registered:missing')
133
+ ->and($result->warnings)->toContain('fallback_used');
134
+ });
135
+
136
+ test('third party providers can be registered through the tracking provider contract', function () {
137
+ $registry = new TrackingProviderRegistry();
138
+ $registry->register(new FakeTrackingProvider('tomtom'));
139
+
140
+ expect($registry->has('tomtom'))->toBeTrue()
141
+ ->and($registry->get('tomtom')?->capabilities()->traffic)->toBeTrue();
142
+ });
143
+
144
+ test('tracking options include route cache ttl and fallback provider settings', function () {
145
+ $options = TrackingOptions::fromArray([
146
+ 'provider' => 'calculated',
147
+ 'fallbacks' => 'osrm,calculated',
148
+ 'route_cache_ttl_seconds' => 900,
149
+ ]);
150
+
151
+ expect($options->provider)->toBe('calculated')
152
+ ->and($options->fallbacks)->toBe(['osrm', 'calculated'])
153
+ ->and($options->routeCacheTtlSeconds)->toBe(900);
154
+ });
155
+
156
+ test('provider cache key varies by route options', function () {
157
+ $registry = new TrackingProviderRegistry();
158
+ $manager = new TrackingProviderManager($registry);
159
+ $provider = new FakeTrackingProvider('fake');
160
+ $context = (new TrackingContextBuilder())->build(trackingOrderWithStops(), TrackingOptions::fromArray([
161
+ 'provider' => 'fake',
162
+ ]));
163
+ $method = new ReflectionMethod($manager, 'providerCacheKey');
164
+ $method->setAccessible(true);
165
+
166
+ $trafficKey = $method->invoke($manager, $provider, $context, TrackingOptions::fromArray([
167
+ 'provider' => 'fake',
168
+ 'traffic_enabled' => true,
169
+ ]));
170
+ $nonTrafficKey = $method->invoke($manager, $provider, $context, TrackingOptions::fromArray([
171
+ 'provider' => 'fake',
172
+ 'traffic_enabled' => false,
173
+ ]));
174
+
175
+ expect($trafficKey)->not->toBe($nonTrafficKey);
176
+ });
@@ -0,0 +1,159 @@
1
+ <?php
2
+
3
+ use Fleetbase\FleetOps\Orchestration\Engines\VroomOrchestrationEngine;
4
+
5
+ test('vroom maps multi stop route tasks to shipments', function () {
6
+ $engine = new VroomOrchestrationEngine();
7
+ $method = new ReflectionMethod($engine, 'mapTaskToShipment');
8
+ $reverse = [];
9
+
10
+ $method->setAccessible(true);
11
+
12
+ $shipment = $method->invokeArgs($engine, [[
13
+ 'id' => 'order_gkATxX2U3P',
14
+ 'stops' => [
15
+ ['role' => 'pickup', 'location' => [103.85, 1.28]],
16
+ ['role' => 'waypoint', 'location' => [103.87, 1.30]],
17
+ ['role' => 'dropoff', 'location' => [103.90, 1.31]],
18
+ ],
19
+ 'service' => 300,
20
+ 'amount' => [1, 0, 0, 1],
21
+ 'description' => 'order_gkATxX2U3P',
22
+ ], &$reverse]);
23
+
24
+ expect($shipment)->toHaveKeys(['pickup', 'delivery', 'amount']);
25
+ expect($shipment)->not->toHaveKey('location');
26
+ expect($shipment['pickup']['location'])->toBe([103.85, 1.28]);
27
+ expect($shipment['delivery']['location'])->toBe([103.90, 1.31]);
28
+ expect($shipment['delivery']['service'])->toBe(300);
29
+ expect(array_values($reverse))->toBe(['order_gkATxX2U3P', 'order_gkATxX2U3P']);
30
+ });
31
+
32
+ test('vroom maps single stop route tasks to jobs without pickup coordinates', function () {
33
+ $engine = new VroomOrchestrationEngine();
34
+ $method = new ReflectionMethod($engine, 'mapTaskToVroomJob');
35
+ $reverse = [];
36
+
37
+ $method->setAccessible(true);
38
+
39
+ $job = $method->invokeArgs($engine, [[
40
+ 'id' => 'order_waypointOnly',
41
+ 'stops' => [
42
+ ['role' => 'waypoint', 'location' => [103.86, 1.29]],
43
+ ],
44
+ 'service' => 300,
45
+ 'amount' => [1, 0, 0, 1],
46
+ 'description' => 'order_waypointOnly',
47
+ ], &$reverse]);
48
+
49
+ expect($job)->toMatchArray([
50
+ 'location' => [103.86, 1.29],
51
+ 'description' => 'order_waypointOnly',
52
+ 'service' => 300,
53
+ 'amount' => [1, 0, 0, 1],
54
+ ]);
55
+ expect($job)->not->toHaveKey('pickup');
56
+ expect(array_values($reverse))->toBe(['order_waypointOnly']);
57
+ });
58
+
59
+ test('vroom dedupes unassigned pickup and delivery task ids to one order id', function () {
60
+ $engine = new VroomOrchestrationEngine();
61
+ $method = new ReflectionMethod($engine, 'mapVroomResponse');
62
+
63
+ $method->setAccessible(true);
64
+
65
+ $result = $method->invoke($engine, [
66
+ 'routes' => [],
67
+ 'unassigned' => [
68
+ ['id' => 1001],
69
+ ['id' => 1002],
70
+ ],
71
+ 'summary' => ['routes' => 0],
72
+ ], [
73
+ 1001 => 'order_gkATxX2U3P',
74
+ 1002 => 'order_gkATxX2U3P',
75
+ ]);
76
+
77
+ expect($result['unassigned'])->toBe(['order_gkATxX2U3P']);
78
+ });
79
+
80
+ test('vroom maps shipment delivery steps back to fleetops assignments', function () {
81
+ $engine = new VroomOrchestrationEngine();
82
+ $method = new ReflectionMethod($engine, 'mapVroomResponse');
83
+
84
+ $method->setAccessible(true);
85
+
86
+ $result = $method->invoke($engine, [
87
+ 'routes' => [
88
+ [
89
+ 'description' => json_encode(['vehicle_id' => 'vehicle_w747Bat', 'driver_id' => null]),
90
+ 'steps' => [
91
+ ['type' => 'start'],
92
+ ['type' => 'pickup', 'id' => 1001],
93
+ ['type' => 'delivery', 'id' => 1002, 'arrival' => 1778918400, 'duration' => 900, 'distance' => 4200],
94
+ ['type' => 'end'],
95
+ ],
96
+ ],
97
+ ],
98
+ 'unassigned' => [],
99
+ 'summary' => ['routes' => 1],
100
+ ], [
101
+ 1001 => 'order_gkATxX2U3P',
102
+ 1002 => 'order_gkATxX2U3P',
103
+ ]);
104
+
105
+ expect($result['assignments'])->toHaveCount(1);
106
+ expect($result['assignments'][0])->toMatchArray([
107
+ 'order_id' => 'order_gkATxX2U3P',
108
+ 'vehicle_id' => 'vehicle_w747Bat',
109
+ 'driver_id' => null,
110
+ 'sequence' => 1,
111
+ 'arrival' => 1778918400,
112
+ 'duration' => 900,
113
+ 'distance' => 4200,
114
+ ]);
115
+ expect($result['summary'])->toBe(['routes' => 1]);
116
+ });
117
+
118
+ test('vroom capacity-only payload uses matrix indexes instead of coordinates', function () {
119
+ $engine = new VroomOrchestrationEngine();
120
+ $method = new ReflectionMethod($engine, 'buildCapacityOnlyPayload');
121
+ $reverse = [];
122
+
123
+ $method->setAccessible(true);
124
+
125
+ $payload = $method->invokeArgs($engine, [[
126
+ [
127
+ 'id' => 'order_capacity',
128
+ 'description' => 'order_capacity',
129
+ 'amount' => [100, 250, 1, 2],
130
+ 'skills' => [123],
131
+ ],
132
+ ], [
133
+ [
134
+ 'id' => 'vehicle_capacity',
135
+ 'driver_id' => null,
136
+ 'capacity' => [500, 1000, 4, 20],
137
+ 'skills' => [123],
138
+ 'max_tasks' => 3,
139
+ ],
140
+ ], [], &$reverse]);
141
+
142
+ expect($payload['vehicles'][0])->toMatchArray([
143
+ 'profile' => 'capacity_only',
144
+ 'start_index' => 0,
145
+ 'capacity' => [500, 1000, 4, 20],
146
+ 'skills' => [123],
147
+ 'max_tasks' => 3,
148
+ ]);
149
+ expect($payload['vehicles'][0])->not->toHaveKey('start');
150
+ expect($payload['jobs'][0])->toMatchArray([
151
+ 'location_index' => 1,
152
+ 'delivery' => [100, 250, 1, 2],
153
+ 'skills' => [123],
154
+ ]);
155
+ expect($payload['jobs'][0])->not->toHaveKey('location');
156
+ expect($payload['jobs'][0])->not->toHaveKey('pickup');
157
+ expect($payload['matrices']['capacity_only']['durations'])->toHaveCount(2);
158
+ expect(array_values($reverse))->toBe(['order_capacity']);
159
+ });
@@ -1989,6 +1989,9 @@ orchestrator:
1989
1989
  mode-allocate: Allocate Orders
1990
1990
  mode-optimize: Optimize Routes
1991
1991
  engine: Engine
1992
+ allocation-strategy: Allocation Strategy
1993
+ allocation-strategy-route-aware: Route + Capacity
1994
+ allocation-strategy-capacity-only: Capacity Only
1992
1995
  respect-skills: Respect Skills
1993
1996
  respect-capacity: Respect Capacity
1994
1997
  return-to-depot: Return to Depot
@@ -2029,6 +2032,7 @@ orchestrator:
2029
2032
  no-search-results: No results match your search.
2030
2033
  search-drivers: Search drivers...
2031
2034
  search-vehicles: Search vehicles...
2035
+ no-position: No position
2032
2036
  filter-online: Online
2033
2037
  filter-offline: Offline
2034
2038
  filter-on-shift: On Job