refactor: dto, requests, connector

This commit is contained in:
2026-01-19 10:46:57 +03:00
parent b2786d8606
commit 78e1e82331
32 changed files with 894 additions and 42 deletions

View File

@@ -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.

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use Apoloz\AlfaBank\AlfaBank;
use Apoloz\AlfaBank\Enums\Environment;
use Apoloz\AlfaBank\Requests\SelfEmployed\RegisterIncomeRequest;
use Apoloz\AlfaBank\Data\SelfEmployed\CreateIncomeRequestData;
/**
* Пример регистрации дохода самозанятого.
* Позволяет автоматически задекларировать доход в ФНС от лица самозанятого.
*/
$baas = AlfaBank::baas(
certificatePath: __DIR__ . '/../tests/test_cert.pem',
privateKeyPath: __DIR__ . '/../tests/test_key.pem',
environment: Environment::SANDBOX
);
// Данные о доходе
$incomeData = new CreateIncomeRequestData(
vatin: '7700123456', // ИНН самозанятого
name: 'Оплата за разработку ПО по договору №123',
amount: '15000.00',
date: date('Y-m-d'),
organizationName: 'ООО Ромашка' // Если плательщик - ЮЛ
);
try {
$response = $baas->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";
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use Apoloz\AlfaBank\AlfaBank;
use Apoloz\AlfaBank\Enums\Environment;
use Apoloz\AlfaBank\Sbp\Requests\Reversal\GetQrcReversalHistoryRequest;
/**
* Пример получения истории возвратов по транзакции СБП.
* Полезно для отладки частичных возвратов или поиска зависших операций.
*/
$sbp = AlfaBank::sbp(
certificatePath: __DIR__ . '/../tests/test_cert.pem',
privateKeyPath: __DIR__ . '/../tests/test_key.pem',
keyNameAlias: 'my-alias',
environment: Environment::SANDBOX
);
$payrrn = '000000022112'; // Референс изначального платежа
$termNo = '90080567';
try {
$response = $sbp->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";
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use Apoloz\AlfaBank\AlfaBank;
use Apoloz\AlfaBank\Enums\Environment;
use Apoloz\AlfaBank\Requests\Files\UploadFileRequest;
/**
* Пример загрузки файла для KYC (Знай своего клиента).
* Сначала создается бенефициар, банк возвращает ссылку для загрузки паспорта.
* Затем SDK загружает файл по этой ссылке.
*/
$baas = AlfaBank::baas(
certificatePath: __DIR__ . '/../tests/test_cert.pem',
privateKeyPath: __DIR__ . '/../tests/test_key.pem',
environment: Environment::SANDBOX
);
// 1. Предположим, мы получили ссылку из ответа CreateBeneficiaryRequest
$uploadUrl = 'https://baas.alfabank.ru/filebox/secure-file-transfer/12345?X-Amz-Signature=...';
// 2. Открываем файл
$filePath = __DIR__ . '/../tests/test_cert.pem'; // Для примера берем любой файл
$fileResource = fopen($filePath, 'r');
try {
// 3. Загружаем файл методом PUT (как требует Alfa Bank S3 Proxy)
$response = $baas->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);
}
}

View File

