@fleetbase/fleetops-engine 0.6.66 → 0.6.68
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/RELEASE.md +9 -8
- package/addon/components/fuel-integration/form.hbs +2 -2
- package/addon/components/fuel-integration/form.js +8 -0
- package/addon/components/fuel-integration/hub.js +1 -1
- package/addon/components/telematic/details.hbs +1 -1
- package/addon/components/telematic/telemetry-status.hbs +93 -34
- package/addon/components/widget/radar.hbs +1 -1
- package/addon/components/widget/radar.js +7 -0
- package/addon/templates/connectivity/fuel-providers/details/index.hbs +1 -1
- package/addon/utils/fuel-transaction.js +2 -0
- package/composer.json +2 -1
- package/docs/AFAQY.md +71 -17
- package/docs/SAFEE.md +130 -0
- package/docs/TELEMATICS_QUEUES.md +230 -0
- package/extension.json +1 -1
- package/package.json +1 -1
- package/server/config/fuel-providers.php +53 -0
- package/server/config/safee.php +10 -0
- package/server/config/telematics.php +8 -1
- package/server/config/telemetry.php +4 -1
- package/server/migrations/2026_09_17_000001_add_device_event_uuid_lookup_index.php +90 -0
- package/server/src/Console/Commands/SyncTelematics.php +11 -5
- package/server/src/Events/DeviceTelemetryUpdated.php +2 -1
- package/server/src/Events/TrailerLocationChanged.php +2 -0
- package/server/src/Events/VehicleLocationChanged.php +5 -0
- package/server/src/Jobs/PollTelematicTelemetry.php +163 -12
- package/server/src/Jobs/ProcessTelematicDelivery.php +64 -10
- package/server/src/Jobs/SyncTelematicDevicesJob.php +77 -1
- package/server/src/Jobs/TestTelematicConnectionJob.php +1 -0
- package/server/src/Providers/FleetOpsServiceProvider.php +5 -2
- package/server/src/Support/FuelProviders/Providers/SascoFuelProvider.php +272 -0
- package/server/src/Support/Telematics/Providers/AfaqyProvider.php +65 -10
- package/server/src/Support/Telematics/Providers/SafeeProvider.php +213 -139
- package/server/src/Support/Telematics/Safee/Transport.php +210 -0
- package/server/src/Support/Telematics/TelematicService.php +45 -1
- package/server/src/Support/Telematics/Telemetry/Configuration.php +16 -0
- package/server/src/Support/Telematics/Telemetry/Inbox.php +49 -3
- package/server/src/Support/Telematics/Telemetry/Ingestor.php +1 -1
- package/server/tests/Feature/Http/AfaqyRealtimeIngestionTest.php +147 -58
- package/server/tests/Feature/Http/Api/DeviceEventUuidIndexMigrationTest.php +106 -0
- package/server/tests/Feature/Http/ManualTelemetryPollingTest.php +545 -0
- package/server/tests/Feature/Http/SafeeRealtimeIngestionTest.php +208 -0
- package/server/tests/Feature/Http/TelematicDeliveryCheckpointTest.php +198 -0
- package/server/tests/Feature/Http/TelematicMixedFleetDeliveryTest.php +166 -0
- package/server/tests/ProviderContractsTest.php +50 -1
- package/server/tests/Support/AfaqyTestCrypto.php +6 -23
- package/server/tests/SupportJobAndAiCoverageTest.php +102 -5
- package/server/tests/TelematicsHardeningTest.php +18 -13
- package/server/tests/Unit/Console/SyncTelematicsCommandTest.php +140 -0
- package/server/tests/Unit/Support/SascoFuelProviderTest.php +338 -0
- package/server/tests/Unit/Support/Telematics/Providers/AfaqyProviderTransportTest.php +132 -0
- package/server/tests/Unit/Support/Telematics/Providers/ProviderTransportSeamsTest.php +1 -1
- package/server/tests/Unit/Support/Telematics/Providers/SafeeLiveContractTest.php +335 -0
- package/server/tests/Unit/Support/Telematics/Providers/SafeeProviderTest.php +17 -27
- package/server/tests/Unit/Support/Telematics/TelematicsQueueRoutingTest.php +36 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Support\FuelProviders\Providers;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Models\FuelProviderConnection;
|
|
6
|
+
use Illuminate\Http\Client\Response;
|
|
7
|
+
use Illuminate\Support\Carbon;
|
|
8
|
+
use Illuminate\Support\Collection;
|
|
9
|
+
use Illuminate\Support\Facades\Cache;
|
|
10
|
+
use Illuminate\Support\Facades\Http;
|
|
11
|
+
|
|
12
|
+
class SascoFuelProvider extends AbstractFuelProvider
|
|
13
|
+
{
|
|
14
|
+
public const BASE_URLS = [
|
|
15
|
+
'sandbox' => 'https://loyalty-api-qc.sasco.sa',
|
|
16
|
+
'production' => 'https://loyalty-api.sasco.com.sa',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
public const LOGIN_ENDPOINT = 'apigateway/api/identity/user/b2badmin-login';
|
|
20
|
+
public const VEHICLES_ENDPOINT = 'apigateway/api/loyaltycore/b2bexternaladmin/b2bvehicles';
|
|
21
|
+
public const TRANSACTIONS_ENDPOINT = 'apigateway/api/analytics/v1/b2bexternal/drivers-transactions';
|
|
22
|
+
|
|
23
|
+
/** Fallback token lifetime when the JWT carries no readable exp claim. */
|
|
24
|
+
protected const DEFAULT_TOKEN_TTL = 3600;
|
|
25
|
+
|
|
26
|
+
public function key(): string
|
|
27
|
+
{
|
|
28
|
+
return 'sasco';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public function name(): string
|
|
32
|
+
{
|
|
33
|
+
return 'SASCO';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public function authenticate(FuelProviderConnection $connection): array
|
|
37
|
+
{
|
|
38
|
+
$credentials = (array) $connection->credentials;
|
|
39
|
+
$response = Http::baseUrl($this->baseUrl($connection))
|
|
40
|
+
->timeout((int) data_get($credentials, 'timeout', 30))
|
|
41
|
+
->acceptJson()
|
|
42
|
+
->asJson()
|
|
43
|
+
->post(self::LOGIN_ENDPOINT, [
|
|
44
|
+
'username' => trim((string) data_get($credentials, 'username')),
|
|
45
|
+
'password' => (string) data_get($credentials, 'password'),
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
$token = $response->json('accessToken');
|
|
49
|
+
if (!$response->successful() || !is_string($token) || $token === '') {
|
|
50
|
+
return [
|
|
51
|
+
'success' => false,
|
|
52
|
+
'message' => $this->errorMessage($response, 'SASCO login failed. Check the B2B admin username and password.'),
|
|
53
|
+
'status' => $response->status(),
|
|
54
|
+
];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
Cache::put($this->tokenCacheKey($connection), $token, $this->tokenTtl($token));
|
|
58
|
+
|
|
59
|
+
return ['success' => true, 'message' => 'SASCO login successful.', 'token' => $token];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public function testConnection(FuelProviderConnection $connection): array
|
|
63
|
+
{
|
|
64
|
+
$metadata = [
|
|
65
|
+
'environment' => $connection->environment ?? 'production',
|
|
66
|
+
'host' => parse_url($this->baseUrl($connection), PHP_URL_HOST),
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
$response = $this->get($connection, self::VEHICLES_ENDPOINT, ['Page_size' => 1, 'Current_page' => 1]);
|
|
71
|
+
$metadata['status'] = $response->status();
|
|
72
|
+
|
|
73
|
+
if (!$response->successful() || !is_array($response->json('vehicles'))) {
|
|
74
|
+
return [
|
|
75
|
+
'success' => false,
|
|
76
|
+
'message' => $this->errorMessage($response, 'Unable to connect to SASCO.'),
|
|
77
|
+
'metadata' => $metadata,
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return [
|
|
82
|
+
'success' => true,
|
|
83
|
+
'message' => 'SASCO connection successful.',
|
|
84
|
+
'metadata' => array_merge($metadata, ['total_vehicles' => $response->json('totalItems')]),
|
|
85
|
+
];
|
|
86
|
+
} catch (\Throwable $e) {
|
|
87
|
+
return ['success' => false, 'message' => $e->getMessage(), 'metadata' => $metadata];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
public function listVehicles(FuelProviderConnection $connection, array $options = []): Collection
|
|
92
|
+
{
|
|
93
|
+
return $this->paginatedGet($connection, self::VEHICLES_ENDPOINT, 'vehicles', $options);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
public function listTransactions(FuelProviderConnection $connection, Carbon $from, Carbon $to, array $options = []): Collection
|
|
97
|
+
{
|
|
98
|
+
$params = [
|
|
99
|
+
'FromDate' => $from->toDateString(),
|
|
100
|
+
'ToDate' => $to->toDateString(),
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
if (array_key_exists('is_trip', $options)) {
|
|
104
|
+
$params['IsTrip'] = $options['is_trip'] ? 'true' : 'false';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return $this->paginatedGet($connection, self::TRANSACTIONS_ENDPOINT, 'driverTransactions', array_merge($options, $params))
|
|
108
|
+
->map(fn ($payload) => $this->normalizeTransaction($payload));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
protected function baseUrl(FuelProviderConnection $connection): string
|
|
112
|
+
{
|
|
113
|
+
$credentials = (array) $connection->credentials;
|
|
114
|
+
$override = trim((string) data_get($credentials, 'base_url', ''));
|
|
115
|
+
|
|
116
|
+
return rtrim($override ?: self::BASE_URLS[$connection->environment ?? 'production'], '/');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
protected function headers(FuelProviderConnection $connection): array
|
|
120
|
+
{
|
|
121
|
+
return ['Authorization' => 'Bearer ' . $this->token($connection)];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Returns a cached access token, logging in when none is cached.
|
|
126
|
+
*/
|
|
127
|
+
protected function token(FuelProviderConnection $connection): string
|
|
128
|
+
{
|
|
129
|
+
$token = Cache::get($this->tokenCacheKey($connection));
|
|
130
|
+
if (is_string($token) && $token !== '') {
|
|
131
|
+
return $token;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
$result = $this->authenticate($connection);
|
|
135
|
+
if (!$result['success']) {
|
|
136
|
+
throw new \RuntimeException($result['message']);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return $result['token'];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Keyed by host and username rather than connection uuid so unsaved
|
|
144
|
+
* connections built for credential tests share the same cache entry.
|
|
145
|
+
*/
|
|
146
|
+
protected function tokenCacheKey(FuelProviderConnection $connection): string
|
|
147
|
+
{
|
|
148
|
+
$credentials = (array) $connection->credentials;
|
|
149
|
+
|
|
150
|
+
return 'fuel-provider:sasco:token:' . hash('sha256', $this->baseUrl($connection) . '|' . trim((string) data_get($credentials, 'username')));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
protected function tokenTtl(string $token): int
|
|
154
|
+
{
|
|
155
|
+
$segments = explode('.', $token);
|
|
156
|
+
$payload = isset($segments[1]) ? json_decode((string) base64_decode(strtr($segments[1], '-_', '+/')), true) : null;
|
|
157
|
+
$expires = (int) data_get($payload, 'exp', 0);
|
|
158
|
+
|
|
159
|
+
if ($expires <= 0) {
|
|
160
|
+
return self::DEFAULT_TOKEN_TTL;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Refresh a minute early so a request never goes out with a token about to lapse.
|
|
164
|
+
return max(1, $expires - now()->timestamp - 60);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Sends an authenticated GET, re-authenticating once when SASCO rejects a cached token.
|
|
169
|
+
*/
|
|
170
|
+
protected function get(FuelProviderConnection $connection, string $endpoint, array $params = []): Response
|
|
171
|
+
{
|
|
172
|
+
$response = $this->client($connection)->get($endpoint, $params);
|
|
173
|
+
|
|
174
|
+
if ($response->status() === 401) {
|
|
175
|
+
Cache::forget($this->tokenCacheKey($connection));
|
|
176
|
+
$response = $this->client($connection)->get($endpoint, $params);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return $response;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
protected function paginatedGet(FuelProviderConnection $connection, string $endpoint, string $dataKey, array $params = []): Collection
|
|
183
|
+
{
|
|
184
|
+
$page = max(1, (int) data_get($params, 'page', 1));
|
|
185
|
+
$pageSize = max(1, (int) data_get($params, 'page_size', 100));
|
|
186
|
+
$maxPages = (int) data_get($params, 'max_pages', 100);
|
|
187
|
+
$query = collect($params)->except(['page', 'page_size', 'max_pages', 'is_trip'])->all();
|
|
188
|
+
|
|
189
|
+
$items = collect();
|
|
190
|
+
|
|
191
|
+
do {
|
|
192
|
+
$response = $this->get($connection, $endpoint, array_merge($query, ['Page_size' => $pageSize, 'Current_page' => $page]));
|
|
193
|
+
|
|
194
|
+
if (!$response->successful()) {
|
|
195
|
+
throw new \RuntimeException($this->errorMessage($response, "SASCO {$endpoint} request failed."));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
$data = $response->json($dataKey);
|
|
199
|
+
if (!is_array($data)) {
|
|
200
|
+
throw new \RuntimeException("SASCO {$endpoint} returned an invalid response.");
|
|
201
|
+
}
|
|
202
|
+
$items = $items->merge($data);
|
|
203
|
+
|
|
204
|
+
$total = (int) $response->json('totalItems', 0);
|
|
205
|
+
$hasMore = count($data) > 0 && $items->count() < $total;
|
|
206
|
+
$page++;
|
|
207
|
+
} while ($hasMore && $page <= $maxPages);
|
|
208
|
+
|
|
209
|
+
if ($hasMore) {
|
|
210
|
+
throw new \RuntimeException('SASCO returned more pages than the sync limit. Choose a shorter date range and retry; the import was not marked complete.');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return $items;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
protected function errorMessage(Response $response, string $fallback): string
|
|
217
|
+
{
|
|
218
|
+
$message = $response->json('message') ?? $response->json('title') ?? $response->json('error');
|
|
219
|
+
|
|
220
|
+
return is_string($message) && $message !== '' ? $message : $fallback;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Saudi plates are printed as three letters and up to four digits, e.g. "NXD 1240".
|
|
225
|
+
*/
|
|
226
|
+
protected function plateNumber(array $plate): ?string
|
|
227
|
+
{
|
|
228
|
+
$number = $this->compactIdentifier($plate['plateNumber'] ?? null);
|
|
229
|
+
$letters = collect([$plate['firstPlateLetter'] ?? null, $plate['secondPlateLetter'] ?? null, $plate['thirdPlateLetter'] ?? null])
|
|
230
|
+
->map(fn ($letter) => $this->compactIdentifier($letter))
|
|
231
|
+
->filter()
|
|
232
|
+
->implode('');
|
|
233
|
+
|
|
234
|
+
return $this->compactIdentifier(trim($letters . ' ' . $number));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
protected function normalizeTransaction(array $transaction): array
|
|
238
|
+
{
|
|
239
|
+
$plate = (array) ($transaction['vehiclePlate'] ?? []);
|
|
240
|
+
$providerId = $this->compactIdentifier($transaction['transactionNumber'] ?? null);
|
|
241
|
+
$fallbackId = $this->transactionHash([
|
|
242
|
+
$transaction['createdAt'] ?? null,
|
|
243
|
+
$plate,
|
|
244
|
+
$transaction['stationName'] ?? null,
|
|
245
|
+
$transaction['noOfLitters'] ?? null,
|
|
246
|
+
$transaction['amount'] ?? null,
|
|
247
|
+
$transaction['driverPhone'] ?? null,
|
|
248
|
+
]);
|
|
249
|
+
|
|
250
|
+
return [
|
|
251
|
+
'provider' => $this->key(),
|
|
252
|
+
'provider_transaction_id' => (string) ($providerId ?: $fallbackId),
|
|
253
|
+
'plate_number' => $this->plateNumber($plate),
|
|
254
|
+
'trip_number' => $this->compactIdentifier($transaction['tripId'] ?? null),
|
|
255
|
+
'station_name' => $this->compactIdentifier($transaction['stationName'] ?? null),
|
|
256
|
+
'transaction_at' => $this->dateFrom($transaction['createdAt'] ?? null),
|
|
257
|
+
'volume' => $transaction['noOfLitters'] ?? null,
|
|
258
|
+
'metric_unit' => 'l',
|
|
259
|
+
'amount' => $this->minorCurrencyUnit($transaction['amount'] ?? null),
|
|
260
|
+
'currency' => 'SAR',
|
|
261
|
+
'normalized_payload' => [
|
|
262
|
+
'fuel_type' => $transaction['fuelType'] ?? null,
|
|
263
|
+
'driver_name' => $transaction['driverName'] ?? null,
|
|
264
|
+
'driver_phone' => $transaction['driverPhone'] ?? null,
|
|
265
|
+
'type' => $transaction['type'] ?? null,
|
|
266
|
+
'period' => $transaction['period'] ?? null,
|
|
267
|
+
'fuel_image_url' => $transaction['fuelImageUrl'] ?? null,
|
|
268
|
+
],
|
|
269
|
+
'raw_payload' => $transaction,
|
|
270
|
+
];
|
|
271
|
+
}
|
|
272
|
+
}
|
|
@@ -5,6 +5,7 @@ namespace Fleetbase\FleetOps\Support\Telematics\Providers;
|
|
|
5
5
|
use Fleetbase\FleetOps\Exceptions\TelematicProviderException;
|
|
6
6
|
use Fleetbase\FleetOps\Exceptions\TelematicRateLimitExceededException;
|
|
7
7
|
use Fleetbase\FleetOps\Support\Telematics\Afaqy\Payload;
|
|
8
|
+
use Illuminate\Contracts\Encryption\DecryptException;
|
|
8
9
|
use Illuminate\Http\Client\ConnectionException;
|
|
9
10
|
use Illuminate\Http\Client\Response;
|
|
10
11
|
use Illuminate\Support\Facades\Cache;
|
|
@@ -20,12 +21,13 @@ use Illuminate\Support\Facades\Log;
|
|
|
20
21
|
*/
|
|
21
22
|
class AfaqyProvider extends AbstractProvider implements \Fleetbase\FleetOps\Contracts\TelemetryProviderInterface
|
|
22
23
|
{
|
|
23
|
-
protected string $baseUrl
|
|
24
|
-
protected int $requestsPerMinute
|
|
25
|
-
protected int $dataTimeout
|
|
26
|
-
protected int $connectTimeout
|
|
27
|
-
protected int $connectionTestTimeout
|
|
28
|
-
protected int $connectionTestConnectTimeout
|
|
24
|
+
protected string $baseUrl = 'https://api.afaqy.sa';
|
|
25
|
+
protected int $requestsPerMinute = 60;
|
|
26
|
+
protected int $dataTimeout = 120;
|
|
27
|
+
protected int $connectTimeout = 15;
|
|
28
|
+
protected int $connectionTestTimeout = 30;
|
|
29
|
+
protected int $connectionTestConnectTimeout = 10;
|
|
30
|
+
protected ?float $requestDeadline = null;
|
|
29
31
|
|
|
30
32
|
protected function prepareAuthentication(): void
|
|
31
33
|
{
|
|
@@ -343,9 +345,11 @@ class AfaqyProvider extends AbstractProvider implements \Fleetbase\FleetOps\Cont
|
|
|
343
345
|
}
|
|
344
346
|
|
|
345
347
|
$this->reserveRequest();
|
|
348
|
+
$timeout = $this->remainingRequestTimeout(30);
|
|
346
349
|
$response = Http::asJson()
|
|
347
350
|
->acceptJson()
|
|
348
|
-
->timeout(
|
|
351
|
+
->timeout($timeout)
|
|
352
|
+
->connectTimeout(min($this->connectTimeout, $timeout))
|
|
349
353
|
->post($this->baseUrl . '/auth/login', [
|
|
350
354
|
'data' => [
|
|
351
355
|
'username' => $this->credentials['username'],
|
|
@@ -373,6 +377,20 @@ class AfaqyProvider extends AbstractProvider implements \Fleetbase\FleetOps\Cont
|
|
|
373
377
|
}
|
|
374
378
|
|
|
375
379
|
protected function authenticatedPost(string $endpoint, array $payload = [], bool $tokenInQuery = false, bool $allowRetry = true, ?int $timeout = null, ?int $connectTimeout = null): array
|
|
380
|
+
{
|
|
381
|
+
$previousDeadline = $this->requestDeadline;
|
|
382
|
+
$this->requestDeadline ??= $this->requestTime() + max(1, $timeout ?? $this->dataTimeout);
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
return $this->postWithinDeadline($endpoint, $payload, $tokenInQuery, $allowRetry, $timeout, $connectTimeout);
|
|
386
|
+
} finally {
|
|
387
|
+
// Providers may be reused. A completed or failed request must not
|
|
388
|
+
// leave its deadline on the next independent request.
|
|
389
|
+
$this->requestDeadline = $previousDeadline;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
protected function postWithinDeadline(string $endpoint, array $payload, bool $tokenInQuery, bool $allowRetry, ?int $timeout, ?int $connectTimeout): array
|
|
376
394
|
{
|
|
377
395
|
[$url, $body] = $this->buildAuthenticatedRequest($endpoint, $payload, $tokenInQuery);
|
|
378
396
|
|
|
@@ -381,6 +399,8 @@ class AfaqyProvider extends AbstractProvider implements \Fleetbase\FleetOps\Cont
|
|
|
381
399
|
$connectTimeout ??= $this->connectTimeout;
|
|
382
400
|
|
|
383
401
|
$this->reserveRequest();
|
|
402
|
+
$timeout = $this->remainingRequestTimeout($timeout);
|
|
403
|
+
$connectTimeout = min(max(1, $connectTimeout), $timeout);
|
|
384
404
|
try {
|
|
385
405
|
$response = Http::withHeaders($this->headers)
|
|
386
406
|
->timeout($timeout)
|
|
@@ -468,9 +488,22 @@ class AfaqyProvider extends AbstractProvider implements \Fleetbase\FleetOps\Cont
|
|
|
468
488
|
{
|
|
469
489
|
$key = 'afaqy:token:' . $this->accountKey() . ':' . hash('sha256', $this->credentials['password'] ?? '');
|
|
470
490
|
|
|
471
|
-
return Cache::lock($key . ':lock', 40)->block(5, function () use ($key, $rejected) {
|
|
491
|
+
return Cache::lock($key . ':lock', 40)->block($this->remainingRequestTimeout(5), function () use ($key, $rejected) {
|
|
472
492
|
$stored = Cache::get($key);
|
|
473
|
-
$token =
|
|
493
|
+
$token = null;
|
|
494
|
+
if ($stored) {
|
|
495
|
+
try {
|
|
496
|
+
$token = Crypt::decryptString($stored);
|
|
497
|
+
} catch (DecryptException) {
|
|
498
|
+
// A cached token is disposable. Evict only this entry while holding
|
|
499
|
+
// the existing account lock, then authenticate with resolved credentials.
|
|
500
|
+
Cache::forget($key);
|
|
501
|
+
Log::warning('AFAQY cached token could not be decrypted; re-authenticating.', [
|
|
502
|
+
'telematic_uuid' => $this->telematic?->uuid,
|
|
503
|
+
'reason' => 'cached_token_decryption_failed',
|
|
504
|
+
]);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
474
507
|
if (!$token || $token === $rejected) {
|
|
475
508
|
$token = $this->authenticate();
|
|
476
509
|
Cache::put($key, Crypt::encryptString($token), 29 * 86400);
|
|
@@ -483,7 +516,8 @@ class AfaqyProvider extends AbstractProvider implements \Fleetbase\FleetOps\Cont
|
|
|
483
516
|
protected function reserveRequest(): void
|
|
484
517
|
{
|
|
485
518
|
$key = 'afaqy:rate:' . $this->accountKey();
|
|
486
|
-
Cache::lock($key . ':lock', 5)->block(2, function () use ($key) {
|
|
519
|
+
Cache::lock($key . ':lock', 5)->block($this->remainingRequestTimeout(2), function () use ($key) {
|
|
520
|
+
$this->remainingRequestTimeout(1);
|
|
487
521
|
$now = microtime(true);
|
|
488
522
|
$until = (float) Cache::get($key . ':blocked', 0);
|
|
489
523
|
$requests = array_values(array_filter(Cache::get($key, []), fn ($at) => $at > $now - 60));
|
|
@@ -495,6 +529,27 @@ class AfaqyProvider extends AbstractProvider implements \Fleetbase\FleetOps\Cont
|
|
|
495
529
|
});
|
|
496
530
|
}
|
|
497
531
|
|
|
532
|
+
/** Monotonic time keeps the request budget independent of wall-clock corrections. */
|
|
533
|
+
protected function requestTime(): float
|
|
534
|
+
{
|
|
535
|
+
return hrtime(true) / 1_000_000_000;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
protected function remainingRequestTimeout(int $maximum): int
|
|
539
|
+
{
|
|
540
|
+
$maximum = max(1, $maximum);
|
|
541
|
+
if ($this->requestDeadline === null) {
|
|
542
|
+
return $maximum;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
$remaining = (int) floor($this->requestDeadline - $this->requestTime());
|
|
546
|
+
if ($remaining < 1) {
|
|
547
|
+
throw new TelematicProviderException('AFAQY request time budget exhausted before another operation could start.');
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return min($maximum, $remaining);
|
|
551
|
+
}
|
|
552
|
+
|
|
498
553
|
protected function checkThrottle(Response $response): void
|
|
499
554
|
{
|
|
500
555
|
if ($response->status() !== 429) {
|