@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,402 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Seeders;
4
+
5
+ use Fleetbase\FleetOps\Models\Entity;
6
+ use Fleetbase\FleetOps\Models\Order;
7
+ use Fleetbase\FleetOps\Models\Payload;
8
+ use Fleetbase\FleetOps\Models\Place;
9
+ use Fleetbase\FleetOps\Models\Vehicle;
10
+ use Fleetbase\FleetOps\Models\Waypoint;
11
+ use Fleetbase\LaravelMysqlSpatial\Types\Point;
12
+ use Fleetbase\Models\Company;
13
+ use Fleetbase\Seeders\Concerns\ResolvesSeedCompany;
14
+ use Illuminate\Database\Seeder;
15
+ use Illuminate\Support\Facades\DB;
16
+ use Illuminate\Support\Str;
17
+
18
+ class VroomOrchestrationSeeder extends Seeder
19
+ {
20
+ use ResolvesSeedCompany;
21
+
22
+ protected const SEED_NAME = 'vroom-orchestration';
23
+
24
+ public function run(): void
25
+ {
26
+ $company = $this->resolveCompany();
27
+ if (!$company) {
28
+ $this->command?->error('No company found. Create a Fleetbase company before running the VROOM orchestration seeder.');
29
+
30
+ return;
31
+ }
32
+
33
+ session(['company' => $company->uuid]);
34
+
35
+ DB::transaction(function () use ($company) {
36
+ $this->purgePreviousSeedData();
37
+
38
+ $places = $this->seedPlaces($company);
39
+ $vehicles = $this->seedVehicles($company);
40
+ $orders = $this->seedOrders($company, $places);
41
+
42
+ $this->command?->info('Seeded VROOM orchestration test data for company: ' . $company->public_id);
43
+ $this->command?->line('Order IDs: ' . implode(', ', array_map(fn (Order $order) => $order->public_id, $orders)));
44
+ $this->command?->line('Vehicle IDs: ' . implode(', ', array_map(fn (Vehicle $vehicle) => $vehicle->public_id, $vehicles)));
45
+ });
46
+ }
47
+
48
+ protected function resolveCompany(): ?Company
49
+ {
50
+ return $this->resolveSeedCompany(
51
+ 'FLEETOPS_VROOM_SEED_COMPANY_UUID',
52
+ 'FLEETOPS_VROOM_SEED_COMPANY_PUBLIC_ID'
53
+ );
54
+ }
55
+
56
+ protected function purgePreviousSeedData(): void
57
+ {
58
+ $payloadUuids = $this->seeded(Payload::query())->pluck('uuid');
59
+
60
+ $this->seeded(Order::query())->forceDelete();
61
+ Entity::whereIn('payload_uuid', $payloadUuids)->forceDelete();
62
+ Waypoint::whereIn('payload_uuid', $payloadUuids)->forceDelete();
63
+ $this->seeded(Payload::query())->forceDelete();
64
+ $this->seeded(Vehicle::query())->forceDelete();
65
+ $this->seeded(Place::query())->forceDelete();
66
+ }
67
+
68
+ protected function seeded($query)
69
+ {
70
+ return $query->where('meta->seed', static::SEED_NAME);
71
+ }
72
+
73
+ protected function meta(string $seedId, array $extra = []): array
74
+ {
75
+ return array_merge([
76
+ 'seed' => static::SEED_NAME,
77
+ 'seed_id' => $seedId,
78
+ ], $extra);
79
+ }
80
+
81
+ protected function seedPlaces(Company $company): array
82
+ {
83
+ $places = [
84
+ 'depot' => [
85
+ 'seed_id' => 'depot_tanjong_pagar',
86
+ 'name' => 'VROOM Test Depot - Tanjong Pagar',
87
+ 'street1' => '1 Raffles Quay',
88
+ 'city' => 'Singapore',
89
+ 'country' => 'SG',
90
+ 'lat' => 1.2816,
91
+ 'lng' => 103.8510,
92
+ ],
93
+ 'orchard' => [
94
+ 'seed_id' => 'dropoff_orchard',
95
+ 'name' => 'VROOM Test Dropoff - Orchard',
96
+ 'street1' => '2 Orchard Turn',
97
+ 'city' => 'Singapore',
98
+ 'country' => 'SG',
99
+ 'lat' => 1.3048,
100
+ 'lng' => 103.8318,
101
+ ],
102
+ 'changi' => [
103
+ 'seed_id' => 'dropoff_changi',
104
+ 'name' => 'VROOM Test Dropoff - Changi',
105
+ 'street1' => '80 Airport Boulevard',
106
+ 'city' => 'Singapore',
107
+ 'country' => 'SG',
108
+ 'lat' => 1.3644,
109
+ 'lng' => 103.9915,
110
+ ],
111
+ 'woodlands' => [
112
+ 'seed_id' => 'dropoff_woodlands',
113
+ 'name' => 'VROOM Test Dropoff - Woodlands',
114
+ 'street1' => '1 Woodlands Square',
115
+ 'city' => 'Singapore',
116
+ 'country' => 'SG',
117
+ 'lat' => 1.4360,
118
+ 'lng' => 103.7860,
119
+ ],
120
+ 'rochor' => [
121
+ 'seed_id' => 'waypoint_rochor',
122
+ 'name' => 'VROOM Test Waypoint - Rochor',
123
+ 'street1' => '1 Rochor Canal Road',
124
+ 'city' => 'Singapore',
125
+ 'country' => 'SG',
126
+ 'lat' => 1.3039,
127
+ 'lng' => 103.8520,
128
+ ],
129
+ 'novena' => [
130
+ 'seed_id' => 'waypoint_novena',
131
+ 'name' => 'VROOM Test Waypoint - Novena',
132
+ 'street1' => '10 Sinaran Drive',
133
+ 'city' => 'Singapore',
134
+ 'country' => 'SG',
135
+ 'lat' => 1.3206,
136
+ 'lng' => 103.8439,
137
+ ],
138
+ 'serangoon' => [
139
+ 'seed_id' => 'waypoint_serangoon',
140
+ 'name' => 'VROOM Test Waypoint - Serangoon',
141
+ 'street1' => '23 Serangoon Central',
142
+ 'city' => 'Singapore',
143
+ 'country' => 'SG',
144
+ 'lat' => 1.3508,
145
+ 'lng' => 103.8723,
146
+ ],
147
+ 'paya_lebar' => [
148
+ 'seed_id' => 'waypoint_paya_lebar',
149
+ 'name' => 'VROOM Test Waypoint - Paya Lebar',
150
+ 'street1' => '10 Paya Lebar Road',
151
+ 'city' => 'Singapore',
152
+ 'country' => 'SG',
153
+ 'lat' => 1.3174,
154
+ 'lng' => 103.8927,
155
+ ],
156
+ 'tampines' => [
157
+ 'seed_id' => 'waypoint_tampines',
158
+ 'name' => 'VROOM Test Waypoint - Tampines',
159
+ 'street1' => '4 Tampines Central 5',
160
+ 'city' => 'Singapore',
161
+ 'country' => 'SG',
162
+ 'lat' => 1.3525,
163
+ 'lng' => 103.9447,
164
+ ],
165
+ ];
166
+
167
+ return collect($places)->mapWithKeys(function (array $place, string $key) use ($company) {
168
+ $model = new Place();
169
+ $model->forceFill([
170
+ 'uuid' => (string) Str::uuid(),
171
+ 'company_uuid' => $company->uuid,
172
+ 'name' => $place['name'],
173
+ 'street1' => $place['street1'],
174
+ 'city' => $place['city'],
175
+ 'country' => $place['country'],
176
+ 'location' => new Point($place['lat'], $place['lng']),
177
+ 'meta' => $this->meta($place['seed_id']),
178
+ ])->save();
179
+
180
+ return [$key => $model];
181
+ })->all();
182
+ }
183
+
184
+ protected function seedVehicles(Company $company): array
185
+ {
186
+ $vehicles = [
187
+ [
188
+ 'seed_id' => 'central_1',
189
+ 'name' => 'VROOM Test Van - Central',
190
+ 'make' => 'Nissan',
191
+ 'model' => 'NV200',
192
+ 'lat' => 1.2798,
193
+ 'lng' => 103.8500,
194
+ 'capacity' => [120, 4.5, 1, 20],
195
+ ],
196
+ [
197
+ 'seed_id' => 'east_1',
198
+ 'name' => 'VROOM Test Van - East',
199
+ 'make' => 'Toyota',
200
+ 'model' => 'HiAce',
201
+ 'lat' => 1.3349,
202
+ 'lng' => 103.9617,
203
+ 'capacity' => [250, 8, 2, 40],
204
+ ],
205
+ [
206
+ 'seed_id' => 'north_1',
207
+ 'name' => 'VROOM Test Van - North',
208
+ 'make' => 'Mercedes-Benz',
209
+ 'model' => 'Sprinter',
210
+ 'lat' => 1.4326,
211
+ 'lng' => 103.7856,
212
+ 'capacity' => [500, 16, 4, 80],
213
+ ],
214
+ [
215
+ 'seed_id' => 'west_1',
216
+ 'name' => 'VROOM Test Van - West',
217
+ 'make' => 'Ford',
218
+ 'model' => 'Transit',
219
+ 'lat' => 1.3345,
220
+ 'lng' => 103.7420,
221
+ 'capacity' => [90, 3, 0, 12],
222
+ ],
223
+ [
224
+ 'seed_id' => 'capacity_only_heavy_1',
225
+ 'name' => 'VROOM Test Capacity Truck - No Position',
226
+ 'make' => 'Isuzu',
227
+ 'model' => 'NPR',
228
+ 'capacity' => [900, 24, 8, 160],
229
+ 'skills' => ['tail_lift'],
230
+ 'max_tasks' => 10,
231
+ ],
232
+ [
233
+ 'seed_id' => 'capacity_only_cold_1',
234
+ 'name' => 'VROOM Test Cold Chain Van - No Position',
235
+ 'make' => 'Hyundai',
236
+ 'model' => 'H350',
237
+ 'capacity' => [350, 10, 3, 60],
238
+ 'skills' => ['cold_chain'],
239
+ 'max_tasks' => 8,
240
+ ],
241
+ ];
242
+
243
+ return collect($vehicles)->mapWithKeys(function (array $vehicle) use ($company) {
244
+ $model = new Vehicle();
245
+ $model->forceFill([
246
+ 'uuid' => (string) Str::uuid(),
247
+ 'company_uuid' => $company->uuid,
248
+ 'name' => $vehicle['name'],
249
+ 'make' => $vehicle['make'],
250
+ 'model' => $vehicle['model'],
251
+ 'year' => '2024',
252
+ 'type' => 'van',
253
+ 'status' => 'active',
254
+ 'online' => true,
255
+ 'location' => isset($vehicle['lat'], $vehicle['lng']) ? new Point($vehicle['lat'], $vehicle['lng']) : null,
256
+ 'skills' => $vehicle['skills'] ?? [],
257
+ 'payload_capacity' => $vehicle['capacity'][0],
258
+ 'payload_capacity_volume' => $vehicle['capacity'][1],
259
+ 'payload_capacity_pallets' => $vehicle['capacity'][2],
260
+ 'payload_capacity_parcels' => $vehicle['capacity'][3],
261
+ 'max_tasks' => $vehicle['max_tasks'] ?? 6,
262
+ 'return_to_depot' => false,
263
+ 'meta' => $this->meta($vehicle['seed_id'], [
264
+ 'location_state' => isset($vehicle['lat'], $vehicle['lng']) ? 'set' : 'not_set',
265
+ 'capacity' => [
266
+ 'weight_kg' => $vehicle['capacity'][0],
267
+ 'volume_m3' => $vehicle['capacity'][1],
268
+ 'pallets' => $vehicle['capacity'][2],
269
+ 'parcels' => $vehicle['capacity'][3],
270
+ ],
271
+ ]),
272
+ ])->save();
273
+
274
+ return [$vehicle['seed_id'] => $model];
275
+ })->all();
276
+ }
277
+
278
+ protected function seedOrders(Company $company, array $places): array
279
+ {
280
+ $orders = [
281
+ 'endpoint_only' => [
282
+ 'payload_seed_id' => 'endpoint_only',
283
+ 'name' => 'VROOM Test Order - Pickup Dropoff',
284
+ 'pickup' => $places['depot'],
285
+ 'dropoff' => $places['orchard'],
286
+ 'waypoints' => [],
287
+ 'entities' => [
288
+ ['name' => 'Small carton', 'type' => 'parcel', 'weight' => 12, 'weight_unit' => 'kg', 'length' => 60, 'width' => 40, 'height' => 35, 'dimensions_unit' => 'cm'],
289
+ ['name' => 'Fragile tote', 'type' => 'parcel', 'weight' => 8, 'weight_unit' => 'kg', 'length' => 50, 'width' => 35, 'height' => 30, 'dimensions_unit' => 'cm'],
290
+ ],
291
+ ],
292
+ 'waypoint_only' => [
293
+ 'payload_seed_id' => 'waypoint_only',
294
+ 'name' => 'VROOM Test Order - Waypoints Only',
295
+ 'pickup' => null,
296
+ 'dropoff' => null,
297
+ 'waypoints' => [$places['rochor'], $places['novena'], $places['serangoon']],
298
+ 'entities' => [
299
+ ['name' => 'Multi-stop parcel 1', 'type' => 'parcel', 'weight' => 25, 'weight_unit' => 'kg', 'length' => 80, 'width' => 50, 'height' => 40, 'dimensions_unit' => 'cm'],
300
+ ['name' => 'Multi-stop parcel 2', 'type' => 'parcel', 'weight' => 18, 'weight_unit' => 'kg', 'length' => 70, 'width' => 45, 'height' => 35, 'dimensions_unit' => 'cm'],
301
+ ['name' => 'Multi-stop parcel 3', 'type' => 'parcel', 'weight' => 15, 'weight_unit' => 'kg', 'length' => 65, 'width' => 40, 'height' => 35, 'dimensions_unit' => 'cm'],
302
+ ],
303
+ ],
304
+ 'mixed' => [
305
+ 'payload_seed_id' => 'mixed',
306
+ 'name' => 'VROOM Test Order - Mixed Stops',
307
+ 'pickup' => $places['depot'],
308
+ 'dropoff' => $places['changi'],
309
+ 'waypoints' => [$places['paya_lebar'], $places['tampines']],
310
+ 'entities' => [
311
+ ['name' => 'Heavy equipment crate', 'type' => 'freight', 'weight' => 180, 'weight_unit' => 'kg', 'length' => 120, 'width' => 80, 'height' => 90, 'dimensions_unit' => 'cm'],
312
+ ['name' => 'Accessory box', 'type' => 'parcel', 'weight' => 22, 'weight_unit' => 'kg', 'length' => 75, 'width' => 55, 'height' => 45, 'dimensions_unit' => 'cm'],
313
+ ],
314
+ ],
315
+ 'single_waypoint' => [
316
+ 'payload_seed_id' => 'single_waypoint',
317
+ 'name' => 'VROOM Test Order - Single Waypoint',
318
+ 'pickup' => null,
319
+ 'dropoff' => null,
320
+ 'waypoints' => [$places['woodlands']],
321
+ 'entities' => [
322
+ ['name' => 'Oversized sample case', 'type' => 'parcel', 'weight' => 60, 'weight_unit' => 'kg', 'length' => 100, 'width' => 60, 'height' => 60, 'dimensions_unit' => 'cm'],
323
+ ],
324
+ ],
325
+ ];
326
+
327
+ return collect($orders)->mapWithKeys(function (array $definition, string $seedId) use ($company) {
328
+ $payload = $this->createPayload($company, $definition);
329
+ $order = new Order();
330
+ $order->forceFill([
331
+ 'uuid' => (string) Str::uuid(),
332
+ 'internal_id' => 'VROOM-SEED-' . Str::of($seedId)->upper(),
333
+ 'company_uuid' => $company->uuid,
334
+ 'payload_uuid' => $payload->uuid,
335
+ 'status' => 'created',
336
+ 'type' => 'transport',
337
+ 'notes' => $definition['name'],
338
+ 'orchestrator_priority' => 50,
339
+ 'required_skills' => [],
340
+ 'meta' => $this->meta($seedId),
341
+ ])->save();
342
+
343
+ $this->seedEntities($company, $payload, $definition['entities'] ?? []);
344
+
345
+ return [$seedId => $order];
346
+ })->all();
347
+ }
348
+
349
+ protected function createPayload(Company $company, array $definition): Payload
350
+ {
351
+ $payload = new Payload();
352
+ $payload->forceFill([
353
+ 'uuid' => (string) Str::uuid(),
354
+ 'company_uuid' => $company->uuid,
355
+ 'pickup_uuid' => $definition['pickup']?->uuid,
356
+ 'dropoff_uuid' => $definition['dropoff']?->uuid,
357
+ 'type' => 'transport',
358
+ 'meta' => $this->meta($definition['payload_seed_id']),
359
+ ])->save();
360
+
361
+ foreach ($definition['waypoints'] as $index => $place) {
362
+ $waypoint = new Waypoint();
363
+ $waypoint->forceFill([
364
+ 'uuid' => (string) Str::uuid(),
365
+ '_import_id' => static::SEED_NAME . ':waypoint:' . $definition['payload_seed_id'] . ':' . ($index + 1),
366
+ 'company_uuid' => $company->uuid,
367
+ 'payload_uuid' => $payload->uuid,
368
+ 'place_uuid' => $place->uuid,
369
+ 'type' => 'dropoff',
370
+ 'order' => $index + 1,
371
+ 'service_time' => 300,
372
+ ]);
373
+
374
+ Waypoint::withoutEvents(fn () => $waypoint->save());
375
+ }
376
+
377
+ return $payload;
378
+ }
379
+
380
+ protected function seedEntities(Company $company, Payload $payload, array $entities): void
381
+ {
382
+ foreach ($entities as $index => $definition) {
383
+ $entity = new Entity();
384
+ $entity->forceFill([
385
+ 'uuid' => (string) Str::uuid(),
386
+ 'company_uuid' => $company->uuid,
387
+ 'payload_uuid' => $payload->uuid,
388
+ 'name' => $definition['name'],
389
+ 'type' => $definition['type'],
390
+ 'weight' => $definition['weight'],
391
+ 'weight_unit' => $definition['weight_unit'],
392
+ 'length' => $definition['length'],
393
+ 'width' => $definition['width'],
394
+ 'height' => $definition['height'],
395
+ 'dimensions_unit' => $definition['dimensions_unit'],
396
+ 'meta' => $this->meta($payload->getMeta('seed_id') . '_' . ($index + 1)),
397
+ ]);
398
+
399
+ Entity::withoutEvents(fn () => $entity->save());
400
+ }
401
+ }
402
+ }
@@ -0,0 +1,64 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Http\Controllers\Api\v1;
4
+
5
+ use Fleetbase\FleetOps\Http\Controllers\Internal\v1\OrchestrationController as InternalOrchestrationController;
6
+ use Illuminate\Http\JsonResponse;
7
+ use Illuminate\Http\Request;
8
+
9
+ /**
10
+ * Public consumable API for FleetOps orchestration.
11
+ *
12
+ * This controller delegates orchestration behavior to the internal workbench
13
+ * controller and only changes the public API serialization boundary.
14
+ */
15
+ class OrchestrationController extends InternalOrchestrationController
16
+ {
17
+ public function run(Request $request): JsonResponse
18
+ {
19
+ return $this->publicResponse(parent::run($request));
20
+ }
21
+
22
+ public function commit(Request $request): JsonResponse
23
+ {
24
+ return $this->publicResponse(parent::commit($request));
25
+ }
26
+
27
+ protected function publicResponse(JsonResponse $response): JsonResponse
28
+ {
29
+ return response()->json(
30
+ $this->sanitizePublicPayload($response->getData(true)),
31
+ $response->getStatusCode()
32
+ );
33
+ }
34
+
35
+ /**
36
+ * Recursively remove internal identifiers from public orchestrator payloads.
37
+ */
38
+ protected function sanitizePublicPayload(mixed $payload): mixed
39
+ {
40
+ if (!is_array($payload)) {
41
+ return $payload;
42
+ }
43
+
44
+ $sanitized = [];
45
+
46
+ foreach ($payload as $key => $value) {
47
+ if (is_string($key) && $this->isInternalIdentifierKey($key)) {
48
+ continue;
49
+ }
50
+
51
+ $sanitized[$key] = $this->sanitizePublicPayload($value);
52
+ }
53
+
54
+ return $sanitized;
55
+ }
56
+
57
+ protected function isInternalIdentifierKey(string $key): bool
58
+ {
59
+ return $key === 'uuid'
60
+ || str_ends_with($key, '_uuid')
61
+ || $key === 'internal_id'
62
+ || $key === 'company_uuid';
63
+ }
64
+ }
@@ -93,17 +93,17 @@ class OrderController extends Controller
93
93
 
