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); } }