@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
@@ -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 = $attributes;
563
- $type = data_get($raw, 'type', 'dropoff');
564
- $customerUuidIn = data_get($raw, 'customer_uuid');
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 = data_get($raw, 'customer_type', 'fleetops:contact');
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 = null;
602
+ $customerUuid = null;
603
603
  $customerTypeNamespace = null;
604
604
 
605
605
  if ($customerType) {
606
606
  $customerTypeNamespace = Utils::getMutationType($customerType);
607
- $customerModel = app($customerTypeNamespace);
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 = Waypoint::create($values);
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 = $this->normalizeCalculatedMoney(Algo::exec(
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
+ }