Compare commits

...

6 Commits

Author SHA1 Message Date
Anatoliy Poloz
a05a6506e8 fix(sign) 2026-03-21 07:10:16 +03:00
Anatoliy Poloz
4f52a189c7 add Identification 2026-03-19 19:38:21 +03:00
Anatoliy Poloz
b7b2a8fd61 refactor(Sbp) 2026-03-17 12:26:30 +03:00
Anatoliy Poloz
e2adc8d530 fix(OpenSslCmsSigner) 2026-03-10 22:29:35 +03:00
Anatoliy Poloz
c070297211 fix(sign) 2026-03-08 18:35:02 +03:00
Anatoliy Poloz
706fcb01b2 fix 2026-03-08 18:03:43 +03:00
29 changed files with 354 additions and 245 deletions

View File

@@ -36,6 +36,7 @@ class CreateBeneficiaryRequestData extends Data
public string $countryOfTaxResidence,
#[DataCollectionOf(AddressData::class)]
public DataCollection $addresses,
public ?bool $isBeneficiary = null,
#[DataCollectionOf(DigestSignatureData::class)]
public ?DataCollection $digestSignatures = null,
public ?string $accountNumber = null,

View File

@@ -29,6 +29,7 @@ class UnidentifiedTransactionData extends Data
public ?string $documentNumber = null,
public ?string $date = null,
public ?string $purpose = null,
public ?string $accountNumber = null,
public ?PayerData $payer = null, // Nested DTO for payer details
) {}
// </SECTION>

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Apoloz\AlfaBank\Requests\NominalAccounts\Identification;
use Apoloz\AlfaBank\Data\NominalAccounts\Identification\UnidentifiedTransactionData;
use Saloon\Contracts\Body\HasBody;
use Saloon\Enums\Method;
use Saloon\Http\Request;
use Saloon\Http\Response;
use Saloon\Traits\Body\HasJsonBody;
class CreateUnidentifiedTransactionRequest extends Request implements HasBody
{
use HasJsonBody;
protected Method $method = Method::POST;
public function __construct(
protected string $documentNumber,
protected string $date,
protected float $amount,
protected string $purpose,
protected string $accountNumber,
protected ?array $payer = null,
) {}
public function resolveEndpoint(): string
{
return '/api/na/jp/v1/unidentified-transactions';
}
public function createDtoFromResponse(Response $response): UnidentifiedTransactionData
{
return UnidentifiedTransactionData::from($response->json());
}
protected function defaultBody(): array
{
return array_filter([
'documentNumber' => $this->documentNumber,
'date' => $this->date,
'amount' => $this->amount,
'purpose' => $this->purpose,
'accountNumber' => $this->accountNumber,
'payer' => $this->payer ?? $this->getDefaultPayer(),
], fn ($value) => $value !== null);
}
protected function getDefaultPayer(): array
{
return [
'inn' => '221284672706',
'kpp' => '641914992',
'accountNumber' => '87149728127096890679',
'name' => 'Тестовый плательщик',
'bankAccount' => '3932755239052037086628',
'bankBik' => '108997635',
'bankName' => 'АО "Альфа-Банк"',
];
}
}

View File

