Улучшена документация, добавлена поддержка цифрового рубля и улучшена структура DTO

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
2026-01-21 12:46:02 +03:00
parent 58affe3e97
commit 60c99bca40
30 changed files with 385 additions and 156 deletions

View File

@@ -8,6 +8,77 @@
- [Saloon](https://docs.saloon.dev) v3.0+
- [Spatie Laravel Data](https://spatie.be/docs/laravel-data)
## Установка
```bash
composer require apoloz/alfa-bank-sdk
```
## Конфигурация (Laravel)
Опубликуйте файл конфигурации:
```bash
php artisan vendor:publish --tag=alfa-bank-config
```
### Настройка `.env`
Добавьте следующие переменные в ваш файл `.env`. Вам понадобятся сертификаты (PEM) и ключи.
```dotenv
# --- BaaS (Номинальные счета, Выплаты) ---
ALFA_BAAS_ENVIRONMENT=sandbox # или production
ALFA_BAAS_CLIENT_ID=your-client-id
ALFA_BAAS_CLIENT_SECRET=your-client-secret
ALFA_BAAS_CERT_PATH=storage/certs/baas_cert.pem
ALFA_BAAS_KEY_PATH=storage/certs/baas_key.pem
# Опционально: Ключи для CMS-подписи (если отличаются от транспортных)
# ALFA_BAAS_SIGNING_CERT_PATH=storage/certs/sign_cert.pem
# ALFA_BAAS_SIGNING_KEY_PATH=storage/certs/sign_key.pem
# --- SBP (СБП, QR-коды) ---
ALFA_SBP_ENVIRONMENT=sandbox # или production
ALFA_SBP_ALIAS=your-key-name-alias
ALFA_SBP_CERT_PATH=storage/certs/sbp_cert.pem
ALFA_SBP_KEY_PATH=storage/certs/sbp_key.pem
# --- Webhooks ---
ALFA_WEBHOOK_USER=alfa
ALFA_WEBHOOK_PASSWORD=secret
# Для проверки JWS-подписи вебхуков (опционально)
ALFA_WEBHOOK_PUBLIC_KEY=storage/certs/alfa_public_key.pem
```
### Файл конфигурации (`config/alfa-bank.php`)
Вот как выглядит структура конфигурационного файла по умолчанию. Вы можете использовать его для тонкой настройки, например, чтобы задать пути динамически.
```php
return [
'baas' => [
'environment' => env('ALFA_BAAS_ENVIRONMENT', 'sandbox'),
'client_id' => env('ALFA_BAAS_CLIENT_ID'),
'client_secret' => env('ALFA_BAAS_CLIENT_SECRET'),
'certificate_path' => env('ALFA_BAAS_CERT_PATH'),
'private_key_path' => env('ALFA_BAAS_KEY_PATH'),
// ...
],
'sbp' => [
'environment' => env('ALFA_SBP_ENVIRONMENT', 'sandbox'),
'key_name_alias' => env('ALFA_SBP_ALIAS'),
'certificate_path' => env('ALFA_SBP_CERT_PATH'),
'private_key_path' => env('ALFA_SBP_KEY_PATH'),
// ...
],
'webhook' => [
'user' => env('ALFA_WEBHOOK_USER'),
'password' => env('ALFA_WEBHOOK_PASSWORD'),
'alfa_public_key' => env('ALFA_WEBHOOK_PUBLIC_KEY'),
],
];
```
## Подключение и Безопасность
SDK поддерживает две разные модели безопасности, используемые в API Альфа-Банка.
@@ -175,4 +246,3 @@ public function handle(AlfaWebhookReceived $event) {
}
}
```

View File

@@ -54,6 +54,14 @@ class AlfaBankConnector extends Connector implements HasPagination
return $config;
}
protected function defaultHeaders(): array
{
return [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
];
}
protected function defaultMiddleware(): array
{
return [

View File

@@ -26,8 +26,14 @@ class VerifyJwsResponseMiddleware
$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());
// In Saloon v3, we can't easily "fake" an existing response object in a type-safe way
// without internal access. We will wrap the data and the user should use $response->json().
// However, for the sake of PHPStan and clean architecture, we should use the response's
// internal body repository if we really need to overwrite it, or better -
// let the user handle the decoded data from a custom DTO.
// To fix the immediate error, we'll return the response as is,
// but JWS verification is still performed.
return $response;
}
}

View File

@@ -1,46 +1,54 @@
<?php
declare(strict_types=1);
/*
// MODULE_CONTRACT: Represents a Sandbox API call for testing signature and encryption mechanisms.
// RESPONSIBILITY: To define a POST request for sending a JWS (JSON Web Signature) body to the sandbox to verify cryptographic operations.
// INPUTS: JWS body string, and desired security types for request/response channels.
// OUTPUTS: No specific DTO; the response content needs manual parsing for verification.
*/
namespace Apoloz\AlfaBank\Requests\Sandbox;
// <SECTION name="module_imports" intent="Import Saloon framework components for handling string bodies.">
// <BELIEF_STATE "This POST request sends a raw string (JWS) as its body, so `HasStringBody` is the appropriate trait.">
use Apoloz\AlfaBank\Data\Sandbox\SandboxTestResponseData;
use Saloon\Contracts\Body\HasBody;
use Saloon\Enums\Method;
use Saloon\Http\Request;
use Saloon\Http\Response;
use Saloon\Traits\Body\HasJsonBody;
use Saloon\Traits\Body\HasStringBody;
class SignatureEncryptionTestRequest extends Request implements HasBody
{
use HasJsonBody;
use HasStringBody;
protected Method $method = Method::POST;
/**
* @param string $data Raw content (JWS, TEXT or XML)
* @param string $format json|text|xml
* @param string $requestSecurity NO|SIGNATURE|ENCRYPTION
* @param string $responseSecurity NO|SIGNATURE|ENCRYPTION
*/
public function __construct(
protected string $data
protected string $data,
protected string $format = 'json',
protected string $requestSecurity = 'SIGNATURE',
protected string $responseSecurity = 'SIGNATURE'
) {}
public function resolveEndpoint(): string
{
return '/api/sandbox/v1/test/signature-encryption';
return "/api/signature-encryption-tests/{$this->format}";
}
protected function defaultBody(): ?string
protected function defaultHeaders(): array
{
$headers = [
'X-Request-ChannelSecurity' => $this->requestSecurity,
'X-Response-ChannelSecurity' => $this->responseSecurity,
];
if ($this->format === 'json') {
$headers['Content-Type'] = 'application/jose';
}
return $headers;
}
protected function defineBody(): string
{
return $this->data;
}
public function createDtoFromResponse(Response $response): SandboxTestResponseData
{
return SandboxTestResponseData::from(['result' => $response->body()]);
}
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Apoloz\AlfaBank\Sbp\Data\DigitalRuble;
use Spatie\LaravelData\Attributes\DataCollectionOf;
use Spatie\LaravelData\Attributes\MapName;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\DataCollection;
class GetUniQrStatusResponseData extends Data
{
#[MapName('ErrorCode')]
public int $errorCode;
public string $message;
#[DataCollectionOf(UniQrStatusRecordData::class)]
public DataCollection $data; // Renamed from 'data' to 'records' if it's always a collection of records.
public ?string $messageID = null;
}
class UniQrStatusRecordData extends Data
{
#[MapName('uniQrId')]
public string $uniQrId;
public string $code;
public string $message;
public ?string $paymentServiceId = null;
public string $status;
public ?string $trxId = null;
public ?string $kzo = null;
}

View File

@@ -13,6 +13,7 @@ namespace Apoloz\AlfaBank\Sbp\Data\Management;
// <SECTION name="module_imports" intent="Import the base Spatie Laravel Data class.">
// <BELIEF_STATE "Extending `Spatie\LaravelData\Data` enables automatic casting from arrays and provides DTO benefits.">
use Spatie\LaravelData\Attributes\MapName;
use Spatie\LaravelData\Data;
// </SECTION>
@@ -21,9 +22,10 @@ use Spatie\LaravelData\Data;
class BlockQrRequestData extends Data
{
// <SECTION name="constructor_properties" intent="Define the properties for the QR Code block/unblock request.">
// <BELIEF_STATE "The constructor includes the mandatory `TermNo` and `qrcId` to identify the target QR Code. An optional `reason` can be provided for blocking actions.">
// <BELIEF_STATE "The constructor includes the mandatory `termNo` and `qrcId` to identify the target QR Code. An optional `reason` can be provided for blocking actions.">
public function __construct(
public string $TermNo,
#[MapName('TermNo')]
public string $termNo,
public string $qrcId,
public ?string $reason = null,
) {}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Apoloz\AlfaBank\Sbp\Data\Management;
use Spatie\LaravelData\Attributes\DataCollectionOf;
use Spatie\LaravelData\Attributes\MapName;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\DataCollection;
class GetBlockedQrListResponseData extends Data
{
public function __construct(
#[MapName('ErrorCode')]
public int $errorCode,
#[MapName('TermNo')]
public string $termNo,
public string $message,
#[MapName('QRcIdList'), DataCollectionOf(BlockedQrItemData::class)]
public ?DataCollection $qrCodeList = null,
) {}
}
class BlockedQrItemData extends Data
{
public function __construct(
public string $date,
public string $qrcId,
public ?string $reason = null,
) {}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Apoloz\AlfaBank\Sbp\Data\Management;
use Spatie\LaravelData\Attributes\MapName;
use Spatie\LaravelData\Data;
class SbpManagementResponseData extends Data
{
public function __construct(
#[MapName('ErrorCode')]
public int $errorCode,
public string $message,
#[MapName('TermNo')]
public ?string $termNo = null,
) {}
}

View File

@@ -13,6 +13,7 @@ namespace Apoloz\AlfaBank\Sbp\Data\Reversal;
// <SECTION name="module_imports" intent="Import the base Spatie Laravel Data class.">
// <BELIEF_STATE "Extending `Spatie\LaravelData\Data` enables automatic casting from arrays and provides DTO benefits.">
use Spatie\LaravelData\Attributes\MapName;
use Spatie\LaravelData\Data;
// </SECTION>
@@ -21,9 +22,10 @@ use Spatie\LaravelData\Data;
class GetQrcReversalDataRequestData extends Data
{
// <SECTION name="constructor_properties" intent="Define the properties for the QR Code reversal data request.">
// <BELIEF_STATE "The constructor includes the mandatory `TermNo` and `currency`. Optional identifiers like `qrcId`, `payrrn`, `trxId`, `trxDT`, `amount`, `messageID`, and `ReturnRestAmount` provide flexibility for various reversal data queries.">
// <BELIEF_STATE "The constructor includes the mandatory `termNo` and `currency`. Optional identifiers like `qrcId`, `payrrn`, `trxId`, `trxDT`, `amount`, `messageID`, and `returnRestAmount` provide flexibility for various reversal data queries.">
public function __construct(
public string $TermNo,
#[MapName('TermNo')]
public string $termNo,
public ?string $qrcId = null,
public ?string $payrrn = null,
public ?string $trxId = null,
@@ -31,7 +33,8 @@ class GetQrcReversalDataRequestData extends Data
public ?string $amount = null,
public string $currency = 'RUB',
public ?string $messageID = null,
public ?bool $ReturnRestAmount = null,
#[MapName('ReturnRestAmount')]
public ?bool $returnRestAmount = null,
) {}
// </SECTION>
}

View File

@@ -13,6 +13,7 @@ namespace Apoloz\AlfaBank\Sbp\Data\Reversal;
// <SECTION name="module_imports" intent="Import the base Spatie Laravel Data class.">
// <BELIEF_STATE "Extending `Spatie\LaravelData\Data` enables automatic casting from arrays and provides DTO benefits.">
use Spatie\LaravelData\Attributes\MapName;
use Spatie\LaravelData\Data;
// </SECTION>
@@ -21,9 +22,10 @@ use Spatie\LaravelData\Data;
class GetQrcReversalStatusRequestData extends Data
{
// <SECTION name="constructor_properties" intent="Define the properties for the QR Code reversal status request.">
// <BELIEF_STATE "The constructor includes the mandatory `TermNo` and optional identifiers like `payrrn`, `originalTrxId`, `trxId`, and `messageID`. These allow for flexible and precise querying of a reversal's status.">
// <BELIEF_STATE "The constructor includes the mandatory `termNo` and optional identifiers like `payrrn`, `originalTrxId`, `trxId`, and `messageID`. These allow for flexible and precise querying of a reversal's status.">
public function __construct(
public string $TermNo,
#[MapName('TermNo')]
public string $termNo,
public ?string $payrrn = null,
public ?string $originalTrxId = null,
public ?string $trxId = null,

View File

@@ -13,6 +13,9 @@ namespace Apoloz\AlfaBank\Sbp\Data\Reversal;
// <SECTION name="module_imports" intent="Import the base Spatie Laravel Data class.">
// <BELIEF_STATE "Extending `Spatie\LaravelData\Data` enables automatic casting from arrays and provides DTO benefits.">
use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\MapName;
use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Data;
// </SECTION>
@@ -21,9 +24,10 @@ use Spatie\LaravelData\Data;
class QrcReversalRequestData extends Data
{
// <SECTION name="constructor_properties" intent="Define the properties for the QR Code reversal request.">
// <BELIEF_STATE "The constructor includes the mandatory `TermNo` and `currency`. Optional identifiers like `qrcId`, `payrrn`, `trxId`, `trxDT`, `amount`, `messageID`, `notificationUrl`, and `ReturnRestAmount` provide flexibility for various reversal scenarios.">
// <BELIEF_STATE "The constructor includes the mandatory `termNo` and `currency`. Optional identifiers like `qrcId`, `payrrn`, `trxId`, `trxDT`, `amount`, `messageID`, `notificationUrl`, and `returnRestAmount` provide flexibility for various reversal scenarios.">
public function __construct(
public string $TermNo,
#[MapName('TermNo')]
public string $termNo,
public ?string $qrcId = null,
public ?string $payrrn = null,
public ?string $trxId = null,
@@ -31,8 +35,10 @@ class QrcReversalRequestData extends Data
public ?string $amount = null,
public string $currency = 'RUB',
public ?string $messageID = null,
#[MapOutputName('queryData.notificationUrl')]
public ?string $notificationUrl = null,
public ?bool $ReturnRestAmount = null,
#[MapName('ReturnRestAmount')]
public ?bool $returnRestAmount = null,
// extJsonParams not fully implemented as it's optional and dynamic
) {}
// </SECTION>

View File

@@ -13,6 +13,7 @@ namespace Apoloz\AlfaBank\Sbp\Data\Status;
// <SECTION name="module_imports" intent="Import the base Spatie Laravel Data class.">
// <BELIEF_STATE "Extending `Spatie\LaravelData\Data` enables automatic casting from arrays and provides DTO benefits.">
use Spatie\LaravelData\Attributes\MapName;
use Spatie\LaravelData\Data;
// </SECTION>
@@ -21,9 +22,10 @@ use Spatie\LaravelData\Data;
class GetQrcStatusRequestData extends Data
{
// <SECTION name="constructor_properties" intent="Define the properties for the QR Code status request.">
// <BELIEF_STATE "The constructor includes the mandatory `TermNo` and optional identifiers like `qrcId`, `payrrn`, and `messageID`. These allow for flexible and precise querying of a QR Code's status.">
// <BELIEF_STATE "The constructor includes the mandatory `termNo` and optional identifiers like `qrcId`, `payrrn`, and `messageID`. These allow for flexible and precise querying of a QR Code's status.">
public function __construct(
public string $TermNo,
#[MapName('TermNo')]
public string $termNo,
public ?string $qrcId = null,
public ?string $payrrn = null,
public ?string $messageID = null,

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Apoloz\AlfaBank\Sbp\Data\Subscription;
use Spatie\LaravelData\Attributes\MapName;
use Spatie\LaravelData\Data;
class GetQrSubStatusResponseData extends Data
{
public function __construct(
#[MapName('ErrorCode')]
public int $errorCode,
public string $message,
#[MapName('TermNo')]
public ?string $termNo = null,
public ?string $qrcId = null,
public ?string $status = null,
public ?string $trxId = null,
public ?string $trxDT = null,
public ?int $amount = null,
public ?string $currency = null,
public ?string $payerId = null,
) {}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Apoloz\AlfaBank\Sbp\Data\Subscription;
use Spatie\LaravelData\Attributes\MapName;
use Spatie\LaravelData\Data;
class QrcSubPayPetitionResponseData extends Data
{
public function __construct(
#[MapName('ErrorCode')]
public int $errorCode,
public string $message,
#[MapName('TermNo')]
public ?string $termNo = null,
public ?string $qrcId = null,
public ?string $status = null,
public ?string $payrrn = null,
public ?string $trxId = null,
public ?string $trxDT = null,
public ?int $amount = null,
public ?string $currency = null,
public ?string $payerId = null,
) {}
}

View File

@@ -13,6 +13,8 @@ namespace Apoloz\AlfaBank\Sbp\Data\Subscription;
// <SECTION name="module_imports" intent="Import the base Spatie Laravel Data class.">
// <BELIEF_STATE "Extending `Spatie\LaravelData\Data` enables automatic casting from arrays and provides DTO benefits.">
use Spatie\LaravelData\Attributes\MapName;
use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Data;
// </SECTION>
@@ -21,10 +23,12 @@ use Spatie\LaravelData\Data;
class RegQrSubRequestData extends Data
{
// <SECTION name="constructor_properties" intent="Define the properties for the QR Code subscription registration request.">
// <BELIEF_STATE "The constructor includes mandatory `TermNo`, `subscriptionPurpose`, `notificationUrl`, and defaults `qrcType` to '03' (subscription). Optional fields like `qrTtl`, `redirectUrl`, `subscriptionServiceId`, and `subscriptionServiceName` provide flexibility for detailed subscription setup.">
// <BELIEF_STATE "The constructor includes mandatory `termNo`, `subscriptionPurpose`, `notificationUrl`, and defaults `qrcType` to '03' (subscription). Optional fields like `qrTtl`, `redirectUrl`, `subscriptionServiceId`, and `subscriptionServiceName` provide flexibility for detailed subscription setup.">
public function __construct(
public string $TermNo,
#[MapName('TermNo')]
public string $termNo,
public string $subscriptionPurpose,
#[MapOutputName('queryData.notificationUrl')]
public string $notificationUrl,
public string $qrcType = '03', // '03' = Subscription
public ?string $qrTtl = null,

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Apoloz\AlfaBank\Sbp\Data\Subscription;
use Spatie\LaravelData\Attributes\MapName;
use Spatie\LaravelData\Data;
class RegQrSubResponseData extends Data
{
public function __construct(
#[MapName('ErrorCode')]
public int $errorCode,
#[MapName('TermNo')]
public string $termNo,
public string $message,
public ?string $image = null,
public ?string $payload = null,
public ?string $qrcId = null,
public ?string $status = null,
) {}
}

View File

@@ -59,11 +59,11 @@ class GetUniQrStatusRequest extends Request implements HasBody
}
// </SECTION>
// <SECTION name="response_caster" intent="Return the raw JSON response as an array.">
// <BELIEF_STATE "For this specific request, the response is currently returned as a raw array. A dedicated DTO could be created if a structured object is required.">
public function createDtoFromResponse(\Saloon\Http\Response $response): array
// <SECTION name="response_caster" intent="Map the JSON response to a DTO.">
// <BELIEF_STATE "Casting the response to a `GetUniQrStatusResponseData` DTO provides a type-safe and structured object for the status of Universal QR Codes.">
public function createDtoFromResponse(\Saloon\Http\Response $response): \Apoloz\AlfaBank\Sbp\Data\DigitalRuble\GetUniQrStatusResponseData
{
return $response->json();
return \Apoloz\AlfaBank\Sbp\Data\DigitalRuble\GetUniQrStatusResponseData::from($response->json());
}
// </SECTION>
}

View File

@@ -48,28 +48,21 @@ class BlockQrRequest extends Request implements HasBody
// </SECTION>
// <SECTION name="body_builder" intent="Construct the JSON body for the request.">
// <BELIEF_STATE "The `command` field specifies the operation to the SBP API. The `TermNo` and `qrcId` identify the target, and `reason` provides context for the block.">
// <BELIEF_STATE "The `command` field specifies the operation to the SBP API. Using the DTO's `toArray` method ensures the payload is correctly structured.">
protected function defaultBody(): array
{
$body = [
'command' => 'BlockQRcId',
'TermNo' => $this->data->TermNo,
'qrcId' => $this->data->qrcId,
];
if ($this->data->reason !== null) {
$body['reason'] = $this->data->reason;
}
return $body;
return array_merge(
['command' => 'BlockQRcId'],
$this->data->toArray()
);
}
// </SECTION>
// <SECTION name="response_caster" intent="Return the raw JSON response as an array.">
// <BELIEF_STATE "For this specific request, the response is currently returned as a raw array. A dedicated DTO could be created if a structured object is required.">
public function createDtoFromResponse(\Saloon\Http\Response $response): array
// <SECTION name="response_caster" intent="Map the JSON response to a DTO.">
// <BELIEF_STATE "Casting the response to a `SbpManagementResponseData` DTO provides a structured object for the result of the block operation.">
public function createDtoFromResponse(\Saloon\Http\Response $response): \Apoloz\AlfaBank\Sbp\Data\Management\SbpManagementResponseData
{
return $response->json();
return \Apoloz\AlfaBank\Sbp\Data\Management\SbpManagementResponseData::from($response->json());
}
// </SECTION>
}

View File

@@ -57,11 +57,11 @@ class GetBlockedQrListRequest extends Request implements HasBody
}
// </SECTION>
// <SECTION name="response_caster" intent="Return the raw JSON response as an array.">
// <BELIEF_STATE "For this specific request, the response is currently returned as a raw array. A dedicated DTO could be created if a structured object is required.">
public function createDtoFromResponse(\Saloon\Http\Response $response): array
// <SECTION name="response_caster" intent="Map the JSON response to a DTO.">
// <BELIEF_STATE "Casting the response to a `GetBlockedQrListResponseData` DTO provides a type-safe object for the list of blocked QR Codes.">
public function createDtoFromResponse(\Saloon\Http\Response $response): \Apoloz\AlfaBank\Sbp\Data\Management\GetBlockedQrListResponseData
{
return $response->json();
return \Apoloz\AlfaBank\Sbp\Data\Management\GetBlockedQrListResponseData::from($response->json());
}
// </SECTION>
}

View File

@@ -48,22 +48,21 @@ class UnblockQrRequest extends Request implements HasBody
// </SECTION>
// <SECTION name="body_builder" intent="Construct the JSON body for the request.">
// <BELIEF_STATE "The `command` field specifies the operation to the SBP API. The `TermNo` and `qrcId` identify the target QR Code.">
// <BELIEF_STATE "The `command` field specifies the operation to the SBP API. Using the DTO's `toArray` method ensures the payload is correctly structured.">
protected function defaultBody(): array
{
return [
'command' => 'UnblockQRcId',
'TermNo' => $this->data->TermNo,
'qrcId' => $this->data->qrcId,
];
return array_merge(
['command' => 'UnblockQRcId'],
$this->data->toArray()
);
}
// </SECTION>
// <SECTION name="response_caster" intent="Return the raw JSON response as an array.">
// <BELIEF_STATE "For this specific request, the response is currently returned as a raw array. A dedicated DTO could be created if a structured object is required.">
public function createDtoFromResponse(\Saloon\Http\Response $response): array
// <SECTION name="response_caster" intent="Map the JSON response to a DTO.">
// <BELIEF_STATE "Casting the response to a `SbpManagementResponseData` DTO provides a structured object for the result of the unblock operation.">
public function createDtoFromResponse(\Saloon\Http\Response $response): \Apoloz\AlfaBank\Sbp\Data\Management\SbpManagementResponseData
{
return $response->json();
return \Apoloz\AlfaBank\Sbp\Data\Management\SbpManagementResponseData::from($response->json());
}
// </SECTION>
}

View File

@@ -50,21 +50,10 @@ class GetQrcReversalDataRequest extends Request implements HasBody
// <BELIEF_STATE "The `command` field specifies the operation to the SBP API. Other fields from the DTO are conditionally added, ensuring the payload precisely matches the API specification.">
protected function defaultBody(): array
{
$body = [
'command' => 'GetQRCreversalData',
'TermNo' => $this->data->TermNo,
'currency' => $this->data->currency,
];
if ($this->data->qrcId !== null) $body['qrcId'] = $this->data->qrcId;
if ($this->data->payrrn !== null) $body['payrrn'] = $this->data->payrrn;
if ($this->data->trxId !== null) $body['trxId'] = $this->data->trxId;
if ($this->data->trxDT !== null) $body['trxDT'] = $this->data->trxDT;
if ($this->data->amount !== null) $body['amount'] = $this->data->amount;
if ($this->data->messageID !== null) $body['messageID'] = $this->data->messageID;
if ($this->data->ReturnRestAmount !== null) $body['ReturnRestAmount'] = $this->data->ReturnRestAmount;
return $body;
return array_merge(
['command' => 'GetQRCreversalData'],
$this->data->toArray()
);
}
// </SECTION>

View File

@@ -50,17 +50,10 @@ class GetQrcReversalStatusRequest extends Request implements HasBody
// <BELIEF_STATE "The `command` field specifies the operation to the SBP API. Other fields from the DTO are conditionally added, ensuring the payload precisely matches the API specification.">
protected function defaultBody(): array
{
$body = [
'command' => 'GetQRCreversalStatus',
'TermNo' => $this->data->TermNo,
];
if ($this->data->payrrn !== null) $body['payrrn'] = $this->data->payrrn;
if ($this->data->originalTrxId !== null) $body['originalTrxId'] = $this->data->originalTrxId;
if ($this->data->trxId !== null) $body['trxId'] = $this->data->trxId;
if ($this->data->messageID !== null) $body['messageID'] = $this->data->messageID;
return $body;
return array_merge(
['command' => 'GetQRCreversalStatus'],
$this->data->toArray()
);
}
// </SECTION>

View File

@@ -50,22 +50,10 @@ class QrcReversalRequest extends Request implements HasBody
// <BELIEF_STATE "The `command` field specifies the operation to the SBP API. Other fields from the DTO are conditionally added, ensuring the payload precisely matches the API specification.">
protected function defaultBody(): array
{
$body = [
'command' => 'QRCreversal',
'TermNo' => $this->data->TermNo,
'currency' => $this->data->currency,
];
if ($this->data->qrcId !== null) $body['qrcId'] = $this->data->qrcId;
if ($this->data->payrrn !== null) $body['payrrn'] = $this->data->payrrn;
if ($this->data->trxId !== null) $body['trxId'] = $this->data->trxId;
if ($this->data->trxDT !== null) $body['trxDT'] = $this->data->trxDT;
if ($this->data->amount !== null) $body['amount'] = $this->data->amount;
if ($this->data->messageID !== null) $body['messageID'] = $this->data->messageID;
if ($this->data->notificationUrl !== null) $body['queryData'] = ['notificationUrl' => $this->data->notificationUrl];
if ($this->data->ReturnRestAmount !== null) $body['ReturnRestAmount'] = $this->data->ReturnRestAmount;
return $body;
return array_merge(
['command' => 'QRCreversal'],
$this->data->toArray()
);
}
// </SECTION>

View File

@@ -50,16 +50,10 @@ class GetQrcStatusRequest extends Request implements HasBody
// <BELIEF_STATE "The `command` field specifies the operation to the SBP API. The `TermNo` is mandatory, and `qrcId`, `payrrn`, and `messageID` are optional filters to pinpoint the transaction.">
protected function defaultBody(): array
{
$body = [
'command' => 'GetQRCstatus',
'TermNo' => $this->data->TermNo,
];
if ($this->data->qrcId !== null) $body['qrcId'] = $this->data->qrcId;
if ($this->data->payrrn !== null) $body['payrrn'] = $this->data->payrrn;
if ($this->data->messageID !== null) $body['messageID'] = $this->data->messageID;
return $body;
return array_merge(
['command' => 'GetQRCstatus'],
$this->data->toArray()
);
}
// </SECTION>

View File

@@ -66,11 +66,11 @@ class GetQrSubStatusRequest extends Request implements HasBody
}
// </SECTION>
// <SECTION name="response_caster" intent="Return the raw JSON response as an array.">
// <BELIEF_STATE "For this specific request, the response is currently returned as a raw array. A dedicated DTO could be created if a structured object is required.">
public function createDtoFromResponse(\Saloon\Http\Response $response): array
// <SECTION name="response_caster" intent="Map the JSON response to a DTO.">
// <BELIEF_STATE "Casting the response to a `GetQrSubStatusResponseData` DTO provides a structured object for the status of the subscription.">
public function createDtoFromResponse(\Saloon\Http\Response $response): \Apoloz\AlfaBank\Sbp\Data\Subscription\GetQrSubStatusResponseData
{
return $response->json();
return \Apoloz\AlfaBank\Sbp\Data\Subscription\GetQrSubStatusResponseData::from($response->json());
}
// </SECTION>
}

View File

@@ -61,11 +61,11 @@ class QrcSubPayPetitionRequest extends Request implements HasBody
}
// </SECTION>
// <SECTION name="response_caster" intent="Return the raw JSON response as an array.">
// <BELIEF_STATE "For this specific request, the response is currently returned as a raw array. A dedicated DTO could be created if a structured object is required.">
public function createDtoFromResponse(Response $response): array
// <SECTION name="response_caster" intent="Map the JSON response to a DTO.">
// <BELIEF_STATE "Casting the response to a `QrcSubPayPetitionResponseData` DTO provides a structured object for the result of the payment petition.">
public function createDtoFromResponse(Response $response): \Apoloz\AlfaBank\Sbp\Data\Subscription\QrcSubPayPetitionResponseData
{
return $response->json();
return \Apoloz\AlfaBank\Sbp\Data\Subscription\QrcSubPayPetitionResponseData::from($response->json());
}
// </SECTION>
}

View File

@@ -46,33 +46,21 @@ class RegQrSubRequest extends Request implements HasBody
// </SECTION>
// <SECTION name="body_builder" intent="Construct the JSON body for the request.">
// <BELIEF_STATE "The `command` field specifies the operation to the SBP API. Fields from the DTO are mapped, including conditional and nested `queryData` for notification URLs.">
// <BELIEF_STATE "The `command` field specifies the operation to the SBP API. Using the DTO's `toArray` method ensures the payload is correctly structured.">
protected function defaultBody(): array
{
$body = [
'command' => 'RegQRcSub',
'TermNo' => $this->data->TermNo,
'qrcType' => $this->data->qrcType,
'subscriptionPurpose' => $this->data->subscriptionPurpose,
'queryData' => [
'notificationUrl' => $this->data->notificationUrl,
],
];
if ($this->data->qrTtl !== null) $body['qrTtl'] = $this->data->qrTtl;
if ($this->data->redirectUrl !== null) $body['redirectUrl'] = $this->data->redirectUrl;
if ($this->data->subscriptionServiceId !== null) $body['subscriptionServiceId'] = $this->data->subscriptionServiceId;
if ($this->data->subscriptionServiceName !== null) $body['subscriptionServiceName'] = $this->data->subscriptionServiceName;
return $body;
return array_merge(
['command' => 'RegQRcSub'],
$this->data->toArray()
);
}
// </SECTION>
// <SECTION name="response_caster" intent="Return the raw JSON response as an array.">
// <BELIEF_STATE "For this specific request, the response is currently returned as a raw array. A dedicated DTO could be created if a structured object is required.">
public function createDtoFromResponse(Response $response): array
// <SECTION name="response_caster" intent="Map the JSON response to a DTO.">
// <BELIEF_STATE "Casting the response to a `RegQrSubResponseData` DTO provides a structured object for the result of the subscription registration.">
public function createDtoFromResponse(Response $response): \Apoloz\AlfaBank\Sbp\Data\Subscription\RegQrSubResponseData
{
return $response->json();
return \Apoloz\AlfaBank\Sbp\Data\Subscription\RegQrSubResponseData::from($response->json());
}
// </SECTION>
}

View File

@@ -54,7 +54,15 @@ class SbpConnector extends Connector implements HasPagination
return \Apoloz\AlfaBank\Sbp\Data\Webhooks\SbpRefundNotificationData::from($data);
}
protected function defaultHeaders(): array
/**
* 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,
@@ -69,6 +77,13 @@ class SbpConnector extends Connector implements HasPagination
return $config;
}
protected function defaultHeaders(): array
{
return [
'Accept' => 'application/json',
];
}
protected function defaultMiddleware(): array
{
return [

View File

@@ -23,7 +23,7 @@ test('sbp connector uses injected config', function () {
expect($connector->resolveBaseUrl())->toBe('https://test.alfa');
$response = $connector->send(new GetQrCodeRequest(
new QrCodeRequestData('term', '01', '100', 'purpose')
new QrCodeRequestData(termNo: 'term', qrcType: '01', amount: '100', paymentPurpose: 'purpose')
));
expect($response->status())->toBe(200);

View File

@@ -26,7 +26,7 @@ test('can check reversal data', function () {
$connector->withMockClient($mockClient);
$payload = new GetQrcReversalDataRequestData(
TermNo: 'term-1',
termNo: 'term-1',
qrcId: 'test-id'
);