Files
alfa-bank-sdk/src/Sbp/SbpConnector.php

145 lines
4.6 KiB
PHP

<?php
declare(strict_types=1);
namespace Apoloz\AlfaBank\Sbp;
use Apoloz\AlfaBank\Core\AlfaConfig;
use Apoloz\AlfaBank\Http\Middleware\RequestIdMiddleware;
use Apoloz\AlfaBank\Http\Pagination\AlfaOffsetPaginator;
use Apoloz\AlfaBank\Traits\HasAlfaErrorHandling;
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\Plugins\AlwaysThrowOnErrors;
use Saloon\Traits\RequestProperties\HasTries;
use Saloon\Traits\Plugins\HasTimeout;
class SbpConnector extends Connector implements HasPagination
{
use HasTimeout;
use HasTries;
use AlwaysThrowOnErrors;
use HasAlfaErrorHandling;
public function __construct(
protected AlfaConfig $alfaConfig,
protected string $keyNameAlias
) {
$this->tries = 3;
$this->retryInterval = 500;
}
public function resolveBaseUrl(): string
{
return $this->alfaConfig->baseUrl;
}
/**
* Parse payment notification from request body.
*/
public function parsePaymentNotification(array $data): \Apoloz\AlfaBank\Sbp\Data\Webhooks\SbpPaymentNotificationData
{
return \Apoloz\AlfaBank\Sbp\Data\Webhooks\SbpPaymentNotificationData::from($data);
}
/**
* Parse refund notification from request body.
*/
public function parseRefundNotification(array $data): \Apoloz\AlfaBank\Sbp\Data\Webhooks\SbpRefundNotificationData
{
return \Apoloz\AlfaBank\Sbp\Data\Webhooks\SbpRefundNotificationData::from($data);
}
/**
* Parse digital ruble payment notification from request body.
*/
public function parseDigitalRubleNotification(array $data): \Apoloz\AlfaBank\Sbp\Data\DigitalRuble\Notifications\PaymentDrNotificationData
{
return \Apoloz\AlfaBank\Sbp\Data\DigitalRuble\Notifications\PaymentDrNotificationData::from($data);
}
protected function defaultConfig(): array
{
$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
{
return [
'Accept' => 'application/json',
];
}
protected function defaultMiddleware(): array
{
return [
new RequestIdMiddleware(),
];
}
public function boot(PendingRequest $pendingRequest): void
{
$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);
// Sign the body using the configured private key (prefer signing key, fallback to transport key).
$keyPath = $this->alfaConfig->signingPrivateKeyPath ?: $this->alfaConfig->privateKeyPath;
if (!file_exists($keyPath)) {
throw new \RuntimeException("Private key file not found: $keyPath");
}
$privateKey = openssl_pkey_get_private(
file_get_contents($keyPath)
);
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);
$pendingRequest->headers()->add('key-name', $this->keyNameAlias);
// The SBP API documentation specifies this content-type for signed requests.
$pendingRequest->headers()->add('Content-Type', 'application/x-www-form-urlencoded');
// 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);
}
}