commit f803480f7b9d3812bdaca12cc5c22e582f4acdc2 Author: Anatoliy Poloz Date: Wed Jan 14 16:40:33 2026 +0300 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7012a8d --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/vendor/ +/.phpunit.cache/ +/node_modules/ +.env +phpunit.xml +composer.lock diff --git a/README.md b/README.md new file mode 100644 index 0000000..2ab6cd2 --- /dev/null +++ b/README.md @@ -0,0 +1,145 @@ +# Alfa Bank SDK (Unofficial) + +This is an unofficial PHP SDK for the [Alfa Bank API](https://developers.alfabank.ru/), built using [Saloon](https://docs.saloon.dev) and [Spatie Laravel Data](https://spatie.be/docs/laravel-data). + +## Requirements + +- PHP 8.2+ +- OpenSSL (for certificate generation/handling) +- [Saloon](https://docs.saloon.dev) +- [Spatie Laravel Data](https://spatie.be/docs/laravel-data) +- [phpseclib](https://phpseclib.org/) (for Card Encryption) + +## Installation + +```bash +composer require gemini-cli/alfa-bank-sdk +``` + +## Laravel Integration + +The package supports auto-discovery. Publish the config file: + +```bash +php artisan vendor:publish --tag=alfa-bank-config +``` + +Configure your `.env`: +```env +ALFA_BAAS_CERT_PATH=/path/to/cert.cer +ALFA_BAAS_KEY_PATH=/path/to/key.key +ALFA_BAAS_ENV=sandbox + +ALFA_SBP_CERT_PATH=/path/to/cert.cer +ALFA_SBP_KEY_PATH=/path/to/key.key +ALFA_SBP_KEY_ALIAS=your-alias +ALFA_SBP_ENV=sandbox +``` + +Now you can inject the connectors: +```php +use Gemini\AlfaBank\AlfaBankConnector; + +public function index(AlfaBankConnector $connector) { + $connector->withToken('...')->send(new GetNominalAccountsRequest()); +} +``` + +## Core Usage (Generic PHP) + +### 1. Initialize the Connector + +```php +use Gemini\AlfaBank\AlfaBankConnector; +use Gemini\AlfaBank\Enums\Environment; + +$connector = new AlfaBankConnector( + certificatePath: '/path/to/client.cer', + privateKeyPath: '/path/to/private.key', + environment: Environment::SANDBOX +); +``` + +### 2. Authentication + +```php +use Gemini\AlfaBank\Requests\Auth\GetTokenRequest; + +$request = new GetTokenRequest( + clientId: 'your-client-id', + clientSecret: 'your-client-secret', + code: 'auth-code', + redirectUri: 'https://your-app.com/callback' +); + +$response = $connector->send($request); +$tokenData = $response->dto(); + +$connector->withToken($tokenData->access_token); +``` + +### 3. SBP / QR Payouts (C2B) + +```php +use Gemini\AlfaBank\Sbp\SbpConnector; + +$sbpConnector = new SbpConnector( + certificatePath: '/path/to/client.cer', + privateKeyPath: '/path/to/private.key', + keyNameAlias: 'your-key-alias', + environment: Environment::SANDBOX +); + +$response = $sbpConnector->send(new GetQrCodeRequest($payload)); +``` + +### 4. Utilities + +#### Card PAN Encryption +Required for card payouts in Nominal Accounts. + +```php +use Gemini\AlfaBank\Utils\CardEncryptor; + +$encryptedPan = CardEncryptor::encrypt( + pan: '4779642610000010', + publicKeyPem: file_get_contents('alfa-card-key.pub') +); +``` + +#### JSON Web Signature (JWS) +Used for signing some BaaS requests. + +```php +use Gemini\AlfaBank\Utils\JwsSigner; + +$jws = JwsSigner::sign( + payload: ['foo' => 'bar'], + privateKeyPem: file_get_contents('my-key.pem'), + certificateSerialNumber: '610003...' +); +``` + +#### JSON Web Encryption (JWE) +Used for encrypting sensitive BaaS requests. + +```php +use Gemini\AlfaBank\Utils\JweEncryptor; + +$jwe = JweEncryptor::encrypt( + jws: $jws, + publicKeyPem: file_get_contents('alfa-cert.cer'), + certificateSerialNumber: '610003...' +); +``` + +## Features + +- **Mutual TLS**: Automated certificate handling. +- **Laravel Ready**: Service Provider and Config included. +- **Typed DTOs**: Comprehensive response parsing. +- **Full Product Coverage**: BaaS (Nominal Accounts, Escrow), SBP (QR, Subscriptions), Digital Ruble. + +## License + +MIT diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..1877433 --- /dev/null +++ b/composer.json @@ -0,0 +1,41 @@ +{ + "name": "gemini-cli/alfa-bank-sdk", + "description": "Unofficial Alfa Bank API SDK for PHP using Saloon and Laravel Data", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Gemini CLI", + "email": "bot@example.com" + } + ], + "require": { + "php": "^8.2", + "saloonphp/saloon": "^3.0", + "spatie/laravel-data": "^4.0", + "guzzlehttp/guzzle": "^7.0", + "phpseclib/phpseclib": "^3.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.0", + "pestphp/pest": "^2.0" + }, + "autoload": { + "psr-4": { + "Gemini\\AlfaBank\\": "src/" + } + }, + "extra": { + "laravel": { + "providers": [ + "Gemini\\AlfaBank\\Laravel\\AlfaBankServiceProvider" + ] + } + }, + "minimum-stability": "stable", + "prefer-stable": true, + "scripts": { + "test": "vendor/bin/pest", + "test-coverage": "vendor/bin/pest --coverage" + } +} diff --git a/src/AlfaBankConnector.php b/src/AlfaBankConnector.php new file mode 100644 index 0000000..f9eb9d7 --- /dev/null +++ b/src/AlfaBankConnector.php @@ -0,0 +1,32 @@ +environment->value; + } + + protected function defaultConfig(): array + { + return [ + 'cert' => $this->certificatePath, + 'ssl_key' => $this->privateKeyPath, + 'verify' => true, // Ensure SSL verification is on + ]; + } +} diff --git a/src/Data/Auth/TokenData.php b/src/Data/Auth/TokenData.php new file mode 100644 index 0000000..68fb19f --- /dev/null +++ b/src/Data/Auth/TokenData.php @@ -0,0 +1,16 @@ +mergeConfigFrom(__DIR__ . '/Config/alfa-bank.php', 'alfa-bank'); + + $this->app->singleton(AlfaBankConnector::class, function ($app) { + $config = $app['config']['alfa-bank.baas']; + + return new AlfaBankConnector( + certificatePath: $config['certificate_path'], + privateKeyPath: $config['private_key_path'], + environment: $config['environment'] === 'production' ? Environment::PRODUCTION : Environment::SANDBOX + ); + }); + + $this->app->singleton(SbpConnector::class, function ($app) { + $config = $app['config']['alfa-bank.sbp']; + + return new SbpConnector( + certificatePath: $config['certificate_path'], + privateKeyPath: $config['private_key_path'], + keyNameAlias: $config['key_name_alias'], + environment: $config['environment'] === 'production' ? Environment::PRODUCTION : Environment::SANDBOX + ); + }); + } + + public function boot(): void + { + if ($this->app->runningInConsole()) { + $this->publishes([ + __DIR__ . '/Config/alfa-bank.php' => config_path('alfa-bank.php'), + ], 'alfa-bank-config'); + } + } +} diff --git a/src/Laravel/Config/alfa-bank.php b/src/Laravel/Config/alfa-bank.php new file mode 100644 index 0000000..8391391 --- /dev/null +++ b/src/Laravel/Config/alfa-bank.php @@ -0,0 +1,15 @@ + [ + 'certificate_path' => env('ALFA_BAAS_CERT_PATH'), + 'private_key_path' => env('ALFA_BAAS_KEY_PATH'), + 'environment' => env('ALFA_BAAS_ENV', 'sandbox'), // sandbox or production + ], + 'sbp' => [ + 'certificate_path' => env('ALFA_SBP_CERT_PATH'), + 'private_key_path' => env('ALFA_SBP_KEY_PATH'), + 'key_name_alias' => env('ALFA_SBP_KEY_ALIAS'), + 'environment' => env('ALFA_SBP_ENV', 'sandbox'), + ], +]; diff --git a/src/Requests/Auth/GetTokenRequest.php b/src/Requests/Auth/GetTokenRequest.php new file mode 100644 index 0000000..90e2191 --- /dev/null +++ b/src/Requests/Auth/GetTokenRequest.php @@ -0,0 +1,46 @@ + $this->grantType, + 'client_id' => $this->clientId, + 'client_secret' => $this->clientSecret, + 'code' => $this->code, + 'redirect_uri' => $this->redirectUri, + ]; + } + + public function createDtoFromResponse(Response $response): TokenData + { + return TokenData::from($response->json()); + } +} diff --git a/src/Requests/Auth/RefreshClientSecretRequest.php b/src/Requests/Auth/RefreshClientSecretRequest.php new file mode 100644 index 0000000..9af52c7 --- /dev/null +++ b/src/Requests/Auth/RefreshClientSecretRequest.php @@ -0,0 +1,26 @@ +clientId}/client-secret"; + } + + public function createDtoFromResponse(Response $response): array + { + return $response->json(); + } +} diff --git a/src/Requests/Auth/RevokeTokenRequest.php b/src/Requests/Auth/RevokeTokenRequest.php new file mode 100644 index 0000000..d0096c5 --- /dev/null +++ b/src/Requests/Auth/RevokeTokenRequest.php @@ -0,0 +1,37 @@ + $this->clientId, + 'client_secret' => $this->clientSecret, + 'token' => $this->token, + 'token_type_hint' => $this->tokenTypeHint, + ]); + } +} diff --git a/src/Requests/Auth/UserInfo/GetClientConsentsRequest.php b/src/Requests/Auth/UserInfo/GetClientConsentsRequest.php new file mode 100644 index 0000000..43d6095 --- /dev/null +++ b/src/Requests/Auth/UserInfo/GetClientConsentsRequest.php @@ -0,0 +1,44 @@ + $this->clientId, + 'sub' => $this->sub, + 'organizationIdHash' => $this->organizationIdHash, + 'isActive' => $this->isActive, + 'limit' => $this->limit, + 'offset' => $this->offset, + ], fn($value) => $value !== null); + } + + public function createDtoFromResponse(Response $response): ClientConsentCollectionData + { + return ClientConsentCollectionData::from($response->json()); + } +} diff --git a/src/Requests/Auth/UserInfo/GetUserInfoRequest.php b/src/Requests/Auth/UserInfo/GetUserInfoRequest.php new file mode 100644 index 0000000..59b58b2 --- /dev/null +++ b/src/Requests/Auth/UserInfo/GetUserInfoRequest.php @@ -0,0 +1,49 @@ + 'application/jwt', // As per doc + ]; + } + + /** + * The response is a JWT. We need to decode it to get the claims. + * Note: This implementation assumes the user will handle JWT verification/decoding + * or we can do a simple base64 decode of the payload if trust is established via mTLS/OIDC. + * + * For now, we will return the raw JWT string in a specific DTO or try to parse it if simple. + */ + public function createDtoFromResponse(Response $response): UserInfoData + { + $jwt = $response->body(); + $parts = explode('.', $jwt); + + if (count($parts) === 3) { + $payload = json_decode(base64_decode($parts[1]), true); + return UserInfoData::from($payload); + } + + // Fallback or throw error? + // If the response is not a valid JWT (e.g. error json), Saloon might have handled it if we throw on error status. + // But if it's 200 OK and not a JWT, it's unexpected. + + throw new \Exception("Invalid UserInfo response format. Expected JWT."); + } +} diff --git a/src/Requests/Crypto/GetCertificatesRequest.php b/src/Requests/Crypto/GetCertificatesRequest.php new file mode 100644 index 0000000..d68c378 --- /dev/null +++ b/src/Requests/Crypto/GetCertificatesRequest.php @@ -0,0 +1,24 @@ +json(), DataCollection::class); + } +} diff --git a/src/Requests/Crypto/UpdateCertificateRequest.php b/src/Requests/Crypto/UpdateCertificateRequest.php new file mode 100644 index 0000000..180b47f --- /dev/null +++ b/src/Requests/Crypto/UpdateCertificateRequest.php @@ -0,0 +1,43 @@ +id}"; + } + + protected function defaultBody(): array + { + return [ + 'op' => $this->op, + 'path' => $this->path, + 'value' => $this->status, + ]; + } + + public function createDtoFromResponse(Response $response): CertificateData + { + return CertificateData::from($response->json()); + } +} diff --git a/src/Requests/Crypto/UploadCertificateRequest.php b/src/Requests/Crypto/UploadCertificateRequest.php new file mode 100644 index 0000000..80b0310 --- /dev/null +++ b/src/Requests/Crypto/UploadCertificateRequest.php @@ -0,0 +1,40 @@ + $this->type, + 'certificateBody' => $this->certificateBody, + ]; + } + + public function createDtoFromResponse(Response $response): CertificateData + { + return CertificateData::from($response->json()); + } +} diff --git a/src/Requests/FileExchange/DownloadFileRequest.php b/src/Requests/FileExchange/DownloadFileRequest.php new file mode 100644 index 0000000..bbf61a9 --- /dev/null +++ b/src/Requests/FileExchange/DownloadFileRequest.php @@ -0,0 +1,27 @@ +downloadUrl; + } + + protected function defaultHeaders(): array + { + return [ + 'Accept' => '*/*', + ]; + } +} diff --git a/src/Requests/FileExchange/UploadFileRequest.php b/src/Requests/FileExchange/UploadFileRequest.php new file mode 100644 index 0000000..cdc59d4 --- /dev/null +++ b/src/Requests/FileExchange/UploadFileRequest.php @@ -0,0 +1,40 @@ +uploadUrl; + } + + protected function defaultHeaders(): array + { + return [ + 'Accept' => '*/*', + 'Content-Length' => (string)$this->fileSize, + 'Content-Type' => 'application/octet-stream', + ]; + } + + protected function defaultBody(): mixed + { + return $this->fileStream; + } +} diff --git a/src/Requests/NominalAccounts/Beneficiaries/CreateBeneficiaryRequest.php b/src/Requests/NominalAccounts/Beneficiaries/CreateBeneficiaryRequest.php new file mode 100644 index 0000000..2b12754 --- /dev/null +++ b/src/Requests/NominalAccounts/Beneficiaries/CreateBeneficiaryRequest.php @@ -0,0 +1,37 @@ +data->toArray(); + } + + public function createDtoFromResponse(Response $response): CreateBeneficiaryResponseData + { + return CreateBeneficiaryResponseData::from($response->json()); + } +} diff --git a/src/Requests/NominalAccounts/Beneficiaries/GetBeneficiariesRequest.php b/src/Requests/NominalAccounts/Beneficiaries/GetBeneficiariesRequest.php new file mode 100644 index 0000000..2204541 --- /dev/null +++ b/src/Requests/NominalAccounts/Beneficiaries/GetBeneficiariesRequest.php @@ -0,0 +1,52 @@ + $this->accountNumber, + 'type' => $this->type, + 'limit' => $this->limit, + 'offset' => $this->offset, + 'inn' => $this->inn, + 'lastName' => $this->lastName, + 'firstName' => $this->firstName, + 'middleName' => $this->middleName, + 'name' => $this->name, + 'isActive' => $this->isActive, + ], fn($value) => $value !== null); + } + + public function createDtoFromResponse(Response $response): BeneficiaryCollectionData + { + return BeneficiaryCollectionData::from($response->json()); + } +} diff --git a/src/Requests/NominalAccounts/Beneficiaries/GetBeneficiaryBalanceRequest.php b/src/Requests/NominalAccounts/Beneficiaries/GetBeneficiaryBalanceRequest.php new file mode 100644 index 0000000..d4c29c6 --- /dev/null +++ b/src/Requests/NominalAccounts/Beneficiaries/GetBeneficiaryBalanceRequest.php @@ -0,0 +1,37 @@ +beneficiaryId}/balance"; + } + + protected function defaultQuery(): array + { + return [ + 'accountNumber' => $this->accountNumber, + 'date' => $this->date, + ]; + } + + public function createDtoFromResponse(Response $response): BeneficiaryBalanceData + { + return BeneficiaryBalanceData::from($response->json()); + } +} diff --git a/src/Requests/NominalAccounts/Beneficiaries/GetBeneficiaryTransactionsRequest.php b/src/Requests/NominalAccounts/Beneficiaries/GetBeneficiaryTransactionsRequest.php new file mode 100644 index 0000000..d32c994 --- /dev/null +++ b/src/Requests/NominalAccounts/Beneficiaries/GetBeneficiaryTransactionsRequest.php @@ -0,0 +1,41 @@ +beneficiaryId}/transactions"; + } + + protected function defaultQuery(): array + { + return [ + 'dateFrom' => $this->dateFrom, + 'dateTo' => $this->dateTo, + 'limit' => $this->limit, + 'offset' => $this->offset, + ]; + } + + public function createDtoFromResponse(Response $response): BeneficiaryTransactionCollectionData + { + return BeneficiaryTransactionCollectionData::from($response->json()); + } +} diff --git a/src/Requests/NominalAccounts/Beneficiaries/Requisites/AddRequisiteRequest.php b/src/Requests/NominalAccounts/Beneficiaries/Requisites/AddRequisiteRequest.php new file mode 100644 index 0000000..f63bd82 --- /dev/null +++ b/src/Requests/NominalAccounts/Beneficiaries/Requisites/AddRequisiteRequest.php @@ -0,0 +1,38 @@ +beneficiaryId}/requisites"; + } + + protected function defaultBody(): array + { + return $this->data->toArray(); + } + + public function createDtoFromResponse(Response $response): RequisiteData + { + return RequisiteData::from($response->json()); + } +} diff --git a/src/Requests/NominalAccounts/Beneficiaries/Requisites/GetRequisitesRequest.php b/src/Requests/NominalAccounts/Beneficiaries/Requisites/GetRequisitesRequest.php new file mode 100644 index 0000000..4f5c687 --- /dev/null +++ b/src/Requests/NominalAccounts/Beneficiaries/Requisites/GetRequisitesRequest.php @@ -0,0 +1,28 @@ +beneficiaryId}/requisites"; + } + + public function createDtoFromResponse(Response $response): DataCollection + { + return RequisiteData::collect($response->json(), DataCollection::class); + } +} diff --git a/src/Requests/NominalAccounts/Beneficiaries/Requisites/UpdateRequisiteRequest.php b/src/Requests/NominalAccounts/Beneficiaries/Requisites/UpdateRequisiteRequest.php new file mode 100644 index 0000000..99798d9 --- /dev/null +++ b/src/Requests/NominalAccounts/Beneficiaries/Requisites/UpdateRequisiteRequest.php @@ -0,0 +1,32 @@ +beneficiaryId}/requisites/{$this->requisiteId}"; + } + + protected function defaultBody(): array + { + return $this->data->toArray(); + } +} diff --git a/src/Requests/NominalAccounts/Beneficiaries/ScoreBeneficiaryRequest.php b/src/Requests/NominalAccounts/Beneficiaries/ScoreBeneficiaryRequest.php new file mode 100644 index 0000000..c8ad5fa --- /dev/null +++ b/src/Requests/NominalAccounts/Beneficiaries/ScoreBeneficiaryRequest.php @@ -0,0 +1,27 @@ +beneficiaryId}/scoring"; + } + + public function createDtoFromResponse(Response $response): CreateBeneficiaryResponseData + { + return CreateBeneficiaryResponseData::from($response->json()); + } +} diff --git a/src/Requests/NominalAccounts/Beneficiaries/UpdateBeneficiaryRequest.php b/src/Requests/NominalAccounts/Beneficiaries/UpdateBeneficiaryRequest.php new file mode 100644 index 0000000..3ea2339 --- /dev/null +++ b/src/Requests/NominalAccounts/Beneficiaries/UpdateBeneficiaryRequest.php @@ -0,0 +1,38 @@ +beneficiaryId}"; + } + + protected function defaultBody(): array + { + return $this->data->toArray(); + } + + public function createDtoFromResponse(Response $response): CreateBeneficiaryResponseData + { + return CreateBeneficiaryResponseData::from($response->json()); + } +} diff --git a/src/Requests/NominalAccounts/Deals/AddDealRequest.php b/src/Requests/NominalAccounts/Deals/AddDealRequest.php new file mode 100644 index 0000000..29d9ece --- /dev/null +++ b/src/Requests/NominalAccounts/Deals/AddDealRequest.php @@ -0,0 +1,45 @@ + $this->accountNumber, + 'expireDate' => $this->expireDate, + 'digestSignatures' => $this->digestSignatures->toArray(), + ]; + } + + public function createDtoFromResponse(Response $response): DealData + { + return DealData::from($response->json()); + } +} diff --git a/src/Requests/NominalAccounts/Deals/DealActionRequest.php b/src/Requests/NominalAccounts/Deals/DealActionRequest.php new file mode 100644 index 0000000..95d00f9 --- /dev/null +++ b/src/Requests/NominalAccounts/Deals/DealActionRequest.php @@ -0,0 +1,38 @@ + $this->dealId, + 'action' => $this->action, + 'digestSignatures' => $this->digestSignatures->toArray(), + ]; + } +} diff --git a/src/Requests/NominalAccounts/Deals/GetDealPayoffsRequest.php b/src/Requests/NominalAccounts/Deals/GetDealPayoffsRequest.php new file mode 100644 index 0000000..2d1f82e --- /dev/null +++ b/src/Requests/NominalAccounts/Deals/GetDealPayoffsRequest.php @@ -0,0 +1,28 @@ +dealId}/payoffs"; + } + + public function createDtoFromResponse(Response $response): DataCollection + { + return DealPayoffData::collect($response->json(), DataCollection::class); + } +} diff --git a/src/Requests/NominalAccounts/Deals/GetDealRequest.php b/src/Requests/NominalAccounts/Deals/GetDealRequest.php new file mode 100644 index 0000000..313cb99 --- /dev/null +++ b/src/Requests/NominalAccounts/Deals/GetDealRequest.php @@ -0,0 +1,27 @@ +dealId}"; + } + + public function createDtoFromResponse(Response $response): DealData + { + return DealData::from($response->json()); + } +} diff --git a/src/Requests/NominalAccounts/Deals/GetDealRequestStatusRequest.php b/src/Requests/NominalAccounts/Deals/GetDealRequestStatusRequest.php new file mode 100644 index 0000000..36f237e --- /dev/null +++ b/src/Requests/NominalAccounts/Deals/GetDealRequestStatusRequest.php @@ -0,0 +1,28 @@ +dealId}/requests/{$this->requestId}/status"; + } + + public function createDtoFromResponse(Response $response): DealRequestStatusData + { + return DealRequestStatusData::from($response->json()); + } +} diff --git a/src/Requests/NominalAccounts/Deals/GetDealsRequest.php b/src/Requests/NominalAccounts/Deals/GetDealsRequest.php new file mode 100644 index 0000000..d58c9bc --- /dev/null +++ b/src/Requests/NominalAccounts/Deals/GetDealsRequest.php @@ -0,0 +1,38 @@ + $this->accountNumber, + 'limit' => $this->limit, + 'offset' => $this->offset, + ]; + } + + public function createDtoFromResponse(Response $response): DealCollectionData + { + return DealCollectionData::from($response->json()); + } +} diff --git a/src/Requests/NominalAccounts/Deals/Steps/GetStepContractorsRequest.php b/src/Requests/NominalAccounts/Deals/Steps/GetStepContractorsRequest.php new file mode 100644 index 0000000..1e4740e --- /dev/null +++ b/src/Requests/NominalAccounts/Deals/Steps/GetStepContractorsRequest.php @@ -0,0 +1,39 @@ +dealId}/steps/{$this->stepId}/contractors"; + } + + protected function defaultQuery(): array + { + return array_filter([ + 'contractorId' => $this->contractorId, + 'role' => $this->role, + ]); + } + + public function createDtoFromResponse(Response $response): DataCollection + { + return StepContractorData::collect($response->json(), DataCollection::class); + } +} diff --git a/src/Requests/NominalAccounts/Deals/Steps/GetStepRequest.php b/src/Requests/NominalAccounts/Deals/Steps/GetStepRequest.php new file mode 100644 index 0000000..ffef37b --- /dev/null +++ b/src/Requests/NominalAccounts/Deals/Steps/GetStepRequest.php @@ -0,0 +1,28 @@ +dealId}/steps/{$this->stepId}"; + } + + public function createDtoFromResponse(Response $response): StepData + { + return StepData::from($response->json()); + } +} diff --git a/src/Requests/NominalAccounts/Deals/Steps/GetStepsRequest.php b/src/Requests/NominalAccounts/Deals/Steps/GetStepsRequest.php new file mode 100644 index 0000000..1b76737 --- /dev/null +++ b/src/Requests/NominalAccounts/Deals/Steps/GetStepsRequest.php @@ -0,0 +1,38 @@ +dealId}/steps"; + } + + protected function defaultQuery(): array + { + return [ + 'limit' => $this->limit, + 'offset' => $this->offset, + ]; + } + + public function createDtoFromResponse(Response $response): DataCollection + { + return StepData::collect($response->json(), DataCollection::class); + } +} diff --git a/src/Requests/NominalAccounts/Deals/Steps/ManageContractorRequest.php b/src/Requests/NominalAccounts/Deals/Steps/ManageContractorRequest.php new file mode 100644 index 0000000..27890a8 --- /dev/null +++ b/src/Requests/NominalAccounts/Deals/Steps/ManageContractorRequest.php @@ -0,0 +1,30 @@ +data->toArray(); + } +} diff --git a/src/Requests/NominalAccounts/Deals/Steps/ManageStepRequest.php b/src/Requests/NominalAccounts/Deals/Steps/ManageStepRequest.php new file mode 100644 index 0000000..1abbe81 --- /dev/null +++ b/src/Requests/NominalAccounts/Deals/Steps/ManageStepRequest.php @@ -0,0 +1,37 @@ +data->toArray(); + } + + public function createDtoFromResponse(Response $response): StepActionResponseData + { + return StepActionResponseData::from($response->json()); + } +} diff --git a/src/Requests/NominalAccounts/Deals/ValidateDealRequest.php b/src/Requests/NominalAccounts/Deals/ValidateDealRequest.php new file mode 100644 index 0000000..4e86f48 --- /dev/null +++ b/src/Requests/NominalAccounts/Deals/ValidateDealRequest.php @@ -0,0 +1,27 @@ +dealId}/is-valid"; + } + + public function createDtoFromResponse(Response $response): ValidateDealResponseData + { + return ValidateDealResponseData::from($response->json()); + } +} diff --git a/src/Requests/NominalAccounts/GetCounterpartyRequestStatusRequest.php b/src/Requests/NominalAccounts/GetCounterpartyRequestStatusRequest.php new file mode 100644 index 0000000..9bd2050 --- /dev/null +++ b/src/Requests/NominalAccounts/GetCounterpartyRequestStatusRequest.php @@ -0,0 +1,27 @@ +requestId}/status"; + } + + public function createDtoFromResponse(Response $response): CounterpartyRequestStatusData + { + return CounterpartyRequestStatusData::from($response->json()); + } +} diff --git a/src/Requests/NominalAccounts/GetNominalAccountsRequest.php b/src/Requests/NominalAccounts/GetNominalAccountsRequest.php new file mode 100644 index 0000000..2d929ce --- /dev/null +++ b/src/Requests/NominalAccounts/GetNominalAccountsRequest.php @@ -0,0 +1,24 @@ +json(), DataCollection::class); + } +} diff --git a/src/Requests/NominalAccounts/Identification/GetIdentifyStatusRequest.php b/src/Requests/NominalAccounts/Identification/GetIdentifyStatusRequest.php new file mode 100644 index 0000000..70b5a0d --- /dev/null +++ b/src/Requests/NominalAccounts/Identification/GetIdentifyStatusRequest.php @@ -0,0 +1,28 @@ +id}/identify/{$this->identifyId}"; + } + + public function createDtoFromResponse(Response $response): IdentifyResponseData + { + return IdentifyResponseData::from($response->json()); + } +} diff --git a/src/Requests/NominalAccounts/Identification/GetUnidentifiedTransactionsRequest.php b/src/Requests/NominalAccounts/Identification/GetUnidentifiedTransactionsRequest.php new file mode 100644 index 0000000..f61650f --- /dev/null +++ b/src/Requests/NominalAccounts/Identification/GetUnidentifiedTransactionsRequest.php @@ -0,0 +1,38 @@ + $this->accountNumber, + 'limit' => $this->limit, + 'offset' => $this->offset, + ]); + } + + public function createDtoFromResponse(Response $response): UnidentifiedTransactionCollectionData + { + return UnidentifiedTransactionCollectionData::from($response->json()); + } +} diff --git a/src/Requests/NominalAccounts/Identification/IdentifyTransactionRequest.php b/src/Requests/NominalAccounts/Identification/IdentifyTransactionRequest.php new file mode 100644 index 0000000..6c1aa02 --- /dev/null +++ b/src/Requests/NominalAccounts/Identification/IdentifyTransactionRequest.php @@ -0,0 +1,38 @@ +id}/identify"; + } + + protected function defaultBody(): array + { + return $this->data->toArray(); + } + + public function createDtoFromResponse(Response $response): IdentifyResponseData + { + return IdentifyResponseData::from($response->json()); + } +} diff --git a/src/Requests/NominalAccounts/Identification/RefundUnidentifiedTransactionRequest.php b/src/Requests/NominalAccounts/Identification/RefundUnidentifiedTransactionRequest.php new file mode 100644 index 0000000..234f580 --- /dev/null +++ b/src/Requests/NominalAccounts/Identification/RefundUnidentifiedTransactionRequest.php @@ -0,0 +1,38 @@ +id}/refunds"; + } + + protected function defaultBody(): array + { + return $this->data->toArray(); + } + + public function createDtoFromResponse(Response $response): RefundUnidentifiedTransactionResponseData + { + return RefundUnidentifiedTransactionResponseData::from($response->json()); + } +} diff --git a/src/Requests/NominalAccounts/Payouts/CreatePayoffRequest.php b/src/Requests/NominalAccounts/Payouts/CreatePayoffRequest.php new file mode 100644 index 0000000..5752f1b --- /dev/null +++ b/src/Requests/NominalAccounts/Payouts/CreatePayoffRequest.php @@ -0,0 +1,37 @@ +data->toArray(); + } + + public function createDtoFromResponse(Response $response): PayoffResponseData + { + return PayoffResponseData::from($response->json()); + } +} diff --git a/src/Requests/NominalAccounts/Payouts/CreateTaxRefundRequest.php b/src/Requests/NominalAccounts/Payouts/CreateTaxRefundRequest.php new file mode 100644 index 0000000..1f36ae5 --- /dev/null +++ b/src/Requests/NominalAccounts/Payouts/CreateTaxRefundRequest.php @@ -0,0 +1,37 @@ +data->toArray(); + } + + public function createDtoFromResponse(Response $response): TaxRefundResponseData + { + return TaxRefundResponseData::from($response->json()); + } +} diff --git a/src/Requests/NominalAccounts/Payouts/GetPayoffRequest.php b/src/Requests/NominalAccounts/Payouts/GetPayoffRequest.php new file mode 100644 index 0000000..f42ea3f --- /dev/null +++ b/src/Requests/NominalAccounts/Payouts/GetPayoffRequest.php @@ -0,0 +1,27 @@ +payoffId}"; + } + + public function createDtoFromResponse(Response $response): DetailedPayoffData + { + return DetailedPayoffData::from($response->json()); + } +} diff --git a/src/Requests/NominalAccounts/Payouts/GetPayoffStatusRequest.php b/src/Requests/NominalAccounts/Payouts/GetPayoffStatusRequest.php new file mode 100644 index 0000000..e2771f1 --- /dev/null +++ b/src/Requests/NominalAccounts/Payouts/GetPayoffStatusRequest.php @@ -0,0 +1,27 @@ +payoffId}/status"; + } + + public function createDtoFromResponse(Response $response): PayoffResponseData + { + return PayoffResponseData::from($response->json()); + } +} diff --git a/src/Requests/NominalAccounts/Payouts/GetTaxRefundStatusRequest.php b/src/Requests/NominalAccounts/Payouts/GetTaxRefundStatusRequest.php new file mode 100644 index 0000000..4da1635 --- /dev/null +++ b/src/Requests/NominalAccounts/Payouts/GetTaxRefundStatusRequest.php @@ -0,0 +1,27 @@ +taxRefundId}/status"; + } + + public function createDtoFromResponse(Response $response): TaxRefundResponseData + { + return TaxRefundResponseData::from($response->json()); + } +} diff --git a/src/Requests/NominalAccounts/Payouts/PayoffAcceptRequest.php b/src/Requests/NominalAccounts/Payouts/PayoffAcceptRequest.php new file mode 100644 index 0000000..2e9e756 --- /dev/null +++ b/src/Requests/NominalAccounts/Payouts/PayoffAcceptRequest.php @@ -0,0 +1,38 @@ +payoffId}/accept"; + } + + protected function defaultBody(): array + { + return [ + 'accept' => $this->accept, + ]; + } + + public function createDtoFromResponse(Response $response): bool + { + return (bool)($response->json('result') ?? false); + } +} diff --git a/src/Requests/NominalAccounts/Payouts/TaxRefundAcceptRequest.php b/src/Requests/NominalAccounts/Payouts/TaxRefundAcceptRequest.php new file mode 100644 index 0000000..9ddf456 --- /dev/null +++ b/src/Requests/NominalAccounts/Payouts/TaxRefundAcceptRequest.php @@ -0,0 +1,36 @@ +taxRefundId}/accept"; + } + + protected function defaultQuery(): array + { + return ['accept' => $this->accept ? 'true' : 'false']; + } + + public function createDtoFromResponse(Response $response): bool + { + return (bool)($response->json('result') ?? false); + } +} diff --git a/src/Requests/NominalAccounts/UniversalRecipients/CreateUniversalRecipientRequest.php b/src/Requests/NominalAccounts/UniversalRecipients/CreateUniversalRecipientRequest.php new file mode 100644 index 0000000..77dd4ec --- /dev/null +++ b/src/Requests/NominalAccounts/UniversalRecipients/CreateUniversalRecipientRequest.php @@ -0,0 +1,37 @@ +data->toArray(); + } + + public function createDtoFromResponse(Response $response): UniversalRecipientResponseData + { + return UniversalRecipientResponseData::from($response->json()); + } +} diff --git a/src/Requests/NominalAccounts/UniversalRecipients/GetUniversalRecipientRequest.php b/src/Requests/NominalAccounts/UniversalRecipients/GetUniversalRecipientRequest.php new file mode 100644 index 0000000..47c8b00 --- /dev/null +++ b/src/Requests/NominalAccounts/UniversalRecipients/GetUniversalRecipientRequest.php @@ -0,0 +1,27 @@ +universalRecipientId}"; + } + + public function createDtoFromResponse(Response $response): UniversalRecipientData + { + return UniversalRecipientData::from($response->json()); + } +} diff --git a/src/Requests/NominalAccounts/UniversalRecipients/GetUniversalRecipientsRequest.php b/src/Requests/NominalAccounts/UniversalRecipients/GetUniversalRecipientsRequest.php new file mode 100644 index 0000000..cd7df66 --- /dev/null +++ b/src/Requests/NominalAccounts/UniversalRecipients/GetUniversalRecipientsRequest.php @@ -0,0 +1,45 @@ + $this->accountNumber, + 'type' => $this->type, + 'limit' => $this->limit, + 'offset' => $this->offset, + 'isActive' => $this->isActive, + 'date' => $this->date, + ], fn($val) => $val !== null); + } + + public function createDtoFromResponse(Response $response): DataCollection + { + return UniversalRecipientData::collect($response->json('universalRecipients'), DataCollection::class); + } +} diff --git a/src/Requests/NominalAccounts/UniversalRecipients/Requisites/AddUniversalRecipientRequisiteRequest.php b/src/Requests/NominalAccounts/UniversalRecipients/Requisites/AddUniversalRecipientRequisiteRequest.php new file mode 100644 index 0000000..4a779f0 --- /dev/null +++ b/src/Requests/NominalAccounts/UniversalRecipients/Requisites/AddUniversalRecipientRequisiteRequest.php @@ -0,0 +1,38 @@ +universalRecipientId}/requisites"; + } + + protected function defaultBody(): array + { + return $this->data->toArray(); + } + + public function createDtoFromResponse(Response $response): RequisiteData + { + return RequisiteData::from($response->json()); + } +} diff --git a/src/Requests/NominalAccounts/UniversalRecipients/Requisites/GetUniversalRecipientRequisitesRequest.php b/src/Requests/NominalAccounts/UniversalRecipients/Requisites/GetUniversalRecipientRequisitesRequest.php new file mode 100644 index 0000000..288f681 --- /dev/null +++ b/src/Requests/NominalAccounts/UniversalRecipients/Requisites/GetUniversalRecipientRequisitesRequest.php @@ -0,0 +1,28 @@ +universalRecipientId}/requisites"; + } + + public function createDtoFromResponse(Response $response): DataCollection + { + return RequisiteData::collect($response->json(), DataCollection::class); + } +} diff --git a/src/Requests/NominalAccounts/UniversalRecipients/Requisites/UpdateUniversalRecipientRequisiteRequest.php b/src/Requests/NominalAccounts/UniversalRecipients/Requisites/UpdateUniversalRecipientRequisiteRequest.php new file mode 100644 index 0000000..27a77d8 --- /dev/null +++ b/src/Requests/NominalAccounts/UniversalRecipients/Requisites/UpdateUniversalRecipientRequisiteRequest.php @@ -0,0 +1,32 @@ +universalRecipientId}/requisites/{$this->requisiteId}"; + } + + protected function defaultBody(): array + { + return $this->data->toArray(); + } +} diff --git a/src/Requests/Sandbox/Payments/CreateRubPaymentRequest.php b/src/Requests/Sandbox/Payments/CreateRubPaymentRequest.php new file mode 100644 index 0000000..1f96bcc --- /dev/null +++ b/src/Requests/Sandbox/Payments/CreateRubPaymentRequest.php @@ -0,0 +1,30 @@ +data->toArray(); + } +} diff --git a/src/Requests/Sandbox/SignatureEncryptionTestRequest.php b/src/Requests/Sandbox/SignatureEncryptionTestRequest.php new file mode 100644 index 0000000..7305f3b --- /dev/null +++ b/src/Requests/Sandbox/SignatureEncryptionTestRequest.php @@ -0,0 +1,41 @@ + 'application/jose', + 'Content-Type' => 'application/jose', + 'X-Request-ChannelSecurity' => $this->requestChannelSecurity, + 'X-Response-ChannelSecurity' => $this->responseChannelSecurity, + ]; + } + + protected function defaultBody(): ?string + { + return $this->jwsBody; + } +} diff --git a/src/Requests/Sandbox/TriggerWebhookEventRequest.php b/src/Requests/Sandbox/TriggerWebhookEventRequest.php new file mode 100644 index 0000000..adabd94 --- /dev/null +++ b/src/Requests/Sandbox/TriggerWebhookEventRequest.php @@ -0,0 +1,31 @@ + $this->object, + ]; + } +} diff --git a/src/Requests/Webhooks/CreateWebhookRequest.php b/src/Requests/Webhooks/CreateWebhookRequest.php new file mode 100644 index 0000000..6f55dbc --- /dev/null +++ b/src/Requests/Webhooks/CreateWebhookRequest.php @@ -0,0 +1,49 @@ + $this->object, + 'callbackUri' => $this->callbackUri, + 'version' => $this->version, + ]; + + if ($this->accounts !== null) { + $body['data'] = ['accounts' => $this->accounts]; + } + + return $body; + } + + public function createDtoFromResponse(Response $response): WebhookData + { + return WebhookData::from($response->json()); + } +} diff --git a/src/Requests/Webhooks/DeleteWebhookRequest.php b/src/Requests/Webhooks/DeleteWebhookRequest.php new file mode 100644 index 0000000..c51b6fc --- /dev/null +++ b/src/Requests/Webhooks/DeleteWebhookRequest.php @@ -0,0 +1,28 @@ +id}"; + } + + protected function defaultQuery(): array + { + return [ + 'object' => $this->object, + ]; + } +} diff --git a/src/Requests/Webhooks/GetWebhooksRequest.php b/src/Requests/Webhooks/GetWebhooksRequest.php new file mode 100644 index 0000000..c0feb78 --- /dev/null +++ b/src/Requests/Webhooks/GetWebhooksRequest.php @@ -0,0 +1,38 @@ + $this->offset, + 'limit' => $this->limit, + 'object' => $this->object, + ]); + } + + public function createDtoFromResponse(Response $response): WebhookCollectionData + { + return WebhookCollectionData::from($response->json()); + } +} diff --git a/src/Requests/Webhooks/UpdateWebhookRequest.php b/src/Requests/Webhooks/UpdateWebhookRequest.php new file mode 100644 index 0000000..ac534f2 --- /dev/null +++ b/src/Requests/Webhooks/UpdateWebhookRequest.php @@ -0,0 +1,55 @@ +id}"; + } + + protected function defaultBody(): array + { + $body = [ + 'object' => $this->object, + 'enabled' => $this->enabled, + 'version' => $this->version, + ]; + + if ($this->callbackUri !== null) { + $body['callbackUri'] = $this->callbackUri; + } + + if ($this->accounts !== null) { + $body['data'] = ['accounts' => $this->accounts]; + } + + return $body; + } + + public function createDtoFromResponse(Response $response): WebhookData + { + return WebhookData::from($response->json()); + } +} diff --git a/src/Sbp/Data/CashLink/ActivateCashQrRequestData.php b/src/Sbp/Data/CashLink/ActivateCashQrRequestData.php new file mode 100644 index 0000000..00fb808 --- /dev/null +++ b/src/Sbp/Data/CashLink/ActivateCashQrRequestData.php @@ -0,0 +1,20 @@ + 'ActivateCashQRc', + 'TermNo' => $this->data->TermNo, + 'qrcId' => $this->data->qrcId, + 'amount' => $this->data->amount, + 'currency' => $this->data->currency, + ]; + + if ($this->data->OrderNumber !== null) $body['OrderNumber'] = $this->data->OrderNumber; + if ($this->data->paymentPurpose !== null) $body['paymentPurpose'] = $this->data->paymentPurpose; + if ($this->data->qrTtl !== null) $body['qrTtl'] = $this->data->qrTtl; + if ($this->data->redirectUrl !== null) $body['redirectUrl'] = $this->data->redirectUrl; + if ($this->data->notificationUrl !== null) { + $body['queryData'] = ['notificationUrl' => $this->data->notificationUrl]; + } + + return $body; + } + + public function createDtoFromResponse(Response $response): array + { + return $response->json(); + } +} diff --git a/src/Sbp/Requests/CashLink/ChangeAccountForCashQrRequest.php b/src/Sbp/Requests/CashLink/ChangeAccountForCashQrRequest.php new file mode 100644 index 0000000..b69c529 --- /dev/null +++ b/src/Sbp/Requests/CashLink/ChangeAccountForCashQrRequest.php @@ -0,0 +1,50 @@ + 'ChangeAccForCashQRc', + 'TermNo' => $this->TermNo, + 'qrcId' => $this->qrcId, + 'account' => $this->account, + ]; + + if ($this->NewTermNo !== null) $body['NewTermNo'] = $this->NewTermNo; + if ($this->NewMerchantId !== null) $body['NewMerchantId'] = $this->NewMerchantId; + if ($this->redirectUrl !== null) $body['redirectUrl'] = $this->redirectUrl; + + return $body; + } + + public function createDtoFromResponse(\Saloon\Http\Response $response): array + { + return $response->json(); + } +} diff --git a/src/Sbp/Requests/CashLink/ChangeTermNoForCashQrRequest.php b/src/Sbp/Requests/CashLink/ChangeTermNoForCashQrRequest.php new file mode 100644 index 0000000..3db61e6 --- /dev/null +++ b/src/Sbp/Requests/CashLink/ChangeTermNoForCashQrRequest.php @@ -0,0 +1,39 @@ + 'ChangeTermNoForCashQRc', + 'TermNo' => $this->TermNo, + 'qrcId' => $this->qrcId, + ]; + } + + public function createDtoFromResponse(\Saloon\Http\Response $response): array + { + return $response->json(); + } +} diff --git a/src/Sbp/Requests/CashLink/DeleteCashQrRequest.php b/src/Sbp/Requests/CashLink/DeleteCashQrRequest.php new file mode 100644 index 0000000..e940ee2 --- /dev/null +++ b/src/Sbp/Requests/CashLink/DeleteCashQrRequest.php @@ -0,0 +1,39 @@ + 'DelCashQRc', + 'TermNo' => $this->TermNo, + 'qrcId' => $this->qrcId, + ]; + } + + public function createDtoFromResponse(\Saloon\Http\Response $response): array + { + return $response->json(); + } +} diff --git a/src/Sbp/Requests/CashLink/GetCashQrHistoryRequest.php b/src/Sbp/Requests/CashLink/GetCashQrHistoryRequest.php new file mode 100644 index 0000000..823ad43 --- /dev/null +++ b/src/Sbp/Requests/CashLink/GetCashQrHistoryRequest.php @@ -0,0 +1,43 @@ + 'GetCashQrcId', + 'TermNo' => $this->TermNo, + 'qrcId' => $this->qrcId, + 'dateFrom' => $this->dateFrom, + 'dateTo' => $this->dateTo, + ]; + } + + public function createDtoFromResponse(\Saloon\Http\Response $response): array + { + return $response->json(); + } +} diff --git a/src/Sbp/Requests/CashLink/GetCashQrStatusRequest.php b/src/Sbp/Requests/CashLink/GetCashQrStatusRequest.php new file mode 100644 index 0000000..afbdd43 --- /dev/null +++ b/src/Sbp/Requests/CashLink/GetCashQrStatusRequest.php @@ -0,0 +1,39 @@ + 'GetCashQRCstatus', + 'TermNo' => $this->TermNo, + 'qrcId' => $this->qrcId, + ]; + } + + public function createDtoFromResponse(\Saloon\Http\Response $response): array + { + return $response->json(); + } +} diff --git a/src/Sbp/Requests/CashLink/RegCashQrRequest.php b/src/Sbp/Requests/CashLink/RegCashQrRequest.php new file mode 100644 index 0000000..192c79e --- /dev/null +++ b/src/Sbp/Requests/CashLink/RegCashQrRequest.php @@ -0,0 +1,48 @@ + 'RegCashQRc', + 'TermNo' => $this->data->TermNo, + ]; + + if ($this->data->mediaType !== null) $body['mediaType'] = $this->data->mediaType; + if ($this->data->width !== null) $body['width'] = $this->data->width; + if ($this->data->height !== null) $body['height'] = $this->data->height; + if ($this->data->redirectUrl !== null) $body['redirectUrl'] = $this->data->redirectUrl; + if ($this->data->qrcId !== null) $body['qrcId'] = $this->data->qrcId; + + return $body; + } + + public function createDtoFromResponse(Response $response): RegCashQrResponseData + { + return RegCashQrResponseData::from($response->json()); + } +} diff --git a/src/Sbp/Requests/DigitalRuble/CashLink/ChangeAccForCashUniQrRequest.php b/src/Sbp/Requests/DigitalRuble/CashLink/ChangeAccForCashUniQrRequest.php new file mode 100644 index 0000000..bdbc8a9 --- /dev/null +++ b/src/Sbp/Requests/DigitalRuble/CashLink/ChangeAccForCashUniQrRequest.php @@ -0,0 +1,52 @@ + 'ChangeAccForCashUniQr', + 'TermNo' => $this->data->TermNo, + ]; + + if ($this->data->messageID !== null) $body['messageID'] = $this->data->messageID; + if ($this->data->qrcId !== null) $body['qrcId'] = $this->data->qrcId; + if ($this->data->account !== null) $body['account'] = $this->data->account; + if ($this->data->accountDR !== null) $body['accountDR'] = $this->data->accountDR; + if ($this->data->NewTermNo !== null) $body['NewTermNo'] = $this->data->NewTermNo; + if ($this->data->NewMerchantId !== null) $body['NewMerchantId'] = $this->data->NewMerchantId; + if ($this->data->redirectUrl !== null) $body['redirectUrl'] = $this->data->redirectUrl; + if ($this->data->paymentPageUrl !== null) $body['paymentPageUrl'] = $this->data->paymentPageUrl; + if ($this->data->fraudScore !== null) $body['fraudScore'] = $this->data->fraudScore; + + return $body; + } + + public function createDtoFromResponse(Response $response): ChangeAccForCashUniQrResponseData + { + return ChangeAccForCashUniQrResponseData::from($response->json()); + } +} diff --git a/src/Sbp/Requests/DigitalRuble/CashLink/CreateCashUniQrRequest.php b/src/Sbp/Requests/DigitalRuble/CashLink/CreateCashUniQrRequest.php new file mode 100644 index 0000000..af40489 --- /dev/null +++ b/src/Sbp/Requests/DigitalRuble/CashLink/CreateCashUniQrRequest.php @@ -0,0 +1,54 @@ + 'ReqCashUniQr', + 'TermNo' => $this->data->TermNo, + ]; + + if ($this->data->messageID !== null) $body['messageID'] = $this->data->messageID; + if ($this->data->redirectUrl !== null) $body['redirectUrl'] = $this->data->redirectUrl; + if ($this->data->paymentPageUrl !== null) $body['paymentPageUrl'] = $this->data->paymentPageUrl; + if ($this->data->mediaType !== null) $body['mediaType'] = $this->data->mediaType; + if ($this->data->width !== null) $body['width'] = $this->data->width; + if ($this->data->height !== null) $body['height'] = $this->data->height; + if ($this->data->qrcId !== null) $body['qrcId'] = $this->data->qrcId; + + if ($this->data->notificationUrl !== null) { + $body['queryData'] = ['notificationUrl' => $this->data->notificationUrl]; + } + + return $body; + } + + public function createDtoFromResponse(Response $response): CreateCashUniQrResponseData + { + return CreateCashUniQrResponseData::from($response->json()); + } +} diff --git a/src/Sbp/Requests/DigitalRuble/CashLink/GetCashUniQrStatusRequest.php b/src/Sbp/Requests/DigitalRuble/CashLink/GetCashUniQrStatusRequest.php new file mode 100644 index 0000000..c93dcb2 --- /dev/null +++ b/src/Sbp/Requests/DigitalRuble/CashLink/GetCashUniQrStatusRequest.php @@ -0,0 +1,47 @@ + 'GetCashUniQrStatus', + 'TermNo' => $this->data->TermNo, + 'uniQrId' => $this->data->uniQrId, + 'paramsId' => $this->data->paramsId, + ]; + + if ($this->data->messageID !== null) $body['messageID'] = $this->data->messageID; + if ($this->data->payrrn !== null) $body['payrrn'] = $this->data->payrrn; + + return $body; + } + + public function createDtoFromResponse(Response $response): GetCashUniQrStatusResponseData + { + return GetCashUniQrStatusResponseData::from($response->json()); + } +} diff --git a/src/Sbp/Requests/DigitalRuble/CreateUniQrRequest.php b/src/Sbp/Requests/DigitalRuble/CreateUniQrRequest.php new file mode 100644 index 0000000..1d77412 --- /dev/null +++ b/src/Sbp/Requests/DigitalRuble/CreateUniQrRequest.php @@ -0,0 +1,62 @@ + 'ReqUniQr', + 'TermNo' => $this->data->TermNo, + 'uniQrType' => $this->data->uniQrType, + ]; + + // Conditional mappings + if ($this->data->amount !== null) $body['amount'] = $this->data->amount; + if ($this->data->paymentPurpose !== null) $body['paymentPurpose'] = $this->data->paymentPurpose; + if ($this->data->redirectUrl !== null) $body['redirectUrl'] = $this->data->redirectUrl; + if ($this->data->paymentPageUrl !== null) $body['paymentPageUrl'] = $this->data->paymentPageUrl; + if ($this->data->mediaType !== null) $body['mediaType'] = $this->data->mediaType; + if ($this->data->width !== null) $body['width'] = $this->data->width; + if ($this->data->height !== null) $body['height'] = $this->data->height; + if ($this->data->qrTtl !== null) $body['qrTtl'] = $this->data->qrTtl; + if ($this->data->messageID !== null) $body['messageID'] = $this->data->messageID; + + $queryData = []; + if ($this->data->notificationUrl !== null) $queryData['notificationUrl'] = $this->data->notificationUrl; + if ($this->data->SenderFIO !== null) $queryData['SenderFIO'] = $this->data->SenderFIO; + if ($this->data->SenderID !== null) $queryData['SenderID'] = $this->data->SenderID; + if ($this->data->SenderBankBIC !== null) $queryData['SenderBankBIC'] = $this->data->SenderBankBIC; + + if (!empty($queryData)) { + $body['queryData'] = $queryData; + } + + return $body; + } + + public function createDtoFromResponse(\Saloon\Http\Response $response): array + { + return $response->json(); + } +} diff --git a/src/Sbp/Requests/DigitalRuble/GetUniQrStatusRequest.php b/src/Sbp/Requests/DigitalRuble/GetUniQrStatusRequest.php new file mode 100644 index 0000000..a518ba9 --- /dev/null +++ b/src/Sbp/Requests/DigitalRuble/GetUniQrStatusRequest.php @@ -0,0 +1,39 @@ + 'GetUniQrStatus', + 'TermNo' => $this->TermNo, + 'uniQrIds' => $this->uniQrIds, + ]; + } + + public function createDtoFromResponse(\Saloon\Http\Response $response): array + { + return $response->json(); + } +} diff --git a/src/Sbp/Requests/DigitalRuble/Notifications/ParsePaymentDrNotification.php b/src/Sbp/Requests/DigitalRuble/Notifications/ParsePaymentDrNotification.php new file mode 100644 index 0000000..78d2b2c --- /dev/null +++ b/src/Sbp/Requests/DigitalRuble/Notifications/ParsePaymentDrNotification.php @@ -0,0 +1,14 @@ + 'GetQRCd', + 'TermNo' => $this->data->TermNo, + 'qrcType' => $this->data->qrcType, + 'currency' => $this->data->currency, + ]; + + // Common Fields + if ($this->data->amount !== null) $body['amount'] = $this->data->amount; + if ($this->data->paymentPurpose !== null) $body['paymentPurpose'] = $this->data->paymentPurpose; + if ($this->data->redirectUrl !== null) $body['redirectUrl'] = $this->data->redirectUrl; + if ($this->data->messageID !== null) $body['messageID'] = $this->data->messageID; + if ($this->data->width !== null) $body['width'] = $this->data->width; + if ($this->data->height !== null) $body['height'] = $this->data->height; + + // Dynamic QR Fields + if ($this->data->OrderNumber !== null) $body['OrderNumber'] = $this->data->OrderNumber; + if ($this->data->qrTtl !== null) $body['qrTtl'] = $this->data->qrTtl; + if ($this->data->subscriptionServiceId !== null) $body['subscriptionServiceId'] = $this->data->subscriptionServiceId; + if ($this->data->subscriptionServiceName !== null) $body['subscriptionServiceName'] = $this->data->subscriptionServiceName; + + // Query Data nesting + $queryData = []; + if ($this->data->notificationUrl !== null) $queryData['notificationUrl'] = $this->data->notificationUrl; + if ($this->data->SenderFIO !== null) $queryData['SenderFIO'] = $this->data->SenderFIO; + if ($this->data->SenderID !== null) $queryData['SenderID'] = $this->data->SenderID; + if ($this->data->SenderBankBIC !== null) $queryData['SenderBankBIC'] = $this->data->SenderBankBIC; + + if (!empty($queryData)) { + $body['queryData'] = $queryData; + } + + return $body; + } + + public function createDtoFromResponse(Response $response): QrCodeResponseData + { + return QrCodeResponseData::from($response->json()); + } +} \ No newline at end of file diff --git a/src/Sbp/Requests/Management/BlockQrRequest.php b/src/Sbp/Requests/Management/BlockQrRequest.php new file mode 100644 index 0000000..5bddaca --- /dev/null +++ b/src/Sbp/Requests/Management/BlockQrRequest.php @@ -0,0 +1,45 @@ + 'BlockQRcId', + 'TermNo' => $this->data->TermNo, + 'qrcId' => $this->data->qrcId, + ]; + + if ($this->data->reason !== null) { + $body['reason'] = $this->data->reason; + } + + return $body; + } + + public function createDtoFromResponse(\Saloon\Http\Response $response): array + { + return $response->json(); + } +} diff --git a/src/Sbp/Requests/Management/GetBlockedQrListRequest.php b/src/Sbp/Requests/Management/GetBlockedQrListRequest.php new file mode 100644 index 0000000..cfc7fad --- /dev/null +++ b/src/Sbp/Requests/Management/GetBlockedQrListRequest.php @@ -0,0 +1,37 @@ + 'GetBlockedQRcIdList', + 'TermNo' => $this->TermNo, + ]; + } + + public function createDtoFromResponse(\Saloon\Http\Response $response): array + { + return $response->json(); + } +} diff --git a/src/Sbp/Requests/Management/UnblockQrRequest.php b/src/Sbp/Requests/Management/UnblockQrRequest.php new file mode 100644 index 0000000..46967cd --- /dev/null +++ b/src/Sbp/Requests/Management/UnblockQrRequest.php @@ -0,0 +1,39 @@ + 'UnblockQRcId', + 'TermNo' => $this->data->TermNo, + 'qrcId' => $this->data->qrcId, + ]; + } + + public function createDtoFromResponse(\Saloon\Http\Response $response): array + { + return $response->json(); + } +} diff --git a/src/Sbp/Requests/Registry/GetBankListRequest.php b/src/Sbp/Requests/Registry/GetBankListRequest.php new file mode 100644 index 0000000..b491276 --- /dev/null +++ b/src/Sbp/Requests/Registry/GetBankListRequest.php @@ -0,0 +1,44 @@ + 'GetBankList', + ]; + + if ($this->messageID !== null) $body['messageID'] = $this->messageID; + if ($this->BankMemberIdFilter !== null) $body['BankMemberIdFilter'] = $this->BankMemberIdFilter; + + return $body; + } + + public function createDtoFromResponse(Response $response): GetBankListResponseData + { + return GetBankListResponseData::from($response->json()); + } +} diff --git a/src/Sbp/Requests/Reversal/GetQrcReversalDataRequest.php b/src/Sbp/Requests/Reversal/GetQrcReversalDataRequest.php new file mode 100644 index 0000000..9d5ecc5 --- /dev/null +++ b/src/Sbp/Requests/Reversal/GetQrcReversalDataRequest.php @@ -0,0 +1,51 @@ + '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; + } + + public function createDtoFromResponse(Response $response): GetQrcReversalDataResponseData + { + return GetQrcReversalDataResponseData::from($response->json()); + } +} diff --git a/src/Sbp/Requests/Reversal/GetQrcReversalStatusRequest.php b/src/Sbp/Requests/Reversal/GetQrcReversalStatusRequest.php new file mode 100644 index 0000000..2716459 --- /dev/null +++ b/src/Sbp/Requests/Reversal/GetQrcReversalStatusRequest.php @@ -0,0 +1,47 @@ + '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; + } + + public function createDtoFromResponse(Response $response): GetQrcReversalStatusResponseData + { + return GetQrcReversalStatusResponseData::from($response->json()); + } +} diff --git a/src/Sbp/Requests/Reversal/QrcReversalRequest.php b/src/Sbp/Requests/Reversal/QrcReversalRequest.php new file mode 100644 index 0000000..b2edaa4 --- /dev/null +++ b/src/Sbp/Requests/Reversal/QrcReversalRequest.php @@ -0,0 +1,52 @@ + '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; + } + + public function createDtoFromResponse(Response $response): QrcReversalResponseData + { + return QrcReversalResponseData::from($response->json()); + } +} diff --git a/src/Sbp/Requests/Status/GetQrcStatusRequest.php b/src/Sbp/Requests/Status/GetQrcStatusRequest.php new file mode 100644 index 0000000..20114c5 --- /dev/null +++ b/src/Sbp/Requests/Status/GetQrcStatusRequest.php @@ -0,0 +1,46 @@ + '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; + } + + public function createDtoFromResponse(Response $response): GetQrcStatusResponseData + { + return GetQrcStatusResponseData::from($response->json()); + } +} diff --git a/src/Sbp/Requests/Subscription/GetQrSubStatusRequest.php b/src/Sbp/Requests/Subscription/GetQrSubStatusRequest.php new file mode 100644 index 0000000..c1ad386 --- /dev/null +++ b/src/Sbp/Requests/Subscription/GetQrSubStatusRequest.php @@ -0,0 +1,46 @@ + 'GetQRCSubStatus', + 'TermNo' => $this->TermNo, + 'qrcId' => $this->qrcId, + ]; + + if ($this->RQperiod !== null) { + $body['RQperiod'] = $this->RQperiod; + } + + return $body; + } + + public function createDtoFromResponse(\Saloon\Http\Response $response): array + { + return $response->json(); + } +} diff --git a/src/Sbp/Requests/Subscription/QrcSubPayPetitionRequest.php b/src/Sbp/Requests/Subscription/QrcSubPayPetitionRequest.php new file mode 100644 index 0000000..62aaf2d --- /dev/null +++ b/src/Sbp/Requests/Subscription/QrcSubPayPetitionRequest.php @@ -0,0 +1,44 @@ + 'QRCSubPayPetition', + 'TermNo' => $this->TermNo, + 'subscriptionToken' => $this->subscriptionToken, + 'SenderMemberId' => $this->SenderMemberId, + 'qrcId' => $this->qrcId, + ]; + } + + public function createDtoFromResponse(Response $response): array + { + return $response->json(); + } +} diff --git a/src/Sbp/Requests/Subscription/RegQrSubRequest.php b/src/Sbp/Requests/Subscription/RegQrSubRequest.php new file mode 100644 index 0000000..54b0152 --- /dev/null +++ b/src/Sbp/Requests/Subscription/RegQrSubRequest.php @@ -0,0 +1,51 @@ + '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; + } + + public function createDtoFromResponse(Response $response): array + { + return $response->json(); + } +} diff --git a/src/Sbp/SbpConnector.php b/src/Sbp/SbpConnector.php new file mode 100644 index 0000000..030e3b4 --- /dev/null +++ b/src/Sbp/SbpConnector.php @@ -0,0 +1,75 @@ +environment) { + Environment::SANDBOX => 'https://217.12.103.132:2443/fsCryptoProxy', + Environment::PRODUCTION => 'https://pay.alfabank.ru/fsCryptoProxy', // Placeholder, user needs to confirm + }; + } + + protected function defaultConfig(): array + { + return [ + 'cert' => $this->certificatePath, + 'ssl_key' => $this->privateKeyPath, + 'verify' => true, + ]; + } + + protected function defaultHeaders(): array + { + return [ + 'Content-Type' => 'application/x-www-form-urlencoded', // As per doc, despite body being JSON + 'key-name' => $this->keyNameAlias, + ]; + } + + public function boot(PendingRequest $pendingRequest): void + { + // Calculate Signature + $body = $pendingRequest->body()?->all(); + + if (empty($body)) { + return; + } + + // Minify JSON for signature + $jsonBody = json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + + // Remove spaces/newlines as per doc requirement "exclude all spaces, tabs, newlines" + // Standard json_encode without pretty print usually does this, but let's be sure. + // The doc example: `{"command":"GetQRCd",...}` implies no spaces after colons/commas. + + $privateKey = file_get_contents($this->privateKeyPath); + $pkeyId = openssl_get_privatekey($privateKey); + + openssl_sign($jsonBody, $signature, $pkeyId, OPENSSL_ALGO_SHA256); + $signatureBase64 = base64_encode($signature); + + $pendingRequest->headers()->add('Authorization', $signatureBase64); + + // Ensure the body sent is the exact string we signed? + // Saloon will encode the array to JSON again. + // We should enforce the body to be the string we just signed to ensure consistency. + $pendingRequest->body()->set($jsonBody); + } +} diff --git a/src/Traits/HasAlfaPagination.php b/src/Traits/HasAlfaPagination.php new file mode 100644 index 0000000..a02123b --- /dev/null +++ b/src/Traits/HasAlfaPagination.php @@ -0,0 +1,19 @@ + $this->limit, + 'offset' => $this->offset, + ], fn($v) => $val !== null); + } +} diff --git a/src/Utils/CardEncryptor.php b/src/Utils/CardEncryptor.php new file mode 100644 index 0000000..3ac5f4f --- /dev/null +++ b/src/Utils/CardEncryptor.php @@ -0,0 +1,28 @@ +withPadding(RSA::ENCRYPTION_OAEP) + ->withHash('sha256') + ->withMGFHash('sha1'); // Doc says MGF1 with SHA1 + + return base64_encode($algo->encrypt($pan)); + } +} diff --git a/src/Utils/CmsSigner.php b/src/Utils/CmsSigner.php new file mode 100644 index 0000000..69d6f5b --- /dev/null +++ b/src/Utils/CmsSigner.php @@ -0,0 +1,89 @@ + $certificateSerialNumber, + 'enc' => 'A256GCM', + 'alg' => 'RSA-OAEP-256', + ]; + + $base64Header = JwsSigner::base64UrlEncode(json_encode($header)); + + // 1. Generate Content Encryption Key (CEK) - 256 bits for A256GCM + $cek = Random::string(32); + + // 2. Encrypt CEK with RSA-OAEP-256 + /** @var RSA $publicKey */ + $publicKey = PublicKeyLoader::load($publicKeyPem); + $publicKey = $publicKey->withHash('sha256')->withMGFHash('sha256')->withPadding(RSA::ENCRYPTION_OAEP); + $encryptedCek = $publicKey->encrypt($cek); + $base64EncryptedKey = JwsSigner::base64UrlEncode($encryptedCek); + + // 3. Generate IV + $iv = Random::string(12); + $base64Iv = JwsSigner::base64UrlEncode($iv); + + // 4. Encrypt Payload (JWS) + $tag = ''; + $ciphertext = openssl_encrypt( + $jws, + 'aes-256-gcm', + $cek, + OPENSSL_RAW_DATA, + $iv, + $tag, + $base64Header // AAD + ); + + $base64Ciphertext = JwsSigner::base64UrlEncode($ciphertext); + $base64Tag = JwsSigner::base64UrlEncode($tag); + + return $base64Header . '.' . $base64EncryptedKey . '.' . $base64Iv . '.' . $base64Ciphertext . '.' . $base64Tag; + } + + /** + * Decrypts a JWE string. + */ + public static function decrypt(string $jwe, string $privateKeyPem, ?string $passphrase = null): ?string + { + $parts = explode('.', $jwe); + if (count($parts) !== 5) { + return null; + } + + [$base64Header, $base64EncKey, $base64Iv, $base64Ciphertext, $base64Tag] = $parts; + + // 1. Decrypt CEK + /** @var RSA $privateKey */ + $privateKey = PublicKeyLoader::load($privateKeyPem, $passphrase ?? ''); + $privateKey = $privateKey->withHash('sha256')->withMGFHash('sha256')->withPadding(RSA::ENCRYPTION_OAEP); + + $cek = $privateKey->decrypt(JwsSigner::base64UrlDecode($base64EncKey)); + if (!$cek) return null; + + // 2. Decrypt Ciphertext + $decrypted = openssl_decrypt( + JwsSigner::base64UrlDecode($base64Ciphertext), + 'aes-256-gcm', + $cek, + OPENSSL_RAW_DATA, + JwsSigner::base64UrlDecode($base64Iv), + JwsSigner::base64UrlDecode($base64Tag), + $base64Header // AAD + ); + + return $decrypted ?: null; + } +} diff --git a/src/Utils/JwsSigner.php b/src/Utils/JwsSigner.php new file mode 100644 index 0000000..4a378d2 --- /dev/null +++ b/src/Utils/JwsSigner.php @@ -0,0 +1,70 @@ + $certificateSerialNumber, + 'typ' => 'JWT', + 'alg' => 'RS256', + ]; + + $base64Header = self::base64UrlEncode(json_encode($header)); + $base64Payload = self::base64UrlEncode(json_encode($payload)); + + $dataToSign = $base64Header . '.' . $base64Payload; + + /** @var RSA $privateKey */ + $privateKey = PublicKeyLoader::load($privateKeyPem, $passphrase ?? ''); + $privateKey = $privateKey->withHash('sha256')->withPadding(RSA::SIGNATURE_PKCS1); + + $signature = $privateKey->sign($dataToSign); + $base64Signature = self::base64UrlEncode($signature); + + return $dataToSign . '.' . $base64Signature; + } + + /** + * Verifies an RS256 JWS. + */ + public static function verify(string $jws, string $publicKeyPem): ?array + { + $parts = explode('.', $jws); + if (count($parts) !== 3) { + return null; + } + + [$base64Header, $base64Payload, $base64Signature] = $parts; + $dataToVerify = $base64Header . '.' . $base64Payload; + $signature = self::base64UrlDecode($base64Signature); + + /** @var RSA $publicKey */ + $publicKey = PublicKeyLoader::load($publicKeyPem); + $publicKey = $publicKey->withHash('sha256')->withPadding(RSA::SIGNATURE_PKCS1); + + if ($publicKey->verify($dataToVerify, $signature)) { + return json_decode(self::base64UrlDecode($base64Payload), true); + } + + return null; + } + + public static function base64UrlEncode(string $data): string + { + return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($data)); + } + + public static function base64UrlDecode(string $data): string + { + return base64_decode(str_replace(['-', '_'], ['+', '/'], $data)); + } +} diff --git "a/src/Webhooks\\Data\\BaseWebhookData.php" "b/src/Webhooks\\Data\\BaseWebhookData.php" new file mode 100644 index 0000000..b62fcd2 --- /dev/null +++ "b/src/Webhooks\\Data\\BaseWebhookData.php" @@ -0,0 +1,21 @@ + MockResponse::make([ + 'access_token' => 'test-access-token', + 'refresh_token' => 'test-refresh-token', + 'expires_in' => 3600, + 'token_type' => 'Bearer', + ], 200), + ]); + + $connector = new AlfaBankConnector('cert.pem', 'key.pem'); + $connector->withMockClient($mockClient); + + $request = new GetTokenRequest('client-id', 'client-secret', 'code', 'redirect-uri'); + $response = $connector->send($request); + + expect($response->status())->toBe(200) + ->and($response->dto()->access_token)->toBe('test-access-token'); +}); diff --git a/tests/Feature/NominalAccounts/BeneficiariesTest.php b/tests/Feature/NominalAccounts/BeneficiariesTest.php new file mode 100644 index 0000000..d57da31 --- /dev/null +++ b/tests/Feature/NominalAccounts/BeneficiariesTest.php @@ -0,0 +1,36 @@ + MockResponse::make([ + 'requestId' => 'req-123', + 'status' => 'PENDING' + ], 202), + ]); + + $connector = new AlfaBankConnector('cert.pem', 'key.pem'); + $connector->withMockClient($mockClient); + + $payload = new CreateBeneficiaryRequestData( + type: 'FL_RESIDENT', + countryOfTaxResidence: 'RU', + addresses: AddressData::collect([ + new AddressData('REGISTRATION_ADDRESS', 'Moscow', 'RU') + ]), + digestSignatures: DigestSignatureData::collect([ + new DigestSignatureData('base64sig', 'rsa') + ]) + ); + + $response = $connector->send(new CreateBeneficiaryRequest($payload)); + + expect($response->status())->toBe(202) + ->and($response->dto()->requestId)->toBe('req-123'); +}); diff --git a/tests/Feature/NominalAccounts/NominalAccountsTest.php b/tests/Feature/NominalAccounts/NominalAccountsTest.php new file mode 100644 index 0000000..f2e21b7 --- /dev/null +++ b/tests/Feature/NominalAccounts/NominalAccountsTest.php @@ -0,0 +1,24 @@ + MockResponse::make([ + [ + 'accountNumber' => '40817810306050000000', + 'isActive' => true + ] + ], 200), + ]); + + $connector = new AlfaBankConnector('cert.pem', 'key.pem'); + $connector->withMockClient($mockClient); + + $response = $connector->send(new GetNominalAccountsRequest()); + + expect($response->status())->toBe(200) + ->and($response->dto()->first()->accountNumber)->toBe('40817810306050000000'); +}); diff --git a/tests/Feature/Sbp/QrCodeTest.php b/tests/Feature/Sbp/QrCodeTest.php new file mode 100644 index 0000000..5a4e1cc --- /dev/null +++ b/tests/Feature/Sbp/QrCodeTest.php @@ -0,0 +1,30 @@ + MockResponse::make([ + 'ErrorCode' => 0, + 'message' => 'OK', + 'payload' => 'https://qr.nspk.ru/test', + 'image' => 'base64image' + ], 200), + ]); + + $connector = new SbpConnector('cert.pem', 'key.pem', 'key-alias'); + $connector->withMockClient($mockClient); + + $payload = new QrCodeRequestData( + TermNo: 'test-term', + amount: '100' + ); + + $response = $connector->send(new GetQrCodeRequest($payload)); + + expect($response->status())->toBe(200) + ->and($response->dto()->payload)->toBe('https://qr.nspk.ru/test'); +});