refactor(core): enhance SDK structure and documentation

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
2026-01-18 21:48:25 +03:00
parent 3da69c03e1
commit d214eecebe
378 changed files with 10514 additions and 496 deletions

View File

@@ -2,74 +2,108 @@
namespace Apoloz\AlfaBank\Sbp;
use Apoloz\AlfaBank\Enums\Environment;
use Apoloz\AlfaBank\Core\AlfaConfig;
use Apoloz\AlfaBank\Http\Middleware\AlfaResponseMiddleware;
use Apoloz\AlfaBank\Http\Middleware\RequestIdMiddleware;
use Apoloz\AlfaBank\Http\Pagination\AlfaOffsetPaginator;
use Apoloz\AlfaBank\Utils\CmsSigner;
use Saloon\Http\Connector;
use Saloon\Http\PendingRequest;
use Saloon\Http\Request;
use Saloon\PaginationPlugin\Contracts\HasPagination;
use Saloon\PaginationPlugin\OffsetPaginator;
use Saloon\Traits\RequestProperties\HasTries;
use Saloon\Traits\Plugins\HasTimeout;
class SbpConnector extends Connector
class SbpConnector extends Connector implements HasPagination
{
use HasTimeout;
use HasTries;
public function __construct(
protected string $certificatePath,
protected string $privateKeyPath,
protected string $keyNameAlias, // 'key-name' header
protected Environment $environment = Environment::SANDBOX
) {}
protected AlfaConfig $alfaConfig,
protected string $keyNameAlias
) {
$this->tries = 3;
$this->retryInterval = 500;
}
public function resolveBaseUrl(): string
{
return match ($this->environment) {
Environment::SANDBOX => 'https://217.12.103.132:2443/fsCryptoProxy',
Environment::PRODUCTION => 'https://pay.alfabank.ru/fsCryptoProxy', // Placeholder, user needs to confirm
};
return $this->alfaConfig->baseUrl;
}
protected function defaultConfig(): array
{
return [
'cert' => $this->certificatePath,
'ssl_key' => $this->privateKeyPath,
$config = [
'cert' => $this->alfaConfig->certificatePath,
'ssl_key' => $this->alfaConfig->privateKeyPath,
'verify' => true,
];
if ($this->alfaConfig->caBundlePath) {
$config['verify'] = $this->alfaConfig->caBundlePath;
}
return $config;
}
protected function defaultHeaders(): array
protected function defaultMiddleware(): array
{
return [
'Content-Type' => 'application/x-www-form-urlencoded', // As per doc, despite body being JSON
'key-name' => $this->keyNameAlias,
];
return [];
}
public function boot(PendingRequest $pendingRequest): void
{
// Calculate Signature
$body = $pendingRequest->body()?->all();
if (empty($body)) {
$pendingRequest->middleware()->onResponse(new AlfaResponseMiddleware());
$pendingRequest->middleware()->onRequest(new RequestIdMiddleware());
$bodyRepo = $pendingRequest->body();
if (!$bodyRepo || empty($bodyRepo->all())) {
return;
}
$body = $bodyRepo->all();
// SBP requires minified JSON for signing. Canonicalization (JCS) is the formal term.
$jsonBody = CmsSigner::canonicalize($body);
// Minify JSON for signature
$jsonBody = json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
// Sign the body using the configured private key (prefer signing key, fallback to transport key).
$keyPath = $this->alfaConfig->signingPrivateKeyPath ?: $this->alfaConfig->privateKeyPath;
// Remove spaces/newlines as per doc requirement "exclude all spaces, tabs, newlines"
// Standard json_encode without pretty print usually does this, but let's be sure.
// The doc example: `{"command":"GetQRCd",...}` implies no spaces after colons/commas.
if (!file_exists($keyPath)) {
throw new \RuntimeException("Private key file not found: $keyPath");
}
$privateKey = openssl_pkey_get_private(
file_get_contents($keyPath)
);
$privateKey = file_get_contents($this->privateKeyPath);
$pkeyId = openssl_get_privatekey($privateKey);
openssl_sign($jsonBody, $signature, $pkeyId, OPENSSL_ALGO_SHA256);
if (!$privateKey) {
throw new \RuntimeException("Could not load private key for SBP signing from: $keyPath");
}
$signature = '';
openssl_sign($jsonBody, $signature, $privateKey, OPENSSL_ALGO_SHA256);
$signatureBase64 = base64_encode($signature);
// Mutate the request to add SBP-specific headers and set the exact signed body.
$pendingRequest->headers()->add('Authorization', $signatureBase64);
// The SBP API documentation specifies this content-type for signed requests.
$pendingRequest->headers()->add('Content-Type', 'application/x-www-form-urlencoded');
// Ensure the body sent is the exact string we signed?
// Saloon will encode the array to JSON again.
// We should enforce the body to be the string we just signed to ensure consistency.
$pendingRequest->body()->set($jsonBody);
// Note: Saloon/Guzzle might re-serialize the body.
// For strict SBP signing, ensuring the body sent matches $jsonBody exactly is crucial.
// Saloon's HasJsonBody trait usually handles this fine as long as array order is preserved.
}
}
public function withCaBundle(string $path): static
{
$this->config()->merge(['verify' => $path]);
return $this;
}
public function paginate(Request $request): OffsetPaginator
{
return new AlfaOffsetPaginator($this, $request);
}
}