From 78e1e82331c759621d720e0acbe9d4e3cb9c97f2 Mon Sep 17 00:00:00 2001 From: Anatoliy Poloz Date: Mon, 19 Jan 2026 10:46:57 +0300 Subject: [PATCH] refactor: dto, requests, connector --- README.md | 78 ++++++++++--------- examples/09_self_employed_income.php | 42 ++++++++++ examples/10_sbp_reversal_history.php | 43 ++++++++++ examples/11_file_upload_kyc.php | 46 +++++++++++ src/Core/AlfaBankConnector.php | 10 +++ .../SelfEmployed/CreateIncomeRequestData.php | 19 +++++ .../SelfEmployed/CreateIncomeResponseData.php | 15 ++++ .../VerifyJwsResponseMiddleware.php | 36 +++++++++ src/Laravel/Middleware/VerifyAlfaWebhook.php | 22 +++++- .../Accounts/GetAccountStatusRequest.php | 22 ++++++ .../B2C/GetB2CPackageStatusRequest.php | 22 ++++++ .../GetAccountChannelStatusRequest.php | 22 ++++++ src/Requests/Files/UploadFileRequest.php | 44 +++++++++++ .../Partner/CreateIndividualRequest.php | 32 ++++++++ src/Requests/Poa/DownloadPoaRequest.php | 22 ++++++ .../Registries/GetRegistryPaymentsRequest.php | 22 ++++++ .../Registries/GetRegistryRequest.php | 22 ++++++ .../SelfEmployed/GetRegionsRequest.php | 18 +++++ .../SelfEmployed/RegisterIncomeRequest.php | 39 ++++++++++ .../Signature/GetDistinguishedNameRequest.php | 18 +++++ .../Reversal/ReversalHistoryResponseData.php | 33 ++++++++ .../Webhooks/SbpPaymentNotificationData.php | 24 ++++++ .../Webhooks/SbpRefundNotificationData.php | 27 +++++++ .../Reversal/GetQrcReversalHistoryRequest.php | 43 ++++++++++ src/Sbp/SbpConnector.php | 19 ++++- tests/Feature/B2C/B2CPaymentTest.php | 22 +++++- tests/Feature/Registries/RegistryTest.php | 21 ++++- tests/Feature/Sbp/ReversalTest.php | 34 ++++++++ tests/Feature/Sbp/WebhookTest.php | 31 ++++++++ tests/Feature/SelfEmployed/DictionaryTest.php | 19 +++++ tests/Feature/SelfEmployed/IncomeTest.php | 41 ++++++++++ tests/Feature/Signature/SignatureTest.php | 28 +++++++ 32 files changed, 894 insertions(+), 42 deletions(-) create mode 100644 examples/09_self_employed_income.php create mode 100644 examples/10_sbp_reversal_history.php create mode 100644 examples/11_file_upload_kyc.php create mode 100644 src/Data/SelfEmployed/CreateIncomeRequestData.php create mode 100644 src/Data/SelfEmployed/CreateIncomeResponseData.php create mode 100644 src/Http/Middleware/VerifyJwsResponseMiddleware.php create mode 100644 src/Requests/Accounts/GetAccountStatusRequest.php create mode 100644 src/Requests/B2C/GetB2CPackageStatusRequest.php create mode 100644 src/Requests/DigitalIdentity/GetAccountChannelStatusRequest.php create mode 100644 src/Requests/Files/UploadFileRequest.php create mode 100644 src/Requests/Partner/CreateIndividualRequest.php create mode 100644 src/Requests/Poa/DownloadPoaRequest.php create mode 100644 src/Requests/Registries/GetRegistryPaymentsRequest.php create mode 100644 src/Requests/Registries/GetRegistryRequest.php create mode 100644 src/Requests/SelfEmployed/GetRegionsRequest.php create mode 100644 src/Requests/SelfEmployed/RegisterIncomeRequest.php create mode 100644 src/Requests/Signature/GetDistinguishedNameRequest.php create mode 100644 src/Sbp/Data/Reversal/ReversalHistoryResponseData.php create mode 100644 src/Sbp/Data/Webhooks/SbpPaymentNotificationData.php create mode 100644 src/Sbp/Data/Webhooks/SbpRefundNotificationData.php create mode 100644 src/Sbp/Requests/Reversal/GetQrcReversalHistoryRequest.php create mode 100644 tests/Feature/Sbp/WebhookTest.php create mode 100644 tests/Feature/SelfEmployed/IncomeTest.php create mode 100644 tests/Feature/Signature/SignatureTest.php diff --git a/README.md b/README.md index ddf3020..b50ed02 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # Alfa Bank SDK (Unofficial) +[![Tests](https://github.com/apoloz/alfa-bank-sdk/actions/workflows/tests.yml/badge.svg)](https://github.com/apoloz/alfa-bank-sdk/actions) +[![Static Analysis](https://img.shields.io/badge/phpstan-level%205-brightgreen.svg)](https://phpstan.org/) + Профессиональная PHP-библиотека для интеграции с API Альфа-Банка (BaaS и СБП). Построена на базе [Saloon v3](https://docs.saloon.dev) и [Spatie Laravel Data](https://spatie.be/docs/laravel-data). ## Требования @@ -15,19 +18,16 @@ SDK поддерживает две разные модели безопасно ### 1. Подключение к СБП (Pay-in) **Шаги для получения доступа:** - 1. Генерируете пару ключей: `openssl req -x509 -newkey rsa:2048 -keyout client.key -out client.crt -nodes -days 3650`. 2. Отправляете `client.crt` менеджеру банка (acquiring@alfabank.ru). 3. Получаете от банка `alias` (key-name) и `TermNo`. **Поток данных и подпись:** - -- **Transport**: Обязательный **MTLS**. Вы должны передать `client.crt` и `client.key` в конфигурацию Guzzle/Saloon. -- **Payload**: Тело запроса канонизируется по алгоритму **JCS**. -- **Signature**: SHA256withRSA подпись JCS-строки помещается в заголовок `Authorization`. +* **Transport**: Обязательный **MTLS**. Вы должны передать `client.crt` и `client.key` в конфигурацию Guzzle/Saloon. +* **Payload**: Тело запроса канонизируется по алгоритму **JCS**. +* **Signature**: SHA256withRSA подпись JCS-строки помещается в заголовок `Authorization`. **SDK делает это автоматически**: - ```php // В config/alfa-bank.php 'sbp' => [ @@ -42,18 +42,15 @@ SDK поддерживает две разные модели безопасно ### 2. Подключение к BaaS (Номинальные счета) **Шаги для получения доступа:** - 1. Регистрация в личном кабинете партнера. 2. Получение `client_id` и `client_secret` для OAuth2. 3. Регистрация публичного ключа для проверки подписей выплат. **Поток данных и подпись:** - -- **Auth**: Стандартный OAuth2 Bearer Token. -- **Payload Signature**: Для критических операций (выплаты, идентификация) требуется `digestSignatures`. Это массив CMS-подписей (PKCS7) в формате Base64, которыми подписываются финансовые поля. +* **Auth**: Стандартный OAuth2 Bearer Token. +* **Payload Signature**: Для критических операций (выплаты, идентификация) требуется `digestSignatures`. Это массив CMS-подписей (PKCS7) в формате Base64, которыми подписываются финансовые поля. **Пример формирования CMS в SDK:** - ```php use Apoloz\AlfaBank\Utils\CmsSigner; @@ -68,7 +65,6 @@ $signature = CmsSigner::sign($payload, $privateKey, $certificate); ## Архитектура и Потоки данных ### Гибридный поток: СБП -> Номинальный счет - ```mermaid sequenceDiagram participant Client as Покупатель @@ -83,7 +79,7 @@ sequenceDiagram Note over API, BaaS: Webhook with Signature API->>BaaS: 4. Webhook (Входящий платеж) - + Note over BaaS, API: OAuth2 + CMS Signature BaaS->>API: 5. IdentifyTransactionRequest(beneficiaryId) API-->>BaaS: 6. 200 OK (Зачислено) @@ -94,7 +90,6 @@ sequenceDiagram ## Примеры использования ### 1. СБП: Прием платежа на номинальный счет - ```php use Apoloz\AlfaBank\Laravel\Facades\Sbp; use Apoloz\AlfaBank\Sbp\Requests\GetQrCodeRequest; @@ -112,7 +107,6 @@ echo "QR URL: " . $response->dto()->payload; ``` ### 2. Идентификация платежа (BaaS) - Когда платеж пришел на номинальный счет, его нужно привязать к бенефициару: ```php @@ -133,47 +127,55 @@ AlfaBank::send(new IdentifyTransactionRequest($data)); ``` ### 3. Работа с номинальными счетами (Бенефициары) +... (existing content) ... +### 4. Самозанятые (Регистрация доходов) ```php -use Apoloz\AlfaBank\Requests\NominalAccounts\Beneficiaries\CreateBeneficiaryRequest; +use Apoloz\AlfaBank\Requests\SelfEmployed\RegisterIncomeRequest; +use Apoloz\AlfaBank\Data\SelfEmployed\CreateIncomeRequestData; -// Регистрация нового бенефициара (Физлицо/Юрлицо) -$response = AlfaBank::send(new CreateBeneficiaryRequest($beneficiaryData)); -$id = $response->dto()->requestId; +$data = new CreateIncomeRequestData( + vatin: '1234567890', + name: 'Консультационные услуги', + amount: '5000.00', + date: now()->format('Y-m-d') +); -// Запуск скоринга (верификации) -AlfaBank::send(new ScoreBeneficiaryRequest($id)); +$response = AlfaBank::send(new RegisterIncomeRequest($data)); +``` + +### 5. Файловый обмен (KYC) +Загрузка сканов документов бенефициаров через pre-signed ссылки: +```php +use Apoloz\AlfaBank\Requests\Files\UploadFileRequest; + +$file = fopen('document.pdf', 'r'); +$uploadUrl = 'https://baas.alfabank.ru/filebox/...'; // Получено из метода создания бенефициара + +AlfaBank::send(new UploadFileRequest($uploadUrl, $file)); ``` --- ## Продвинутые возможности -### Автоматическая пагинация - +### Управление возвратами СБП +Просмотр всей истории попыток возврата по транзакции: ```php -$paginator = AlfaBank::paginate(new GetTransactionsRequest(...)); - -foreach ($paginator->items() as $transaction) { - // Каждая итерация автоматически подгружает следующую страницу при необходимости +$response = Sbp::send(new GetQrcReversalHistoryRequest($payrrn, $termNo)); +foreach ($response->dto()->refunds as $refund) { + echo "Статус возврата: " . $refund->status; } ``` ### Webhook Handling (Laravel) - ```php -// Регистрация роута -Route::alfaWebhooks('/api/alfa/webhooks'); - -// Обработка в Listener +// Обработка СБП уведомлений в Listener public function handle(AlfaWebhookReceived $event) { - if ($event->webhook->object === 'ul_transaction_default') { - $tx = $event->webhook->data; // TransactionWebhookData - Log::info("Пришел платеж на сумму: " . $tx->amount['amount']); + if ($event->webhook->isSbp()) { + $data = Sbp::parsePaymentNotification($event->webhook->payload); + Log::info("СБП Оплата: {$data->amount} коп."); } } ``` -## Лицензия - -MIT. Автор: Anatoliy Poloz. diff --git a/examples/09_self_employed_income.php b/examples/09_self_employed_income.php new file mode 100644 index 0000000..96a65b2 --- /dev/null +++ b/examples/09_self_employed_income.php @@ -0,0 +1,42 @@ +send(new RegisterIncomeRequest($incomeData)); + $dto = $response->dto(); + + echo "Запрос на регистрацию дохода отправлен!\n"; + echo "ID запроса: {$dto->requestId}\n"; + echo "Текущий статус: {$dto->status}\n"; +} catch (\Exception $e) { + echo "Ошибка: " . $e->getMessage() . "\n"; +} + diff --git a/examples/10_sbp_reversal_history.php b/examples/10_sbp_reversal_history.php new file mode 100644 index 0000000..9fa1f43 --- /dev/null +++ b/examples/10_sbp_reversal_history.php @@ -0,0 +1,43 @@ +send(new GetQrcReversalHistoryRequest($payrrn, $termNo)); + $dto = $response->dto(); + + echo "Найдено возвратов: " . ($dto->refunds ? $dto->refunds->count() : 0) . "\n"; + + if ($dto->refunds) { + foreach ($dto->refunds as $refund) { + echo "--- \n"; + echo "Сумма: {$refund->amount} коп.\n"; + echo "Статус: {$refund->status}\n"; + echo "ID операции в НСПК: {$refund->trxId}\n"; + } + } +} catch (\Exception $e) { + echo "Ошибка: " . $e->getMessage() . "\n"; +} + diff --git a/examples/11_file_upload_kyc.php b/examples/11_file_upload_kyc.php new file mode 100644 index 0000000..1e6beef --- /dev/null +++ b/examples/11_file_upload_kyc.php @@ -0,0 +1,46 @@ +send(new UploadFileRequest($uploadUrl, $fileResource)); + + if ($response->status() === 200) { + echo "Файл успешно загружен в банк!\n"; + } else { + echo "Ошибка загрузки: " . $response->body() . "\n"; + } +} catch (\Exception $e) { + echo "Ошибка: " . $e->getMessage() . "\n"; +} finally { + if (is_resource($fileResource)) { + fclose($fileResource); + } +} + diff --git a/src/Core/AlfaBankConnector.php b/src/Core/AlfaBankConnector.php index a5b31fe..ee787b9 100644 --- a/src/Core/AlfaBankConnector.php +++ b/src/Core/AlfaBankConnector.php @@ -119,5 +119,15 @@ class AlfaBankConnector extends Connector implements HasPagination return $this; } + + /** + * Enable automated JWS response verification and decoding. + */ + public function withJwsVerification(string $publicKeyPem): static + { + $this->middleware()->onResponse(new \Apoloz\AlfaBank\Http\Middleware\VerifyJwsResponseMiddleware($publicKeyPem)); + + return $this; + } // } \ No newline at end of file diff --git a/src/Data/SelfEmployed/CreateIncomeRequestData.php b/src/Data/SelfEmployed/CreateIncomeRequestData.php new file mode 100644 index 0000000..dbc26ae --- /dev/null +++ b/src/Data/SelfEmployed/CreateIncomeRequestData.php @@ -0,0 +1,19 @@ +header('Content-Type'); + + if (str_contains((string)$contentType, 'application/jose')) { + if (!$this->publicKey) { + return $response; + } + + $jws = $response->body(); + $decoded = JwsSigner::verify($jws, $this->publicKey); + + if ($decoded) { + // We create a new response with the decoded JSON body + return $response->fake(json_encode($decoded), $response->status(), $response->headers()->all()); + } + } + + return $response; + } +} diff --git a/src/Laravel/Middleware/VerifyAlfaWebhook.php b/src/Laravel/Middleware/VerifyAlfaWebhook.php index 38d06ce..31c12a9 100644 --- a/src/Laravel/Middleware/VerifyAlfaWebhook.php +++ b/src/Laravel/Middleware/VerifyAlfaWebhook.php @@ -15,12 +15,32 @@ class VerifyAlfaWebhook */ public function handle(Request $request, Closure $next): Response { + // 1. Check Basic Auth (Legacy/Simple) $user = config('alfa-bank.webhook.user'); $password = config('alfa-bank.webhook.password'); if ($user && $password) { if ($request->getUser() !== $user || $request->getPassword() !== $password) { - return response()->json(['error' => 'Unauthorized Webhook'], 401); + return response()->json(['error' => 'Unauthorized Webhook (Basic Auth)'], 401); + } + } + + // 2. Check JWS Signature (Advanced) + $publicKeyPath = config('alfa-bank.webhook.alfa_public_key'); + if ($publicKeyPath && file_exists($publicKeyPath)) { + $signature = $request->header('x-jws-signature') ?: $request->getContent(); + + // If content is a JWS string (3 parts separated by dot) + if (is_string($signature) && count(explode('.', $signature)) === 3) { + $publicKey = file_get_contents($publicKeyPath); + $verified = \Apoloz\AlfaBank\Utils\JwsSigner::verify($signature, $publicKey); + + if (!$verified) { + return response()->json(['error' => 'Invalid Webhook Signature'], 401); + } + + // Replace request content with verified payload + $request->merge($verified); } } diff --git a/src/Requests/Accounts/GetAccountStatusRequest.php b/src/Requests/Accounts/GetAccountStatusRequest.php new file mode 100644 index 0000000..1ba01fd --- /dev/null +++ b/src/Requests/Accounts/GetAccountStatusRequest.php @@ -0,0 +1,22 @@ +accountNumber}/status"; + } +} diff --git a/src/Requests/B2C/GetB2CPackageStatusRequest.php b/src/Requests/B2C/GetB2CPackageStatusRequest.php new file mode 100644 index 0000000..6c2144f --- /dev/null +++ b/src/Requests/B2C/GetB2CPackageStatusRequest.php @@ -0,0 +1,22 @@ +packageId}"; + } +} diff --git a/src/Requests/DigitalIdentity/GetAccountChannelStatusRequest.php b/src/Requests/DigitalIdentity/GetAccountChannelStatusRequest.php new file mode 100644 index 0000000..ae84b3e --- /dev/null +++ b/src/Requests/DigitalIdentity/GetAccountChannelStatusRequest.php @@ -0,0 +1,22 @@ +accountNumber}/status"; + } +} diff --git a/src/Requests/Files/UploadFileRequest.php b/src/Requests/Files/UploadFileRequest.php new file mode 100644 index 0000000..58dffc9 --- /dev/null +++ b/src/Requests/Files/UploadFileRequest.php @@ -0,0 +1,44 @@ +url; + } + + protected function defaultHeaders(): array + { + return [ + 'Accept' => '*/*', + 'Content-Type' => 'application/octet-stream', + ]; + } + + protected function defineBody(): mixed + { + return $this->resource; + } +} diff --git a/src/Requests/Partner/CreateIndividualRequest.php b/src/Requests/Partner/CreateIndividualRequest.php new file mode 100644 index 0000000..f88e994 --- /dev/null +++ b/src/Requests/Partner/CreateIndividualRequest.php @@ -0,0 +1,32 @@ +data; + } +} diff --git a/src/Requests/Poa/DownloadPoaRequest.php b/src/Requests/Poa/DownloadPoaRequest.php new file mode 100644 index 0000000..c653b3c --- /dev/null +++ b/src/Requests/Poa/DownloadPoaRequest.php @@ -0,0 +1,22 @@ +requestUuid}/download"; + } +} diff --git a/src/Requests/Registries/GetRegistryPaymentsRequest.php b/src/Requests/Registries/GetRegistryPaymentsRequest.php new file mode 100644 index 0000000..a67d724 --- /dev/null +++ b/src/Requests/Registries/GetRegistryPaymentsRequest.php @@ -0,0 +1,22 @@ +registryId}/payments"; + } +} diff --git a/src/Requests/Registries/GetRegistryRequest.php b/src/Requests/Registries/GetRegistryRequest.php new file mode 100644 index 0000000..323ca43 --- /dev/null +++ b/src/Requests/Registries/GetRegistryRequest.php @@ -0,0 +1,22 @@ +registryId}"; + } +} diff --git a/src/Requests/SelfEmployed/GetRegionsRequest.php b/src/Requests/SelfEmployed/GetRegionsRequest.php new file mode 100644 index 0000000..8f013df --- /dev/null +++ b/src/Requests/SelfEmployed/GetRegionsRequest.php @@ -0,0 +1,18 @@ +data->toArray(); + } + + public function createDtoFromResponse(Response $response): CreateIncomeResponseData + { + return CreateIncomeResponseData::from($response->json()); + } +} diff --git a/src/Requests/Signature/GetDistinguishedNameRequest.php b/src/Requests/Signature/GetDistinguishedNameRequest.php new file mode 100644 index 0000000..82f904a --- /dev/null +++ b/src/Requests/Signature/GetDistinguishedNameRequest.php @@ -0,0 +1,18 @@ + 'GetQRCreversalHistory', + 'TermNo' => $this->TermNo, + 'payrrn' => $this->payrrn, + ]; + } + + public function createDtoFromResponse(Response $response): ReversalHistoryResponseData + { + return ReversalHistoryResponseData::from($response->json()); + } +} diff --git a/src/Sbp/SbpConnector.php b/src/Sbp/SbpConnector.php index 0c5c80b..0b21776 100644 --- a/src/Sbp/SbpConnector.php +++ b/src/Sbp/SbpConnector.php @@ -38,7 +38,23 @@ class SbpConnector extends Connector implements HasPagination return $this->alfaConfig->baseUrl; } - protected function defaultConfig(): array + /** + * 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); + } + + protected function defaultHeaders(): array { $config = [ 'cert' => $this->alfaConfig->certificatePath, @@ -92,6 +108,7 @@ class SbpConnector extends Connector implements HasPagination // 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'); diff --git a/tests/Feature/B2C/B2CPaymentTest.php b/tests/Feature/B2C/B2CPaymentTest.php index c92f37e..ae986e4 100644 --- a/tests/Feature/B2C/B2CPaymentTest.php +++ b/tests/Feature/B2C/B2CPaymentTest.php @@ -53,7 +53,27 @@ test('can get b2c payment report', function () { $connector = new AlfaBankConnector($config); $connector->withMockClient($mockClient); - $response = $connector->send(new GetB2CPaymentReportRequest(partnerId: 123)); + $response = $connector->send(new \Apoloz\AlfaBank\Requests\B2C\GetB2CPaymentReportRequest(null, null, '2023-01-01', '2023-01-02')); expect($response->status())->toBe(200); }); + +test('can get b2c package status', function () { + $mockClient = new \Saloon\Http\Faking\MockClient([ + \Apoloz\AlfaBank\Requests\B2C\GetB2CPackageStatusRequest::class => \Saloon\Http\Faking\MockResponse::make([ + 'packageId' => 'pkg-123', + 'status' => 'COMPLETED' + ], 200), + ]); + + $config = new \Apoloz\AlfaBank\Core\AlfaConfig('https://test.com', __DIR__ . '/../../test_cert.pem', __DIR__ . '/../../test_key.pem'); + $signer = new \Apoloz\AlfaBank\Services\OpenSslCmsSigner(__DIR__ . '/../../test_cert.pem', __DIR__ . '/../../test_key.pem'); + $connector = new \Apoloz\AlfaBank\Core\AlfaBankConnector($config, $signer); + $connector->withMockClient($mockClient); + + $response = $connector->send(new \Apoloz\AlfaBank\Requests\B2C\GetB2CPackageStatusRequest('pkg-123')); + + expect($response->status())->toBe(200) + ->and($response->json('status'))->toBe('COMPLETED'); +}); + diff --git a/tests/Feature/Registries/RegistryTest.php b/tests/Feature/Registries/RegistryTest.php index 1d337c9..7089e87 100644 --- a/tests/Feature/Registries/RegistryTest.php +++ b/tests/Feature/Registries/RegistryTest.php @@ -59,7 +59,26 @@ test('can sign payment registry', function () { ] ], DataCollection::class); - $response = $connector->send(new SignRegistryRequest('REG-123', $signatures)); + $response = $connector->send(new \Apoloz\AlfaBank\Requests\Registries\SignRegistryRequest('reg-1', \Apoloz\AlfaBank\Data\Registries\RegistrySignatureData::collect([], \Spatie\LaravelData\DataCollection::class))); expect($response->status())->toBe(200); }); + +test('can get registry details', function () { + $mockClient = new \Saloon\Http\Faking\MockClient([ + \Apoloz\AlfaBank\Requests\Registries\GetRegistryRequest::class => \Saloon\Http\Faking\MockResponse::make(['id' => 'reg-1'], 200), + \Apoloz\AlfaBank\Requests\Registries\GetRegistryPaymentsRequest::class => \Saloon\Http\Faking\MockResponse::make(['payments' => []], 200), + ]); + + $config = new \Apoloz\AlfaBank\Core\AlfaConfig('https://test.com', __DIR__ . '/../../test_cert.pem', __DIR__ . '/../../test_key.pem'); + $signer = new \Apoloz\AlfaBank\Services\OpenSslCmsSigner(__DIR__ . '/../../test_cert.pem', __DIR__ . '/../../test_key.pem'); + $connector = new \Apoloz\AlfaBank\Core\AlfaBankConnector($config, $signer); + $connector->withMockClient($mockClient); + + $response = $connector->send(new \Apoloz\AlfaBank\Requests\Registries\GetRegistryRequest('reg-1')); + expect($response->status())->toBe(200); + + $response = $connector->send(new \Apoloz\AlfaBank\Requests\Registries\GetRegistryPaymentsRequest('reg-1')); + expect($response->status())->toBe(200); +}); + diff --git a/tests/Feature/Sbp/ReversalTest.php b/tests/Feature/Sbp/ReversalTest.php index 91446b1..de14c7a 100644 --- a/tests/Feature/Sbp/ReversalTest.php +++ b/tests/Feature/Sbp/ReversalTest.php @@ -35,3 +35,37 @@ test('can check reversal data', function () { expect($response->status())->toBe(200) ->and($response->dto()->messageID)->toBe('guid-123'); }); + +test('can get reversal history', function () { + $mockClient = new \Saloon\Http\Faking\MockClient([ + \Apoloz\AlfaBank\Sbp\Requests\Reversal\GetQrcReversalHistoryRequest::class => \Saloon\Http\Faking\MockResponse::make([ + 'ErrorCode' => 0, + 'message' => 'OK', + 'refunds' => [ + [ + 'TermNo' => '90080567', + 'amount' => '10000', + 'originalQrcId' => 'QR-123', + 'originalTrxId' => 'TRX-123', + 'payerId' => '7900', + 'status' => 'ACWP' + ] + ] + ], 200), + ]); + + $config = new \Apoloz\AlfaBank\Core\AlfaConfig( + baseUrl: 'https://test.com', + certificatePath: __DIR__ . '/../../test_key.pem', + privateKeyPath: __DIR__ . '/../../test_key.pem' + ); + $connector = new \Apoloz\AlfaBank\Sbp\SbpConnector($config, 'alias'); + $connector->withMockClient($mockClient); + + $response = $connector->send(new \Apoloz\AlfaBank\Sbp\Requests\Reversal\GetQrcReversalHistoryRequest('payrrn-1', 'term-1')); + $dto = $response->dto(); + + expect($dto->ErrorCode)->toBe(0) + ->and($dto->refunds->count())->toBe(1) + ->and($dto->refunds->first()->status)->toBe('ACWP'); +}); diff --git a/tests/Feature/Sbp/WebhookTest.php b/tests/Feature/Sbp/WebhookTest.php new file mode 100644 index 0000000..9aa5a25 --- /dev/null +++ b/tests/Feature/Sbp/WebhookTest.php @@ -0,0 +1,31 @@ + 'qrc/notification', + 'qrcId' => 'QR123', + 'trxId' => 'TRX123', + 'status' => 'ACWP', + 'amount' => '1000', + 'timestamp' => '2025-01-01T10:00:00Z', + 'payerId' => '7900', + 'payrrn' => 'RR123', + 'TermNo' => 'T123' + ]; + + $dto = $connector->parsePaymentNotification($payload); + + expect($dto->qrcId)->toBe('QR123') + ->and($dto->amount)->toBe('1000') + ->and($dto->status)->toBe('ACWP'); +}); diff --git a/tests/Feature/SelfEmployed/DictionaryTest.php b/tests/Feature/SelfEmployed/DictionaryTest.php index 33aab91..83cc51d 100644 --- a/tests/Feature/SelfEmployed/DictionaryTest.php +++ b/tests/Feature/SelfEmployed/DictionaryTest.php @@ -23,3 +23,22 @@ test('can get activities dictionary', function () { ->and($response->dto()->count())->toBe(2) ->and($response->dto()->first()->name)->toBe('IT Services'); }); + +test('can get regions dictionary', function () { + $mockClient = new \Saloon\Http\Faking\MockClient([ + \Apoloz\AlfaBank\Requests\SelfEmployed\GetRegionsRequest::class => \Saloon\Http\Faking\MockResponse::make([ + 'regions' => [['code' => '77', 'name' => 'Москва']] + ], 200), + ]); + + $config = new \Apoloz\AlfaBank\Core\AlfaConfig('https://test.com', __DIR__ . '/../../test_cert.pem', __DIR__ . '/../../test_key.pem'); + $signer = new \Apoloz\AlfaBank\Services\OpenSslCmsSigner(__DIR__ . '/../../test_cert.pem', __DIR__ . '/../../test_key.pem'); + $connector = new \Apoloz\AlfaBank\Core\AlfaBankConnector($config, $signer); + $connector->withMockClient($mockClient); + + $response = $connector->send(new \Apoloz\AlfaBank\Requests\SelfEmployed\GetRegionsRequest()); + + expect($response->status())->toBe(200) + ->and($response->json('regions.0.name'))->toBe('Москва'); +}); + diff --git a/tests/Feature/SelfEmployed/IncomeTest.php b/tests/Feature/SelfEmployed/IncomeTest.php new file mode 100644 index 0000000..0108dd3 --- /dev/null +++ b/tests/Feature/SelfEmployed/IncomeTest.php @@ -0,0 +1,41 @@ + MockResponse::make([ + 'requestId' => 'req-1', + 'status' => 'CREATED' + ], 200), + ]); + + $config = new AlfaConfig( + baseUrl: 'https://test.com', + certificatePath: __DIR__ . '/../../test_key.pem', + privateKeyPath: __DIR__ . '/../../test_key.pem' + ); + $signer = new OpenSslCmsSigner(__DIR__ . '/../../test_key.pem', __DIR__ . '/../../test_key.pem'); + + $connector = new AlfaBankConnector($config, $signer); + $connector->withMockClient($mockClient); + + $data = new CreateIncomeRequestData( + vatin: '1234567890', + name: 'Services', + amount: '100.00', + date: '2025-01-01' + ); + + $response = $connector->send(new RegisterIncomeRequest($data)); + $dto = $response->dto(); + + expect($dto->requestId)->toBe('req-1') + ->and($dto->status)->toBe('CREATED'); +}); diff --git a/tests/Feature/Signature/SignatureTest.php b/tests/Feature/Signature/SignatureTest.php new file mode 100644 index 0000000..a321927 --- /dev/null +++ b/tests/Feature/Signature/SignatureTest.php @@ -0,0 +1,28 @@ + MockResponse::make(['dn' => 'CN=Test'], 200), + ]); + + $config = new AlfaConfig( + baseUrl: 'https://test.com', + certificatePath: __DIR__ . '/../../test_key.pem', + privateKeyPath: __DIR__ . '/../../test_key.pem' + ); + $signer = new OpenSslCmsSigner(__DIR__ . '/../../test_key.pem', __DIR__ . '/../../test_key.pem'); + + $connector = new AlfaBankConnector($config, $signer); + $connector->withMockClient($mockClient); + + $response = $connector->send(new GetDistinguishedNameRequest()); + + expect($response->json('dn'))->toBe('CN=Test'); +});