http() ->asForm() ->post($tokenUrl, [ 'grant_type' => 'client_credentials', 'client_id' => $clientId, 'client_secret' => $clientSecret, ]); $payload = $response->json(); if (!is_array($payload)) { $payload = []; } if (!$response->successful()) { Log::error('BSAS OAuth token request failed', [ 'status' => $response->status(), 'body' => $payload ?: $response->body(), ]); throw new RuntimeException('Gagal mendapatkan BSAS access token.'); } $token = $payload['access_token'] ?? null; if (!is_string($token) || $token === '') { Log::error('BSAS OAuth response missing string access_token', [ 'body' => $payload, ]); throw new RuntimeException('BSAS OAuth response missing access_token.'); } $expiresIn = (int) ($payload['expires_in'] ?? self::TOKEN_CACHE_SECONDS); $ttl = max(60, min(self::TOKEN_CACHE_SECONDS, $expiresIn - 300)); Cache::put(self::TOKEN_CACHE_KEY, $token, $ttl); return $token; } public function placeOrder(array $payload): array { return $this->requestJson('post', '/process/svc/bsas/order', $payload); } public function orderResult(string $uuid, array $body = []): array { $maxWait = (string) ($body['maxWaitTime'] ?? config('bsas.max_wait_time', 10)); $payload = [ 'header' => [ 'memberShortName' => config('bsas.member_short_name'), 'uuid' => $uuid, ], 'body' => [ 'serialNumbers' => (string) ($body['serialNumbers'] ?? '1'), 'forceYN' => (string) ($body['forceYN'] ?? 'Y'), 'maxWaitTime' => $maxWait, 'waitAllDoneYN' => (string) ($body['waitAllDoneYN'] ?? 'Y'), ], ]; // Long poll: client timeout must exceed BSAS maxWaitTime. $timeout = max( (int) config('bsas.http_timeout', 30), (int) $maxWait + 20 ); return $this->requestJson('get', '/process/svc/bsas/orderResult', $payload, $timeout); } public function getBidEcert(string $ecertNo): array { return $this->requestEcert('/process/svc/bsas/bidXML', $ecertNo); } public function getOtcEcert(string $ecertNo): array { return $this->requestEcert('/process/svc/bsas/otcXML', $ecertNo); } public function getStbEcert(string $ecertNo): array { return $this->requestEcert('/process/svc/bsas/stbXML', $ecertNo); } protected function requestEcert(string $path, string $ecertNo): array { $payload = [ 'input' => [ 'ecertno' => $ecertNo, 'membershortname' => config('bsas.member_short_name'), ], ]; return $this->requestJson('get', $path, $payload); } protected function requestJson(string $method, string $path, array $payload, ?int $timeout = null): array { $url = rtrim(config('bsas.base_url'), '/') . '/' . ltrim($path, '/'); $timeout = $timeout ?? (int) config('bsas.http_timeout', 30); $token = $this->getAccessToken(); $pending = $this->http($timeout) ->withToken($token) ->acceptJson(); if (strtolower($method) === 'get') { $response = $pending ->withHeaders(['Content-Type' => 'application/json']) ->send('GET', $url, [ 'body' => json_encode($payload), ]); } else { $response = $pending->asJson()->post($url, $payload); } $json = $response->json(); if (!is_array($json)) { $body = $response->body(); $cloudflare = stripos($body, 'cloudflare') !== false || stripos($body, 'Attention Required') !== false; if ($cloudflare) { throw new RuntimeException( 'BSAS blocked by Cloudflare (check IP whitelist). HTTP ' . $response->status() ); } throw new RuntimeException('BSAS returned invalid JSON response.'); } if (!$response->successful()) { Log::error('BSAS HTTP error', [ 'method' => $method, 'url' => $url, 'status' => $response->status(), 'body' => $json, ]); throw new RuntimeException('BSAS request failed (HTTP ' . $response->status() . ').'); } Log::info('BSAS API call', [ 'method' => $method, 'path' => $path, 'status' => $response->status(), 'header' => $json['header'] ?? null, ]); return $json; } protected function http(?int $timeout = null): PendingRequest { $timeout = $timeout ?? (int) config('bsas.http_timeout', 30); return Http::withOptions([ 'timeout' => $timeout, 'connect_timeout' => min(15, $timeout), ]); } }