@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,290 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Support\Telematics\Providers;
|
|
4
|
+
|
|
5
|
+
use Illuminate\Support\Carbon;
|
|
6
|
+
use Illuminate\Support\Facades\Http;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* AFAQY telematics provider implementation.
|
|
10
|
+
*
|
|
11
|
+
* AFAQY uses token-based REST endpoints for authentication, unit discovery,
|
|
12
|
+
* latest unit telemetry, historical signals, and track generation.
|
|
13
|
+
*/
|
|
14
|
+
class AfaqyProvider extends AbstractProvider
|
|
15
|
+
{
|
|
16
|
+
protected string $baseUrl = 'https://api.afaqy.sa';
|
|
17
|
+
protected int $requestsPerMinute = 60;
|
|
18
|
+
|
|
19
|
+
protected function prepareAuthentication(): void
|
|
20
|
+
{
|
|
21
|
+
$this->baseUrl = rtrim($this->credentials['base_url'] ?? $this->baseUrl, '/');
|
|
22
|
+
$token = $this->credentials['token'] ?? $this->authenticate();
|
|
23
|
+
|
|
24
|
+
$this->credentials['token'] = $token;
|
|
25
|
+
$this->headers = [
|
|
26
|
+
'Accept' => 'application/json',
|
|
27
|
+
'Content-Type' => 'application/json',
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public function testConnection(array $credentials): array
|
|
32
|
+
{
|
|
33
|
+
try {
|
|
34
|
+
$this->credentials = $credentials;
|
|
35
|
+
$this->prepareAuthentication();
|
|
36
|
+
|
|
37
|
+
$response = $this->afaqyPost('/units/lists', [
|
|
38
|
+
'data' => [
|
|
39
|
+
'projection' => ['_id', 'name', 'imei', 'last_update'],
|
|
40
|
+
'filters' => new \stdClass(),
|
|
41
|
+
],
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
return [
|
|
45
|
+
'success' => true,
|
|
46
|
+
'message' => 'Connection successful',
|
|
47
|
+
'metadata' => [
|
|
48
|
+
'units_count' => count($response['data'] ?? []),
|
|
49
|
+
'status_code' => $response['status_code'] ?? null,
|
|
50
|
+
],
|
|
51
|
+
];
|
|
52
|
+
} catch (\Throwable $e) {
|
|
53
|
+
return [
|
|
54
|
+
'success' => false,
|
|
55
|
+
'message' => $e->getMessage(),
|
|
56
|
+
'metadata' => [],
|
|
57
|
+
];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public function fetchDevices(array $options = []): array
|
|
62
|
+
{
|
|
63
|
+
$limit = (int) ($options['limit'] ?? 100);
|
|
64
|
+
$offset = (int) ($options['cursor'] ?? 0);
|
|
65
|
+
|
|
66
|
+
$response = $this->afaqyPost('/units/lists', [
|
|
67
|
+
'data' => [
|
|
68
|
+
'address' => $options['address'] ?? false,
|
|
69
|
+
'filters' => $options['filters'] ?? new \stdClass(),
|
|
70
|
+
'projection' => $options['projection'] ?? [
|
|
71
|
+
'_id',
|
|
72
|
+
'name',
|
|
73
|
+
'imei',
|
|
74
|
+
'sim_number',
|
|
75
|
+
'device_serial',
|
|
76
|
+
'active',
|
|
77
|
+
'driver_id',
|
|
78
|
+
'last_update',
|
|
79
|
+
'counters',
|
|
80
|
+
'profile',
|
|
81
|
+
'sensors',
|
|
82
|
+
'sensors_last_val',
|
|
83
|
+
],
|
|
84
|
+
],
|
|
85
|
+
'limit' => $limit,
|
|
86
|
+
'offset' => $offset,
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
$devices = $response['data'] ?? [];
|
|
90
|
+
$pagination = $response['pagination'] ?? [];
|
|
91
|
+
$total = (int) ($pagination['filtersCount'] ?? $pagination['allCount'] ?? count($devices));
|
|
92
|
+
$nextCursor = ($offset + $limit) < $total ? $offset + $limit : null;
|
|
93
|
+
|
|
94
|
+
return [
|
|
95
|
+
'devices' => $devices,
|
|
96
|
+
'next_cursor' => $nextCursor,
|
|
97
|
+
'has_more' => $nextCursor !== null,
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
public function fetchDeviceDetails(string $externalId): array
|
|
102
|
+
{
|
|
103
|
+
$response = $this->afaqyPost('/units/view', [
|
|
104
|
+
'data' => ['id' => $externalId],
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
return $response['data'] ?? [];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
public function normalizeDevice(array $payload): array
|
|
111
|
+
{
|
|
112
|
+
$lastUpdate = $payload['last_update'] ?? [];
|
|
113
|
+
|
|
114
|
+
return [
|
|
115
|
+
'external_id' => $payload['_id'] ?? $payload['id'] ?? null,
|
|
116
|
+
'device_name' => $payload['name'] ?? data_get($payload, 'profile.plate_number') ?? 'Unknown Unit',
|
|
117
|
+
'device_provider' => 'afaqy',
|
|
118
|
+
'device_model' => data_get($payload, 'profile.model') ?? $payload['device'] ?? null,
|
|
119
|
+
'imei' => $payload['imei'] ?? null,
|
|
120
|
+
'phone' => $payload['sim_number'] ?? null,
|
|
121
|
+
'vin' => data_get($payload, 'profile.vin'),
|
|
122
|
+
'status' => ($payload['active'] ?? false) ? 'active' : 'inactive',
|
|
123
|
+
'location' => [
|
|
124
|
+
'lat' => $lastUpdate['lat'] ?? null,
|
|
125
|
+
'lng' => $lastUpdate['lng'] ?? null,
|
|
126
|
+
],
|
|
127
|
+
'meta' => [
|
|
128
|
+
'raw' => $payload,
|
|
129
|
+
'plate_number' => data_get($payload, 'profile.plate_number'),
|
|
130
|
+
'driver_id' => $payload['driver_id'] ?? null,
|
|
131
|
+
'last_update' => $this->normalizeEvent($payload),
|
|
132
|
+
'capabilities' => [
|
|
133
|
+
'tracking' => isset($lastUpdate['lat'], $lastUpdate['lng']),
|
|
134
|
+
'odometer' => data_get($payload, 'counters.odometer') !== null,
|
|
135
|
+
'fuel_level' => $this->extractFuelLevel($payload) !== null,
|
|
136
|
+
'ignition_state' => $this->extractIgnition($payload) !== null,
|
|
137
|
+
],
|
|
138
|
+
],
|
|
139
|
+
];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
public function normalizeEvent(array $payload): array
|
|
143
|
+
{
|
|
144
|
+
$lastUpdate = $payload['last_update'] ?? $payload;
|
|
145
|
+
|
|
146
|
+
return [
|
|
147
|
+
'external_id' => $payload['_id'] ?? $payload['id'] ?? null,
|
|
148
|
+
'event_type' => $payload['event'] ?? $payload['event_type'] ?? 'telemetry_update',
|
|
149
|
+
'occurred_at' => $this->parseTimestamp($lastUpdate['dtt'] ?? $lastUpdate['dts'] ?? null),
|
|
150
|
+
'location' => [
|
|
151
|
+
'lat' => $lastUpdate['lat'] ?? data_get($lastUpdate, 'loc.coordinates.1'),
|
|
152
|
+
'lng' => $lastUpdate['lng'] ?? data_get($lastUpdate, 'loc.coordinates.0'),
|
|
153
|
+
],
|
|
154
|
+
'speed' => $lastUpdate['speed'] ?? $lastUpdate['spd'] ?? null,
|
|
155
|
+
'heading' => $lastUpdate['angle'] ?? $lastUpdate['ang'] ?? null,
|
|
156
|
+
'odometer' => data_get($payload, 'counters.odometer'),
|
|
157
|
+
'ignition' => $this->extractIgnition($payload),
|
|
158
|
+
'fuel_level' => $this->extractFuelLevel($payload),
|
|
159
|
+
'meta' => $payload,
|
|
160
|
+
];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
public function normalizeSensor(array $payload): array
|
|
164
|
+
{
|
|
165
|
+
return [
|
|
166
|
+
'sensor_type' => $payload['type'] ?? $payload['name'] ?? $payload['param'] ?? 'generic',
|
|
167
|
+
'value' => $payload['value'] ?? $payload['last_val']['value'] ?? $payload['last_update'] ?? null,
|
|
168
|
+
'unit' => $payload['units'] ?? null,
|
|
169
|
+
'recorded_at' => $this->parseTimestamp($payload['updated_at'] ?? $payload['created_at'] ?? null),
|
|
170
|
+
'meta' => $payload,
|
|
171
|
+
];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
public function getCredentialSchema(): array
|
|
175
|
+
{
|
|
176
|
+
return [
|
|
177
|
+
[
|
|
178
|
+
'name' => 'base_url',
|
|
179
|
+
'label' => 'Base URL',
|
|
180
|
+
'type' => 'text',
|
|
181
|
+
'placeholder' => 'https://api.afaqy.sa',
|
|
182
|
+
'required' => false,
|
|
183
|
+
'validation' => 'nullable|url',
|
|
184
|
+
],
|
|
185
|
+
[
|
|
186
|
+
'name' => 'username',
|
|
187
|
+
'label' => 'Username',
|
|
188
|
+
'type' => 'text',
|
|
189
|
+
'placeholder' => 'Enter your AFAQY username',
|
|
190
|
+
'required' => false,
|
|
191
|
+
'validation' => 'required_without:token|string',
|
|
192
|
+
],
|
|
193
|
+
[
|
|
194
|
+
'name' => 'password',
|
|
195
|
+
'label' => 'Password',
|
|
196
|
+
'type' => 'password',
|
|
197
|
+
'placeholder' => 'Enter your AFAQY password',
|
|
198
|
+
'required' => false,
|
|
199
|
+
'validation' => 'required_without:token|string',
|
|
200
|
+
],
|
|
201
|
+
[
|
|
202
|
+
'name' => 'token',
|
|
203
|
+
'label' => 'Token',
|
|
204
|
+
'type' => 'password',
|
|
205
|
+
'placeholder' => 'Use an existing AFAQY token',
|
|
206
|
+
'required' => false,
|
|
207
|
+
'validation' => 'required_without:username|string',
|
|
208
|
+
],
|
|
209
|
+
];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
protected function authenticate(): string
|
|
213
|
+
{
|
|
214
|
+
if (empty($this->credentials['username']) || empty($this->credentials['password'])) {
|
|
215
|
+
throw new \InvalidArgumentException('AFAQY username/password or token is required.');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
$response = Http::asJson()
|
|
219
|
+
->acceptJson()
|
|
220
|
+
->timeout(30)
|
|
221
|
+
->post($this->baseUrl . '/auth/login', [
|
|
222
|
+
'data' => [
|
|
223
|
+
'username' => $this->credentials['username'],
|
|
224
|
+
'password' => $this->credentials['password'],
|
|
225
|
+
'expire' => $this->credentials['expire'] ?? 1,
|
|
226
|
+
],
|
|
227
|
+
]);
|
|
228
|
+
|
|
229
|
+
if ($response->failed()) {
|
|
230
|
+
throw new \RuntimeException('AFAQY authentication failed: ' . $response->body());
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
$token = data_get($response->json(), 'data.token');
|
|
234
|
+
|
|
235
|
+
if (!$token) {
|
|
236
|
+
throw new \RuntimeException('AFAQY authentication did not return a token.');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return $token;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
protected function afaqyPost(string $endpoint, array $payload = []): array
|
|
243
|
+
{
|
|
244
|
+
$response = Http::withHeaders($this->headers)
|
|
245
|
+
->timeout(30)
|
|
246
|
+
->post($this->baseUrl . $endpoint . '?token=' . urlencode($this->credentials['token']), $payload);
|
|
247
|
+
|
|
248
|
+
if ($response->failed()) {
|
|
249
|
+
throw new \RuntimeException('AFAQY API request failed: ' . $response->body());
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return $response->json() ?? [];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
protected function parseTimestamp($value): ?string
|
|
256
|
+
{
|
|
257
|
+
if (!$value) {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (is_numeric($value)) {
|
|
262
|
+
$timestamp = (float) $value;
|
|
263
|
+
$seconds = $timestamp > 9999999999 ? $timestamp / 1000 : $timestamp;
|
|
264
|
+
|
|
265
|
+
return Carbon::createFromTimestamp($seconds)->toDateTimeString();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return Carbon::parse($value)->toDateTimeString();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
protected function extractIgnition(array $payload): ?bool
|
|
272
|
+
{
|
|
273
|
+
$params = data_get($payload, 'last_update.params', []);
|
|
274
|
+
$value = data_get($params, 'acc') ?? data_get($params, 'di1') ?? data_get($payload, 'counters.last_acc');
|
|
275
|
+
|
|
276
|
+
if ($value === null) {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? ((int) $value === 1);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
protected function extractFuelLevel(array $payload): mixed
|
|
284
|
+
{
|
|
285
|
+
return data_get($payload, 'last_update.params.fuel')
|
|
286
|
+
?? data_get($payload, 'last_update.params.fuel_level')
|
|
287
|
+
?? data_get($payload, 'fc.level')
|
|
288
|
+
?? null;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Support\Telematics\Providers;
|
|
4
|
+
|
|
5
|
+
use Illuminate\Support\Carbon;
|
|
6
|
+
use Illuminate\Support\Facades\Http;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Safee Tracking REST provider implementation.
|
|
10
|
+
*
|
|
11
|
+
* Safee authenticates through an OpenID Connect token endpoint and exposes
|
|
12
|
+
* vehicle discovery plus latest state/position endpoints under api/v2.
|
|
13
|
+
*/
|
|
14
|
+
class SafeeProvider extends AbstractProvider
|
|
15
|
+
{
|
|
16
|
+
protected string $baseUrl = '';
|
|
17
|
+
protected int $requestsPerMinute = 3000;
|
|
18
|
+
protected ?string $accessToken = null;
|
|
19
|
+
|
|
20
|
+
protected function prepareAuthentication(): void
|
|
21
|
+
{
|
|
22
|
+
$this->baseUrl = rtrim($this->credentials['api_base_url'] ?? $this->credentials['server_uri'] ?? '', '/');
|
|
23
|
+
$this->accessToken = $this->credentials['access_token'] ?? $this->authenticate();
|
|
24
|
+
$scheme = $this->credentials['authorization_scheme'] ?? 'Bearer';
|
|
25
|
+
|
|
26
|
+
$this->headers = [
|
|
27
|
+
'Accept' => 'application/json',
|
|
28
|
+
'Content-Type' => 'application/json',
|
|
29
|
+
'Accept-Language' => $this->credentials['language'] ?? 'en',
|
|
30
|
+
'Authorization' => trim($scheme . ' ' . $this->accessToken),
|
|
31
|
+
];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public function testConnection(array $credentials): array
|
|
35
|
+
{
|
|
36
|
+
try {
|
|
37
|
+
$this->credentials = $credentials;
|
|
38
|
+
$this->prepareAuthentication();
|
|
39
|
+
|
|
40
|
+
$response = $this->safeeGet('/api/v2/status');
|
|
41
|
+
|
|
42
|
+
return [
|
|
43
|
+
'success' => (int) ($response['code'] ?? 0) === 0,
|
|
44
|
+
'message' => $response['message'] ?? 'Connection successful',
|
|
45
|
+
'metadata' => [
|
|
46
|
+
'status' => $response['status'] ?? null,
|
|
47
|
+
'time' => $response['time'] ?? null,
|
|
48
|
+
],
|
|
49
|
+
];
|
|
50
|
+
} catch (\Throwable $e) {
|
|
51
|
+
return [
|
|
52
|
+
'success' => false,
|
|
53
|
+
'message' => $e->getMessage(),
|
|
54
|
+
'metadata' => [],
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public function fetchDevices(array $options = []): array
|
|
60
|
+
{
|
|
61
|
+
if (!empty($options['vehicles'])) {
|
|
62
|
+
$response = $this->safeePost('/api/v2/vehicle/last-state', [
|
|
63
|
+
'live' => $options['live'] ?? true,
|
|
64
|
+
'startDate' => $options['start_date'] ?? null,
|
|
65
|
+
'endDate' => $options['end_date'] ?? null,
|
|
66
|
+
'vehicles' => $options['vehicles'],
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
$devices = $response['result'] ?? [];
|
|
70
|
+
} else {
|
|
71
|
+
$response = $this->safeePost('/api/v2/vehicle/list-info', $options['filter'] ?? new \stdClass());
|
|
72
|
+
$devices = $response['result'] ?? [];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return [
|
|
76
|
+
'devices' => $devices,
|
|
77
|
+
'next_cursor' => null,
|
|
78
|
+
'has_more' => false,
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public function fetchDeviceDetails(string $externalId): array
|
|
83
|
+
{
|
|
84
|
+
$response = $this->safeePost('/api/v2/vehicle/info', [
|
|
85
|
+
'id' => is_numeric($externalId) ? (int) $externalId : $externalId,
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
return $response['result'] ?? [];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
public function normalizeDevice(array $payload): array
|
|
92
|
+
{
|
|
93
|
+
$position = $this->extractPosition($payload);
|
|
94
|
+
|
|
95
|
+
return [
|
|
96
|
+
'external_id' => $payload['id'] ?? data_get($payload, 'vehicle.id') ?? null,
|
|
97
|
+
'device_name' => $payload['name'] ?? $payload['plateNumber'] ?? data_get($payload, 'vehicle.name') ?? 'Unknown Vehicle',
|
|
98
|
+
'device_provider' => 'safee',
|
|
99
|
+
'device_model' => $payload['model'] ?? data_get($payload, 'device.model') ?? null,
|
|
100
|
+
'imei' => data_get($payload, 'device.imei') ?? data_get($payload, 'device.serial') ?? null,
|
|
101
|
+
'vin' => $payload['vin'] ?? null,
|
|
102
|
+
'status' => $this->normalizeVehicleStatus($payload['status'] ?? $payload['vehicleStatus'] ?? null),
|
|
103
|
+
'location' => [
|
|
104
|
+
'lat' => $position['lat'] ?? null,
|
|
105
|
+
'lng' => $position['lng'] ?? null,
|
|
106
|
+
],
|
|
107
|
+
'meta' => [
|
|
108
|
+
'raw' => $payload,
|
|
109
|
+
'plate_number' => $payload['plateNumber'] ?? $payload['plate_number'] ?? null,
|
|
110
|
+
'door_number' => $payload['doorNumber'] ?? null,
|
|
111
|
+
'driver' => $payload['driver'] ?? null,
|
|
112
|
+
'last_update' => $this->normalizeEvent($payload),
|
|
113
|
+
'capabilities' => [
|
|
114
|
+
'tracking' => isset($position['lat'], $position['lng']),
|
|
115
|
+
'odometer' => $this->extractOdometer($payload) !== null,
|
|
116
|
+
'fuel_level' => $this->extractFuelLevel($payload) !== null,
|
|
117
|
+
'ignition_state' => $this->extractIgnition($payload) !== null,
|
|
118
|
+
],
|
|
119
|
+
],
|
|
120
|
+
];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
public function normalizeEvent(array $payload): array
|
|
124
|
+
{
|
|
125
|
+
$position = $this->extractPosition($payload);
|
|
126
|
+
|
|
127
|
+
return [
|
|
128
|
+
'external_id' => $payload['id'] ?? data_get($payload, 'vehicle.id') ?? null,
|
|
129
|
+
'event_type' => data_get($payload, 'event.code') ?? data_get($payload, 'event.name') ?? 'telemetry_update',
|
|
130
|
+
'occurred_at' => $this->parseTimestamp($payload['date'] ?? $payload['deviceTime'] ?? $payload['time'] ?? null),
|
|
131
|
+
'location' => [
|
|
132
|
+
'lat' => $position['lat'] ?? null,
|
|
133
|
+
'lng' => $position['lng'] ?? null,
|
|
134
|
+
],
|
|
135
|
+
'speed' => $payload['speed'] ?? $payload['lastSpeed'] ?? null,
|
|
136
|
+
'heading' => $payload['heading'] ?? $payload['angle'] ?? null,
|
|
137
|
+
'odometer' => $this->extractOdometer($payload),
|
|
138
|
+
'ignition' => $this->extractIgnition($payload),
|
|
139
|
+
'fuel_level' => $this->extractFuelLevel($payload),
|
|
140
|
+
'meta' => $payload,
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
public function normalizeSensor(array $payload): array
|
|
145
|
+
{
|
|
146
|
+
return [
|
|
147
|
+
'sensor_type' => $payload['type'] ?? $payload['name'] ?? 'generic',
|
|
148
|
+
'value' => $payload['value'] ?? $payload['lastValue'] ?? null,
|
|
149
|
+
'unit' => $payload['unit'] ?? null,
|
|
150
|
+
'recorded_at' => $this->parseTimestamp($payload['date'] ?? $payload['deviceTime'] ?? null),
|
|
151
|
+
'meta' => $payload,
|
|
152
|
+
];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
public function getCredentialSchema(): array
|
|
156
|
+
{
|
|
157
|
+
return [
|
|
158
|
+
[
|
|
159
|
+
'name' => 'server_uri',
|
|
160
|
+
'label' => 'Server URI',
|
|
161
|
+
'type' => 'text',
|
|
162
|
+
'placeholder' => 'https://tracking.example.com',
|
|
163
|
+
'required' => true,
|
|
164
|
+
'validation' => 'required|url',
|
|
165
|
+
],
|
|
166
|
+
[
|
|
167
|
+
'name' => 'realm_id',
|
|
168
|
+
'label' => 'Realm ID',
|
|
169
|
+
'type' => 'text',
|
|
170
|
+
'placeholder' => 'Enter Safee realm ID',
|
|
171
|
+
'required' => true,
|
|
172
|
+
],
|
|
173
|
+
[
|
|
174
|
+
'name' => 'client_id',
|
|
175
|
+
'label' => 'Client ID',
|
|
176
|
+
'type' => 'text',
|
|
177
|
+
'placeholder' => 'Enter Safee client ID',
|
|
178
|
+
'required' => true,
|
|
179
|
+
],
|
|
180
|
+
[
|
|
181
|
+
'name' => 'client_secret',
|
|
182
|
+
'label' => 'Client Secret',
|
|
183
|
+
'type' => 'password',
|
|
184
|
+
'placeholder' => 'Enter Safee client secret',
|
|
185
|
+
'required' => true,
|
|
186
|
+
],
|
|
187
|
+
[
|
|
188
|
+
'name' => 'username',
|
|
189
|
+
'label' => 'Username',
|
|
190
|
+
'type' => 'text',
|
|
191
|
+
'placeholder' => 'Enter Safee username',
|
|
192
|
+
'required' => true,
|
|
193
|
+
],
|
|
194
|
+
[
|
|
195
|
+
'name' => 'password',
|
|
196
|
+
'label' => 'Password',
|
|
197
|
+
'type' => 'password',
|
|
198
|
+
'placeholder' => 'Enter Safee password',
|
|
199
|
+
'required' => true,
|
|
200
|
+
],
|
|
201
|
+
];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
protected function authenticate(): string
|
|
205
|
+
{
|
|
206
|
+
foreach (['server_uri', 'realm_id', 'client_id', 'client_secret', 'username', 'password'] as $field) {
|
|
207
|
+
if (empty($this->credentials[$field])) {
|
|
208
|
+
throw new \InvalidArgumentException("Safee credential '{$field}' is required.");
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
$tokenUrl = rtrim($this->credentials['server_uri'], '/') . '/auth/realms/' . $this->credentials['realm_id'] . '/protocol/openid-connect/token';
|
|
213
|
+
|
|
214
|
+
$response = Http::asForm()
|
|
215
|
+
->acceptJson()
|
|
216
|
+
->timeout(30)
|
|
217
|
+
->post($tokenUrl, [
|
|
218
|
+
'grant_type' => 'password',
|
|
219
|
+
'client_secret' => $this->credentials['client_secret'],
|
|
220
|
+
'client_id' => $this->credentials['client_id'],
|
|
221
|
+
'username' => $this->credentials['username'],
|
|
222
|
+
'password' => $this->credentials['password'],
|
|
223
|
+
]);
|
|
224
|
+
|
|
225
|
+
if ($response->failed()) {
|
|
226
|
+
throw new \RuntimeException('Safee authentication failed: ' . $response->body());
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
$token = $response->json('access_token');
|
|
230
|
+
|
|
231
|
+
if (!$token) {
|
|
232
|
+
throw new \RuntimeException('Safee authentication did not return an access token.');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return $token;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
protected function safeeGet(string $endpoint): array
|
|
239
|
+
{
|
|
240
|
+
$response = Http::withHeaders($this->headers)
|
|
241
|
+
->timeout(30)
|
|
242
|
+
->get($this->baseUrl . $endpoint);
|
|
243
|
+
|
|
244
|
+
if ($response->failed()) {
|
|
245
|
+
throw new \RuntimeException('Safee API request failed: ' . $response->body());
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return $response->json() ?? [];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
protected function safeePost(string $endpoint, array|\stdClass $payload = []): array
|
|
252
|
+
{
|
|
253
|
+
$response = Http::withHeaders($this->headers)
|
|
254
|
+
->timeout(30)
|
|
255
|
+
->post($this->baseUrl . $endpoint, $payload);
|
|
256
|
+
|
|
257
|
+
if ($response->failed()) {
|
|
258
|
+
throw new \RuntimeException('Safee API request failed: ' . $response->body());
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return $response->json() ?? [];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
protected function extractPosition(array $payload): array
|
|
265
|
+
{
|
|
266
|
+
$position = $payload['position'] ?? $payload['lastPosition'] ?? $payload['location'] ?? $payload;
|
|
267
|
+
|
|
268
|
+
return [
|
|
269
|
+
'lat' => $position['lat'] ?? $position['latitude'] ?? data_get($position, 'loc.coordinates.1'),
|
|
270
|
+
'lng' => $position['lon'] ?? $position['lng'] ?? $position['longitude'] ?? data_get($position, 'loc.coordinates.0'),
|
|
271
|
+
];
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
protected function parseTimestamp($value): ?string
|
|
275
|
+
{
|
|
276
|
+
if (!$value) {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (is_numeric($value)) {
|
|
281
|
+
$timestamp = (float) $value;
|
|
282
|
+
$seconds = $timestamp > 9999999999 ? $timestamp / 1000 : $timestamp;
|
|
283
|
+
|
|
284
|
+
return Carbon::createFromTimestamp($seconds)->toDateTimeString();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return Carbon::parse($value)->toDateTimeString();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
protected function normalizeVehicleStatus(?string $status): string
|
|
291
|
+
{
|
|
292
|
+
if (!$status) {
|
|
293
|
+
return 'active';
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return in_array(strtolower($status), ['offline', 'inactive', 'deleted', 'expired'], true) ? 'inactive' : 'active';
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
protected function extractOdometer(array $payload): mixed
|
|
300
|
+
{
|
|
301
|
+
return data_get($payload, 'canbus.odometer')
|
|
302
|
+
?? data_get($payload, 'vehicleCanbus.odometer')
|
|
303
|
+
?? data_get($payload, 'odometer')
|
|
304
|
+
?? null;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
protected function extractIgnition(array $payload): ?bool
|
|
308
|
+
{
|
|
309
|
+
$value = $payload['ignition'] ?? data_get($payload, 'event.code');
|
|
310
|
+
|
|
311
|
+
if ($value === null) {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (is_string($value) && str_contains(strtolower($value), 'ignition_on')) {
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (is_string($value) && str_contains(strtolower($value), 'ignition_off')) {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
protected function extractFuelLevel(array $payload): mixed
|
|
327
|
+
{
|
|
328
|
+
return data_get($payload, 'fuel.level')
|
|
329
|
+
?? data_get($payload, 'fuelLevel')
|
|
330
|
+
?? data_get($payload, 'vehicleFuel.level')
|
|
331
|
+
?? null;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
@@ -392,7 +392,7 @@ class Utils extends FleetbaseUtils
|
|
|
392
392
|
return null;
|
|
393
393
|
}
|
|
394
394
|
|
|
395
|
-
if (preg_match(
|
|
395
|
+
if (preg_match('/POINT\\(\\s*([-+]?\\d*\\.?\\d+)\\s+([-+]?\\d*\\.?\\d+)\\s*\\)/i', $expressionValue, $matches)) {
|
|
396
396
|
return sprintf('POINT(%s %s)', $matches[1], $matches[2]);
|
|
397
397
|
}
|
|
398
398
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Tracking\Contracts;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Tracking\TrackingContext;
|
|
6
|
+
use Fleetbase\FleetOps\Tracking\TrackingOptions;
|
|
7
|
+
use Fleetbase\FleetOps\Tracking\TrackingProviderCapabilities;
|
|
8
|
+
use Fleetbase\FleetOps\Tracking\TrackingProviderResult;
|
|
9
|
+
|
|
10
|
+
interface TrackingProviderInterface
|
|
11
|
+
{
|
|
12
|
+
public function key(): string;
|
|
13
|
+
|
|
14
|
+
public function capabilities(): TrackingProviderCapabilities;
|
|
15
|
+
|
|
16
|
+
public function canTrack(TrackingContext $context): bool;
|
|
17
|
+
|
|
18
|
+
public function track(TrackingContext $context, TrackingOptions $options): TrackingProviderResult;
|
|
19
|
+
}
|