@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.
- package/addon/components/admin/routing-settings.hbs +71 -0
- package/addon/components/admin/routing-settings.js +124 -0
- package/addon/components/customer/orders.js +1 -1
- package/addon/components/display-place.hbs +1 -1
- package/addon/components/map/order-list-overlay/order.hbs +18 -3
- package/addon/components/map/toolbar.hbs +2 -2
- package/addon/components/map/toolbar.js +10 -2
- package/addon/components/orchestrator/phase-builder.hbs +28 -5
- package/addon/components/orchestrator/phase-builder.js +21 -1
- package/addon/components/orchestrator/resource-panel.hbs +19 -1
- package/addon/components/orchestrator/resource-panel.js +4 -0
- package/addon/components/orchestrator-workbench.js +3 -1
- package/addon/components/order/details/purchase-rate.hbs +19 -22
- package/addon/components/order/details/route.hbs +26 -1
- package/addon/components/order/details/route.js +50 -0
- package/addon/components/order/details/tracking.hbs +171 -27
- package/addon/components/order/details/tracking.js +457 -0
- package/addon/components/order/form/details.hbs +1 -2
- package/addon/components/order/form/details.js +6 -0
- package/addon/components/order/form/route.js +9 -1
- package/addon/components/order/kanban-card.hbs +18 -3
- package/addon/components/order-list-overlay/order.hbs +18 -3
- package/addon/components/order-progress-card.hbs +21 -6
- package/addon/components/order-tracking-lookup.hbs +7 -7
- package/addon/components/order-tracking-lookup.js +1 -1
- package/addon/components/route-list.hbs +78 -8
- package/addon/components/route-list.js +48 -6
- package/addon/components/tracking-stop-progress.hbs +45 -0
- package/addon/components/tracking-stop-progress.js +79 -0
- package/addon/controllers/settings/payments/index.js +1 -1
- package/addon/controllers/settings/routing.js +98 -1
- package/addon/helpers/format-duration.js +38 -0
- package/addon/routes/application.js +6 -0
- package/addon/services/order-list-overlay.js +2 -2
- package/addon/styles/fleetops-engine.css +733 -0
- package/addon/templates/settings/routing.hbs +60 -2
- package/app/components/tracking-stop-progress.js +1 -0
- package/app/helpers/format-duration.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +3 -3
- package/pnpm-workspace.yaml +7 -0
- package/server/config/fleetops.php +32 -11
- package/server/migrations/2026_05_13_000001_repair_customer_contact_user_types.php +86 -0
- package/server/seeders/VroomOrchestrationSeeder.php +402 -0
- package/server/src/Http/Controllers/Api/v1/OrchestrationController.php +64 -0
- package/server/src/Http/Controllers/Api/v1/OrderController.php +26 -26
- package/server/src/Http/Controllers/Api/v1/PayloadController.php +10 -10
- package/server/src/Http/Controllers/Api/v1/ServiceQuoteController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/ContactController.php +41 -0
- package/server/src/Http/Controllers/Internal/v1/LiveController.php +6 -42
- package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +69 -15
- package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/SettingController.php +119 -12
- package/server/src/Http/Resources/v1/Index/Payload.php +2 -2
- package/server/src/Models/Contact.php +122 -13
- package/server/src/Models/Order.php +1 -1
- package/server/src/Models/Payload.php +8 -8
- package/server/src/Models/Place.php +0 -3
- package/server/src/Models/ServiceRate.php +1 -1
- package/server/src/Observers/ContactObserver.php +8 -0
- package/server/src/Orchestration/Engines/CapacityAllocationEngine.php +220 -0
- package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +330 -38
- package/server/src/Orchestration/Support/OrchestrationPayloadBuilder.php +259 -51
- package/server/src/Providers/FleetOpsServiceProvider.php +27 -0
- package/server/src/Support/LiveOrderQuery.php +75 -0
- package/server/src/Support/Metrics.php +15 -0
- package/server/src/Support/OSRM.php +11 -6
- package/server/src/Support/OrderTracker.php +7 -582
- package/server/src/Support/Utils.php +1 -1
- package/server/src/Tracking/Contracts/TrackingProviderInterface.php +19 -0
- package/server/src/Tracking/Providers/CalculatedTrackingProvider.php +65 -0
- package/server/src/Tracking/Providers/GoogleRoutesTrackingProvider.php +118 -0
- package/server/src/Tracking/Providers/OsrmTrackingProvider.php +71 -0
- package/server/src/Tracking/README.md +25 -0
- package/server/src/Tracking/Support/FakeTrackingProvider.php +42 -0
- package/server/src/Tracking/TrackingContext.php +63 -0
- package/server/src/Tracking/TrackingContextBuilder.php +204 -0
- package/server/src/Tracking/TrackingIntelligenceService.php +180 -0
- package/server/src/Tracking/TrackingOptions.php +49 -0
- package/server/src/Tracking/TrackingProviderCapabilities.php +30 -0
- package/server/src/Tracking/TrackingProviderManager.php +89 -0
- package/server/src/Tracking/TrackingProviderRegistry.php +35 -0
- package/server/src/Tracking/TrackingProviderResult.php +20 -0
- package/server/src/Tracking/TrackingStop.php +59 -0
- package/server/src/routes.php +11 -0
- package/server/tests/CapacityAllocationEngineTest.php +108 -0
- package/server/tests/LiveOrderQueryTest.php +34 -0
- package/server/tests/OrchestrationPayloadBuilderTest.php +132 -0
- package/server/tests/OrchestratorConsumableApiTest.php +74 -0
- package/server/tests/PingDriverEndpointTest.php +19 -0
- package/server/tests/TrackingIntelligenceTest.php +176 -0
- package/server/tests/VroomOrchestrationEngineTest.php +159 -0
- 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
|
+
});
|
package/translations/en-us.yaml
CHANGED
|
@@ -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
|