@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
|
@@ -555,15 +555,15 @@ class Payload extends Model
|
|
|
555
555
|
}
|
|
556
556
|
|
|
557
557
|
$this->loadMissing('waypointMarkers');
|
|
558
|
-
$keptWaypointIds
|
|
558
|
+
$keptWaypointIds = [];
|
|
559
559
|
$availableWaypointMarkers = $this->waypointMarkers()->get();
|
|
560
560
|
|
|
561
561
|
foreach ($waypoints as $index => $attributes) {
|
|
562
|
-
$raw
|
|
563
|
-
$type
|
|
564
|
-
$customerUuidIn
|
|
562
|
+
$raw = $attributes;
|
|
563
|
+
$type = data_get($raw, 'type', 'dropoff');
|
|
564
|
+
$customerUuidIn = data_get($raw, 'customer_uuid');
|
|
565
565
|
$customerPubIdIn = data_get($raw, 'customer_id');
|
|
566
|
-
$customerType
|
|
566
|
+
$customerType = data_get($raw, 'customer_type', 'fleetops:contact');
|
|
567
567
|
|
|
568
568
|
if (Utils::isset($attributes, 'place') && is_array(Utils::get($attributes, 'place'))) {
|
|
569
569
|
$attributes = Utils::get($attributes, 'place');
|
|
@@ -599,12 +599,12 @@ class Payload extends Model
|
|
|
599
599
|
$placeUuid = $place->uuid;
|
|
600
600
|
}
|
|
601
601
|
|
|
602
|
-
$customerUuid
|
|
602
|
+
$customerUuid = null;
|
|
603
603
|
$customerTypeNamespace = null;
|
|
604
604
|
|
|
605
605
|
if ($customerType) {
|
|
606
606
|
$customerTypeNamespace = Utils::getMutationType($customerType);
|
|
607
|
-
$customerModel
|
|
607
|
+
$customerModel = app($customerTypeNamespace);
|
|
608
608
|
|
|
609
609
|
if ($customerUuidIn && $customerModel->where('uuid', $customerUuidIn)->exists()) {
|
|
610
610
|
$customerUuid = $customerUuidIn;
|
|
@@ -642,7 +642,7 @@ class Payload extends Model
|
|
|
642
642
|
continue;
|
|
643
643
|
}
|
|
644
644
|
|
|
645
|
-
$waypointRecord
|
|
645
|
+
$waypointRecord = Waypoint::create($values);
|
|
646
646
|
$keptWaypointIds[] = $waypointRecord->uuid;
|
|
647
647
|
}
|
|
648
648
|
|
|
@@ -21,7 +21,6 @@ use Fleetbase\Traits\Searchable;
|
|
|
21
21
|
use Fleetbase\Traits\SendsWebhooks;
|
|
22
22
|
use Fleetbase\Traits\TracksApiCredential;
|
|
23
23
|
use Illuminate\Support\Carbon;
|
|
24
|
-
use Illuminate\Support\Facades\DB;
|
|
25
24
|
use Illuminate\Support\Str;
|
|
26
25
|
|
|
27
26
|
class Place extends Model
|
|
@@ -383,8 +382,6 @@ class Place extends Model
|
|
|
383
382
|
|
|
384
383
|
/**
|
|
385
384
|
* Create a new Place instance from a geocoding lookup.
|
|
386
|
-
*
|
|
387
|
-
* @return array
|
|
388
385
|
*/
|
|
389
386
|
public static function getValuesFromGeocodingLookup(string $address): array
|
|
390
387
|
{
|
|
@@ -740,7 +740,7 @@ class ServiceRate extends Model
|
|
|
740
740
|
|
|
741
741
|
if ($this->isAlgorithm()) {
|
|
742
742
|
$resolvedEndpointCount = $endpointCount ?? $this->inferEndpointCountFromStops($waypoints);
|
|
743
|
-
$rateFee
|
|
743
|
+
$rateFee = $this->normalizeCalculatedMoney(Algo::exec(
|
|
744
744
|
$this->algorithm,
|
|
745
745
|
$this->buildAlgorithmVariables(
|
|
746
746
|
$entities,
|
|
@@ -27,11 +27,19 @@ class ContactObserver
|
|
|
27
27
|
*/
|
|
28
28
|
public function saving(Contact $contact)
|
|
29
29
|
{
|
|
30
|
+
if ($contact->exists && $contact->getOriginal('type') === 'customer' && $contact->isDirty('type') && $contact->type !== 'customer') {
|
|
31
|
+
throw new \Exception('Customer contact type cannot be changed.');
|
|
32
|
+
}
|
|
33
|
+
|
|
30
34
|
// Get the contacts assosciated user
|
|
31
35
|
if ($contact->doesntHaveUser()) {
|
|
32
36
|
$contact->createUser();
|
|
33
37
|
}
|
|
34
38
|
|
|
39
|
+
if ($contact->isCustomer()) {
|
|
40
|
+
$contact->normalizeCustomerUser();
|
|
41
|
+
}
|
|
42
|
+
|
|
35
43
|
// Validate email is available to user
|
|
36
44
|
if (!empty($contact->email) && $contact->wasChanged('email') && $this->isEmailUnavailable($contact)) {
|
|
37
45
|
throw new \Exception('Email attempting to update for ' . $contact->type . ' is not available.');
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Orchestration\Engines;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Orchestration\Contracts\OrchestrationEngineInterface;
|
|
6
|
+
use Fleetbase\FleetOps\Orchestration\Support\OrchestrationPayloadBuilder;
|
|
7
|
+
use Illuminate\Support\Collection;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* CapacityAllocationEngine.
|
|
11
|
+
*
|
|
12
|
+
* Deterministic built-in engine for clients who need feasibility allocation
|
|
13
|
+
* by capacity, skills, and task limits without route geometry or vehicle GPS.
|
|
14
|
+
*/
|
|
15
|
+
class CapacityAllocationEngine implements OrchestrationEngineInterface
|
|
16
|
+
{
|
|
17
|
+
public function getName(): string
|
|
18
|
+
{
|
|
19
|
+
return 'Capacity (built-in)';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public function getIdentifier(): string
|
|
23
|
+
{
|
|
24
|
+
return 'capacity';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public function allocate(Collection $orders, Collection $vehicles, array $options = []): array
|
|
28
|
+
{
|
|
29
|
+
$respectCapacity = (bool) ($options['respect_capacity'] ?? true);
|
|
30
|
+
$respectSkills = (bool) ($options['respect_skills'] ?? true);
|
|
31
|
+
$balanceWorkload = (bool) ($options['balance_workload'] ?? false);
|
|
32
|
+
|
|
33
|
+
$tasks = OrchestrationPayloadBuilder::buildCapacityTasks($orders);
|
|
34
|
+
$vehiclePool = $this->buildVehiclePool(OrchestrationPayloadBuilder::buildCapacityVehicles($vehicles));
|
|
35
|
+
|
|
36
|
+
$assignments = [];
|
|
37
|
+
$unassigned = [];
|
|
38
|
+
$reasons = [];
|
|
39
|
+
|
|
40
|
+
foreach ($this->sortTasks($tasks) as $task) {
|
|
41
|
+
if (!empty($task['invalid'])) {
|
|
42
|
+
$this->markUnassigned($task['id'], $task['reason'] ?? 'invalid_task', $unassigned, $reasons);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
$bestIndex = $this->findVehicleIndex($vehiclePool, $task, $respectCapacity, $respectSkills, $balanceWorkload);
|
|
47
|
+
if ($bestIndex === null) {
|
|
48
|
+
$this->markUnassigned($task['id'], $this->resolveFailureReason($vehiclePool, $task, $respectCapacity, $respectSkills), $unassigned, $reasons);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
$sequence = ++$vehiclePool[$bestIndex]['assigned'];
|
|
53
|
+
$this->consumeCapacity($vehiclePool[$bestIndex]['remaining'], $task['amount'] ?? [0, 0, 0, 0]);
|
|
54
|
+
|
|
55
|
+
$assignments[] = [
|
|
56
|
+
'order_id' => $task['id'],
|
|
57
|
+
'vehicle_id' => $vehiclePool[$bestIndex]['id'],
|
|
58
|
+
'driver_id' => $vehiclePool[$bestIndex]['driver_id'],
|
|
59
|
+
'sequence' => $sequence,
|
|
60
|
+
'arrival' => null,
|
|
61
|
+
'duration' => null,
|
|
62
|
+
'distance' => null,
|
|
63
|
+
];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return [
|
|
67
|
+
'assignments' => $assignments,
|
|
68
|
+
'unassigned' => array_values(array_unique($unassigned)),
|
|
69
|
+
'summary' => [
|
|
70
|
+
'engine' => 'capacity',
|
|
71
|
+
'allocation_strategy' => 'capacity_only',
|
|
72
|
+
'assigned' => count($assignments),
|
|
73
|
+
'unassigned' => count(array_unique($unassigned)),
|
|
74
|
+
'unassigned_reasons' => $reasons,
|
|
75
|
+
'respect_capacity' => $respectCapacity,
|
|
76
|
+
'respect_skills' => $respectSkills,
|
|
77
|
+
'balance_workload' => $balanceWorkload,
|
|
78
|
+
],
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
protected function buildVehiclePool(array $vehicles): array
|
|
83
|
+
{
|
|
84
|
+
return array_map(fn (array $vehicle) => [
|
|
85
|
+
'id' => $vehicle['id'],
|
|
86
|
+
'driver_id' => $vehicle['driver_id'] ?? null,
|
|
87
|
+
'capacity' => $this->normalizeVector($vehicle['capacity'] ?? [0, 0, 0, 0]),
|
|
88
|
+
'remaining' => $this->normalizeVector($vehicle['capacity'] ?? [0, 0, 0, 0]),
|
|
89
|
+
'skills' => $vehicle['skills'] ?? [],
|
|
90
|
+
'max_tasks' => $vehicle['max_tasks'] ?? null,
|
|
91
|
+
'assigned' => 0,
|
|
92
|
+
], $vehicles);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
protected function sortTasks(array $tasks): array
|
|
96
|
+
{
|
|
97
|
+
usort($tasks, fn (array $a, array $b) => ($b['priority'] ?? 0) <=> ($a['priority'] ?? 0));
|
|
98
|
+
|
|
99
|
+
return $tasks;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
protected function findVehicleIndex(array $vehiclePool, array $task, bool $respectCapacity, bool $respectSkills, bool $balanceWorkload): ?int
|
|
103
|
+
{
|
|
104
|
+
$eligible = [];
|
|
105
|
+
|
|
106
|
+
foreach ($vehiclePool as $index => $vehicle) {
|
|
107
|
+
if (!$this->isVehicleFeasible($vehicle, $task, $respectCapacity, $respectSkills)) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
$eligible[] = $index;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (empty($eligible)) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!$balanceWorkload) {
|
|
119
|
+
return $eligible[0];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
usort($eligible, function (int $a, int $b) use ($vehiclePool) {
|
|
123
|
+
$assignedCompare = $vehiclePool[$a]['assigned'] <=> $vehiclePool[$b]['assigned'];
|
|
124
|
+
if ($assignedCompare !== 0) {
|
|
125
|
+
return $assignedCompare;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return array_sum($vehiclePool[$b]['remaining']) <=> array_sum($vehiclePool[$a]['remaining']);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return $eligible[0];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
protected function isVehicleFeasible(array $vehicle, array $task, bool $respectCapacity, bool $respectSkills): bool
|
|
135
|
+
{
|
|
136
|
+
if ($vehicle['max_tasks'] !== null && $vehicle['assigned'] >= $vehicle['max_tasks']) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if ($respectSkills && !$this->hasRequiredSkills($vehicle['skills'], $task['skills'] ?? [])) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if ($respectCapacity && !$this->hasCapacity($vehicle['remaining'], $task['amount'] ?? [0, 0, 0, 0])) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
protected function hasRequiredSkills(array $vehicleSkills, array $requiredSkills): bool
|
|
152
|
+
{
|
|
153
|
+
if (empty($requiredSkills)) {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return empty(array_diff($requiredSkills, $vehicleSkills));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
protected function hasCapacity(array $remaining, array $demand): bool
|
|
161
|
+
{
|
|
162
|
+
foreach ($this->normalizeVector($demand) as $index => $amount) {
|
|
163
|
+
if ($amount > ($remaining[$index] ?? 0)) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
protected function consumeCapacity(array &$remaining, array $demand): void
|
|
172
|
+
{
|
|
173
|
+
foreach ($this->normalizeVector($demand) as $index => $amount) {
|
|
174
|
+
$remaining[$index] = max(0, ($remaining[$index] ?? 0) - $amount);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
protected function resolveFailureReason(array $vehiclePool, array $task, bool $respectCapacity, bool $respectSkills): string
|
|
179
|
+
{
|
|
180
|
+
if (empty($vehiclePool)) {
|
|
181
|
+
return 'no_available_vehicle';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
$capacityEligible = false;
|
|
185
|
+
$skillsEligible = false;
|
|
186
|
+
$taskEligible = false;
|
|
187
|
+
|
|
188
|
+
foreach ($vehiclePool as $vehicle) {
|
|
189
|
+
$capacityEligible = $capacityEligible || !$respectCapacity || $this->hasCapacity($vehicle['remaining'], $task['amount'] ?? [0, 0, 0, 0]);
|
|
190
|
+
$skillsEligible = $skillsEligible || !$respectSkills || $this->hasRequiredSkills($vehicle['skills'], $task['skills'] ?? []);
|
|
191
|
+
$taskEligible = $taskEligible || $vehicle['max_tasks'] === null || $vehicle['assigned'] < $vehicle['max_tasks'];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!$capacityEligible) {
|
|
195
|
+
return 'capacity_exceeded';
|
|
196
|
+
}
|
|
197
|
+
if (!$skillsEligible) {
|
|
198
|
+
return 'missing_required_skills';
|
|
199
|
+
}
|
|
200
|
+
if (!$taskEligible) {
|
|
201
|
+
return 'max_tasks_exceeded';
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return 'no_available_vehicle';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
protected function normalizeVector(array $vector): array
|
|
208
|
+
{
|
|
209
|
+
return array_pad(array_map(fn ($value) => max(0, (int) round((float) $value)), array_values($vector)), 4, 0);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
protected function markUnassigned(string $orderId, string $reason, array &$unassigned, array &$reasons): void
|
|
213
|
+
{
|
|
214
|
+
$unassigned[] = $orderId;
|
|
215
|
+
$reasons[] = [
|
|
216
|
+
'order_id' => $orderId,
|
|
217
|
+
'reason' => $reason,
|
|
218
|
+
];
|
|
219
|
+
}
|
|
220
|
+
}
|