145 lines
4.6 KiB
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);
|
|
}
|
|
} |