@@ -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;
}
// </SECTION>
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Apoloz\AlfaBank\Data\SelfEmployed;
use Spatie\LaravelData\Data;
class CreateIncomeRequestData extends Data
{
public function __construct(
public string $vatin,
public string $name,
public string $amount,
public string $date,
public ?string $inn = null,
public ?string $organizationName = null,
) {}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Apoloz\AlfaBank\Data\SelfEmployed;
use Spatie\LaravelData\Data;
class CreateIncomeResponseData extends Data
{
public function __construct(
public string $requestId,
public string $status,
) {}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Apoloz\AlfaBank\Http\Middleware;
use Apoloz\AlfaBank\Utils\JwsSigner;
use Saloon\Http\Response;
class VerifyJwsResponseMiddleware
{
public function __construct(
protected ?string $publicKey = null
) {}
public function __invoke(Response $response): Response
{
$contentType = $response->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;
}
}

View File

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

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Apoloz\AlfaBank\Requests\Accounts;
use Saloon\Enums\Method;
use Saloon\Http\Request;
class GetAccountStatusRequest extends Request
{
protected Method $method = Method::GET;
public function __construct(
protected string $accountNumber
) {}
public function resolveEndpoint(): string
{
return "/api/gd/v1/accounts/{$this->accountNumber}/status";
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Apoloz\AlfaBank\Requests\B2C;
use Saloon\Enums\Method;
use Saloon\Http\Request;
class GetB2CPackageStatusRequest extends Request
{
protected Method $method = Method::GET;
public function __construct(
protected string $packageId
) {}
public function resolveEndpoint(): string
{
return "/api/b2cpay/jp/v1/payments/packages/{$this->packageId}";
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Apoloz\AlfaBank\Requests\DigitalIdentity;
use Saloon\Enums\Method;
use Saloon\Http\Request;
class GetAccountChannelStatusRequest extends Request
{
protected Method $method = Method::GET;
public function __construct(
protected string $accountNumber
) {}
public function resolveEndpoint(): string
{
return "/api/gd/v1/accounts/{$this->accountNumber}/status";
}
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Apoloz\AlfaBank\Requests\Files;
use Saloon\Contracts\Body\HasBody;
use Saloon\Enums\Method;
use Saloon\Http\Request;
use Saloon\Traits\Body\HasStreamBody;
class UploadFileRequest extends Request implements HasBody
{
use HasStreamBody;
protected Method $method = Method::PUT;
/**
* @param string $url Full pre-signed URL from Alfa Bank
* @param mixed $resource File resource or stream
*/
public function __construct(
protected string $url,
protected $resource
) {}
public function resolveEndpoint(): string
{
return $this->url;
}
protected function defaultHeaders(): array
{
return [
'Accept' => '*/*',
'Content-Type' => 'application/octet-stream',
];
}
protected function defineBody(): mixed
{
return $this->resource;
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Apoloz\AlfaBank\Requests\Partner;
use Spatie\LaravelData\Data;
use Saloon\Contracts\Body\HasBody;
use Saloon\Enums\Method;
use Saloon\Http\Request;
use Saloon\Traits\Body\HasJsonBody;
class CreateIndividualRequest extends Request implements HasBody
{
use HasJsonBody;
protected Method $method = Method::POST;
public function __construct(
protected array $data
) {}
public function resolveEndpoint(): string
{
return '/partner/jp/v1/individuals';
}
protected function defaultBody(): array
{
return $this->data;
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Apoloz\AlfaBank\Requests\Poa;
use Saloon\Enums\Method;
use Saloon\Http\Request;
class DownloadPoaRequest extends Request
{
protected Method $method = Method::GET;
public function __construct(
protected string $requestUuid
) {}
public function resolveEndpoint(): string
{
return "/api/jp/v1/poa/requests/{$this->requestUuid}/download";
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Apoloz\AlfaBank\Requests\Registries;
use Saloon\Enums\Method;
use Saloon\Http\Request;
class GetRegistryPaymentsRequest extends Request
{
protected Method $method = Method::GET;
public function __construct(
protected string $registryId
) {}
public function resolveEndpoint(): string
{
return "/api/jp/v1/registries/{$this->registryId}/payments";
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Apoloz\AlfaBank\Requests\Registries;
use Saloon\Enums\Method;
use Saloon\Http\Request;
class GetRegistryRequest extends Request
{
protected Method $method = Method::GET;
public function __construct(
protected string $registryId
) {}
public function resolveEndpoint(): string
{
return "/api/jp/v1/registries/{$this->registryId}";
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Apoloz\AlfaBank\Requests\SelfEmployed;
use Saloon\Enums\Method;
use Saloon\Http\Request;
class GetRegionsRequest extends Request
{
protected Method $method = Method::GET;
public function resolveEndpoint(): string
{
return '/semp/v1/dictionary/regions';
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Apoloz\AlfaBank\Requests\SelfEmployed;
use Apoloz\AlfaBank\Data\SelfEmployed\CreateIncomeRequestData;
use Apoloz\AlfaBank\Data\SelfEmployed\CreateIncomeResponseData;
use Saloon\Contracts\Body\HasBody;
use Saloon\Enums\Method;
use Saloon\Http\Request;
use Saloon\Http\Response;
use Saloon\Traits\Body\HasJsonBody;
class RegisterIncomeRequest extends Request implements HasBody
{
use HasJsonBody;
protected Method $method = Method::POST;
public function __construct(
protected CreateIncomeRequestData $data
) {}
public function resolveEndpoint(): string
{
return '/semp/v1/selfemployed/income';
}
protected function defaultBody(): array
{
return $this->data->toArray();
}
public function createDtoFromResponse(Response $response): CreateIncomeResponseData
{
return CreateIncomeResponseData::from($response->json());
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Apoloz\AlfaBank\Requests\Signature;
use Saloon\Enums\Method;
use Saloon\Http\Request;
class GetDistinguishedNameRequest extends Request
{
protected Method $method = Method::GET;
public function resolveEndpoint(): string
{
return '/jp/v3/signature/dn';
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Apoloz\AlfaBank\Sbp\Data\Reversal;
use Spatie\LaravelData\Attributes\DataCollectionOf;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\DataCollection;
class ReversalHistoryResponseData extends Data
{
public function __construct(
public int $ErrorCode,
public string $message,
#[DataCollectionOf(ReversalHistoryItemData::class)]
public ?DataCollection $refunds = null,
) {}
}
class ReversalHistoryItemData extends Data
{
public function __construct(
public string $TermNo,
public string $amount,
public string $originalQrcId,
public string $originalTrxId,
public string $payerId,
public string $status,
public ?string $trxId = null,
public ?string $trxDT = null,
) {}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Apoloz\AlfaBank\Sbp\Data\Webhooks;
use Spatie\LaravelData\Data;
class SbpPaymentNotificationData extends Data
{
public function __construct(
public string $command,
public string $qrcId,
public string $trxId,
public string $status,
public string $amount,
public string $timestamp,
public string $payerId,
public string $payrrn,
public string $TermNo,
public ?string $kzo = null,
public ?string $CreditDatePos = null,
) {}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Apoloz\AlfaBank\Sbp\Data\Webhooks;
use Spatie\LaravelData\Data;
class SbpRefundNotificationData extends Data
{
public function __construct(
public string $command,
public string $TermNo,
public string $originalQrcId,
public string $originalTrxId,
public string $trxId,
public string $trxDT,
public string $status,
public string $amount,
public string $ErrorCode,
public string $message,
public string $payerId,
public ?string $messageID = null,
public ?string $agentRefundRequestId = null,
public ?string $opkcRefundRequestId = null,
) {}
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Apoloz\AlfaBank\Sbp\Requests\Reversal;
use Apoloz\AlfaBank\Sbp\Data\Reversal\ReversalHistoryResponseData;
use Saloon\Contracts\Body\HasBody;
use Saloon\Enums\Method;
use Saloon\Http\Request;
use Saloon\Http\Response;
use Saloon\Traits\Body\HasJsonBody;
class GetQrcReversalHistoryRequest extends Request implements HasBody
{
use HasJsonBody;
protected Method $method = Method::POST;
public function __construct(
protected string $payrrn,
protected string $TermNo,
) {}
public function resolveEndpoint(): string
{
return '';
}
protected function defaultBody(): array
{
return [
'command' => 'GetQRCreversalHistory',
'TermNo' => $this->TermNo,
'payrrn' => $this->payrrn,
];
}
public function createDtoFromResponse(Response $response): ReversalHistoryResponseData
{
return ReversalHistoryResponseData::from($response->json());
}
}

View File

@@ -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');

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,31 @@
<?php
use Apoloz\AlfaBank\Sbp\SbpConnector;
use Apoloz\AlfaBank\Core\AlfaConfig;
test('can parse payment notification webhook', function () {
$config = new AlfaConfig(
baseUrl: 'https://test.com',
certificatePath: __DIR__ . '/../../test_key.pem',
privateKeyPath: __DIR__ . '/../../test_key.pem'
);
$connector = new SbpConnector($config, 'alias');
$payload = [
'command' => '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');
});

View File

@@ -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('Москва');
});

View File

@@ -0,0 +1,41 @@
<?php
use Apoloz\AlfaBank\Core\AlfaBankConnector;
use Apoloz\AlfaBank\Core\AlfaConfig;
use Apoloz\AlfaBank\Services\OpenSslCmsSigner;
use Apoloz\AlfaBank\Requests\SelfEmployed\RegisterIncomeRequest;
use Apoloz\AlfaBank\Data\SelfEmployed\CreateIncomeRequestData;
use Saloon\Http\Faking\MockResponse;
use Saloon\Http\Faking\MockClient;
test('can register income', function () {
$mockClient = new MockClient([
RegisterIncomeRequest::class => 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');
});

View File

@@ -0,0 +1,28 @@
<?php
use Apoloz\AlfaBank\Core\AlfaBankConnector;
use Apoloz\AlfaBank\Core\AlfaConfig;
use Apoloz\AlfaBank\Services\OpenSslCmsSigner;
use Apoloz\AlfaBank\Requests\Signature\GetDistinguishedNameRequest;
use Saloon\Http\Faking\MockResponse;
use Saloon\Http\Faking\MockClient;
test('can get distinguished name', function () {
$mockClient = new MockClient([
GetDistinguishedNameRequest::class => 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');
});