@fleetbase/fleetops-engine 0.6.47 → 0.6.49
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/driver/details.hbs +5 -1
- package/addon/components/driver/form.hbs +11 -1
- package/addon/components/fleet-listing-panel.hbs +2 -2
- package/addon/components/fleet-listing-panel.js +10 -0
- package/addon/components/map/google-live-map.js +12 -73
- package/addon/components/map/leaflet-live-map.hbs +149 -33
- 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 +49 -5
- package/addon/components/orchestrator/phase-builder.js +34 -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 +4 -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.hbs +2 -2
- 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/components/widget/fleet-ops-key-metrics.hbs +19 -17
- package/addon/components/widget/fleet-ops-key-metrics.js +26 -0
- package/addon/components/widget/fuel-efficiency.hbs +26 -0
- package/addon/components/widget/fuel-efficiency.js +43 -0
- package/addon/components/widget/geofence-violations.hbs +44 -0
- package/addon/components/widget/geofence-violations.js +55 -0
- package/addon/components/widget/issues-insights.hbs +43 -0
- package/addon/components/widget/issues-insights.js +62 -0
- package/addon/components/widget/kpi-active-orders.hbs +10 -0
- package/addon/components/widget/kpi-active-orders.js +5 -0
- package/addon/components/widget/kpi-aov.hbs +10 -0
- package/addon/components/widget/kpi-aov.js +5 -0
- package/addon/components/widget/kpi-distance.hbs +10 -0
- package/addon/components/widget/kpi-distance.js +5 -0
- package/addon/components/widget/kpi-drivers-online.hbs +10 -0
- package/addon/components/widget/kpi-drivers-online.js +5 -0
- package/addon/components/widget/kpi-earnings.hbs +10 -0
- package/addon/components/widget/kpi-earnings.js +5 -0
- package/addon/components/widget/kpi-open-issues.hbs +11 -0
- package/addon/components/widget/kpi-open-issues.js +5 -0
- package/addon/components/widget/kpi-tile.hbs +59 -0
- package/addon/components/widget/kpi-tile.js +168 -0
- package/addon/components/widget/live-fleet.hbs +122 -0
- package/addon/components/widget/live-fleet.js +365 -0
- package/addon/components/widget/maintenance-overview.hbs +45 -0
- package/addon/components/widget/maintenance-overview.js +40 -0
- package/addon/components/widget/on-time-delivery.hbs +27 -0
- package/addon/components/widget/on-time-delivery.js +73 -0
- package/addon/components/widget/operations-pulse.hbs +51 -0
- package/addon/components/widget/operations-pulse.js +173 -0
- package/addon/components/widget/orders-by-status.hbs +26 -0
- package/addon/components/widget/orders-by-status.js +54 -0
- package/addon/components/widget/revenue-trend.hbs +34 -0
- package/addon/components/widget/revenue-trend.js +67 -0
- package/addon/components/widget/top-drivers.hbs +44 -0
- package/addon/components/widget/top-drivers.js +54 -0
- package/addon/controllers/settings/routing.js +124 -1
- package/addon/extension.js +172 -3
- package/addon/helpers/format-duration.js +38 -0
- package/addon/routes/application.js +6 -0
- package/addon/services/map-adapter/google.js +13 -2
- package/addon/services/order-list-overlay.js +2 -2
- package/addon/styles/fleetops-engine.css +806 -46
- package/addon/templates/maintenance/equipment/index.hbs +1 -1
- package/addon/templates/maintenance/parts/index.hbs +1 -1
- package/addon/templates/maintenance/schedules/index.hbs +1 -1
- package/addon/templates/management/vehicles/index.hbs +1 -1
- package/addon/templates/settings/avatars.hbs +1 -1
- package/addon/templates/settings/custom-fields.hbs +1 -1
- package/addon/templates/settings/map.hbs +1 -1
- package/addon/templates/settings/navigator-app.hbs +1 -1
- package/addon/templates/settings/notifications.hbs +1 -1
- package/addon/templates/settings/orchestrator.hbs +1 -1
- package/addon/templates/settings/payments/index.hbs +1 -1
- package/addon/templates/settings/payments/onboard.hbs +1 -1
- package/addon/templates/settings/routing.hbs +82 -4
- package/addon/templates/settings/scheduling.hbs +1 -1
- package/addon/utils/live-map-card-content.js +171 -0
- package/app/components/tracking-stop-progress.js +1 -0
- package/app/components/widget/fuel-efficiency.js +1 -0
- package/app/components/widget/geofence-violations.js +1 -0
- package/app/components/widget/issues-insights.js +1 -0
- package/app/components/widget/kpi-active-orders.js +1 -0
- package/app/components/widget/kpi-aov.js +1 -0
- package/app/components/widget/kpi-distance.js +1 -0
- package/app/components/widget/kpi-drivers-online.js +1 -0
- package/app/components/widget/kpi-earnings.js +1 -0
- package/app/components/widget/kpi-open-issues.js +1 -0
- package/app/components/widget/kpi-tile.js +1 -0
- package/app/components/widget/live-fleet.js +1 -0
- package/app/components/widget/maintenance-overview.js +1 -0
- package/app/components/widget/on-time-delivery.js +1 -0
- package/app/components/widget/operations-pulse.js +1 -0
- package/app/components/widget/orders-by-status.js +1 -0
- package/app/components/widget/revenue-trend.js +1 -0
- package/app/components/widget/top-drivers.js +1 -0
- package/app/helpers/format-duration.js +1 -0
- package/app/utils/live-map-card-content.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +3 -3
- package/pnpm-workspace.yaml +1 -0
- package/server/config/fleetops.php +32 -11
- package/server/config/telematics.php +111 -1
- package/server/migrations/2026_05_13_000001_repair_customer_contact_user_types.php +86 -0
- package/server/migrations/2026_05_18_000001_add_license_expiry_to_drivers_table.php +25 -0
- package/server/seeders/Testing/Concerns/SeedsTestingData.php +183 -0
- package/server/seeders/Testing/ConnectivitySeeder.php +212 -0
- package/server/seeders/Testing/FleetSeeder.php +341 -0
- package/server/seeders/Testing/MaintenanceSeeder.php +238 -0
- package/server/seeders/Testing/NetworkSeeder.php +250 -0
- package/server/seeders/Testing/OrdersSeeder.php +214 -0
- package/server/seeders/Testing/TestingSeeder.php +40 -0
- package/server/seeders/Testing/VroomOrchestrationSeeder.php +409 -0
- package/server/src/Console/Commands/ProcessOperationalAlerts.php +279 -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/AnalyticsController.php +119 -0
- 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/MetricsController.php +82 -3
- package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +52 -21
- package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/SettingController.php +164 -12
- package/server/src/Http/Resources/v1/Driver.php +2 -0
- package/server/src/Http/Resources/v1/Fleet.php +4 -0
- package/server/src/Http/Resources/v1/Index/Driver.php +48 -0
- package/server/src/Http/Resources/v1/Index/Payload.php +2 -2
- package/server/src/Http/Resources/v1/Index/Vehicle.php +74 -1
- package/server/src/Models/Contact.php +122 -13
- package/server/src/Models/Driver.php +2 -0
- package/server/src/Models/Order.php +1 -1
- package/server/src/Models/OrderConfig.php +14 -11
- 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/Notifications/LateDeparture.php +48 -0
- package/server/src/Notifications/ProlongedStoppage.php +48 -0
- package/server/src/Notifications/RouteDeviation.php +48 -0
- package/server/src/Observers/ContactObserver.php +8 -0
- package/server/src/Orchestration/Engines/CapacityAllocationEngine.php +220 -0
- package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +357 -38
- package/server/src/Orchestration/Support/OrchestrationPayloadBuilder.php +259 -51
- package/server/src/Providers/FleetOpsServiceProvider.php +32 -0
- package/server/src/Providers/NotificationServiceProvider.php +3 -0
- package/server/src/Support/Analytics/AbstractAnalytics.php +77 -0
- package/server/src/Support/Analytics/FuelEfficiency.php +83 -0
- package/server/src/Support/Analytics/GeofenceViolations.php +63 -0
- package/server/src/Support/Analytics/IssuesInsights.php +67 -0
- package/server/src/Support/Analytics/LiveFleet.php +127 -0
- package/server/src/Support/Analytics/MaintenanceOverview.php +70 -0
- package/server/src/Support/Analytics/OnTimeDelivery.php +75 -0
- package/server/src/Support/Analytics/OperationsPulse.php +86 -0
- package/server/src/Support/Analytics/OrdersByStatus.php +64 -0
- package/server/src/Support/Analytics/RevenueTrend.php +79 -0
- package/server/src/Support/Analytics/TopDrivers.php +92 -0
- package/server/src/Support/LiveOrderQuery.php +75 -0
- package/server/src/Support/Metrics/AbstractMetric.php +170 -0
- package/server/src/Support/Metrics/ActiveLiveOrdersMetric.php +28 -0
- package/server/src/Support/Metrics/AvgOrderValueMetric.php +53 -0
- package/server/src/Support/Metrics/DriversOnlineMetric.php +30 -0
- package/server/src/Support/Metrics/EarningsMetric.php +30 -0
- package/server/src/Support/Metrics/FuelCostsMetric.php +30 -0
- package/server/src/Support/Metrics/MoneyMetric.php +30 -0
- package/server/src/Support/Metrics/OpenIssuesMetric.php +35 -0
- package/server/src/Support/Metrics/OrdersCanceledMetric.php +35 -0
- package/server/src/Support/Metrics/OrdersCompletedMetric.php +35 -0
- package/server/src/Support/Metrics/OrdersInProgressMetric.php +51 -0
- package/server/src/Support/Metrics/OrdersScheduledMetric.php +37 -0
- package/server/src/Support/Metrics/Registry.php +46 -0
- package/server/src/Support/Metrics/ResolvedIssuesMetric.php +35 -0
- package/server/src/Support/Metrics/TotalCustomersMetric.php +29 -0
- package/server/src/Support/Metrics/TotalDistanceTraveledMetric.php +35 -0
- package/server/src/Support/Metrics/TotalDriversMetric.php +28 -0
- package/server/src/Support/Metrics/TotalTimeTraveledMetric.php +35 -0
- package/server/src/Support/Metrics.php +46 -234
- package/server/src/Support/OSRM.php +11 -6
- package/server/src/Support/OrderTracker.php +7 -582
- package/server/src/Support/Telematics/Providers/AfaqyProvider.php +290 -0
- package/server/src/Support/Telematics/Providers/SafeeProvider.php +333 -0
- 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 +27 -0
- package/server/tests/AnalyticsRoutesTest.php +41 -0
- package/server/tests/CapacityAllocationEngineTest.php +108 -0
- package/server/tests/LiveOrderQueryTest.php +34 -0
- package/server/tests/MetricsRegistryTest.php +88 -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 +250 -0
- package/translations/en-us.yaml +41 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
use Fleetbase\FleetOps\Support\LiveOrderQuery;
|
|
4
|
+
|
|
5
|
+
test('active live order query uses the same active status rules as the map overlay', function () {
|
|
6
|
+
$query = LiveOrderQuery::make('company_test', [
|
|
7
|
+
'active' => true,
|
|
8
|
+
'apply_permissions' => false,
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
$bindings = $query->getBindings();
|
|
12
|
+
|
|
13
|
+
expect($bindings)->toContain('company_test')
|
|
14
|
+
->and($bindings)->toContain('created')
|
|
15
|
+
->and($bindings)->toContain('pending')
|
|
16
|
+
->and($bindings)->toContain('completed')
|
|
17
|
+
->and($bindings)->toContain('canceled')
|
|
18
|
+
->and($bindings)->toContain('expired')
|
|
19
|
+
->and($bindings)->toContain('order_canceled');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('live order query requires renderable payload and tracking data', function () {
|
|
23
|
+
$query = LiveOrderQuery::make('company_test', [
|
|
24
|
+
'active' => true,
|
|
25
|
+
'apply_permissions' => false,
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
$sql = $query->toSql();
|
|
29
|
+
|
|
30
|
+
expect($sql)->toContain('exists')
|
|
31
|
+
->and($sql)->toContain('payload')
|
|
32
|
+
->and($sql)->toContain('tracking')
|
|
33
|
+
->and($sql)->toContain('driver');
|
|
34
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
use Fleetbase\FleetOps\Support\Metrics\AbstractMetric;
|
|
4
|
+
use Fleetbase\FleetOps\Support\Metrics\AvgOrderValueMetric;
|
|
5
|
+
use Fleetbase\FleetOps\Support\Metrics\EarningsMetric;
|
|
6
|
+
use Fleetbase\FleetOps\Support\Metrics\OpenIssuesMetric;
|
|
7
|
+
use Fleetbase\FleetOps\Support\Metrics\OrdersInProgressMetric;
|
|
8
|
+
use Fleetbase\FleetOps\Support\Metrics\Registry;
|
|
9
|
+
use Fleetbase\FleetOps\Support\Metrics\ResolvedIssuesMetric;
|
|
10
|
+
use Fleetbase\FleetOps\Support\Metrics\TotalTimeTraveledMetric;
|
|
11
|
+
|
|
12
|
+
test('registry exposes every known metric slug', function () {
|
|
13
|
+
$slugs = Registry::slugs();
|
|
14
|
+
|
|
15
|
+
expect($slugs)->toContain('earnings');
|
|
16
|
+
expect($slugs)->toContain('fuel_costs');
|
|
17
|
+
expect($slugs)->toContain('total_distance_traveled');
|
|
18
|
+
expect($slugs)->toContain('total_time_traveled');
|
|
19
|
+
expect($slugs)->toContain('orders_completed');
|
|
20
|
+
expect($slugs)->toContain('orders_canceled');
|
|
21
|
+
expect($slugs)->toContain('orders_in_progress');
|
|
22
|
+
expect($slugs)->toContain('orders_scheduled');
|
|
23
|
+
expect($slugs)->toContain('active_live_orders');
|
|
24
|
+
expect($slugs)->toContain('drivers_online');
|
|
25
|
+
expect($slugs)->toContain('total_drivers');
|
|
26
|
+
expect($slugs)->toContain('total_customers');
|
|
27
|
+
expect($slugs)->toContain('open_issues');
|
|
28
|
+
expect($slugs)->toContain('resolved_issues');
|
|
29
|
+
expect($slugs)->toContain('avg_order_value');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('registry resolves unknown slugs to null', function () {
|
|
33
|
+
expect(Registry::resolve('does_not_exist'))->toBeNull();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('every registered metric extends the abstract base', function () {
|
|
37
|
+
foreach (Registry::all() as $slug => $class) {
|
|
38
|
+
expect(is_subclass_of($class, AbstractMetric::class))
|
|
39
|
+
->toBeTrue("Metric class for slug '{$slug}' must extend AbstractMetric");
|
|
40
|
+
expect($class::slug())->toBe($slug);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('open and resolved issue metrics scope to the constructor company (not session)', function () {
|
|
45
|
+
$sourceOpen = file_get_contents(dirname(__DIR__) . '/src/Support/Metrics/OpenIssuesMetric.php');
|
|
46
|
+
$sourceResolved = file_get_contents(dirname(__DIR__) . '/src/Support/Metrics/ResolvedIssuesMetric.php');
|
|
47
|
+
|
|
48
|
+
expect($sourceOpen)->not->toContain("session('company')");
|
|
49
|
+
expect($sourceOpen)->toContain('$this->company->uuid');
|
|
50
|
+
expect($sourceResolved)->not->toContain("session('company')");
|
|
51
|
+
expect($sourceResolved)->toContain('$this->company->uuid');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('total_time_traveled metric sums the time column, not distance', function () {
|
|
55
|
+
$source = file_get_contents(dirname(__DIR__) . '/src/Support/Metrics/TotalTimeTraveledMetric.php');
|
|
56
|
+
|
|
57
|
+
expect($source)->toContain("\$query->sum('time')");
|
|
58
|
+
expect($source)->not->toContain("\$query->sum('distance')");
|
|
59
|
+
expect(TotalTimeTraveledMetric::slug())->toBe('total_time_traveled');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('money metrics return float values to preserve cents', function () {
|
|
63
|
+
$earnings = file_get_contents(dirname(__DIR__) . '/src/Support/Metrics/EarningsMetric.php');
|
|
64
|
+
$fuelCosts = file_get_contents(dirname(__DIR__) . '/src/Support/Metrics/FuelCostsMetric.php');
|
|
65
|
+
|
|
66
|
+
expect($earnings)->toContain('(float) $query->sum');
|
|
67
|
+
expect($earnings)->not->toMatch('/\(int\)\s*\$query->sum/');
|
|
68
|
+
expect($fuelCosts)->toContain('(float) $query->sum');
|
|
69
|
+
expect($fuelCosts)->not->toMatch('/\(int\)\s*\$query->sum/');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('money metrics filter by currency to avoid mixed-currency sums', function () {
|
|
73
|
+
$earnings = file_get_contents(dirname(__DIR__) . '/src/Support/Metrics/EarningsMetric.php');
|
|
74
|
+
$fuelCosts = file_get_contents(dirname(__DIR__) . '/src/Support/Metrics/FuelCostsMetric.php');
|
|
75
|
+
$aov = file_get_contents(dirname(__DIR__) . '/src/Support/Metrics/AvgOrderValueMetric.php');
|
|
76
|
+
|
|
77
|
+
expect($earnings)->toContain("->where('currency'");
|
|
78
|
+
expect($fuelCosts)->toContain("->where('currency'");
|
|
79
|
+
expect($aov)->toContain("->where('currency'");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('ordersInProgress uses an explicit allowlist rather than an exclusion list', function () {
|
|
83
|
+
$statuses = OrdersInProgressMetric::IN_PROGRESS_STATUSES;
|
|
84
|
+
expect($statuses)->toBeArray();
|
|
85
|
+
expect($statuses)->not->toBeEmpty();
|
|
86
|
+
expect($statuses)->toContain('dispatched');
|
|
87
|
+
expect($statuses)->toContain('started');
|
|
88
|
+
});
|
|
@@ -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
|
+
});
|