94
94
  // create payload
95
95
  if ($request->has('payload') && $request->isArray('payload')) {
96
- $payload = new Payload();
97
- $payloadInput = $request->input('payload');
98
- $entities = data_get($payloadInput, 'entities', []);
99
- $waypoints = data_get($payloadInput, 'waypoints', []);
100
- $pickup = data_get($payloadInput, 'pickup');
101
- $dropoff = data_get($payloadInput, 'dropoff');
102
- $return = data_get($payloadInput, 'return');
103
- $hasPickupField = array_key_exists('pickup', $payloadInput);
104
- $hasDropoffField = array_key_exists('dropoff', $payloadInput);
105
- $hasReturnField = array_key_exists('return', $payloadInput);
106
- $hasWaypointsField = array_key_exists('waypoints', $payloadInput);
96
+ $payload = new Payload();
97
+ $payloadInput = $request->input('payload');
98
+ $entities = data_get($payloadInput, 'entities', []);
99
+ $waypoints = data_get($payloadInput, 'waypoints', []);
100
+ $pickup = data_get($payloadInput, 'pickup');
101
+ $dropoff = data_get($payloadInput, 'dropoff');
102
+ $return = data_get($payloadInput, 'return');
103
+ $hasPickupField = array_key_exists('pickup', $payloadInput);
104
+ $hasDropoffField = array_key_exists('dropoff', $payloadInput);
105
+ $hasReturnField = array_key_exists('return', $payloadInput);
106
+ $hasWaypointsField = array_key_exists('waypoints', $payloadInput);
107
107
  $hasRouteEndpointFields = $hasPickupField || $hasDropoffField || $hasReturnField;
108
108
 
109
109
  if ($pickup) {
@@ -445,17 +445,17 @@ class OrderController extends Controller
445
445
 
446
446
  // create a payload if missing payload[] but has pickup/dropoff/etc
447
447
  if ($request->missing('payload')) {
448
- $payload = data_get($order, 'payload', new Payload());
449
- $payloadInput = $request->only(['pickup', 'dropoff', 'return', 'waypoints', 'entities']);
450
- $entities = data_get($payloadInput, 'entities', []);
451
- $waypoints = data_get($payloadInput, 'waypoints', []);
452
- $pickup = data_get($payloadInput, 'pickup');
453
- $dropoff = data_get($payloadInput, 'dropoff');
454
- $return = data_get($payloadInput, 'return');
455
- $hasPickupField = $request->exists('pickup');
456
- $hasDropoffField = $request->exists('dropoff');
457
- $hasReturnField = $request->exists('return');
458
- $hasWaypointsField = $request->exists('waypoints');
448
+ $payload = data_get($order, 'payload', new Payload());
449
+ $payloadInput = $request->only(['pickup', 'dropoff', 'return', 'waypoints', 'entities']);
450
+ $entities = data_get($payloadInput, 'entities', []);
451
+ $waypoints = data_get($payloadInput, 'waypoints', []);
452
+ $pickup = data_get($payloadInput, 'pickup');
453
+ $dropoff = data_get($payloadInput, 'dropoff');
454
+ $return = data_get($payloadInput, 'return');
455
+ $hasPickupField = $request->exists('pickup');
456
+ $hasDropoffField = $request->exists('dropoff');
457
+ $hasReturnField = $request->exists('return');
458
+ $hasWaypointsField = $request->exists('waypoints');
459
459
  $hasRouteEndpointFields = $hasPickupField || $hasDropoffField || $hasReturnField;
460
460
 
461
461
  // if no pickup and dropoff extract from waypoints
@@ -1328,13 +1328,13 @@ class OrderController extends Controller
1328
1328
  *
1329
1329
  * @return \Illuminate\Http\Response
1330
1330
  */
1331
- public function trackerData(string $id)
1331
+ public function trackerData(Request $request, string $id)
1332
1332
  {
1333
1333
  set_time_limit(280);
1334
1334
 
1335
1335
  try {
1336
1336
  $order = Order::findRecordOrFail($id);
1337
- $data = $order->tracker()->toArray();
1337
+ $data = $order->tracker()->toArray($request->only(['provider', 'fallbacks', 'traffic_enabled']));
1338
1338
 
1339
1339
  return response()->json($data);
1340
1340
  } catch (ModelNotFoundException $e) {
@@ -1351,11 +1351,11 @@ class OrderController extends Controller
1351
1351
  *
1352
1352
  * @return \Illuminate\Http\Response
1353
1353
  */
1354
- public function etaData(string $id)
1354
+ public function etaData(Request $request, string $id)
1355
1355
  {
1356
1356
  try {
1357
1357
  $order = Order::findRecordOrFail($id);
1358
- $data = $order->tracker()->eta();
1358
+ $data = $order->tracker()->eta($request->only(['provider', 'fallbacks', 'traffic_enabled']));
1359
1359
 
1360
1360
  return response()->json($data);
1361
1361
  } catch (ModelNotFoundException $e) {
@@ -25,16 +25,16 @@ class PayloadController extends Controller
25
25
  */
26
26
  public function create(CreatePayloadRequest $request)
27
27
  {
28
- $input = $request->all();
29
- $entities = data_get($input, 'entities', []);
30
- $waypoints = data_get($input, 'waypoints', []);
31
- $pickup = data_get($input, 'pickup');
32
- $dropoff = data_get($input, 'dropoff');
33
- $return = data_get($input, 'return');
34
- $hasPickupField = array_key_exists('pickup', $input);
35
- $hasDropoffField = array_key_exists('dropoff', $input);
36
- $hasReturnField = array_key_exists('return', $input);
37
- $hasWaypointsField = array_key_exists('waypoints', $input);
28
+ $input = $request->all();
29
+ $entities = data_get($input, 'entities', []);
30
+ $waypoints = data_get($input, 'waypoints', []);
31
+ $pickup = data_get($input, 'pickup');
32
+ $dropoff = data_get($input, 'dropoff');
33
+ $return = data_get($input, 'return');
34
+ $hasPickupField = array_key_exists('pickup', $input);
35
+ $hasDropoffField = array_key_exists('dropoff', $input);
36
+ $hasReturnField = array_key_exists('return', $input);
37
+ $hasWaypointsField = array_key_exists('waypoints', $input);
38
38
  $hasRouteEndpointFields = $hasPickupField || $hasDropoffField || $hasReturnField;
39
39
 
40
40
  // make sure company is set
@@ -231,7 +231,7 @@ class ServiceQuoteController extends Controller
231
231
  $entities = collect($entities)->mapInto(Entity::class);
232
232
 
233
233
  // should all be Place like
234
- $waypoints = collect([$pickup, ...$waypoints, $dropoff])->filter();
234
+ $waypoints = collect([$pickup, ...$waypoints, $dropoff])->filter();
235
235
  $endpointCount = (int) ($pickup instanceof Place) + (int) ($dropoff instanceof Place);
236
236
 
237
237
  // if facilitator is an integrated partner resolve service quotes from bridge
@@ -8,6 +8,7 @@ use Fleetbase\FleetOps\Imports\ContactImport;
8
8
  use Fleetbase\FleetOps\Models\Contact;
9
9
  use Fleetbase\Http\Requests\ExportRequest;
10
10
  use Fleetbase\Http\Requests\ImportRequest;
11
+ use Fleetbase\Models\User;
11
12
  use Illuminate\Http\Request;
12
13
  use Illuminate\Support\Str;
13
14
  use Maatwebsite\Excel\Facades\Excel;
@@ -21,11 +22,35 @@ class ContactController extends FleetOpsController
21
22
  */
22
23
  public $resource = 'contact';
23
24
 
25
+ /**
26
+ * Handle pre-create transactions.
27
+ */
28
+ public function onBeforeCreate(Request $request, array &$input)
29
+ {
30
+ $this->resolveUserInput($request, $input);
31
+ }
32
+
33
+ /**
34
+ * Handle pre-update transactions.
35
+ */
36
+ public function onBeforeUpdate(Request $request, Contact $contact, array &$input)
37
+ {
38
+ if ($contact->type === 'customer' && isset($input['type']) && $input['type'] !== 'customer') {
39
+ throw new \Exception('Customer contact type cannot be changed.');
40
+ }
41
+
42
+ $this->resolveUserInput($request, $input);
43
+ }
44
+
24
45
  /**
25
46
  * Handle post save transactions.
26
47
  */
27
48
  public function afterSave(Request $request, Contact $contact)
28
49
  {
50
+ if ($contact->type === 'customer') {
51
+ $contact->normalizeCustomerUser();
52
+ }
53
+
29
54
  $customFieldValues = $request->array('contact.custom_field_values');
30
55
  if ($customFieldValues) {
31
56
  $contact->syncCustomFieldValues($customFieldValues);
@@ -105,4 +130,20 @@ class ContactController extends FleetOpsController
105
130
 
106
131
  return response()->json(['status' => 'ok', 'message' => 'Import completed', 'imported' => $importedCount]);
107
132
  }
133
+
134
+ private function resolveUserInput(Request $request, array &$input): void
135
+ {
136
+ $user = data_get($input, 'user_uuid') ?? data_get($input, 'user') ?? $request->input('contact.user_uuid') ?? $request->input('contact.user');
137
+
138
+ if (is_array($user)) {
139
+ $user = data_get($user, 'uuid') ?? data_get($user, 'id');
140
+ }
141
+
142
+ if (!$user) {
143
+ return;
144
+ }
145
+
146
+ $input['user_uuid'] = User::where('uuid', $user)->orWhere('public_id', $user)->value('uuid') ?? $user;
147
+ unset($input['user']);
148
+ }
108
149
  }