@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
|
@@ -50,7 +50,7 @@ class VroomOrchestrationEngine implements OrchestrationEngineInterface
|
|
|
50
50
|
/**
|
|
51
51
|
* Run the VROOM VRP solver.
|
|
52
52
|
*
|
|
53
|
-
* Constructs a VROOM-format payload from the normalized
|
|
53
|
+
* Constructs a VROOM-format payload from the normalized route tasks/vehicles
|
|
54
54
|
* produced by OrchestrationPayloadBuilder, calls the VROOM HTTP API, and
|
|
55
55
|
* maps the response back to the standard OrchestrationEngineInterface result
|
|
56
56
|
* shape.
|
|
@@ -59,31 +59,57 @@ class VroomOrchestrationEngine implements OrchestrationEngineInterface
|
|
|
59
59
|
*/
|
|
60
60
|
public function allocate(Collection $orders, Collection $vehicles, array $options = []): array
|
|
61
61
|
{
|
|
62
|
-
|
|
62
|
+
if (($options['allocation_strategy'] ?? null) === 'capacity_only') {
|
|
63
|
+
return $this->allocateCapacityOnly($orders, $vehicles, $options);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
$tasks = OrchestrationPayloadBuilder::buildRouteTasks($orders);
|
|
63
67
|
$vroomVehicles = OrchestrationPayloadBuilder::buildVehicles($vehicles);
|
|
64
68
|
|
|
65
|
-
|
|
66
|
-
|
|
69
|
+
[$routeTasks, $invalidTasks] = $this->partitionRouteTasks($tasks);
|
|
70
|
+
|
|
71
|
+
if (empty($routeTasks)) {
|
|
72
|
+
return $this->mergeInvalidTasks([
|
|
73
|
+
'assignments' => [],
|
|
74
|
+
'unassigned' => [],
|
|
75
|
+
'summary' => [],
|
|
76
|
+
], $invalidTasks);
|
|
67
77
|
}
|
|
68
78
|
|
|
79
|
+
$jobIdReverse = [];
|
|
80
|
+
$profile = $options['profile'] ?? env('VROOM_PROFILE', 'driving');
|
|
81
|
+
|
|
69
82
|
// Map normalized vehicle entries to VROOM vehicle objects.
|
|
70
83
|
// The 'driver_id' key is not part of the VROOM spec — we carry it in
|
|
71
84
|
// a description field and use it when mapping results back.
|
|
72
85
|
$vroomPayload = [
|
|
73
|
-
'jobs'
|
|
74
|
-
'
|
|
86
|
+
'jobs' => [],
|
|
87
|
+
'shipments' => [],
|
|
88
|
+
'vehicles' => array_map(function (array $v) use ($profile) {
|
|
75
89
|
$vehicle = [
|
|
76
90
|
'id' => crc32($v['id']), // VROOM requires integer IDs
|
|
77
91
|
'description' => json_encode(['vehicle_id' => $v['id'], 'driver_id' => $v['driver_id']]),
|
|
78
92
|
'start' => $v['start'],
|
|
79
93
|
'capacity' => $v['capacity'],
|
|
80
94
|
];
|
|
95
|
+
if ($profile) {
|
|
96
|
+
$vehicle['profile'] = $profile;
|
|
97
|
+
}
|
|
98
|
+
if (isset($v['end'])) {
|
|
99
|
+
$vehicle['end'] = $v['end'];
|
|
100
|
+
}
|
|
81
101
|
if (isset($v['time_window'])) {
|
|
82
102
|
$vehicle['time_window'] = $v['time_window'];
|
|
83
103
|
}
|
|
84
104
|
if (isset($v['skills'])) {
|
|
85
105
|
$vehicle['skills'] = $v['skills'];
|
|
86
106
|
}
|
|
107
|
+
if (isset($v['max_tasks'])) {
|
|
108
|
+
$vehicle['max_tasks'] = $v['max_tasks'];
|
|
109
|
+
}
|
|
110
|
+
if (isset($v['max_travel_time'])) {
|
|
111
|
+
$vehicle['max_travel_time'] = $v['max_travel_time'];
|
|
112
|
+
}
|
|
87
113
|
|
|
88
114
|
return $vehicle;
|
|
89
115
|
}, $vroomVehicles),
|
|
@@ -92,48 +118,182 @@ class VroomOrchestrationEngine implements OrchestrationEngineInterface
|
|
|
92
118
|
],
|
|
93
119
|
];
|
|
94
120
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
121
|
+
foreach ($routeTasks as $task) {
|
|
122
|
+
if (count($task['stops']) > 1) {
|
|
123
|
+
$shipment = $this->mapTaskToShipment($task, $jobIdReverse);
|
|
124
|
+
if ($shipment) {
|
|
125
|
+
$vroomPayload['shipments'][] = $shipment;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
$job = $this->mapTaskToVroomJob($task, $jobIdReverse);
|
|
132
|
+
if ($job) {
|
|
133
|
+
$vroomPayload['jobs'][] = $job;
|
|
134
|
+
}
|
|
103
135
|
}
|
|
104
|
-
unset($job);
|
|
105
136
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
137
|
+
if (empty($vroomPayload['jobs'])) {
|
|
138
|
+
unset($vroomPayload['jobs']);
|
|
139
|
+
}
|
|
140
|
+
if (empty($vroomPayload['shipments'])) {
|
|
141
|
+
unset($vroomPayload['shipments']);
|
|
142
|
+
}
|
|
110
143
|
|
|
111
|
-
$
|
|
144
|
+
$result = $this->callVroom($vroomPayload);
|
|
112
145
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
146
|
+
return $this->mergeInvalidTasks(
|
|
147
|
+
$this->mapVroomResponse($result, $jobIdReverse),
|
|
148
|
+
$invalidTasks
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
protected function allocateCapacityOnly(Collection $orders, Collection $vehicles, array $options = []): array
|
|
153
|
+
{
|
|
154
|
+
$tasks = OrchestrationPayloadBuilder::buildCapacityTasks($orders);
|
|
155
|
+
$vroomVehicles = OrchestrationPayloadBuilder::buildCapacityVehicles($vehicles);
|
|
156
|
+
|
|
157
|
+
[$capacityTasks, $invalidTasks] = $this->partitionRouteTasks($tasks);
|
|
158
|
+
|
|
159
|
+
if (empty($capacityTasks)) {
|
|
160
|
+
return $this->mergeInvalidTasks([
|
|
161
|
+
'assignments' => [],
|
|
162
|
+
'unassigned' => [],
|
|
163
|
+
'summary' => [
|
|
164
|
+
'engine' => 'vroom',
|
|
165
|
+
'allocation_strategy' => 'capacity_only',
|
|
166
|
+
],
|
|
167
|
+
], $invalidTasks);
|
|
117
168
|
}
|
|
118
|
-
|
|
119
|
-
|
|
169
|
+
|
|
170
|
+
$jobIdReverse = [];
|
|
171
|
+
$vroomPayload = $this->buildCapacityOnlyPayload($capacityTasks, $vroomVehicles, $options, $jobIdReverse);
|
|
172
|
+
|
|
173
|
+
if (empty($vroomPayload['vehicles'])) {
|
|
174
|
+
return $this->mergeInvalidTasks([
|
|
175
|
+
'assignments' => [],
|
|
176
|
+
'unassigned' => array_column($capacityTasks, 'id'),
|
|
177
|
+
'summary' => [
|
|
178
|
+
'engine' => 'vroom',
|
|
179
|
+
'allocation_strategy' => 'capacity_only',
|
|
180
|
+
'assigned' => 0,
|
|
181
|
+
'unassigned' => count($capacityTasks),
|
|
182
|
+
'unassigned_reasons' => array_map(fn (array $task) => [
|
|
183
|
+
'order_id' => $task['id'],
|
|
184
|
+
'reason' => 'no_available_vehicle',
|
|
185
|
+
], $capacityTasks),
|
|
186
|
+
],
|
|
187
|
+
], $invalidTasks);
|
|
120
188
|
}
|
|
121
189
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
190
|
+
$result = $this->mapVroomResponse($this->callVroom($vroomPayload), $jobIdReverse);
|
|
191
|
+
|
|
192
|
+
$result['summary'] = array_merge($result['summary'] ?? [], [
|
|
193
|
+
'engine' => 'vroom',
|
|
194
|
+
'allocation_strategy' => 'capacity_only',
|
|
195
|
+
]);
|
|
196
|
+
|
|
197
|
+
return $this->mergeInvalidTasks($result, $invalidTasks);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
protected function buildCapacityOnlyPayload(array $capacityTasks, array $vroomVehicles, array $options, array &$jobIdReverse): array
|
|
201
|
+
{
|
|
202
|
+
$matrixSize = count($capacityTasks) + 1;
|
|
203
|
+
$profile = $options['profile'] ?? 'capacity_only';
|
|
204
|
+
$respectCapacity = (bool) ($options['respect_capacity'] ?? true);
|
|
205
|
+
$respectSkills = (bool) ($options['respect_skills'] ?? true);
|
|
206
|
+
|
|
207
|
+
$vroomPayload = [
|
|
208
|
+
'jobs' => [],
|
|
209
|
+
'vehicles' => array_map(function (array $v) use ($profile, $respectCapacity, $respectSkills) {
|
|
210
|
+
$vehicle = [
|
|
211
|
+
'id' => crc32($v['id']),
|
|
212
|
+
'description' => json_encode(['vehicle_id' => $v['id'], 'driver_id' => $v['driver_id'] ?? null]),
|
|
213
|
+
'profile' => $profile,
|
|
214
|
+
'start_index' => 0,
|
|
215
|
+
];
|
|
216
|
+
|
|
217
|
+
if ($respectCapacity) {
|
|
218
|
+
$vehicle['capacity'] = $v['capacity'];
|
|
219
|
+
}
|
|
220
|
+
if ($respectSkills && isset($v['skills'])) {
|
|
221
|
+
$vehicle['skills'] = $v['skills'];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
foreach (['time_window', 'max_tasks', 'max_travel_time'] as $key) {
|
|
225
|
+
if (isset($v[$key])) {
|
|
226
|
+
$vehicle[$key] = $v[$key];
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return $vehicle;
|
|
231
|
+
}, $vroomVehicles),
|
|
232
|
+
'matrices' => [
|
|
233
|
+
$profile => [
|
|
234
|
+
'durations' => $this->buildUniformMatrix($matrixSize),
|
|
235
|
+
'distances' => $this->buildUniformMatrix($matrixSize),
|
|
236
|
+
],
|
|
237
|
+
],
|
|
238
|
+
'options' => [
|
|
239
|
+
'g' => false,
|
|
240
|
+
],
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
foreach ($capacityTasks as $index => $task) {
|
|
244
|
+
$orderId = $task['id'];
|
|
245
|
+
$intId = crc32($orderId);
|
|
246
|
+
$jobIdReverse[$intId] = $orderId;
|
|
247
|
+
|
|
248
|
+
$job = [
|
|
249
|
+
'id' => $intId,
|
|
250
|
+
'location_index' => $index + 1,
|
|
251
|
+
'description' => $task['description'] ?? $orderId,
|
|
252
|
+
];
|
|
253
|
+
|
|
254
|
+
if ($respectCapacity) {
|
|
255
|
+
$job['delivery'] = $task['amount'] ?? [0, 0, 0, 0];
|
|
256
|
+
}
|
|
257
|
+
if ($respectSkills && isset($task['skills'])) {
|
|
258
|
+
$job['skills'] = $task['skills'];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
$this->copyTaskFields($task, $job, ['priority']);
|
|
262
|
+
$vroomPayload['jobs'][] = $job;
|
|
128
263
|
}
|
|
129
264
|
|
|
130
|
-
|
|
131
|
-
|
|
265
|
+
return $vroomPayload;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
protected function buildUniformMatrix(int $size): array
|
|
269
|
+
{
|
|
270
|
+
$matrix = [];
|
|
271
|
+
|
|
272
|
+
for ($row = 0; $row < $size; $row++) {
|
|
273
|
+
$matrix[$row] = [];
|
|
274
|
+
for ($column = 0; $column < $size; $column++) {
|
|
275
|
+
$matrix[$row][$column] = $row === $column ? 0 : 1;
|
|
276
|
+
}
|
|
132
277
|
}
|
|
133
278
|
|
|
134
|
-
|
|
279
|
+
return $matrix;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
protected function partitionRouteTasks(array $tasks): array
|
|
283
|
+
{
|
|
284
|
+
$routeTasks = [];
|
|
285
|
+
$invalidTasks = [];
|
|
286
|
+
|
|
287
|
+
foreach ($tasks as $task) {
|
|
288
|
+
if (!empty($task['invalid'])) {
|
|
289
|
+
$invalidTasks[] = $task;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
$routeTasks[] = $task;
|
|
294
|
+
}
|
|
135
295
|
|
|
136
|
-
return $
|
|
296
|
+
return [$routeTasks, $invalidTasks];
|
|
137
297
|
}
|
|
138
298
|
|
|
139
299
|
/**
|
|
@@ -152,7 +312,7 @@ class VroomOrchestrationEngine implements OrchestrationEngineInterface
|
|
|
152
312
|
$sequence = 0;
|
|
153
313
|
|
|
154
314
|
foreach ($route['steps'] ?? [] as $step) {
|
|
155
|
-
if ($step['type']
|
|
315
|
+
if (!in_array($step['type'], ['job', 'delivery'], true)) {
|
|
156
316
|
continue;
|
|
157
317
|
}
|
|
158
318
|
$orderId = $jobIdReverse[$step['id']] ?? null;
|
|
@@ -178,11 +338,143 @@ class VroomOrchestrationEngine implements OrchestrationEngineInterface
|
|
|
178
338
|
|
|
179
339
|
return [
|
|
180
340
|
'assignments' => $assignments,
|
|
181
|
-
'unassigned' => array_values($unassigned),
|
|
341
|
+
'unassigned' => array_values(array_unique($unassigned)),
|
|
182
342
|
'summary' => $vroomResult['summary'] ?? [],
|
|
183
343
|
];
|
|
184
344
|
}
|
|
185
345
|
|
|
346
|
+
protected function mapTaskToVroomJob(array $task, array &$jobIdReverse): ?array
|
|
347
|
+
{
|
|
348
|
+
$stop = $task['stops'][0] ?? null;
|
|
349
|
+
if (!$stop || empty($stop['location'])) {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
$orderId = $task['id'];
|
|
354
|
+
$intId = crc32($orderId);
|
|
355
|
+
$jobIdReverse[$intId] = $orderId;
|
|
356
|
+
|
|
357
|
+
$vroomJob = [
|
|
358
|
+
'id' => $intId,
|
|
359
|
+
'location' => $stop['location'],
|
|
360
|
+
'description' => $task['description'] ?? $orderId,
|
|
361
|
+
];
|
|
362
|
+
|
|
363
|
+
$this->copyTaskFields($task, $vroomJob, ['service', 'amount', 'time_windows', 'skills', 'priority']);
|
|
364
|
+
|
|
365
|
+
return $vroomJob;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
protected function mapTaskToShipment(array $task, array &$jobIdReverse): ?array
|
|
369
|
+
{
|
|
370
|
+
$stops = $task['stops'] ?? [];
|
|
371
|
+
$first = $stops[0] ?? null;
|
|
372
|
+
$last = $stops[count($stops) - 1] ?? null;
|
|
373
|
+
|
|
374
|
+
if (!$first || !$last || empty($first['location']) || empty($last['location'])) {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
$orderId = $task['id'];
|
|
379
|
+
$pickupId = crc32($orderId . ':pickup');
|
|
380
|
+
$deliveryId = crc32($orderId . ':delivery');
|
|
381
|
+
|
|
382
|
+
// We only create one FleetOps assignment per order, so route pickup
|
|
383
|
+
// steps are ignored while delivery and unassigned entries still map
|
|
384
|
+
// back to the order public_id.
|
|
385
|
+
$jobIdReverse[$pickupId] = $orderId;
|
|
386
|
+
$jobIdReverse[$deliveryId] = $orderId;
|
|
387
|
+
|
|
388
|
+
$shipment = [
|
|
389
|
+
'pickup' => [
|
|
390
|
+
'id' => $pickupId,
|
|
391
|
+
'location' => $first['location'],
|
|
392
|
+
'description' => ($task['description'] ?? $orderId) . ' pickup',
|
|
393
|
+
],
|
|
394
|
+
'delivery' => [
|
|
395
|
+
'id' => $deliveryId,
|
|
396
|
+
'location' => $last['location'],
|
|
397
|
+
'description' => ($task['description'] ?? $orderId) . ' delivery',
|
|
398
|
+
],
|
|
399
|
+
];
|
|
400
|
+
|
|
401
|
+
if (array_key_exists('service', $task)) {
|
|
402
|
+
$shipment['delivery']['service'] = $task['service'];
|
|
403
|
+
}
|
|
404
|
+
if (array_key_exists('time_windows', $task)) {
|
|
405
|
+
$shipment['delivery']['time_windows'] = $task['time_windows'];
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
$this->copyTaskFields($task, $shipment, ['amount', 'skills', 'priority']);
|
|
409
|
+
|
|
410
|
+
return $shipment;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
protected function copyTaskFields(array $task, array &$target, array $keys): void
|
|
414
|
+
{
|
|
415
|
+
foreach ($keys as $key) {
|
|
416
|
+
if (array_key_exists($key, $task)) {
|
|
417
|
+
$target[$key] = $task[$key];
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
protected function mergeInvalidTasks(array $result, array $invalidTasks): array
|
|
423
|
+
{
|
|
424
|
+
if (empty($invalidTasks)) {
|
|
425
|
+
return $result;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
$invalid = array_map(fn (array $task) => [
|
|
429
|
+
'order_id' => $task['id'],
|
|
430
|
+
'reason' => $task['reason'] ?? 'Order has invalid route coordinates.',
|
|
431
|
+
], $invalidTasks);
|
|
432
|
+
|
|
433
|
+
$result['unassigned'] = array_values(array_unique(array_merge(
|
|
434
|
+
$result['unassigned'] ?? [],
|
|
435
|
+
array_column($invalid, 'order_id')
|
|
436
|
+
)));
|
|
437
|
+
|
|
438
|
+
$summary = $result['summary'] ?? [];
|
|
439
|
+
$summary['invalid'] = array_values(array_merge($summary['invalid'] ?? [], $invalid));
|
|
440
|
+
$result['summary'] = $summary;
|
|
441
|
+
|
|
442
|
+
return $result;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
protected function callVroom(array $vroomPayload): array
|
|
446
|
+
{
|
|
447
|
+
// ── Resolve connection config ─────────────────────────────────────────
|
|
448
|
+
$baseUri = $this->resolveVroomBaseUri();
|
|
449
|
+
$endpointMode = $this->resolveVroomEndpointMode();
|
|
450
|
+
$timeout = (int) env('VROOM_TIMEOUT', 30);
|
|
451
|
+
|
|
452
|
+
$apiKey = $this->resolveVroomApiKey();
|
|
453
|
+
|
|
454
|
+
// Build the solve URL — append api_key as query param when present.
|
|
455
|
+
$solveUrl = rtrim($baseUri, '/');
|
|
456
|
+
if ($endpointMode !== 'binary') {
|
|
457
|
+
$solveUrl .= '/solve';
|
|
458
|
+
}
|
|
459
|
+
if ($apiKey) {
|
|
460
|
+
$solveUrl .= '?api_key=' . urlencode($apiKey);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ── Call VROOM ────────────────────────────────────────────────────────
|
|
464
|
+
try {
|
|
465
|
+
$response = Http::timeout($timeout)->post($solveUrl, $vroomPayload);
|
|
466
|
+
} catch (\Exception $e) {
|
|
467
|
+
Log::error('[VroomOrchestrationEngine] HTTP request failed: ' . $e->getMessage());
|
|
468
|
+
throw new \RuntimeException('VROOM allocation engine is unavailable: ' . $e->getMessage() . ' — ensure VROOM_HOST is reachable or switch to the built-in greedy engine.', 0, $e);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (!$response->successful()) {
|
|
472
|
+
throw new \RuntimeException('VROOM returned an error: HTTP ' . $response->status() . ' — ' . $response->body());
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return $response->json();
|
|
476
|
+
}
|
|
477
|
+
|
|
186
478
|
protected function resolveVroomBaseUri(): string
|
|
187
479
|
{
|
|
188
480
|
return $this->resolveVroomSetting('api_host', config('vroom.base_uri', env('VROOM_HOST', 'https://api.verso-optim.com/vrp/v1')));
|