Files
ismailmasseran 0451ad3dbe Feature/bsas integration (#24)
Reviewed-on: #24
Co-authored-by: ISMAIL MASSERAN <ismail@koppkb.com>
Co-committed-by: ISMAIL MASSERAN <ismail@koppkb.com>
2026-10-26 15:35:48 +08:00

194 lines
6.1 KiB
PHP

<?php
namespace App\Services\Bsas;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use RuntimeException;
class BsasClient
{
const TOKEN_CACHE_KEY = 'bsas.oauth.token';
const TOKEN_CACHE_SECONDS = 82800; // 23 hours
public function getAccessToken(): string
{
$cached = Cache::get(self::TOKEN_CACHE_KEY);
if (is_string($cached) && $cached !== '') {
return $cached;
}
// Drop bad cache entries (e.g. whole JSON array accidentally stored).
if ($cached !== null) {
Cache::forget(self::TOKEN_CACHE_KEY);
}
$tokenUrl = config('bsas.token_url');
$clientId = config('bsas.client_id');
$clientSecret = config('bsas.client_secret');
if (!$tokenUrl || !$clientId || !$clientSecret) {
throw new RuntimeException('BSAS OAuth is not configured (token_url / client_id / client_secret).');
}
$response = $this->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),
]);
}
}