@@ -13,11 +13,12 @@ namespace Apoloz\AlfaBank\Sbp\Requests\CashLink;
// <SECTION name="module_imports" intent="Import DTO and Saloon components for a JSON body request.">
// <BELIEF_STATE "This POST request requires the `ActivateCashQrRequestData` DTO for its JSON body and utilizes `HasBody` and `HasJsonBody` for proper payload serialization. It currently returns a raw array response.">
use Apoloz\AlfaBank\Sbp\Data\CashLink\ActivateCashQrRequestData;
use Apoloz\AlfaBank\Utils\CmsSigner;
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;
// </SECTION>
@@ -25,7 +26,7 @@ use Saloon\Traits\Body\HasJsonBody;
// <BELIEF_STATE "This class encapsulates the logic for activating a CashLink QR Code, a key management operation in SBP payment systems.">
class ActivateCashQrRequest extends Request implements HasBody
{
use HasJsonBody;
use HasStringBody;
// <SECTION name="http_method" intent="Declare the HTTP verb.">
// <BELIEF_STATE "Activating a resource is an action that changes the system state, so POST is the correct verb.">
@@ -56,9 +57,9 @@ class ActivateCashQrRequest extends Request implements HasBody
// <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 parameters for order number, payment purpose, QR TTL, and notification URLs.">
protected function defaultBody(): array
protected function defaultBody(): string
{
$body = [
$bodyArray = [
'command' => 'ActivateCashQRc',
'TermNo' => $this->data->TermNo,
'qrcId' => $this->data->qrcId,
@@ -67,22 +68,22 @@ class ActivateCashQrRequest extends Request implements HasBody
];
if ($this->data->OrderNumber !== null) {
$body['OrderNumber'] = $this->data->OrderNumber;
$bodyArray['OrderNumber'] = $this->data->OrderNumber;
}
if ($this->data->paymentPurpose !== null) {
$body['paymentPurpose'] = $this->data->paymentPurpose;
$bodyArray['paymentPurpose'] = $this->data->paymentPurpose;
}
if ($this->data->qrTtl !== null) {
$body['qrTtl'] = $this->data->qrTtl;
$bodyArray['qrTtl'] = $this->data->qrTtl;
}
if ($this->data->redirectUrl !== null) {
$body['redirectUrl'] = $this->data->redirectUrl;
$bodyArray['redirectUrl'] = $this->data->redirectUrl;
}
if ($this->data->notificationUrl !== null) {
$body['queryData'] = ['notificationUrl' => $this->data->notificationUrl];
$bodyArray['queryData'] = ['notificationUrl' => $this->data->notificationUrl];
}
return $body;
return CmsSigner::canonicalize($bodyArray);
}
// </SECTION>
}

View File

@@ -12,20 +12,20 @@ namespace Apoloz\AlfaBank\Sbp\Requests\CashLink;
// <SECTION name="module_imports" intent="Import Saloon components for a JSON body request.">
// <BELIEF_STATE "This POST request utilizes `HasBody` and `HasJsonBody` for proper payload serialization. It currently returns a raw array response.">
use Apoloz\AlfaBank\Utils\CmsSigner;
use Saloon\Contracts\Body\HasBody;
use Saloon\Enums\Method;
use Saloon\Http\Request;
use Saloon\Traits\Body\HasJsonBody;
use Saloon\Http\Response;
use Saloon\Traits\Body\HasStringBody;
// <BELIEF_STATE "The `Response` object is fully qualified in `createDtoFromResponse` as it is not explicitly imported with a `use` statement at the top. This is acceptable but less explicit.">
// use Saloon\Http\Response; // Explicit import would be cleaner
// </SECTION>
// <SECTION name="class_definition" intent="Define the request to change the account for a CashLink QR Code.">
// <BELIEF_STATE "This class encapsulates the logic for modifying a CashLink QR Code, which is a key management operation.">
class ChangeAccountForCashQrRequest extends Request implements HasBody
{
use HasJsonBody;
use HasStringBody;
// <SECTION name="http_method" intent="Declare the HTTP verb.">
// <BELIEF_STATE "Modifying a resource via an action is correctly represented by a POST request in the SBP API context.">
@@ -53,7 +53,7 @@ class ChangeAccountForCashQrRequest extends Request implements HasBody
// <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
public function createDtoFromResponse(Response $response): array
{
return $response->json();
}
@@ -61,9 +61,9 @@ class ChangeAccountForCashQrRequest extends Request implements HasBody
// <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 are conditionally added based on constructor parameters, ensuring the payload precisely matches the API specification.">
protected function defaultBody(): array
protected function defaultBody(): string
{
$body = [
$bodyArray = [
'command' => 'ChangeAccForCashQRc',
'TermNo' => $this->TermNo,
'qrcId' => $this->qrcId,
@@ -71,16 +71,16 @@ class ChangeAccountForCashQrRequest extends Request implements HasBody
];
if ($this->NewTermNo !== null) {
$body['NewTermNo'] = $this->NewTermNo;
$bodyArray['NewTermNo'] = $this->NewTermNo;
}
if ($this->NewMerchantId !== null) {
$body['NewMerchantId'] = $this->NewMerchantId;
$bodyArray['NewMerchantId'] = $this->NewMerchantId;
}
if ($this->redirectUrl !== null) {
$body['redirectUrl'] = $this->redirectUrl;
$bodyArray['redirectUrl'] = $this->redirectUrl;
}
return $body;
return CmsSigner::canonicalize($bodyArray);
}
// </SECTION>
}

View File

@@ -12,20 +12,20 @@ namespace Apoloz\AlfaBank\Sbp\Requests\CashLink;
// <SECTION name="module_imports" intent="Import Saloon components for a JSON body request.">
// <BELIEF_STATE "This POST request utilizes `HasBody` and `HasJsonBody` for proper payload serialization. It currently returns a raw array response.">
use Apoloz\AlfaBank\Utils\CmsSigner;
use Saloon\Contracts\Body\HasBody;
use Saloon\Enums\Method;
use Saloon\Http\Request;
use Saloon\Traits\Body\HasJsonBody;
use Saloon\Http\Response;
use Saloon\Traits\Body\HasStringBody;
// <BELIEF_STATE "The `Response` object is fully qualified in `createDtoFromResponse` as it is not explicitly imported with a `use` statement at the top. This is acceptable but less explicit.">
// use Saloon\Http\Response; // Explicit import would be cleaner
// </SECTION>
// <SECTION name="class_definition" intent="Define the request to change the terminal number for a CashLink QR Code.">
// <BELIEF_STATE "This class encapsulates the logic for modifying a CashLink QR Code, specifically its associated terminal, which is a key management operation.">
class ChangeTermNoForCashQrRequest extends Request implements HasBody
{
use HasJsonBody;
use HasStringBody;
// <SECTION name="http_method" intent="Declare the HTTP verb.">
// <BELIEF_STATE "Modifying a resource via an action is correctly represented by a POST request in the SBP API context.">
@@ -49,7 +49,7 @@ class ChangeTermNoForCashQrRequest extends Request implements HasBody
// <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
public function createDtoFromResponse(Response $response): array
{
return $response->json();
}
@@ -57,13 +57,15 @@ class ChangeTermNoForCashQrRequest extends Request implements HasBody
// <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` are included in the body to identify the target.">
protected function defaultBody(): array
protected function defaultBody(): string
{
return [
$bodyArray = [
'command' => 'ChangeTermNoForCashQRc',
'TermNo' => $this->TermNo,
'qrcId' => $this->qrcId,
];
return CmsSigner::canonicalize($bodyArray);
}
// </SECTION>
}

View File

@@ -12,20 +12,20 @@ namespace Apoloz\AlfaBank\Sbp\Requests\CashLink;
// <SECTION name="module_imports" intent="Import Saloon components for a JSON body request.">
// <BELIEF_STATE "This POST request utilizes `HasBody` and `HasJsonBody` for proper payload serialization. It currently returns a raw array response.">
use Apoloz\AlfaBank\Utils\CmsSigner;
use Saloon\Contracts\Body\HasBody;
use Saloon\Enums\Method;
use Saloon\Http\Request;
use Saloon\Traits\Body\HasJsonBody;
use Saloon\Http\Response;
use Saloon\Traits\Body\HasStringBody;
// <BELIEF_STATE "The `Response` object is fully qualified in `createDtoFromResponse` as it is not explicitly imported with a `use` statement at the top. This is acceptable but less explicit.">
// use Saloon\Http\Response; // Explicit import would be cleaner
// </SECTION>
// <SECTION name="class_definition" intent="Define the request to delete a CashLink QR Code.">
// <BELIEF_STATE "This class encapsulates the logic for deleting a CashLink QR Code, which is a critical management operation.">
class DeleteCashQrRequest extends Request implements HasBody
{
use HasJsonBody;
use HasStringBody;
// <SECTION name="http_method" intent="Declare the HTTP verb.">
// <BELIEF_STATE "Deleting a resource via an action is correctly represented by a POST request in the SBP API context.">
@@ -49,7 +49,7 @@ class DeleteCashQrRequest extends Request implements HasBody
// <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
public function createDtoFromResponse(Response $response): array
{
return $response->json();
}
@@ -57,13 +57,15 @@ class DeleteCashQrRequest extends Request implements HasBody
// <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` are included in the body to identify the target.">
protected function defaultBody(): array
protected function defaultBody(): string
{
return [
$bodyArray = [
'command' => 'DelCashQRc',
'TermNo' => $this->TermNo,
'qrcId' => $this->qrcId,
];
return CmsSigner::canonicalize($bodyArray);
}
// </SECTION>
}

View File

@@ -12,20 +12,20 @@ namespace Apoloz\AlfaBank\Sbp\Requests\CashLink;
// <SECTION name="module_imports" intent="Import Saloon components for a JSON body request.">
// <BELIEF_STATE "This POST request utilizes `HasBody` and `HasJsonBody` for proper payload serialization. It currently returns a raw array response.">
use Apoloz\AlfaBank\Utils\CmsSigner;
use Saloon\Contracts\Body\HasBody;
use Saloon\Enums\Method;
use Saloon\Http\Request;
use Saloon\Traits\Body\HasJsonBody;
use Saloon\Http\Response;
use Saloon\Traits\Body\HasStringBody;
// <BELIEF_STATE "The `Response` object is fully qualified in `createDtoFromResponse` as it is not explicitly imported with a `use` statement at the top. This is acceptable but less explicit.">
// use Saloon\Http\Response; // Explicit import would be cleaner
// </SECTION>
// <SECTION name="class_definition" intent="Define the request to get the history of a CashLink QR Code.">
// <BELIEF_STATE "This class encapsulates the logic for querying the history of a CashLink QR Code, which is useful for auditing and reconciliation.">
class GetCashQrHistoryRequest extends Request implements HasBody
{
use HasJsonBody;
use HasStringBody;
// <SECTION name="http_method" intent="Declare the HTTP verb.">
// <BELIEF_STATE "Even though it retrieves data, SBP history requests are often performed via POST requests with a JSON body.">
@@ -51,7 +51,7 @@ class GetCashQrHistoryRequest extends Request implements HasBody
// <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
public function createDtoFromResponse(Response $response): array
{
return $response->json();
}
@@ -59,15 +59,17 @@ class GetCashQrHistoryRequest extends Request implements HasBody
// <SECTION name="body_builder" intent="Construct the JSON body for the request.">
// <BELIEF_STATE "The `command` field specifies the operation to the SBP API. All constructor properties are directly mapped to the request body to identify the QR Code and the date range.">
protected function defaultBody(): array
protected function defaultBody(): string
{
return [
$bodyArray = [
'command' => 'GetCashQrcId',
'TermNo' => $this->TermNo,
'qrcId' => $this->qrcId,
'dateFrom' => $this->dateFrom,
'dateTo' => $this->dateTo,
];
return CmsSigner::canonicalize($bodyArray);
}
// </SECTION>
}

View File

@@ -12,20 +12,20 @@ namespace Apoloz\AlfaBank\Sbp\Requests\CashLink;
// <SECTION name="module_imports" intent="Import Saloon components for a JSON body request.">
// <BELIEF_STATE "This POST request utilizes `HasBody` and `HasJsonBody` for proper payload serialization. It currently returns a raw array response.">
use Apoloz\AlfaBank\Utils\CmsSigner;
use Saloon\Contracts\Body\HasBody;
use Saloon\Enums\Method;
use Saloon\Http\Request;
use Saloon\Traits\Body\HasJsonBody;
use Saloon\Http\Response;
use Saloon\Traits\Body\HasStringBody;
// <BELIEF_STATE "The `Response` object is fully qualified in `createDtoFromResponse` as it is not explicitly imported with a `use` statement at the top. This is acceptable but less explicit.">
// use Saloon\Http\Response; // Explicit import would be cleaner
// </SECTION>
// <SECTION name="class_definition" intent="Define the request to get the status of a CashLink QR Code.">
// <BELIEF_STATE "This class encapsulates the logic for querying the status of a CashLink QR Code, which is crucial for tracking payment flows.">
class GetCashQrStatusRequest extends Request implements HasBody
{
use HasJsonBody;
use HasStringBody;
// <SECTION name="http_method" intent="Declare the HTTP verb.">
// <BELIEF_STATE "Even though it retrieves data, SBP status checks are often performed via POST requests with a JSON body.">
@@ -49,7 +49,7 @@ class GetCashQrStatusRequest extends Request implements HasBody
// <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
public function createDtoFromResponse(Response $response): array
{
return $response->json();
}
@@ -57,13 +57,15 @@ class GetCashQrStatusRequest extends Request implements HasBody
// <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.">
protected function defaultBody(): array
protected function defaultBody(): string
{
return [
$bodyArray = [
'command' => 'GetCashQRCstatus',
'TermNo' => $this->TermNo,
'qrcId' => $this->qrcId,
];
return CmsSigner::canonicalize($bodyArray);
}
// </SECTION>
}

View File

@@ -14,11 +14,12 @@ namespace Apoloz\AlfaBank\Sbp\Requests\CashLink;
// <BELIEF_STATE "This POST request requires specific DTOs for its JSON body and its response, along with Saloon's body-handling traits.">
use Apoloz\AlfaBank\Sbp\Data\CashLink\RegCashQrRequestData;
use Apoloz\AlfaBank\Sbp\Data\CashLink\RegCashQrResponseData;
use Apoloz\AlfaBank\Utils\CmsSigner;
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;
// </SECTION>
@@ -26,7 +27,7 @@ use Saloon\Traits\Body\HasJsonBody;
// <BELIEF_STATE "This class encapsulates the logic for generating a CashLink QR Code, a key feature in SBP payment systems.">
class RegCashQrRequest extends Request implements HasBody
{
use HasJsonBody;
use HasStringBody;
// <SECTION name="http_method" intent="Declare the HTTP verb.">
// <BELIEF_STATE "Creating a new QR Code resource or initiating a generation action is correctly represented by a POST request.">
@@ -57,30 +58,30 @@ class RegCashQrRequest extends Request implements HasBody
// <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 parameters for media type, dimensions, redirect URL, and QR Code ID.">
protected function defaultBody(): array
protected function defaultBody(): string
{
$body = [
$bodyArray = [
'command' => 'RegCashQRc',
'TermNo' => $this->data->TermNo,
];
if ($this->data->mediaType !== null) {
$body['mediaType'] = $this->data->mediaType;
$bodyArray['mediaType'] = $this->data->mediaType;
}
if ($this->data->width !== null) {
$body['width'] = $this->data->width;
$bodyArray['width'] = $this->data->width;
}
if ($this->data->height !== null) {
$body['height'] = $this->data->height;
$bodyArray['height'] = $this->data->height;
}
if ($this->data->redirectUrl !== null) {
$body['redirectUrl'] = $this->data->redirectUrl;
$bodyArray['redirectUrl'] = $this->data->redirectUrl;
}
if ($this->data->qrcId !== null) {
$body['qrcId'] = $this->data->qrcId;
$bodyArray['qrcId'] = $this->data->qrcId;
}
return $body;
return CmsSigner::canonicalize($bodyArray);
}
// </SECTION>
}

View File

@@ -18,7 +18,6 @@ use Saloon\Contracts\Body\HasBody;
use Saloon\Enums\Method;
use Saloon\Http\Request;
use Saloon\Http\Response;
use Saloon\Traits\Body\HasJsonBody;
// </SECTION>
@@ -26,7 +25,7 @@ use Saloon\Traits\Body\HasJsonBody;
// <BELIEF_STATE "This class encapsulates the logic for modifying a Digital Ruble CashLink QR Code, which is a key management operation.">
class ChangeAccForCashUniQrRequest extends Request implements HasBody
{
use HasJsonBody;
use HasStringBody;
// <SECTION name="http_method" intent="Declare the HTTP verb.">
// <BELIEF_STATE "Modifying a resource via an action is correctly represented by a POST request in the SBP API context.">
@@ -57,42 +56,42 @@ class ChangeAccForCashUniQrRequest extends Request implements HasBody
// <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 parameters for the new account, merchant, redirect URL, etc.">
protected function defaultBody(): array
protected function defaultBody(): string
{
$body = [
$bodyArray = [
'command' => 'ChangeAccForCashUniQr',
'TermNo' => $this->data->TermNo,
];
if ($this->data->messageID !== null) {
$body['messageID'] = $this->data->messageID;
$bodyArray['messageID'] = $this->data->messageID;
}
if ($this->data->qrcId !== null) {
$body['qrcId'] = $this->data->qrcId;
$bodyArray['qrcId'] = $this->data->qrcId;
}
if ($this->data->account !== null) {
$body['account'] = $this->data->account;
$bodyArray['account'] = $this->data->account;
}
if ($this->data->accountDR !== null) {
$body['accountDR'] = $this->data->accountDR;
$bodyArray['accountDR'] = $this->data->accountDR;
}
if ($this->data->NewTermNo !== null) {
$body['NewTermNo'] = $this->data->NewTermNo;
$bodyArray['NewTermNo'] = $this->data->NewTermNo;
}
if ($this->data->NewMerchantId !== null) {
$body['NewMerchantId'] = $this->data->NewMerchantId;
$bodyArray['NewMerchantId'] = $this->data->NewMerchantId;
}
if ($this->data->redirectUrl !== null) {
$body['redirectUrl'] = $this->data->redirectUrl;
$bodyArray['redirectUrl'] = $this->data->redirectUrl;
}
if ($this->data->paymentPageUrl !== null) {
$body['paymentPageUrl'] = $this->data->paymentPageUrl;
$bodyArray['paymentPageUrl'] = $this->data->paymentPageUrl;
}
if ($this->data->fraudScore !== null) {
$body['fraudScore'] = $this->data->fraudScore;
$bodyArray['fraudScore'] = $this->data->fraudScore;
}
return $body;
return CmsSigner::canonicalize($bodyArray);
}
// </SECTION>
}

View File

@@ -18,7 +18,6 @@ use Saloon\Contracts\Body\HasBody;
use Saloon\Enums\Method;
use Saloon\Http\Request;
use Saloon\Http\Response;
use Saloon\Traits\Body\HasJsonBody;
// </SECTION>
@@ -26,7 +25,7 @@ use Saloon\Traits\Body\HasJsonBody;
// <BELIEF_STATE "This class encapsulates the logic for generating a Digital Ruble CashLink QR Code, a key feature in the SBP Digital Ruble operations.">
class CreateCashUniQrRequest extends Request implements HasBody
{
use HasJsonBody;
use HasStringBody;
// <SECTION name="http_method" intent="Declare the HTTP verb.">
// <BELIEF_STATE "Creating a new QR Code resource or initiating a generation action is correctly represented by a POST request.">
@@ -57,40 +56,40 @@ class CreateCashUniQrRequest extends Request implements HasBody
// <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.">
protected function defaultBody(): array
protected function defaultBody(): string
{
$body = [
$bodyArray = [
'command' => 'ReqCashUniQr',
'TermNo' => $this->data->TermNo,
];
if ($this->data->messageID !== null) {
$body['messageID'] = $this->data->messageID;
$bodyArray['messageID'] = $this->data->messageID;
}
if ($this->data->redirectUrl !== null) {
$body['redirectUrl'] = $this->data->redirectUrl;
$bodyArray['redirectUrl'] = $this->data->redirectUrl;
}
if ($this->data->paymentPageUrl !== null) {
$body['paymentPageUrl'] = $this->data->paymentPageUrl;
$bodyArray['paymentPageUrl'] = $this->data->paymentPageUrl;
}
if ($this->data->mediaType !== null) {
$body['mediaType'] = $this->data->mediaType;
$bodyArray['mediaType'] = $this->data->mediaType;
}
if ($this->data->width !== null) {
$body['width'] = $this->data->width;
$bodyArray['width'] = $this->data->width;
}
if ($this->data->height !== null) {
$body['height'] = $this->data->height;
$bodyArray['height'] = $this->data->height;
}
if ($this->data->qrcId !== null) {
$body['qrcId'] = $this->data->qrcId;
$bodyArray['qrcId'] = $this->data->qrcId;
}
if ($this->data->notificationUrl !== null) {
$body['queryData'] = ['notificationUrl' => $this->data->notificationUrl];
$bodyArray['queryData'] = ['notificationUrl' => $this->data->notificationUrl];
}
return $body;
return CmsSigner::canonicalize($bodyArray);
}
// </SECTION>
}

View File

@@ -18,7 +18,6 @@ use Saloon\Contracts\Body\HasBody;
use Saloon\Enums\Method;
use Saloon\Http\Request;
use Saloon\Http\Response;
use Saloon\Traits\Body\HasJsonBody;
// </SECTION>
@@ -26,7 +25,7 @@ use Saloon\Traits\Body\HasJsonBody;
// <BELIEF_STATE "This class encapsulates the logic for querying the status of a Digital Ruble CashLink QR Code, which is crucial for tracking payment flows.">
class GetCashUniQrStatusRequest extends Request implements HasBody
{
use HasJsonBody;
use HasStringBody;
// <SECTION name="http_method" intent="Declare the HTTP verb.">
// <BELIEF_STATE "Even though it retrieves data, SBP status checks are often performed via POST requests with a JSON body.">
@@ -57,9 +56,9 @@ class GetCashUniQrStatusRequest extends Request implements HasBody
// <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`, `uniQrId`, and `paramsId` identify the target. Optional fields like `messageID` and `payrrn` provide additional context.">
protected function defaultBody(): array
protected function defaultBody(): string
{
$body = [
$bodyArray = [
'command' => 'GetCashUniQrStatus',
'TermNo' => $this->data->TermNo,
'uniQrId' => $this->data->uniQrId,
@@ -67,13 +66,13 @@ class GetCashUniQrStatusRequest extends Request implements HasBody
];
if ($this->data->messageID !== null) {
$body['messageID'] = $this->data->messageID;
$bodyArray['messageID'] = $this->data->messageID;
}
if ($this->data->payrrn !== null) {
$body['payrrn'] = $this->data->payrrn;
$bodyArray['payrrn'] = $this->data->payrrn;
}
return $body;
return CmsSigner::canonicalize($bodyArray);
}
// </SECTION>
}

View File

@@ -6,15 +6,16 @@ namespace Apoloz\AlfaBank\Sbp\Requests\DigitalRuble;
use Apoloz\AlfaBank\Sbp\Data\DigitalRuble\CreateUniQrRequestData;
use Apoloz\AlfaBank\Sbp\Data\DigitalRuble\UniQrResponseData;
use Apoloz\AlfaBank\Utils\CmsSigner;
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 CreateUniQrRequest extends Request implements HasBody
{
use HasJsonBody;
use HasStringBody;
protected Method $method = Method::POST;
@@ -32,40 +33,40 @@ class CreateUniQrRequest extends Request implements HasBody
return UniQrResponseData::from($response->json());
}
protected function defaultBody(): array
protected function defaultBody(): string
{
$body = [
$bodyArray = [
'command' => 'ReqUniQr',
'TermNo' => $this->data->TermNo,
'uniQrType' => $this->data->uniQrType,
];
if ($this->data->amount !== null) {
$body['amount'] = $this->data->amount;
$bodyArray['amount'] = $this->data->amount;
}
if ($this->data->paymentPurpose !== null) {
$body['paymentPurpose'] = $this->data->paymentPurpose;
$bodyArray['paymentPurpose'] = $this->data->paymentPurpose;
}
if ($this->data->redirectUrl !== null) {
$body['redirectUrl'] = $this->data->redirectUrl;
$bodyArray['redirectUrl'] = $this->data->redirectUrl;
}
if ($this->data->paymentPageUrl !== null) {
$body['paymentPageUrl'] = $this->data->paymentPageUrl;
$bodyArray['paymentPageUrl'] = $this->data->paymentPageUrl;
}
if ($this->data->mediaType !== null) {
$body['mediaType'] = $this->data->mediaType;
$bodyArray['mediaType'] = $this->data->mediaType;
}
if ($this->data->width !== null) {
$body['width'] = $this->data->width;
$bodyArray['width'] = $this->data->width;
}
if ($this->data->height !== null) {
$body['height'] = $this->data->height;
$bodyArray['height'] = $this->data->height;
}
if ($this->data->qrTtl !== null) {
$body['qrTtl'] = $this->data->qrTtl;
$bodyArray['qrTtl'] = $this->data->qrTtl;
}
if ($this->data->messageID !== null) {
$body['messageID'] = $this->data->messageID;
$bodyArray['messageID'] = $this->data->messageID;
}
$queryData = [];
@@ -83,9 +84,9 @@ class CreateUniQrRequest extends Request implements HasBody
}
if (! empty($queryData)) {
$body['queryData'] = $queryData;
$bodyArray['queryData'] = $queryData;
}
return $body;
return CmsSigner::canonicalize($bodyArray);
}
}

View File

@@ -12,20 +12,21 @@ namespace Apoloz\AlfaBank\Sbp\Requests\DigitalRuble;
// <SECTION name="module_imports" intent="Import Saloon components for a JSON body request.">
// <BELIEF_STATE "This POST request utilizes `HasBody` and `HasJsonBody` for proper payload serialization. It currently returns a raw array response.">
use Apoloz\AlfaBank\Sbp\Data\DigitalRuble\GetUniQrStatusResponseData;
use Apoloz\AlfaBank\Utils\CmsSigner;
use Saloon\Contracts\Body\HasBody;
use Saloon\Enums\Method;
use Saloon\Http\Request;
use Saloon\Traits\Body\HasJsonBody;
use Saloon\Http\Response;
use Saloon\Traits\Body\HasStringBody;
// <BELIEF_STATE "The `Response` object is fully qualified in `createDtoFromResponse` as it is not explicitly imported with a `use` statement at the top. This is acceptable but less explicit.">
// use Saloon\Http\Response; // Explicit import would be cleaner
// </SECTION>
// <SECTION name="class_definition" intent="Define the request to get the status of Universal QR Codes.">
// <BELIEF_STATE "This class encapsulates the logic for querying the status of multiple Uni QR Codes, which is part of managing digital ruble transactions.">
class GetUniQrStatusRequest extends Request implements HasBody
{
use HasJsonBody;
use HasStringBody;
// <SECTION name="http_method" intent="Declare the HTTP verb.">
// <BELIEF_STATE "Even though it retrieves data, SBP status checks are often performed via POST requests with a JSON body.">
@@ -49,21 +50,23 @@ class GetUniQrStatusRequest extends Request implements HasBody
// <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
public function createDtoFromResponse(Response $response): GetUniQrStatusResponseData
{
return \Apoloz\AlfaBank\Sbp\Data\DigitalRuble\GetUniQrStatusResponseData::from($response->json());
return GetUniQrStatusResponseData::from($response->json());
}
// </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 `uniQrIds` are included in the body to identify the targets.">
protected function defaultBody(): array
protected function defaultBody(): string
{
return [
$bodyArray = [
'command' => 'GetUniQrStatus',
'TermNo' => $this->TermNo,
'uniQrIds' => $this->uniQrIds,
];
return CmsSigner::canonicalize($bodyArray);
}
// </SECTION>
}

View File

@@ -13,20 +13,21 @@ namespace Apoloz\AlfaBank\Sbp\Requests\Management;
// <SECTION name="module_imports" intent="Import DTO and Saloon components for a JSON body request.">
// <BELIEF_STATE "This POST request requires the `BlockQrRequestData` DTO for its JSON body and utilizes `HasBody` and `HasJsonBody` for proper payload serialization. It currently returns a raw array response.">
use Apoloz\AlfaBank\Sbp\Data\Management\BlockQrRequestData;
use Apoloz\AlfaBank\Sbp\Data\Management\SbpManagementResponseData;
use Apoloz\AlfaBank\Utils\CmsSigner;
use Saloon\Contracts\Body\HasBody;
use Saloon\Enums\Method;
use Saloon\Http\Request;
use Saloon\Traits\Body\HasJsonBody;
use Saloon\Http\Response;
use Saloon\Traits\Body\HasStringBody;
// <BELIEF_STATE "The `Response` object is fully qualified in `createDtoFromResponse` as it is not explicitly imported with a `use` statement at the top. This is acceptable but less explicit.">
// use Saloon\Http\Response; // Explicit import would be cleaner
// </SECTION>
// <SECTION name="class_definition" intent="Define the request to block a QR Code.">
// <BELIEF_STATE "This class encapsulates the logic for blocking an SBP QR Code, which is a critical management operation.">
class BlockQrRequest extends Request implements HasBody
{
use HasJsonBody;
use HasStringBody;
// <SECTION name="http_method" intent="Declare the HTTP verb.">
// <BELIEF_STATE "Blocking a resource is an action that changes the system state, so POST is the correct verb.">
@@ -49,20 +50,22 @@ class BlockQrRequest extends Request implements HasBody
// <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
public function createDtoFromResponse(Response $response): SbpManagementResponseData
{
return \Apoloz\AlfaBank\Sbp\Data\Management\SbpManagementResponseData::from($response->json());
return SbpManagementResponseData::from($response->json());
}
// </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. Using the DTO's `toArray` method ensures the payload is correctly structured.">
protected function defaultBody(): array
protected function defaultBody(): string
{
return array_merge(
$bodyArray = array_merge(
['command' => 'BlockQRcId'],
$this->data->toArray()
);
return CmsSigner::canonicalize($bodyArray);
}
// </SECTION>
}

View File

@@ -12,20 +12,21 @@ namespace Apoloz\AlfaBank\Sbp\Requests\Management;
// <SECTION name="module_imports" intent="Import Saloon components for a JSON body request.">
// <BELIEF_STATE "This POST request utilizes `HasBody` and `HasJsonBody` for proper payload serialization. It currently returns a raw array response.">
use Apoloz\AlfaBank\Sbp\Data\Management\GetBlockedQrListResponseData;
use Apoloz\AlfaBank\Utils\CmsSigner;
use Saloon\Contracts\Body\HasBody;
use Saloon\Enums\Method;
use Saloon\Http\Request;
use Saloon\Traits\Body\HasJsonBody;
use Saloon\Http\Response;
use Saloon\Traits\Body\HasStringBody;
// <BELIEF_STATE "The `Response` object is fully qualified in `createDtoFromResponse` as it is not explicitly imported with a `use` statement at the top. This is acceptable but less explicit.">
// use Saloon\Http\Response; // Explicit import would be cleaner
// </SECTION>
// <SECTION name="class_definition" intent="Define the request to get a list of blocked QR Codes.">
// <BELIEF_STATE "This class encapsulates the logic for querying blocked QR Codes, which is part of managing payment terminals.">
class GetBlockedQrListRequest extends Request implements HasBody
{
use HasJsonBody;
use HasStringBody;
// <SECTION name="http_method" intent="Declare the HTTP verb.">
// <BELIEF_STATE "Even though it retrieves data, SBP list requests are often performed via POST requests with a JSON body.">
@@ -48,20 +49,22 @@ class GetBlockedQrListRequest extends Request implements HasBody
// <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
public function createDtoFromResponse(Response $response): GetBlockedQrListResponseData
{
return \Apoloz\AlfaBank\Sbp\Data\Management\GetBlockedQrListResponseData::from($response->json());
return GetBlockedQrListResponseData::from($response->json());
}
// </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` is included in the body to identify the terminal.">
protected function defaultBody(): array
protected function defaultBody(): string
{
return [
$bodyArray = [
'command' => 'GetBlockedQRcIdList',
'TermNo' => $this->TermNo,
];
return CmsSigner::canonicalize($bodyArray);
}
// </SECTION>
}

View File

@@ -13,20 +13,21 @@ namespace Apoloz\AlfaBank\Sbp\Requests\Management;
// <SECTION name="module_imports" intent="Import DTO and Saloon components for a JSON body request.">
// <BELIEF_STATE "This POST request requires the `BlockQrRequestData` DTO for its JSON body and utilizes `HasBody` and `HasJsonBody` for proper payload serialization. It currently returns a raw array response.">
use Apoloz\AlfaBank\Sbp\Data\Management\BlockQrRequestData;
use Apoloz\AlfaBank\Sbp\Data\Management\SbpManagementResponseData;
use Apoloz\AlfaBank\Utils\CmsSigner;
use Saloon\Contracts\Body\HasBody;
use Saloon\Enums\Method;
use Saloon\Http\Request;
use Saloon\Traits\Body\HasJsonBody;
use Saloon\Http\Response;
use Saloon\Traits\Body\HasStringBody;
// <BELIEF_STATE "The `Response` object is fully qualified in `createDtoFromResponse` as it is not explicitly imported with a `use` statement at the top. This is acceptable but less explicit.">
// use Saloon\Http\Response; // Explicit import would be cleaner
// </SECTION>
// <SECTION name="class_definition" intent="Define the request to unblock a QR Code.">
// <BELIEF_STATE "This class encapsulates the logic for unblocking an SBP QR Code, which is a critical management operation.">
class UnblockQrRequest extends Request implements HasBody
{
use HasJsonBody;
use HasStringBody;
// <SECTION name="http_method" intent="Declare the HTTP verb.">
// <BELIEF_STATE "Unblocking a resource is an action that changes the system state, so POST is the correct verb.">
@@ -49,20 +50,22 @@ class UnblockQrRequest extends Request implements HasBody
// <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
public function createDtoFromResponse(Response $response): SbpManagementResponseData
{
return \Apoloz\AlfaBank\Sbp\Data\Management\SbpManagementResponseData::from($response->json());
return SbpManagementResponseData::from($response->json());
}
// </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. Using the DTO's `toArray` method ensures the payload is correctly structured.">
protected function defaultBody(): array
protected function defaultBody(): string
{
return array_merge(
$bodyArray = array_merge(
['command' => 'UnblockQRcId'],
$this->data->toArray()
);
return CmsSigner::canonicalize($bodyArray);
}
// </SECTION>
}

View File

@@ -13,11 +13,12 @@ namespace Apoloz\AlfaBank\Sbp\Requests\Registry;
// <SECTION name="module_imports" intent="Import DTO and Saloon components for a JSON body request.">
// <BELIEF_STATE "This POST request requires the `GetBankListResponseData` DTO for its response and utilizes `HasBody` and `HasJsonBody` for proper payload serialization.">
use Apoloz\AlfaBank\Sbp\Data\Registry\GetBankListResponseData;
use Apoloz\AlfaBank\Utils\CmsSigner;
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;
// </SECTION>
@@ -25,7 +26,7 @@ use Saloon\Traits\Body\HasJsonBody;
// <BELIEF_STATE "This class encapsulates the logic for querying the list of SBP participating banks, which is essential for routing payments.">
class GetBankListRequest extends Request implements HasBody
{
use HasJsonBody;
use HasStringBody;
// <SECTION name="http_method" intent="Declare the HTTP verb.">
// <BELIEF_STATE "Even though it retrieves data, SBP list requests are often performed via POST requests with a JSON body.">
@@ -57,20 +58,20 @@ class GetBankListRequest extends Request implements HasBody
// <SECTION name="body_builder" intent="Construct the JSON body for the request.">
// <BELIEF_STATE "The `command` field specifies the operation to the SBP API. Optional fields are conditionally added to the body.">
protected function defaultBody(): array
protected function defaultBody(): string
{
$body = [
$bodyArray = [
'command' => 'GetBankList',
];
if ($this->messageID !== null) {
$body['messageID'] = $this->messageID;
$bodyArray['messageID'] = $this->messageID;
}
if ($this->BankMemberIdFilter !== null) {
$body['BankMemberIdFilter'] = $this->BankMemberIdFilter;
$bodyArray['BankMemberIdFilter'] = $this->BankMemberIdFilter;
}
return $body;
return CmsSigner::canonicalize($bodyArray);
}
// </SECTION>
}

View File

@@ -14,11 +14,12 @@ namespace Apoloz\AlfaBank\Sbp\Requests\Reversal;
// <BELIEF_STATE "This POST request requires specific DTOs for its JSON body and its response, along with Saloon's body-handling traits.">
use Apoloz\AlfaBank\Sbp\Data\Reversal\GetQrcReversalDataRequestData;
use Apoloz\AlfaBank\Sbp\Data\Reversal\GetQrcReversalDataResponseData;
use Apoloz\AlfaBank\Utils\CmsSigner;
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;
// </SECTION>
@@ -26,7 +27,7 @@ use Saloon\Traits\Body\HasJsonBody;
// <BELIEF_STATE "This class encapsulates the logic for querying detailed SBP reversal information, which is crucial for auditing and reconciliation.">
class GetQrcReversalDataRequest extends Request implements HasBody
{
use HasJsonBody;
use HasStringBody;
// <SECTION name="http_method" intent="Declare the HTTP verb.">
// <BELIEF_STATE "Even though it retrieves data, SBP data requests are often performed via POST requests with a JSON body.">
@@ -57,12 +58,14 @@ class GetQrcReversalDataRequest extends Request implements HasBody
// <SECTION name="body_builder" intent="Construct the JSON body for the request.">
// <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
protected function defaultBody(): string
{
return array_merge(
$bodyArray = array_merge(
['command' => 'GetQRCreversalData'],
$this->data->toArray()
);
return CmsSigner::canonicalize($bodyArray);
}
// </SECTION>
}

View File

@@ -5,15 +5,16 @@ declare(strict_types=1);
namespace Apoloz\AlfaBank\Sbp\Requests\Reversal;
use Apoloz\AlfaBank\Sbp\Data\Reversal\ReversalHistoryResponseData;
use Apoloz\AlfaBank\Utils\CmsSigner;
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 GetQrcReversalHistoryRequest extends Request implements HasBody
{
use HasJsonBody;
use HasStringBody;
protected Method $method = Method::POST;
@@ -32,12 +33,14 @@ class GetQrcReversalHistoryRequest extends Request implements HasBody
return ReversalHistoryResponseData::from($response->json());
}
protected function defaultBody(): array
protected function defaultBody(): string
{
return [
$bodyArray = [
'command' => 'GetQRCreversalHistory',
'TermNo' => $this->TermNo,
'payrrn' => $this->payrrn,
];
return CmsSigner::canonicalize($bodyArray);
}
}

View File

@@ -14,11 +14,12 @@ namespace Apoloz\AlfaBank\Sbp\Requests\Reversal;
// <BELIEF_STATE "This POST request requires specific DTOs for its JSON body and its response, along with Saloon's body-handling traits.">
use Apoloz\AlfaBank\Sbp\Data\Reversal\GetQrcReversalStatusRequestData;
use Apoloz\AlfaBank\Sbp\Data\Reversal\GetQrcReversalStatusResponseData;
use Apoloz\AlfaBank\Utils\CmsSigner;
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;
// </SECTION>
@@ -26,7 +27,7 @@ use Saloon\Traits\Body\HasJsonBody;
// <BELIEF_STATE "This class encapsulates the logic for querying the status of an SBP reversal, which is a critical operation in payment systems.">
class GetQrcReversalStatusRequest extends Request implements HasBody
{
use HasJsonBody;
use HasStringBody;
// <SECTION name="http_method" intent="Declare the HTTP verb.">
// <BELIEF_STATE "Even though it retrieves data, SBP status checks are often performed via POST requests with a JSON body.">
@@ -57,12 +58,14 @@ class GetQrcReversalStatusRequest extends Request implements HasBody
// <SECTION name="body_builder" intent="Construct the JSON body for the request.">
// <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
protected function defaultBody(): string
{
return array_merge(
$bodyArray = array_merge(
['command' => 'GetQRCreversalStatus'],
$this->data->toArray()
);
return CmsSigner::canonicalize($bodyArray);
}
// </SECTION>
}

View File

@@ -14,11 +14,12 @@ namespace Apoloz\AlfaBank\Sbp\Requests\Reversal;
// <BELIEF_STATE "This POST request requires specific DTOs for its JSON body and its response, along with Saloon's body-handling traits.">
use Apoloz\AlfaBank\Sbp\Data\Reversal\QrcReversalRequestData;
use Apoloz\AlfaBank\Sbp\Data\Reversal\QrcReversalResponseData;
use Apoloz\AlfaBank\Utils\CmsSigner;
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;
// </SECTION>
@@ -26,7 +27,7 @@ use Saloon\Traits\Body\HasJsonBody;
// <BELIEF_STATE "This class encapsulates the logic for initiating an SBP reversal, which is a critical operation in payment systems.">
class QrcReversalRequest extends Request implements HasBody
{
use HasJsonBody;
use HasStringBody;
// <SECTION name="http_method" intent="Declare the HTTP verb.">
// <BELIEF_STATE "Initiating a new transaction or action that changes the system state (like a reversal) is correctly represented by a POST request.">
@@ -57,12 +58,14 @@ class QrcReversalRequest extends Request implements HasBody
// <SECTION name="body_builder" intent="Construct the JSON body for the request.">
// <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
protected function defaultBody(): string
{
return array_merge(
$bodyArray = array_merge(
['command' => 'QRCreversal'],
$this->data->toArray()
);
return CmsSigner::canonicalize($bodyArray);
}
// </SECTION>
}

View File

@@ -14,11 +14,12 @@ namespace Apoloz\AlfaBank\Sbp\Requests\Status;
// <BELIEF_STATE "This POST request requires specific DTOs for its JSON body and its response, along with Saloon's body-handling traits.">
use Apoloz\AlfaBank\Sbp\Data\Status\GetQrcStatusRequestData;
use Apoloz\AlfaBank\Sbp\Data\Status\GetQrcStatusResponseData;
use Apoloz\AlfaBank\Utils\CmsSigner;
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;
// </SECTION>
@@ -26,7 +27,7 @@ use Saloon\Traits\Body\HasJsonBody;
// <BELIEF_STATE "This class encapsulates the logic for querying the status of an SBP QR Code, which is crucial for tracking payment flows.">
class GetQrcStatusRequest extends Request implements HasBody
{
use HasJsonBody;
use HasStringBody;
// <SECTION name="http_method" intent="Declare the HTTP verb.">
// <BELIEF_STATE "Even though it retrieves data, SBP status checks are often performed via POST requests with a JSON body.">
@@ -57,12 +58,14 @@ class GetQrcStatusRequest extends Request implements HasBody
// <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` is mandatory, and `qrcId`, `payrrn`, and `messageID` are optional filters to pinpoint the transaction.">
protected function defaultBody(): array
protected function defaultBody(): string
{
return array_merge(
$bodyArray = array_merge(
['command' => 'GetQRCstatus'],
$this->data->toArray()
);
return CmsSigner::canonicalize($bodyArray);
}
// </SECTION>
}

View File

@@ -12,20 +12,21 @@ namespace Apoloz\AlfaBank\Sbp\Requests\Subscription;
// <SECTION name="module_imports" intent="Import Saloon components for a JSON body request.">
// <BELIEF_STATE "This POST request utilizes `HasBody` and `HasJsonBody` for proper payload serialization. It currently returns a raw array response.">
use Apoloz\AlfaBank\Sbp\Data\Subscription\GetQrSubStatusResponseData;
use Apoloz\AlfaBank\Utils\CmsSigner;
use Saloon\Contracts\Body\HasBody;
use Saloon\Enums\Method;
use Saloon\Http\Request;
use Saloon\Traits\Body\HasJsonBody;
use Saloon\Http\Response;
use Saloon\Traits\Body\HasStringBody;
// <BELIEF_STATE "The `Response` object is fully qualified in `createDtoFromResponse` as it is not explicitly imported with a `use` statement at the top. This is acceptable but less explicit.">
// use Saloon\Http\Response; // Explicit import would be cleaner
// </SECTION>
// <SECTION name="class_definition" intent="Define the request to get the status of a QR Code subscription.">
// <BELIEF_STATE "This class encapsulates the logic for querying the status of an SBP subscription, which is crucial for managing recurring payments.">
class GetQrSubStatusRequest extends Request implements HasBody
{
use HasJsonBody;
use HasStringBody;
// <SECTION name="http_method" intent="Declare the HTTP verb.">
// <BELIEF_STATE "Even though it retrieves data, SBP status checks are often performed via POST requests with a JSON body.">
@@ -50,27 +51,27 @@ class GetQrSubStatusRequest extends Request implements HasBody
// <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
public function createDtoFromResponse(Response $response): GetQrSubStatusResponseData
{
return \Apoloz\AlfaBank\Sbp\Data\Subscription\GetQrSubStatusResponseData::from($response->json());
return GetQrSubStatusResponseData::from($response->json());
}
// </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. Other fields are conditionally added, ensuring the payload precisely matches the API specification.">
protected function defaultBody(): array
protected function defaultBody(): string
{
$body = [
$bodyArray = [
'command' => 'GetQRCSubStatus',
'TermNo' => $this->TermNo,
'qrcId' => $this->qrcId,
];
if ($this->RQperiod !== null) {
$body['RQperiod'] = $this->RQperiod;
$bodyArray['RQperiod'] = $this->RQperiod;
}
return $body;
return CmsSigner::canonicalize($bodyArray);
}
// </SECTION>
}

View File

@@ -12,11 +12,13 @@ namespace Apoloz\AlfaBank\Sbp\Requests\Subscription;
// <SECTION name="module_imports" intent="Import Saloon components for a JSON body request.">
// <BELIEF_STATE "This POST request utilizes `HasBody` and `HasJsonBody` for proper payload serialization. It currently returns a raw array response.">
use Apoloz\AlfaBank\Sbp\Data\Subscription\QrcSubPayPetitionResponseData;
use Apoloz\AlfaBank\Utils\CmsSigner;
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;
// </SECTION>
@@ -24,7 +26,7 @@ use Saloon\Traits\Body\HasJsonBody;
// <BELIEF_STATE "This class encapsulates the logic for initiating a payment through an SBP subscription, which is a key part of recurring payment flows.">
class QrcSubPayPetitionRequest extends Request implements HasBody
{
use HasJsonBody;
use HasStringBody;
// <SECTION name="http_method" intent="Declare the HTTP verb.">
// <BELIEF_STATE "Initiating a payment action that changes the system state is correctly represented by a POST request.">
@@ -50,23 +52,25 @@ class QrcSubPayPetitionRequest extends Request implements HasBody
// <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
public function createDtoFromResponse(Response $response): QrcSubPayPetitionResponseData
{
return \Apoloz\AlfaBank\Sbp\Data\Subscription\QrcSubPayPetitionResponseData::from($response->json());
return QrcSubPayPetitionResponseData::from($response->json());
}
// </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. All constructor properties are directly mapped to the request body.">
protected function defaultBody(): array
protected function defaultBody(): string
{
return [
$bodyArray = [
'command' => 'QRCSubPayPetition',
'TermNo' => $this->TermNo,
'subscriptionToken' => $this->subscriptionToken,
'SenderMemberId' => $this->SenderMemberId,
'qrcId' => $this->qrcId,
];
return CmsSigner::canonicalize($bodyArray);
}
// </SECTION>
}

View File

@@ -13,11 +13,13 @@ namespace Apoloz\AlfaBank\Sbp\Requests\Subscription;
// <SECTION name="module_imports" intent="Import DTO and Saloon components for a JSON body request.">
// <BELIEF_STATE "This POST request requires the `RegQrSubRequestData` DTO for its JSON body and utilizes `HasBody` and `HasJsonBody` for proper payload serialization. It currently returns a raw array response.">
use Apoloz\AlfaBank\Sbp\Data\Subscription\RegQrSubRequestData;
use Apoloz\AlfaBank\Sbp\Data\Subscription\RegQrSubResponseData;
use Apoloz\AlfaBank\Utils\CmsSigner;
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;
// </SECTION>
@@ -25,7 +27,7 @@ use Saloon\Traits\Body\HasJsonBody;
// <BELIEF_STATE "This class encapsulates the logic for initiating an SBP subscription, which is crucial for recurring payment models.">
class RegQrSubRequest extends Request implements HasBody
{
use HasJsonBody;
use HasStringBody;
// <SECTION name="http_method" intent="Declare the HTTP verb.">
// <BELIEF_STATE "Registering a new resource or initiating an action that changes the system state is correctly represented by a POST request.">
@@ -48,20 +50,22 @@ class RegQrSubRequest extends Request implements HasBody
// <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
public function createDtoFromResponse(Response $response): RegQrSubResponseData
{
return \Apoloz\AlfaBank\Sbp\Data\Subscription\RegQrSubResponseData::from($response->json());
return RegQrSubResponseData::from($response->json());
}
// </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. Using the DTO's `toArray` method ensures the payload is correctly structured.">
protected function defaultBody(): array
protected function defaultBody(): string
{
return array_merge(
$bodyArray = array_merge(
['command' => 'RegQRcSub'],
$this->data->toArray()
);
return CmsSigner::canonicalize($bodyArray);
}
// </SECTION>
}

View File

@@ -1,47 +1,33 @@
<?php
declare(strict_types=1);
/*
// MODULE_CONTRACT: Implements `SignerInterface` using OpenSSL for PKCS#7 (CMS) data signing.
// RESPONSIBILITY: To provide a concrete implementation of digital signing that takes a raw data string (e.g., canonicalized JSON), signs it using the configured mTLS certificate and private key, and returns a Base64 encoded PKCS#7 detached signature, adhering to Alfa Bank's security requirements.
// INPUTS: Data string to be signed. Certificate path and private key path are provided during instantiation.
// OUTPUTS: Base64 encoded PKCS#7 detached signature string.
// LINKS: Contracts/SecurityContracts.php, Utils/CmsSigner.php, README.md#security
*/
namespace Apoloz\AlfaBank\Services;
// <SECTION name="module_imports" intent="Import necessary classes for the signer contract.">
// <BELIEF_STATE "The `SignerInterface` is imported to ensure that `OpenSslCmsSigner` adheres to a predefined contract for digital signing, promoting interoperability and testability within the SDK.">
use Apoloz\AlfaBank\Contracts\SignerInterface;
use Apoloz\AlfaBank\Exceptions\SignerException;
// </SECTION>
// <SECTION name="class_definition" intent="Define the OpenSSL-based CMS signer service.">
// <BELIEF_STATE "The `OpenSslCmsSigner` class provides a robust and compliant solution for PKCS#7 CMS signing. By implementing `SignerInterface` and utilizing OpenSSL functions, it ensures that data integrity and authenticity can be cryptographically verified, which is critical for secure communication with financial institutions like Alfa Bank. The class takes certificate and private key paths during construction, allowing for flexible configuration.">
class OpenSslCmsSigner implements SignerInterface
{
// <SECTION name="constructor_definition" intent="Initialize the signer with certificate and private key paths.">
// <BELIEF_STATE "The constructor receives the file paths to the mTLS certificate and private key. These paths are stored as protected properties, ensuring that the signer instance is configured with the necessary credentials to perform digital signing operations, aligning with the principle of dependency injection for external resources.">
public function __construct(
protected string $certificatePath,
protected string $privateKeyPath
) {}
// </SECTION>
// <SECTION name="sign_method" intent="Sign a given data string using PKCS#7 CMS.">
// <BELIEF_STATE "The `sign` method performs the digital signature process as required by Alfa Bank. It reads the certificate and private key from the configured paths, then uses `openssl_pkcs7_sign` with `PKCS7_BINARY | PKCS7_DETACHED` flags to create a detached CMS signature. Error handling is included for robust operation, and the final signature is Base64 encoded after stripping unnecessary headers, ensuring compatibility with API expectations.">
public function sign(string $data): string
{
$privateKey = openssl_pkey_get_private(file_get_contents($this->privateKeyPath)); // assuming no passphrase for simplicity here
if (! $privateKey) {
throw new SignerException("Could not read private key for signing from {$this->privateKeyPath}.");
if (!file_exists($this->privateKeyPath)) {
throw new SignerException("Private key file not found: {$this->privateKeyPath}");
}
if (!file_exists($this->certificatePath)) {
throw new SignerException("Certificate file not found: {$this->certificatePath}");
}
$privateKey = openssl_pkey_get_private(file_get_contents($this->privateKeyPath));
$cert = openssl_x509_read(file_get_contents($this->certificatePath));
if (! $cert) {
throw new SignerException("Could not read certificate for signing from {$this->certificatePath}.");
if (!$privateKey || !$cert) {
throw new SignerException("Could not read private key or certificate for signing.");
}
$inFile = tempnam(sys_get_temp_dir(), 'alfa_in');
@@ -49,6 +35,7 @@ class OpenSslCmsSigner implements SignerInterface
file_put_contents($inFile, $data);
// Используем PKCS7_DETACHED согласно документации Альфа-Банка для digestSignatures
$status = openssl_pkcs7_sign(
$inFile,
$outFile,
@@ -58,25 +45,27 @@ class OpenSslCmsSigner implements SignerInterface
PKCS7_BINARY | PKCS7_DETACHED
);
if (! $status) {
if (file_exists($inFile)) {
unlink($inFile);
}
if (file_exists($outFile)) {
unlink($outFile);
}
throw new SignerException('Signing failed: ' . openssl_error_string());
if (!$status) {
@unlink($inFile);
@unlink($outFile);
throw new SignerException("OpenSSL sign failed: " . openssl_error_string());
}
$signedData = file_get_contents($outFile);
unlink($inFile);
unlink($outFile);
@unlink($inFile);
@unlink($outFile);
// Надежно извлекаем только Base64 блок подписи из S/MIME или PEM структуры
$parts = explode("\n\n", $signedData);
$signature = end($parts);
foreach ($parts as $part) {
// Очищаем часть от пробелов и переносов
$cleaned = str_replace(["\n", "\r", ' '], '', $part);
// Ищем блок, который выглядит как длинный Base64 (больше 128 символов) и не содержит S/MIME заголовков
if (preg_match('/^[a-zA-Z0-9\/+]+=*$/', $cleaned) && strlen($cleaned) > 128) {
return $cleaned;
}
}
return str_replace(["\n", "\r"], '', $signature);
throw new SignerException("Failed to extract signature body from CMS container. Output: " . substr($signedData, 0, 100));
}
// </SECTION>
}
// </SECTION>

View File

@@ -12,37 +12,47 @@ trait SignableData
{
public function sign(SignerInterface $signer): self
{
// 1. Create a clone to represent the data-to-be-signed (without existing signatures)
$clone = clone $this;
// 1. Get raw data as array
$data = $this->toArray();
// We explicitly set digestSignatures to null or empty for the signature calculation
// assuming the bank expects the "business payload" only.
// If the bank expects the field "digestSignatures": [] to be present in signed string,
// we should initialize it as empty collection.
// Based on common practices, we exclude it or keep it null.
// Let's assume we exclude it (set to null if the property is nullable).
$clone->digestSignatures = null;
// 2. Remove existing signatures if any
unset($data['digestSignatures']);
// 2. Serialize to JSON
// We use JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES to match typical API expectations
$jsonToSign = $clone->toJson(JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
// 3. Canonicalize (RFC 8785 - simple version: sort keys, remove nulls, no spaces)
$canonicalData = $this->canonicalizeArray($data);
// 3. Sign
$jsonToSign = json_encode($canonicalData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
// 4. Sign
$signature = $signer->sign($jsonToSign);
// 4. Attach signature to the original object
// 5. Attach signature to the original object
if ($this->digestSignatures === null) {
$this->digestSignatures = new DataCollection(DigestSignatureData::class, []);
}
// We can't push to DataCollection directly if it's not array-access compatible in all versions,
// but Spatie DataCollection is usually iterable.
// Safest way is to reconstruct it.
$current = $this->digestSignatures->toCollection()->all();
$current[] = new DigestSignatureData($signature, 'cms');
$current = array_values($this->digestSignatures->toCollection()->all());
$current[] = new DigestSignatureData($signature, 'RSA');
$this->digestSignatures = new DataCollection(DigestSignatureData::class, $current);
return $this;
}
private function canonicalizeArray(array $array): array
{
// Remove nulls and empty strings
$array = array_filter($array, fn ($v) => $v !== null);
// Sort keys lexically
ksort($array);
foreach ($array as $key => $value) {
if (is_array($value)) {
$array[$key] = $this->canonicalizeArray($value);
}
}
return $array;
}
}