├── .gitignore ├── phive.xml ├── src ├── Exception │ ├── ResponseParsingException.php │ ├── ActionException.php │ ├── NetworkException.php │ ├── SberbankAcquiringException.php │ └── BadResponseException.php ├── Currency.php ├── HttpClient │ ├── HttpClientInterface.php │ ├── SymfonyAdapter.php │ ├── GuzzleAdapter.php │ ├── Psr18Adapter.php │ └── CurlClient.php ├── OrderStatus.php ├── ClientFactory.php └── Client.php ├── Makefile ├── .php-cs-fixer.dist.php ├── composer.lock ├── phpunit.xml.dist ├── composer.json ├── .travis.yml ├── LICENSE ├── README.md └── tests └── ClientTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | ### Composer 2 | composer.phar 3 | /vendor/ 4 | 5 | ### Phive 6 | /tools/ 7 | -------------------------------------------------------------------------------- /phive.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/Exception/ResponseParsingException.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class ResponseParsingException extends SberbankAcquiringException 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := build 2 | 3 | .PHONY: build 4 | build: lint test 5 | 6 | .PHONY: cs-fix 7 | cs-fix: 8 | @tools/php-cs-fixer fix 9 | 10 | .PHONY: lint 11 | lint: 12 | @composer validate --strict 13 | @tools/php-cs-fixer fix --diff --dry-run -v 14 | 15 | .PHONY: test 16 | test: 17 | @tools/phpunit 18 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | in('src') 5 | ; 6 | 7 | return (new PhpCsFixer\Config()) 8 | ->setRules([ 9 | '@PSR2' => true, 10 | 'array_syntax' => ['syntax' => 'short'], 11 | ]) 12 | ->setFinder($finder) 13 | ->setUsingCache(false) 14 | ; 15 | -------------------------------------------------------------------------------- /src/Exception/ActionException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class ActionException extends SberbankAcquiringException 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Exception/NetworkException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class NetworkException extends SberbankAcquiringException 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Exception/SberbankAcquiringException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class SberbankAcquiringException extends \Exception 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Currency.php: -------------------------------------------------------------------------------- 1 | 11 | * @see https://en.wikipedia.org/wiki/ISO_4217 12 | */ 13 | class Currency 14 | { 15 | const EUR = 978; 16 | const RUB = 643; 17 | const UAH = 980; 18 | const USD = 840; 19 | } 20 | -------------------------------------------------------------------------------- /src/Exception/BadResponseException.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class BadResponseException extends SberbankAcquiringException 11 | { 12 | /** 13 | * @var string 14 | */ 15 | private $response; 16 | 17 | public function getResponse(): ?string 18 | { 19 | return $this->response; 20 | } 21 | 22 | public function setResponse(string $response): void 23 | { 24 | $this->response = $response; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "77e0b283fc7e1a2d9933622f2fd70a80", 8 | "packages": [], 9 | "packages-dev": [], 10 | "aliases": [], 11 | "minimum-stability": "stable", 12 | "stability-flags": [], 13 | "prefer-stable": false, 14 | "prefer-lowest": false, 15 | "platform": { 16 | "php": "^7.1||^8", 17 | "ext-json": "*" 18 | }, 19 | "platform-dev": [], 20 | "plugin-api-version": "1.1.0" 21 | } 22 | -------------------------------------------------------------------------------- /src/HttpClient/HttpClientInterface.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | interface HttpClientInterface 15 | { 16 | const METHOD_GET = 'GET'; 17 | const METHOD_POST = 'POST'; 18 | 19 | /** 20 | * Send an HTTP request. 21 | * 22 | * @throws NetworkException 23 | * 24 | * @return array A response 25 | */ 26 | public function request(string $uri, string $method = self::METHOD_GET, array $headers = [], string $data = ''): array; 27 | } 28 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | src 13 | 14 | 15 | 16 | 17 | tests 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "voronkovich/sberbank-acquiring-client", 3 | "type": "library", 4 | "description": "Client for working with Sberbank's acquiring REST API", 5 | "keywords": ["sberbank", "acquiring", "credit card", "client"], 6 | "homepage": "https://github.com/voronkovich/sberbank-acquiring-client", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Oleg Voronkovich", 11 | "email": "oleg-voronkovich@yandex.ru" 12 | } 13 | ], 14 | "require": { 15 | "php": "^7.1||^8", 16 | "ext-json": "*" 17 | }, 18 | "autoload": { 19 | "psr-4": { 20 | "Voronkovich\\SberbankAcquiring\\": "src/" 21 | } 22 | }, 23 | "autoload-dev": { 24 | "psr-4": { 25 | "Voronkovich\\SberbankAcquiring\\Tests\\": "tests/" 26 | } 27 | }, 28 | "minimum-stability": "stable" 29 | } 30 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | os: linux 3 | dist: xenial 4 | 5 | jobs: 6 | fast_finish: true 7 | include: 8 | - php: 7.3 9 | - php: 7.4 10 | - php: 8.0 11 | - php: nightly 12 | allow_failures: 13 | - php: nightly 14 | 15 | cache: 16 | directories: 17 | - $HOME/.composer 18 | - $HOME/.phive 19 | 20 | before_install: 21 | - sudo apt-get update 22 | - sudo apt-get -y install gnupg-curl 23 | - composer self-update 24 | - travis_retry wget https://phar.io/releases/phive.phar 25 | - travis_retry wget https://phar.io/releases/phive.phar.asc 26 | - travis_retry gpg --keyserver hkps://keys.openpgp.org --recv-keys 0x6AF725270AB81E04D79442549D8A98B29B2D5D79 27 | - gpg --verify phive.phar.asc phive.phar 28 | 29 | install: 30 | - composer install 31 | - travis_retry php phive.phar install --trust-gpg-keys 4AA394086372C20A,E82B2FB314E9906E 32 | 33 | script: 34 | - make build 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Oleg Voronkovich 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /src/HttpClient/SymfonyAdapter.php: -------------------------------------------------------------------------------- 1 | 13 | * @see https://symfony.com/doc/current/http_client.html 14 | */ 15 | class SymfonyAdapter implements HttpClientInterface 16 | { 17 | private $client; 18 | 19 | public function __construct(ClientInterface $client) 20 | { 21 | $this->client = $client; 22 | } 23 | 24 | public function request(string $uri, string $method = HttpClientInterface::METHOD_GET, array $headers = [], string $data = ''): array 25 | { 26 | $requestUri = $uri; 27 | $options = [ 28 | 'headers' => $headers 29 | ]; 30 | 31 | switch ($method) { 32 | case HttpClientInterface::METHOD_GET: 33 | $requestUri = $uri . '?' . $data; 34 | break; 35 | case HttpClientInterface::METHOD_POST: 36 | $options['body'] = $data; 37 | break; 38 | default: 39 | throw new \InvalidArgumentException( 40 | sprintf( 41 | 'Invalid HTTP method "%s". Use "%s" or "%s".', 42 | $method, 43 | HttpClientInterface::METHOD_GET, 44 | HttpClientInterface::METHOD_POST 45 | ) 46 | ); 47 | break; 48 | } 49 | 50 | $response = $this->client->request($method, $requestUri, $options); 51 | 52 | $statusCode = $response->getStatusCode(); 53 | $body = $response->getContent(); 54 | 55 | return [$statusCode, $body]; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/HttpClient/GuzzleAdapter.php: -------------------------------------------------------------------------------- 1 | 14 | * @see http://docs.guzzlephp.org/en/latest/ 15 | */ 16 | class GuzzleAdapter implements HttpClientInterface 17 | { 18 | private $client; 19 | private $version; 20 | 21 | public function __construct(ClientInterface $client) 22 | { 23 | $this->client = $client; 24 | 25 | $class = \get_class($client); 26 | 27 | if (\defined($class.'::MAJOR_VERSION')) { 28 | $this->version = (int) $client::MAJOR_VERSION; 29 | } elseif (\defined($class.'::VERSION')) { 30 | $this->version = (int) $client::VERSION; 31 | } 32 | } 33 | 34 | public function request(string $uri, string $method = HttpClientInterface::METHOD_GET, array $headers = [], string $data = ''): array 35 | { 36 | $options = ['headers' => $headers]; 37 | 38 | switch ($method) { 39 | case HttpClientInterface::METHOD_GET: 40 | $options['query'] = $data; 41 | break; 42 | case HttpClientInterface::METHOD_POST: 43 | $options['body'] = $data; 44 | break; 45 | default: 46 | throw new \InvalidArgumentException( 47 | sprintf( 48 | 'Invalid HTTP method "%s". Use "%s" or "%s".', 49 | $method, 50 | HttpClientInterface::METHOD_GET, 51 | HttpClientInterface::METHOD_POST 52 | ) 53 | ); 54 | break; 55 | } 56 | 57 | if (6 > $this->version) { 58 | $request = $this->client->createRequest($method, $uri, $options); 59 | $response = $this->client->send($request); 60 | } else { 61 | $response = $this->client->request($method, $uri, $options); 62 | } 63 | 64 | $statusCode = $response->getStatusCode(); 65 | $body = $response->getBody()->getContents(); 66 | 67 | return [$statusCode, $body]; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/HttpClient/Psr18Adapter.php: -------------------------------------------------------------------------------- 1 | 15 | * @see https://www.php-fig.org/psr/psr-18/ 16 | */ 17 | class Psr18Adapter implements HttpClientInterface 18 | { 19 | private $client; 20 | private $requestFactory; 21 | private $streamFactory; 22 | 23 | public function __construct(ClientInterface $client, RequestFactoryInterface $requestFactory, StreamFactoryInterface $streamFactory) 24 | { 25 | $this->client = $client; 26 | $this->requestFactory = $requestFactory; 27 | $this->streamFactory = $streamFactory; 28 | } 29 | 30 | public function request(string $uri, string $method = HttpClientInterface::METHOD_GET, array $headers = [], string $data = ''): array 31 | { 32 | switch ($method) { 33 | case HttpClientInterface::METHOD_GET: 34 | $request = $this->requestFactory->createRequest('GET', $uri . '?' . $data); 35 | break; 36 | case HttpClientInterface::METHOD_POST: 37 | $request = $this->requestFactory->createRequest('POST', $uri); 38 | 39 | $body = $this->streamFactory->createStream($data); 40 | 41 | $request = $request->withBody($body); 42 | break; 43 | default: 44 | throw new \InvalidArgumentException( 45 | sprintf( 46 | 'Invalid HTTP method "%s". Use "%s" or "%s".', 47 | $method, 48 | HttpClientInterface::METHOD_GET, 49 | HttpClientInterface::METHOD_POST 50 | ) 51 | ); 52 | break; 53 | } 54 | 55 | foreach ($headers as $key => $value) { 56 | $request = $request->withHeader($key, $value); 57 | } 58 | 59 | $response = $this->client->sendRequest($request); 60 | 61 | $statusCode = $response->getStatusCode(); 62 | $body = (string) $response->getBody(); 63 | 64 | return [$statusCode, $body]; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/OrderStatus.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class OrderStatus 13 | { 14 | // An order was successfully registered, but is'nt paid yet 15 | const CREATED = 0; 16 | 17 | // An order's amount was successfully holded (for two-stage payments only) 18 | const APPROVED = 1; 19 | 20 | // An order was deposited 21 | // If you want to check whether payment was successfully paid - use this constant 22 | const DEPOSITED = 2; 23 | 24 | // An order was reversed 25 | const REVERSED = 3; 26 | 27 | // An order was refunded 28 | const REFUNDED = 4; 29 | 30 | // An order authorization was initialized by card emitter's ACS 31 | const AUTHORIZATION_INITIALIZED = 5; 32 | 33 | // An order was declined 34 | const DECLINED = 6; 35 | 36 | public static function isCreated($status): bool 37 | { 38 | // (int) '' === 0 39 | return '' !== $status && self::CREATED === (int) $status; 40 | } 41 | 42 | public static function isApproved($status): bool 43 | { 44 | return self::APPROVED === (int) $status; 45 | } 46 | 47 | public static function isDeposited($status): bool 48 | { 49 | return self::DEPOSITED === (int) $status; 50 | } 51 | 52 | public static function isReversed($status): bool 53 | { 54 | return self::REVERSED === (int) $status; 55 | } 56 | 57 | public static function isRefunded($status): bool 58 | { 59 | return self::REFUNDED === (int) $status; 60 | } 61 | 62 | public static function isAuthorizationInitialized($status): bool 63 | { 64 | return self::AUTHORIZATION_INITIALIZED === (int) $status; 65 | } 66 | 67 | public static function isDeclined($status): bool 68 | { 69 | return self::DECLINED === (int) $status; 70 | } 71 | 72 | public static function statusToString($status): string 73 | { 74 | switch ((int) $status) { 75 | case self::CREATED: 76 | return 'CREATED'; 77 | break; 78 | case self::APPROVED: 79 | return 'APPROVED'; 80 | break; 81 | case self::DEPOSITED: 82 | return 'DEPOSITED'; 83 | break; 84 | case self::REVERSED: 85 | return 'REVERSED'; 86 | break; 87 | case self::DECLINED: 88 | return 'DECLINED'; 89 | break; 90 | case self::REFUNDED: 91 | return 'REFUNDED'; 92 | break; 93 | } 94 | 95 | return ''; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/HttpClient/CurlClient.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class CurlClient implements HttpClientInterface 15 | { 16 | /** 17 | * @var resource 18 | */ 19 | private $curl; 20 | 21 | /** 22 | * @var array 23 | */ 24 | private $curlOptions = []; 25 | 26 | public function __construct(array $curlOptions) 27 | { 28 | if (!\extension_loaded('curl')) { 29 | throw new \RuntimeException('Curl extension is not loaded.'); 30 | } 31 | 32 | $this->curlOptions = $curlOptions; 33 | } 34 | 35 | /** 36 | * @return resource 37 | */ 38 | private function getCurl() 39 | { 40 | if (null === $this->curl) { 41 | $this->curl = \curl_init(); 42 | \curl_setopt_array($this->curl, $this->curlOptions); 43 | } 44 | 45 | return $this->curl; 46 | } 47 | 48 | public function request(string $uri, string $method = HttpClientInterface::METHOD_GET, array $headers = [], string $data = ''): array 49 | { 50 | if (HttpClientInterface::METHOD_GET === $method) { 51 | $curlOptions[\CURLOPT_HTTPGET] = true; 52 | $curlOptions[\CURLOPT_URL] = $uri . '?' . $data; 53 | } elseif (HttpClientInterface::METHOD_POST === $method) { 54 | $curlOptions[\CURLOPT_POST] = true; 55 | $curlOptions[\CURLOPT_URL] = $uri; 56 | $curlOptions[\CURLOPT_POSTFIELDS] = $data; 57 | } else { 58 | throw new \InvalidArgumentException( 59 | \sprintf( 60 | 'An HTTP method "%s" is not supported. Use "%s" or "%s".', 61 | $method, 62 | HttpClientInterface::METHOD_GET, 63 | HttpClientInterface::METHOD_POST 64 | ) 65 | ); 66 | } 67 | 68 | foreach ($headers as $key => $value) { 69 | $curlOptions[\CURLOPT_HTTPHEADER][] = "$key: $value"; 70 | } 71 | 72 | $curlOptions[\CURLOPT_RETURNTRANSFER] = true; 73 | 74 | $curl = $this->getCurl(); 75 | \curl_setopt_array($curl, $curlOptions); 76 | 77 | $response = \curl_exec($curl); 78 | 79 | if (false === $response) { 80 | $error = \curl_error($curl); 81 | $errorCode = \curl_errno($curl); 82 | 83 | throw new NetworkException('Curl error: ' . $error, $errorCode); 84 | } 85 | 86 | $httpCode = \curl_getinfo($this->curl, \CURLINFO_HTTP_CODE); 87 | 88 | return [$httpCode, $response]; 89 | } 90 | 91 | public function __destruct() 92 | { 93 | if (null !== $this->curl) { 94 | \curl_close($this->curl); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/ClientFactory.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class ClientFactory 13 | { 14 | /** 15 | * Create a client for the Sberbank production environment. 16 | * 17 | * @param array $options Client options (username, password and etc.) 18 | * 19 | * @see https://ecomtest.sberbank.ru/doc#section/Obshaya-informaciya/Obrabotka-soobshenij 20 | * 21 | * @return Client instance 22 | */ 23 | public static function sberbank(array $options): Client 24 | { 25 | return new Client( 26 | \array_merge( 27 | [ 28 | 'apiUri' => 'https://ecommerce.sberbank.ru', 29 | 'prefixDefault' => '/ecomm/gw/partner/api/v1/', 30 | 'ecom' => true, 31 | ], 32 | $options 33 | ) 34 | ); 35 | } 36 | 37 | /** 38 | * Create a client for the Sberbank testing environment. 39 | * 40 | * @param array $options Client options (username, password and etc.) 41 | * 42 | * @see https://ecomtest.sberbank.ru/doc#section/Obshaya-informaciya/Obrabotka-soobshenij 43 | * 44 | * @return Client instance 45 | */ 46 | public static function sberbankTest(array $options): Client 47 | { 48 | return new Client( 49 | \array_merge( 50 | [ 51 | 'apiUri' => 'https://ecomtest.sberbank.ru', 52 | 'prefixDefault' => '/ecomm/gw/partner/api/v1/', 53 | 'ecom' => true, 54 | ], 55 | $options 56 | ) 57 | ); 58 | } 59 | 60 | /** 61 | * Create a client for the Alfabank production environment. 62 | * 63 | * @param array $options Client options (username, password and etc.) 64 | * 65 | * @see https://pay.alfabank.ru/ecommerce/instructions/merchantManual/pages/fz_index.html#koordinati_podkljuchenija 66 | * 67 | * @return Client instance 68 | */ 69 | public static function alfabank(array $options): Client 70 | { 71 | return new Client( 72 | \array_merge( 73 | [ 74 | 'apiUri' => 'https://pay.alfabank.ru', 75 | 'prefixDefault' => '/payment/rest/', 76 | 'prefixSbpQr' => '/payment/rest/sbp/c2b/qr/dynamic/', 77 | 'prefixApple' => '/payment/applepay/', 78 | 'prefixGoogle' => '/payment/google/', 79 | 'prefixSamsung' => '/payment/samsung/', 80 | ], 81 | $options 82 | ) 83 | ); 84 | } 85 | 86 | /** 87 | * Create a client for the Alfabank testing environment. 88 | * 89 | * @param array $options Client options (username, password and etc.) 90 | * 91 | * @see https://ecomtest.sberbank.ru/doc#section/Obshaya-informaciya/Obrabotka-soobshenij 92 | * 93 | * @return Client instance 94 | */ 95 | public static function alfabankTest(array $options): Client 96 | { 97 | return new Client( 98 | \array_merge( 99 | [ 100 | 'apiUri' => 'https://alfa.rbsuat.com', 101 | 'prefixDefault' => '/payment/rest/', 102 | 'prefixSbpQr' => '/payment/rest/sbp/c2b/qr/dynamic/', 103 | 'prefixApple' => '/payment/applepay/', 104 | 'prefixGoogle' => '/payment/google/', 105 | 'prefixSamsung' => '/payment/samsung/', 106 | ], 107 | $options 108 | ) 109 | ); 110 | } 111 | 112 | /** 113 | * Create a client for the YooKassa production environment. 114 | * 115 | * @param array $options Client options (username, password and etc.) 116 | * 117 | * @see https://yoomoney.ru/i/forms/yc-program-interface-api-sberbank.pdf 118 | * 119 | * @return Client instance 120 | */ 121 | public static function yookassa(array $options): Client 122 | { 123 | return new Client( 124 | \array_merge( 125 | [ 126 | 'apiUri' => 'https://3dsec-payments.yookassa.ru', 127 | 'prefixDefault' => '/payment/rest/', 128 | ], 129 | $options 130 | ) 131 | ); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sberbank-acquiring-client 2 | 3 | [![Build Status](https://app.travis-ci.com/voronkovich/sberbank-acquiring-client.svg?branch=master)](https://app.travis-ci.com/github/voronkovich/sberbank-acquiring-client) 4 | [![Latest Stable Version](https://poser.pugx.org/voronkovich/sberbank-acquiring-client/v/stable)](https://packagist.org/packages/voronkovich/sberbank-acquiring-client) 5 | [![Total Downloads](https://poser.pugx.org/voronkovich/sberbank-acquiring-client/downloads)](https://packagist.org/packages/voronkovich/sberbank-acquiring-client/stats) 6 | [![License](https://poser.pugx.org/voronkovich/sberbank-acquiring-client/license)](./LICENSE) 7 | 8 | PHP client for [Sberbank](https://ecomtest.sberbank.ru/doc), [Alfabank](https://pay.alfabank.ru/ecommerce/instructions/merchantManual/pages/index/rest.html) and [YooKassa](https://yoomoney.ru/i/forms/yc-program-interface-api-sberbank.pdf) REST APIs. 9 | 10 | ## Requirements 11 | 12 | - PHP 7.1 or above (Old version for PHP 5 you can find [here](https://github.com/voronkovich/sberbank-acquiring-client/tree/1.x)) 13 | - TLS 1.2 or above (more information you can find [here](https://civicrm.org/blog/yashodha/are-you-ready-for-tls-12-update-cant-escape-it)) 14 | - `php-json` extension installed 15 | 16 | ## Installation 17 | 18 | ```sh 19 | composer require 'voronkovich/sberbank-acquiring-client' 20 | ``` 21 | 22 | ## Usage 23 | 24 | ### Instantiating a client 25 | 26 | In most cases to instantiate a client you need to pass your `username` and `password` to a factory: 27 | 28 | ```php 29 | use Voronkovich\SberbankAcquiring\ClientFactory; 30 | 31 | // Sberbank production environment 32 | $client = ClientFactory::sberbank(['userName' => 'username', 'password' => 'password']); 33 | 34 | // Sberbank testing environment 35 | $client = ClientFactory::sberbankTest(['userName' => 'username', 'password' => 'password']); 36 | 37 | // Alfabank production environment 38 | $client = ClientFactory::alfabank(['userName' => 'username', 'password' => 'password']); 39 | 40 | // Alfabank testing environment 41 | $client = ClientFactory::alfabankTest(['userName' => 'username', 'password' => 'password']); 42 | 43 | // YooKassa production environment 44 | $client = ClientFactory::yookassa(['userName' => 'username', 'password' => 'password']); 45 | ``` 46 | 47 | Alternatively you can use an authentication `token`: 48 | 49 | ```php 50 | $client = ClientFactory::sberbank(['token' => 'sberbank-token']); 51 | ``` 52 | 53 | More advanced example: 54 | 55 | ```php 56 | use Voronkovich\SberbankAcquiring\ClientFactory; 57 | use Voronkovich\SberbankAcquiring\Currency; 58 | use Voronkovich\SberbankAcquiring\HttpClient\HttpClientInterface; 59 | 60 | $client = ClientFactory::sberbank([ 61 | 'userName' => 'username', 62 | 'password' => 'password', 63 | // A language code in ISO 639-1 format. 64 | // Use this option to set a language of error messages. 65 | 'language' => 'ru', 66 | 67 | // A currency code in ISO 4217 format. 68 | // Use this option to set a currency used by default. 69 | 'currency' => Currency::RUB, 70 | 71 | // An HTTP method to use in requests. 72 | // Must be "GET" or "POST" ("POST" is used by default). 73 | 'httpMethod' => HttpClientInterface::METHOD_GET, 74 | 75 | // An HTTP client for sending requests. 76 | // Use this option when you don't want to use 77 | // a default HTTP client implementation distributed 78 | // with this package (for example, when you have'nt 79 | // a CURL extension installed in your server). 80 | 'httpClient' => new YourCustomHttpClient(), 81 | ]); 82 | ``` 83 | 84 | Also you can use an adapter for the [Guzzle](https://github.com/guzzle/guzzle): 85 | 86 | ```php 87 | use Voronkovich\SberbankAcquiring\ClientFactory; 88 | use Voronkovich\SberbankAcquiring\HttpClient\GuzzleAdapter; 89 | 90 | use GuzzleHttp\Client as Guzzle; 91 | 92 | $client = ClientFactory::sberbank([ 93 | 'userName' => 'username', 94 | 'password' => 'password', 95 | 'httpClient' => new GuzzleAdapter(new Guzzle()), 96 | ]); 97 | ``` 98 | 99 | Also, there are available adapters for [Symfony](https://symfony.com/doc/current/http_client.html) and [PSR-18](https://www.php-fig.org/psr/psr-18/) HTTP clents. 100 | 101 | ### Low level method "execute" 102 | 103 | You can interact with the Gateway REST API using a low level method `execute`: 104 | 105 | ```php 106 | $client->execute('/ecomm/gw/partner/api/v1/register.do', [ 107 | 'orderNumber' => 1111, 108 | 'amount' => 10, 109 | 'returnUrl' => 'http://localhost/sberbank/success', 110 | ]); 111 | 112 | $status = $client->execute('/ecomm/gw/partner/api/v1/getOrderStatusExtended.do', [ 113 | 'orderId' => '64fc8831-a2b0-721b-64fc-883100001553', 114 | ]); 115 | ``` 116 | But it's more convenient to use one of the shortcuts listed below. 117 | 118 | ### Creating a new order 119 | 120 | [Sberbank](https://ecomtest.sberbank.ru/doc#tag/basicServices/operation/register) 121 | [Alfabank](https://pay.alfabank.ru/ecommerce/instructions/merchantManual/pages/index/rest.html#zapros_registratsii_zakaza_rest_) 122 | 123 | ```php 124 | use Voronkovich\SberbankAcquiring\Currency; 125 | 126 | // Required arguments 127 | $orderId = 1234; 128 | $orderAmount = 1000; 129 | $returnUrl = 'http://mycoolshop.local/payment-success'; 130 | 131 | // You can pass additional parameters like a currency code and etc. 132 | $params['currency'] = Currency::EUR; 133 | $params['failUrl'] = 'http://mycoolshop.local/payment-failure'; 134 | 135 | $result = $client->registerOrder($orderId, $orderAmount, $returnUrl, $params); 136 | 137 | $paymentOrderId = $result['orderId']; 138 | $paymentFormUrl = $result['formUrl']; 139 | 140 | header('Location: ' . $paymentFormUrl); 141 | ``` 142 | 143 | If you want to use UUID identifiers ([ramsey/uuid](https://github.com/ramsey/uuid)) for orders you should convert them to a hex format: 144 | ```php 145 | use Ramsey\Uuid\Uuid; 146 | 147 | $orderId = Uuid::uuid4(); 148 | 149 | $result = $client->registerOrder($orderId->getHex(), $orderAmount, $returnUrl); 150 | ``` 151 | 152 | Use a `registerOrderPreAuth` method to create a 2-step order. 153 | 154 | ### Getting a status of an exising order 155 | 156 | [Sberbank](https://ecomtest.sberbank.ru/doc#tag/basicServices/operation/getOrderStatusExtended) 157 | [Alfabank](https://pay.alfabank.ru/ecommerce/instructions/merchantManual/pages/index/rest.html#rasshirenniy_zapros_sostojanija_zakaza_rest_) 158 | 159 | ```php 160 | use Voronkovich\SberbankAcquiring\OrderStatus; 161 | 162 | $result = $client->getOrderStatus($orderId); 163 | 164 | if (OrderStatus::isDeposited($result['orderStatus'])) { 165 | echo "Order #$orderId is deposited!"; 166 | } 167 | 168 | if (OrderStatus::isDeclined($result['orderStatus'])) { 169 | echo "Order #$orderId was declined!"; 170 | } 171 | ``` 172 | 173 | Also, you can get an order's status by using you own identifier (e.g. assigned by your database): 174 | 175 | ```php 176 | $result = $client->getOrderStatusByOwnId($orderId); 177 | ``` 178 | 179 | ### Reversing an exising order 180 | 181 | [Sberbank](https://ecomtest.sberbank.ru/doc#tag/basicServices/operation/reverse) 182 | [Alfabank](https://pay.alfabank.ru/ecommerce/instructions/merchantManual/pages/index/rest.html#zapros_otmeni_oplati_zakaza_rest_) 183 | 184 | ```php 185 | $result = $client->reverseOrder($orderId); 186 | ``` 187 | 188 | ### Refunding an exising order 189 | 190 | [Sberbank](https://ecomtest.sberbank.ru/doc#tag/basicServices/operation/refund) 191 | [Alfabank](https://pay.alfabank.ru/ecommerce/instructions/merchantManual/pages/index/rest.html#zapros_vozvrata_sredstv_oplati_zakaza_rest_) 192 | 193 | ```php 194 | $result = $client->refundOrder($orderId, $amountToRefund); 195 | ``` 196 | 197 | ### SBP payments using QR codes 198 | 199 | _Currently only supported by Alfabank, see [docs](https://pay.alfabank.ru/ecommerce/instructions/SBP_C2B.pdf)._ 200 | 201 | ```php 202 | $result = $client->getSbpDynamicQr($orderId, [ 203 | 'qrHeight' => 100, 204 | 'qrWidth' => 100, 205 | 'qrFormat' => 'image', 206 | ]); 207 | 208 | echo sprintf( 209 | '', 210 | $result['payload'], 211 | 'data:image/png;base64,' . $result['renderedQr'] 212 | ); 213 | ``` 214 | 215 | --- 216 | See `Client` source code to find methods for payment bindings and dealing with 2-step payments. 217 | 218 | ## License 219 | 220 | Copyright (c) Voronkovich Oleg. Distributed under the MIT. 221 | -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | 18 | * @see https://securepayments.sberbank.ru/wiki/doku.php/integration:api:start#%D0%B8%D0%BD%D1%82%D0%B5%D1%80%D1%84%D0%B5%D0%B9%D1%81_rest 19 | */ 20 | class Client 21 | { 22 | const ACTION_SUCCESS = 0; 23 | 24 | const API_URI = 'https://securepayments.sberbank.ru'; 25 | const API_URI_TEST = 'https://3dsec.sberbank.ru'; 26 | const API_PREFIX_DEFAULT = '/payment/rest/'; 27 | const API_PREFIX_CREDIT = '/sbercredit/'; 28 | const API_PREFIX_APPLE = '/payment/applepay/'; 29 | const API_PREFIX_GOOGLE = '/payment/google/'; 30 | const API_PREFIX_SAMSUNG = '/payment/samsung/'; 31 | 32 | /** 33 | * @var string 34 | */ 35 | private $userName; 36 | 37 | /** 38 | * @var string 39 | */ 40 | private $password; 41 | 42 | /** 43 | * Authentication token. 44 | * 45 | * @var string 46 | */ 47 | private $token; 48 | 49 | /** 50 | * Currency code in ISO 4217 format. 51 | * 52 | * @var int 53 | */ 54 | private $currency; 55 | 56 | /** 57 | * A language code in ISO 639-1 format ('en', 'ru' and etc.). 58 | * 59 | * @var string 60 | */ 61 | private $language; 62 | 63 | /** 64 | * An API uri. 65 | * 66 | * @var string 67 | */ 68 | private $apiUri; 69 | 70 | /** 71 | * Default API endpoints prefix. 72 | * 73 | * @var string 74 | */ 75 | private $prefixDefault; 76 | 77 | /** 78 | * Credit API endpoints prefix. 79 | * 80 | * @var string 81 | */ 82 | private $prefixCredit; 83 | 84 | /** 85 | * Apple Pay endpoint prefix. 86 | * 87 | * @var string 88 | */ 89 | private $prefixApple; 90 | 91 | /** 92 | * Google Pay endpoint prefix. 93 | * 94 | * @var string 95 | */ 96 | private $prefixGoogle; 97 | 98 | /** 99 | * Samsung Pay endpoint prefix. 100 | * 101 | * @var string 102 | */ 103 | private $prefixSamsung; 104 | 105 | /** 106 | * SBP QR endpoint prefix. 107 | * 108 | * @var string 109 | */ 110 | private $prefixSbpQr; 111 | 112 | /** 113 | * An HTTP method. 114 | * 115 | * @var string 116 | */ 117 | private $httpMethod = HttpClientInterface::METHOD_POST; 118 | 119 | private $dateFormat = 'YmdHis'; 120 | 121 | /** 122 | * @var HttpClientInterface 123 | */ 124 | private $httpClient; 125 | 126 | /** 127 | * Use "ecom" protocol. 128 | * 129 | * @var bool 130 | */ 131 | private $ecom = false; 132 | 133 | public function __construct(array $options = []) 134 | { 135 | if (!\extension_loaded('json')) { 136 | throw new \RuntimeException('JSON extension is not loaded.'); 137 | } 138 | 139 | $allowedOptions = [ 140 | 'apiUri', 141 | 'currency', 142 | 'ecom', 143 | 'httpClient', 144 | 'httpMethod', 145 | 'language', 146 | 'password', 147 | 'token', 148 | 'userName', 149 | 'prefixDefault', 150 | 'prefixCredit', 151 | 'prefixApple', 152 | 'prefixGoogle', 153 | 'prefixSamsung', 154 | 'prefixSbpQr', 155 | ]; 156 | 157 | $unknownOptions = \array_diff(\array_keys($options), $allowedOptions); 158 | 159 | if (!empty($unknownOptions)) { 160 | throw new \InvalidArgumentException( 161 | \sprintf( 162 | 'Unknown option "%s". Allowed options: "%s".', 163 | \reset($unknownOptions), 164 | \implode('", "', $allowedOptions) 165 | ) 166 | ); 167 | } 168 | 169 | if (isset($options['userName']) && isset($options['password'])) { 170 | if (isset($options['token'])) { 171 | throw new \InvalidArgumentException('You can use either "userName" and "password" or "token".'); 172 | } 173 | 174 | $this->userName = $options['userName']; 175 | $this->password = $options['password']; 176 | } elseif (isset($options['token'])) { 177 | $this->token = $options['token']; 178 | } else { 179 | throw new \InvalidArgumentException('You must provide authentication credentials: "userName" and "password", or "token".'); 180 | } 181 | 182 | $this->language = $options['language'] ?? null; 183 | $this->currency = $options['currency'] ?? null; 184 | $this->apiUri = $options['apiUri'] ?? self::API_URI; 185 | $this->prefixDefault = $options['prefixDefault'] ?? self::API_PREFIX_DEFAULT; 186 | $this->prefixCredit = $options['prefixCredit'] ?? self::API_PREFIX_CREDIT; 187 | $this->prefixApple = $options['prefixApple'] ?? self::API_PREFIX_APPLE; 188 | $this->prefixGoogle = $options['prefixGoogle'] ?? self::API_PREFIX_GOOGLE; 189 | $this->prefixSamsung = $options['prefixSamsung'] ?? self::API_PREFIX_SAMSUNG; 190 | $this->prefixSbpQr = $options['prefixSbpQr'] ?? null; 191 | 192 | if (isset($options['httpMethod'])) { 193 | if (!\in_array($options['httpMethod'], [ HttpClientInterface::METHOD_GET, HttpClientInterface::METHOD_POST ])) { 194 | throw new \InvalidArgumentException( 195 | \sprintf( 196 | 'An HTTP method "%s" is not supported. Use "%s" or "%s".', 197 | $options['httpMethod'], 198 | HttpClientInterface::METHOD_GET, 199 | HttpClientInterface::METHOD_POST 200 | ) 201 | ); 202 | } 203 | 204 | $this->httpMethod = $options['httpMethod']; 205 | } 206 | 207 | if (isset($options['httpClient'])) { 208 | if (!$options['httpClient'] instanceof HttpClientInterface) { 209 | throw new \InvalidArgumentException('An HTTP client must implement HttpClientInterface.'); 210 | } 211 | 212 | $this->httpClient = $options['httpClient']; 213 | } 214 | 215 | $this->ecom = $options['ecom'] ?? false; 216 | } 217 | 218 | /** 219 | * Register a new order. 220 | * 221 | * @see https://securepayments.sberbank.ru/wiki/doku.php/integration:api:rest:requests:register 222 | * 223 | * @param int|string $orderId An order identifier 224 | * @param int $amount An order amount 225 | * @param string $returnUrl An url for redirecting a user after successfull order handling 226 | * @param array $data Additional data 227 | * 228 | * @return array A server's response 229 | */ 230 | public function registerOrder($orderId, int $amount, string $returnUrl, array $data = []): array 231 | { 232 | return $this->doRegisterOrder($orderId, $amount, $returnUrl, $data, $this->prefixDefault . 'register.do'); 233 | } 234 | 235 | /** 236 | * Register a new order using a 2-step payment process. 237 | * 238 | * @see https://securepayments.sberbank.ru/wiki/doku.php/integration:api:rest:requests:registerpreauth 239 | * 240 | * @param int|string $orderId An order identifier 241 | * @param int $amount An order amount 242 | * @param string $returnUrl An url for redirecting a user after successfull order handling 243 | * @param array $data Additional data 244 | * 245 | * @return array A server's response 246 | */ 247 | public function registerOrderPreAuth($orderId, int $amount, string $returnUrl, array $data = []): array 248 | { 249 | return $this->doRegisterOrder($orderId, $amount, $returnUrl, $data, $this->prefixDefault . 'registerPreAuth.do'); 250 | } 251 | 252 | /** 253 | * Register a new credit order. 254 | * 255 | * @see https://securepayments.sberbank.ru/wiki/doku.php/integration:api:rest:requests:register_cart_credit 256 | * 257 | * @param int|string $orderId An order identifier 258 | * @param int $amount An order amount 259 | * @param string $returnUrl An url for redirecting a user after successfull order handling 260 | * @param array $data Additional data 261 | * 262 | * @return array A server's response 263 | */ 264 | public function registerCreditOrder($orderId, int $amount, string $returnUrl, array $data = []): array 265 | { 266 | return $this->doRegisterOrder($orderId, $amount, $returnUrl, $data, $this->prefixCredit . 'register.do'); 267 | } 268 | 269 | /** 270 | * Register a new credit order using a 2-step payment process. 271 | * 272 | * @param int|string $orderId An order identifier 273 | * @param int $amount An order amount 274 | * @param string $returnUrl An url for redirecting a user after successfull order handling 275 | * @param array $data Additional data 276 | * 277 | * @return array A server's response 278 | */ 279 | public function registerCreditOrderPreAuth($orderId, int $amount, string $returnUrl, array $data = []): array 280 | { 281 | return $this->doRegisterOrder($orderId, $amount, $returnUrl, $data, $this->prefixCredit . 'registerPreAuth.do'); 282 | } 283 | 284 | private function doRegisterOrder($orderId, int $amount, string $returnUrl, array $data = [], $method = 'register.do'): array 285 | { 286 | $data['orderNumber'] = (string) $orderId; 287 | $data['amount'] = $amount; 288 | $data['returnUrl'] = $returnUrl; 289 | 290 | if (isset($data['currency'])) { 291 | $data['currency'] = (string) $data['currency']; 292 | } elseif (null !== $this->currency) { 293 | $data['currency'] = (string) $this->currency; 294 | } 295 | 296 | if (isset($data['jsonParams'])) { 297 | if (!is_array($data['jsonParams'])) { 298 | throw new \InvalidArgumentException('The "jsonParams" parameter must be an array.'); 299 | } 300 | 301 | if (!$this->ecom) { 302 | $data['jsonParams'] = \json_encode($data['jsonParams']); 303 | } 304 | } 305 | 306 | if (isset($data['orderBundle'])) { 307 | if (!$this->ecom && is_array($data['orderBundle'])) { 308 | $data['orderBundle'] = \json_encode($data['orderBundle']); 309 | } 310 | } 311 | 312 | return $this->execute($method, $data); 313 | } 314 | 315 | /** 316 | * Deposit an existing order. 317 | * 318 | * @see https://securepayments.sberbank.ru/wiki/doku.php/integration:api:rest:requests:deposit 319 | * 320 | * @param int|string $orderId An order identifier 321 | * @param int $amount An order amount 322 | * @param array $data Additional data 323 | * 324 | * @return array A server's response 325 | */ 326 | public function deposit($orderId, int $amount, array $data = []): array 327 | { 328 | $data['orderId'] = (string) $orderId; 329 | $data['amount'] = $amount; 330 | 331 | return $this->execute($this->prefixDefault . 'deposit.do', $data); 332 | } 333 | 334 | /** 335 | * Reverse an existing order. 336 | * 337 | * @see https://securepayments.sberbank.ru/wiki/doku.php/integration:api:rest:requests:reverse 338 | * 339 | * @param int|string $orderId An order identifier 340 | * @param array $data Additional data 341 | * 342 | * @return array A server's response 343 | */ 344 | public function reverseOrder($orderId, array $data = []): array 345 | { 346 | $data['orderId'] = (string) $orderId; 347 | 348 | return $this->execute($this->prefixDefault . 'reverse.do', $data); 349 | } 350 | 351 | /** 352 | * Refund an existing order. 353 | * 354 | * @see https://securepayments.sberbank.ru/wiki/doku.php/integration:api:rest:requests:refund 355 | * 356 | * @param int|string $orderId An order identifier 357 | * @param int $amount An amount to refund 358 | * @param array $data Additional data 359 | * 360 | * @return array A server's response 361 | */ 362 | public function refundOrder($orderId, int $amount, array $data = []): array 363 | { 364 | $data['orderId'] = (string) $orderId; 365 | $data['amount'] = $amount; 366 | 367 | return $this->execute($this->prefixDefault . 'refund.do', $data); 368 | } 369 | 370 | /** 371 | * Get an existing order's status by Sberbank's gateway identifier. 372 | * 373 | * @see https://securepayments.sberbank.ru/wiki/doku.php/integration:api:rest:requests:getorderstatusextended 374 | * 375 | * @param int|string $orderId A Sberbank's gateway order identifier 376 | * @param array $data Additional data 377 | * 378 | * @return array A server's response 379 | */ 380 | public function getOrderStatus($orderId, array $data = []): array 381 | { 382 | $data['orderId'] = (string) $orderId; 383 | 384 | return $this->execute($this->prefixDefault . 'getOrderStatusExtended.do', $data); 385 | } 386 | 387 | /** 388 | * Get an existing order's status by own identifier. 389 | * 390 | * @see https://securepayments.sberbank.ru/wiki/doku.php/integration:api:rest:requests:getorderstatusextended 391 | * 392 | * @param int|string $orderId An own order identifier 393 | * @param array $data Additional data 394 | * 395 | * @return array A server's response 396 | */ 397 | public function getOrderStatusByOwnId($orderId, array $data = []): array 398 | { 399 | $data['orderNumber'] = (string) $orderId; 400 | 401 | return $this->execute($this->prefixDefault . 'getOrderStatusExtended.do', $data); 402 | } 403 | 404 | /** 405 | * Verify card enrollment in the 3DS. 406 | * 407 | * @see https://securepayments.sberbank.ru/wiki/doku.php/integration:api:rest:requests:verifyEnrollment 408 | * 409 | * @param string $pan A primary account number 410 | * @param array $data Additional data 411 | * 412 | * @return array A server's response 413 | */ 414 | public function verifyEnrollment(string $pan, array $data = []): array 415 | { 416 | $data['pan'] = $pan; 417 | 418 | return $this->execute($this->prefixDefault . 'verifyEnrollment.do', $data); 419 | } 420 | 421 | /** 422 | * Update an SSL card list. 423 | * 424 | * @see https://securepayments.sberbank.ru/wiki/doku.php/integration:api:rest:requests:updateSSLCardList 425 | * 426 | * @param int|string $orderId An order identifier 427 | * @param array $data Additional data 428 | * 429 | * @return array A server's response 430 | */ 431 | public function updateSSLCardList($orderId, array $data = []): array 432 | { 433 | $data['mdorder'] = (string) $orderId; 434 | 435 | return $this->execute($this->prefixDefault . 'updateSSLCardList.do', $data); 436 | } 437 | 438 | /** 439 | * Get last orders for merchants. 440 | * 441 | * @see https://securepayments.sberbank.ru/wiki/doku.php/integration:api:rest:requests:getLastOrdersForMerchants 442 | * 443 | * @param \DateTimeInterface $from A begining date of a period 444 | * @param \DateTimeInterface|null $to An ending date of a period 445 | * @param array $data Additional data 446 | * 447 | * @return array A server's response 448 | */ 449 | public function getLastOrdersForMerchants(\DateTimeInterface $from, ?\DateTimeInterface $to = null, array $data = []): array 450 | { 451 | if (null === $to) { 452 | $to = new \DateTime(); 453 | } 454 | 455 | if ($from >= $to) { 456 | throw new \InvalidArgumentException('A "from" parameter must be less than "to" parameter.'); 457 | } 458 | 459 | $allowedStatuses = [ 460 | OrderStatus::CREATED, 461 | OrderStatus::APPROVED, 462 | OrderStatus::DEPOSITED, 463 | OrderStatus::REVERSED, 464 | OrderStatus::DECLINED, 465 | OrderStatus::REFUNDED, 466 | ]; 467 | 468 | if (isset($data['transactionStates'])) { 469 | if (!is_array($data['transactionStates'])) { 470 | throw new \InvalidArgumentException('A "transactionStates" parameter must be an array.'); 471 | } 472 | 473 | if (empty($data['transactionStates'])) { 474 | throw new \InvalidArgumentException('A "transactionStates" parameter cannot be empty.'); 475 | } elseif (0 < count(array_diff($data['transactionStates'], $allowedStatuses))) { 476 | throw new \InvalidArgumentException('A "transactionStates" parameter contains not allowed values.'); 477 | } 478 | } else { 479 | $data['transactionStates'] = $allowedStatuses; 480 | } 481 | 482 | $data['transactionStates'] = array_map('Voronkovich\SberbankAcquiring\OrderStatus::statusToString', $data['transactionStates']); 483 | 484 | if (isset($data['merchants'])) { 485 | if (!is_array($data['merchants'])) { 486 | throw new \InvalidArgumentException('A "merchants" parameter must be an array.'); 487 | } 488 | } else { 489 | $data['merchants'] = []; 490 | } 491 | 492 | $data['from'] = $from->format($this->dateFormat); 493 | $data['to'] = $to->format($this->dateFormat); 494 | $data['transactionStates'] = implode(',', array_unique($data['transactionStates'])); 495 | $data['merchants'] = implode(',', array_unique($data['merchants'])); 496 | 497 | return $this->execute($this->prefixDefault . 'getLastOrdersForMerchants.do', $data); 498 | } 499 | 500 | /** 501 | * Payment order binding. 502 | * 503 | * @see https://securepayments.sberbank.ru/wiki/doku.php/integration:api:rest:requests:paymentOrderBinding 504 | * 505 | * @param int|string $orderId An order identifier 506 | * @param int|string $bindingId A binding identifier 507 | * @param array $data Additional data 508 | * 509 | * @return array A server's response 510 | */ 511 | public function paymentOrderBinding($orderId, $bindingId, array $data = []): array 512 | { 513 | $data['mdOrder'] = (string) $orderId; 514 | $data['bindingId'] = (string) $bindingId; 515 | 516 | return $this->execute($this->prefixDefault . 'paymentOrderBinding.do', $data); 517 | } 518 | 519 | /** 520 | * Activate a binding. 521 | * 522 | * @see https://securepayments.sberbank.ru/wiki/doku.php/integration:api:rest:requests:bindCard 523 | * 524 | * @param int|string $bindingId A binding identifier 525 | * @param array $data Additional data 526 | * 527 | * @return array A server's response 528 | */ 529 | public function bindCard($bindingId, array $data = []): array 530 | { 531 | $data['bindingId'] = (string) $bindingId; 532 | 533 | return $this->execute($this->prefixDefault . 'bindCard.do', $data); 534 | } 535 | 536 | /** 537 | * Deactivate a binding. 538 | * 539 | * @see https://securepayments.sberbank.ru/wiki/doku.php/integration:api:rest:requests:unBindCard 540 | * 541 | * @param int|string $bindingId A binding identifier 542 | * @param array $data Additional data 543 | * 544 | * @return array A server's response 545 | */ 546 | public function unBindCard($bindingId, array $data = []): array 547 | { 548 | $data['bindingId'] = (string) $bindingId; 549 | 550 | return $this->execute($this->prefixDefault . ($this->ecom ? 'unbindCard.do' : 'unBindCard.do'), $data); 551 | } 552 | 553 | /** 554 | * Extend a binding. 555 | * 556 | * @see https://securepayments.sberbank.ru/wiki/doku.php/integration:api:rest:requests:extendBinding 557 | * 558 | * @param int|string $bindingId A binding identifier 559 | * @param \DateTimeInterface $newExprity A new expiration date 560 | * @param array $data Additional data 561 | * 562 | * @return array A server's response 563 | */ 564 | public function extendBinding($bindingId, \DateTimeInterface $newExpiry, array $data = []): array 565 | { 566 | $data['bindingId'] = (string) $bindingId; 567 | $data['newExpiry'] = $newExpiry->format('Ym'); 568 | 569 | return $this->execute($this->prefixDefault . 'extendBinding.do', $data); 570 | } 571 | 572 | /** 573 | * Get bindings. 574 | * 575 | * @see https://securepayments.sberbank.ru/wiki/doku.php/integration:api:rest:requests:getBindings 576 | * 577 | * @param int|string $clientId A binding identifier 578 | * @param array $data Additional data 579 | * 580 | * @return array A server's response 581 | */ 582 | public function getBindings($clientId, array $data = []): array 583 | { 584 | $data['clientId'] = (string) $clientId; 585 | 586 | return $this->execute($this->prefixDefault . 'getBindings.do', $data); 587 | } 588 | 589 | /** 590 | * Get a receipt status. 591 | * 592 | * @see https://securepayments.sberbank.ru/wiki/doku.php/integration:api:rest:requests:getreceiptstatus 593 | * 594 | * @param array $data A data 595 | * 596 | * @return array A server's response 597 | */ 598 | public function getReceiptStatus(array $data): array 599 | { 600 | return $this->execute($this->prefixDefault . 'getReceiptStatus.do', $data); 601 | } 602 | 603 | /** 604 | * Pay with Apple Pay. 605 | * 606 | * @see https://securepayments.sberbank.ru/wiki/doku.php/integration:api:rest:requests:payment_applepay 607 | * 608 | * @param int|string $orderNumber Order identifier 609 | * @param string $merchant Merchant 610 | * @param string $paymentToken Payment token 611 | * @param array $data Additional data 612 | * 613 | * @return array A server's response 614 | */ 615 | public function payWithApplePay($orderNumber, string $merchant, string $paymentToken, array $data = []): array 616 | { 617 | $data['orderNumber'] = (string) $orderNumber; 618 | $data['merchant'] = $merchant; 619 | $data['paymentToken'] = $paymentToken; 620 | 621 | return $this->execute($this->prefixApple . 'payment.do', $data); 622 | } 623 | 624 | /** 625 | * Pay with Google Pay. 626 | * 627 | * @see https://securepayments.sberbank.ru/wiki/doku.php/integration:api:rest:requests:payment_googlepay 628 | * 629 | * @param int|string $orderNumber Order identifier 630 | * @param string $merchant Merchant 631 | * @param string $paymentToken Payment token 632 | * @param array $data Additional data 633 | * 634 | * @return array A server's response 635 | */ 636 | public function payWithGooglePay($orderNumber, string $merchant, string $paymentToken, array $data = []): array 637 | { 638 | $data['orderNumber'] = (string) $orderNumber; 639 | $data['merchant'] = $merchant; 640 | $data['paymentToken'] = $paymentToken; 641 | 642 | return $this->execute($this->prefixGoogle . 'payment.do', $data); 643 | } 644 | 645 | /** 646 | * Pay with Samsung Pay. 647 | * 648 | * @see https://securepayments.sberbank.ru/wiki/doku.php/integration:api:rest:requests:payment_samsungpay 649 | * 650 | * @param int|string $orderNumber Order identifier 651 | * @param string $merchant Merchant 652 | * @param string $paymentToken Payment token 653 | * @param array $data Additional data 654 | * 655 | * @return array A server's response 656 | */ 657 | public function payWithSamsungPay($orderNumber, string $merchant, string $paymentToken, array $data = []): array 658 | { 659 | $data['orderNumber'] = (string) $orderNumber; 660 | $data['merchant'] = $merchant; 661 | $data['paymentToken'] = $paymentToken; 662 | 663 | return $this->execute($this->prefixSamsung . 'payment.do', $data); 664 | } 665 | 666 | /** 667 | * Get QR code for payment through SBP. 668 | * 669 | * @param int|string $orderId An order identifier 670 | * @param array $data Additional data 671 | * 672 | * @return array A server's response 673 | */ 674 | public function getSbpDynamicQr($orderId, array $data = []): array 675 | { 676 | if (empty($this->prefixSbpQr)) { 677 | throw new \RuntimeException('The "prefixSbpQr" option is unspecified.'); 678 | } 679 | 680 | $data['mdOrder'] = (string) $orderId; 681 | 682 | return $this->execute($this->prefixSbpQr . 'dynamic/get.do', $data); 683 | } 684 | 685 | /** 686 | * Get QR code status. 687 | * 688 | * @param int|string $orderId An order identifier 689 | * @param string $qrId A QR code identifier 690 | * @param array $data Additional data 691 | * 692 | * @return array A server's response 693 | */ 694 | public function getSbpQrStatus($orderId, string $qrId, array $data = []): array 695 | { 696 | if (empty($this->prefixSbpQr)) { 697 | throw new \RuntimeException('The "prefixSbpQr" option is unspecified.'); 698 | } 699 | 700 | $data['mdOrder'] = (string) $orderId; 701 | $data['qrId'] = $qrId; 702 | 703 | return $this->execute($this->prefixSbpQr . 'status.do', $data); 704 | } 705 | 706 | /** 707 | * Execute an action. 708 | * 709 | * @param string $action An action's name e.g. 'register.do' 710 | * @param array $data An action's data 711 | * 712 | * @throws NetworkException 713 | * 714 | * @return array A server's response 715 | */ 716 | public function execute(string $action, array $data = []): array 717 | { 718 | // Add '/payment/rest/' prefix for BC compatibility if needed 719 | if ($action[0] !== '/') { 720 | $action = $this->prefixDefault . $action; 721 | } 722 | 723 | $rest = (0 === \strpos($action, $this->prefixDefault)) || (0 === \strpos($action, $this->prefixCredit)); 724 | 725 | $uri = $this->apiUri . $action; 726 | 727 | if (!isset($data['language']) && null !== $this->language) { 728 | $data['language'] = $this->language; 729 | } 730 | 731 | $method = $this->httpMethod; 732 | 733 | if ($rest) { 734 | if (null !== $this->token) { 735 | $data['token'] = $this->token; 736 | } else { 737 | $data['userName'] = $this->userName; 738 | $data['password'] = $this->password; 739 | } 740 | } 741 | 742 | if ($rest && !$this->ecom) { 743 | $headers['Content-Type'] = 'application/x-www-form-urlencoded'; 744 | $data = \http_build_query($data, '', '&'); 745 | } else { 746 | $headers['Content-Type'] = 'application/json'; 747 | $data = \json_encode($data); 748 | $method = HttpClientInterface::METHOD_POST; 749 | } 750 | 751 | $httpClient = $this->getHttpClient(); 752 | 753 | list($httpCode, $response) = $httpClient->request($uri, $method, $headers, $data); 754 | 755 | if (200 !== $httpCode) { 756 | $badResponseException = new BadResponseException(sprintf('Bad HTTP code: %d.', $httpCode), $httpCode); 757 | $badResponseException->setResponse($response); 758 | 759 | throw $badResponseException; 760 | } 761 | 762 | $response = $this->parseResponse($response); 763 | $this->handleErrors($response); 764 | 765 | return $response; 766 | } 767 | 768 | /** 769 | * Parse a servers's response. 770 | * 771 | * @param string $response A string in the JSON format 772 | * 773 | * @throws ResponseParsingException 774 | * 775 | * @return array 776 | */ 777 | private function parseResponse(string $response): array 778 | { 779 | $response = \json_decode($response, true); 780 | $errorCode = \json_last_error(); 781 | 782 | if (\JSON_ERROR_NONE !== $errorCode || null === $response) { 783 | throw new ResponseParsingException(\json_last_error_msg(), $errorCode); 784 | } 785 | 786 | return $response; 787 | } 788 | 789 | /** 790 | * Normalize server's response. 791 | * 792 | * @param array $response A response 793 | * 794 | * @throws ActionException 795 | */ 796 | private function handleErrors(array &$response) 797 | { 798 | // Server's response can contain an error code and an error message in differend fields. 799 | if (isset($response['errorCode'])) { 800 | $errorCode = (int) $response['errorCode']; 801 | } elseif (isset($response['ErrorCode'])) { 802 | $errorCode = (int) $response['ErrorCode']; 803 | } elseif (isset($response['error']['code'])) { 804 | $errorCode = (int) $response['error']['code']; 805 | } else { 806 | $errorCode = self::ACTION_SUCCESS; 807 | } 808 | 809 | unset($response['errorCode']); 810 | unset($response['ErrorCode']); 811 | 812 | if (isset($response['errorMessage'])) { 813 | $errorMessage = $response['errorMessage']; 814 | } elseif (isset($response['ErrorMessage'])) { 815 | $errorMessage = $response['ErrorMessage']; 816 | } elseif (isset($response['error']['message'])) { 817 | $errorMessage = $response['error']['message']; 818 | } elseif (isset($response['error']['description'])) { 819 | $errorMessage = $response['error']['description']; 820 | } else { 821 | $errorMessage = 'Unknown error.'; 822 | } 823 | 824 | unset($response['errorMessage']); 825 | unset($response['ErrorMessage']); 826 | unset($response['error']); 827 | unset($response['success']); 828 | 829 | if (self::ACTION_SUCCESS !== $errorCode) { 830 | throw new ActionException($errorMessage, $errorCode); 831 | } 832 | } 833 | 834 | /** 835 | * Get an HTTP client. 836 | */ 837 | private function getHttpClient(): HttpClientInterface 838 | { 839 | if (null === $this->httpClient) { 840 | $this->httpClient = new CurlClient([ 841 | \CURLOPT_VERBOSE => false, 842 | \CURLOPT_SSL_VERIFYHOST => false, 843 | \CURLOPT_SSL_VERIFYPEER => false, 844 | ]); 845 | } 846 | 847 | return $this->httpClient; 848 | } 849 | } 850 | -------------------------------------------------------------------------------- /tests/ClientTest.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class ClientTest extends TestCase 19 | { 20 | public function testThrowsAnExceptionIfUnkownOptionProvided() 21 | { 22 | $this->expectException(\InvalidArgumentException::class); 23 | $this->expectExceptionMessage('Unknown option "foo".'); 24 | 25 | $client = new Client(['token' => 'token', 'foo' => 'bar']); 26 | } 27 | 28 | public function testAllowsToUseAUsernameAndAPasswordForAuthentication() 29 | { 30 | $httpClient = $this->mockHttpClient(); 31 | $httpClient 32 | ->expects($this->atLeastOnce()) 33 | ->method('request') 34 | ->with( 35 | $this->anything(), 36 | $this->anything(), 37 | $this->anything(), 38 | $this->equalTo('anything=anything&userName=oleg&password=querty123') 39 | ) 40 | ; 41 | 42 | $client = new Client([ 43 | 'userName' => 'oleg', 44 | 'password' => 'querty123', 45 | 'httpClient' => $httpClient, 46 | ]); 47 | 48 | $client->execute('/payment/rest/somethig.do', ['anything' => 'anything']); 49 | } 50 | 51 | public function testAllowsToUseATokenForAuthentication() 52 | { 53 | $httpClient = $this->mockHttpClient(); 54 | $httpClient 55 | ->expects($this->atLeastOnce()) 56 | ->method('request') 57 | ->with( 58 | $this->anything(), 59 | $this->anything(), 60 | $this->anything(), 61 | $this->equalTo('anything=anything&token=querty123') 62 | ) 63 | ; 64 | 65 | $client = new Client([ 66 | 'token' => 'querty123', 67 | 'httpClient' => $httpClient, 68 | ]); 69 | 70 | $client->execute('/payment/rest/somethig.do', ['anything' => 'anything']); 71 | } 72 | 73 | public function testThrowsAnExceptionIfBothAPasswordAndATokenUsed() 74 | { 75 | $this->expectException(\InvalidArgumentException::class); 76 | $this->expectExceptionMessage('You can use either "userName" and "password" or "token".'); 77 | 78 | $client = new Client(['userName' => 'username', 'password' => 'password', 'token' => 'token']); 79 | } 80 | 81 | public function testThrowsAnExceptionIfNoCredentialsProvided() 82 | { 83 | $this->expectException(\InvalidArgumentException::class); 84 | $this->expectExceptionMessage('You must provide authentication credentials: "userName" and "password", or "token".'); 85 | 86 | $client = new Client(); 87 | } 88 | 89 | public function testThrowsAnExceptionIfAnInvalidHttpMethodSpecified() 90 | { 91 | $this->expectException(\InvalidArgumentException::class); 92 | $this->expectExceptionMessage('An HTTP method "PUT" is not supported. Use "GET" or "POST".'); 93 | 94 | $client = new Client([ 95 | 'userName' => 'oleg', 96 | 'password' => 'qwerty123', 97 | 'httpMethod' => 'PUT' 98 | ]); 99 | } 100 | 101 | public function testAllowsToUseACustomHttpClient() 102 | { 103 | $httpClient = $this->mockHttpClient(); 104 | 105 | $httpClient 106 | ->expects($this->atLeastOnce()) 107 | ->method('request') 108 | ; 109 | 110 | $client = new Client([ 111 | 'userName' => 'oleg', 112 | 'password' => 'qwerty123', 113 | 'httpClient' => $httpClient 114 | ]); 115 | 116 | $client->execute('testAction'); 117 | } 118 | 119 | public function testThrowsAnExceptionIfAnInvalidHttpClientSpecified() 120 | { 121 | $this->expectException(\InvalidArgumentException::class); 122 | $this->expectExceptionMessage('An HTTP client must implement HttpClientInterface.'); 123 | 124 | $client = new Client([ 125 | 'userName' => 'oleg', 126 | 'password' => 'qwerty123', 127 | 'httpClient' => new \stdClass(), 128 | ]); 129 | } 130 | 131 | /** 132 | * @testdox Uses an HTTP method POST by default 133 | */ 134 | public function testUsesAPostHttpMethodByDefault() 135 | { 136 | $httpClient = $this->mockHttpClient(); 137 | 138 | $httpClient 139 | ->expects($this->atLeastOnce()) 140 | ->method('request') 141 | ->with( 142 | $this->anything(), 143 | HttpClientInterface::METHOD_POST, 144 | $this->anything(), 145 | $this->anything() 146 | ) 147 | ; 148 | 149 | $client = new Client([ 150 | 'userName' => 'oleg', 151 | 'password' => 'qwerty123', 152 | 'httpClient' => $httpClient, 153 | ]); 154 | 155 | $client->execute('/payment/rest/testAction.do'); 156 | } 157 | 158 | public function testAllowsToSetAnHttpMethodAndApiUrl() 159 | { 160 | $httpClient = $this->mockHttpClient(); 161 | 162 | $httpClient 163 | ->expects($this->once()) 164 | ->method('request') 165 | ->with( 166 | 'https://github.com/voronkovich/sberbank-acquiring-client/payment/rest/testAction.do', 167 | HttpClientInterface::METHOD_GET 168 | ) 169 | ; 170 | 171 | $client = new Client([ 172 | 'userName' => 'oleg', 173 | 'password' => 'qwerty123', 174 | 'httpClient' => $httpClient, 175 | 'httpMethod' => HttpClientInterface::METHOD_GET, 176 | 'apiUri' => 'https://github.com/voronkovich/sberbank-acquiring-client', 177 | ]); 178 | 179 | $client->execute('/payment/rest/testAction.do'); 180 | } 181 | 182 | public function testThrowsAnExceptionIfABadResponseReturned() 183 | { 184 | $httpClient = $this->mockHttpClient([500, 'Internal server error.']); 185 | 186 | $client = new Client([ 187 | 'userName' => 'oleg', 188 | 'password' => 'qwerty123', 189 | 'httpClient' => $httpClient, 190 | ]); 191 | 192 | $this->expectException(BadResponseException::class); 193 | $this->expectExceptionMessage('Bad HTTP code: 500.'); 194 | 195 | $client->execute('testAction'); 196 | } 197 | 198 | public function testThrowsAnExceptionIfAMalformedJsonReturned() 199 | { 200 | $httpClient = $this->mockHttpClient([200, 'Malformed json!']); 201 | 202 | $client = new Client([ 203 | 'userName' => 'oleg', 204 | 'password' => 'qwerty123', 205 | 'httpClient' => $httpClient, 206 | ]); 207 | 208 | $this->expectException(ResponseParsingException::class); 209 | 210 | $client->execute('testAction'); 211 | } 212 | 213 | /** 214 | * @dataProvider provideErredResponses 215 | */ 216 | public function testThrowsAnExceptionIfAServerSetAnErrorCode(array $response) 217 | { 218 | $httpClient = $this->mockHttpClient($response); 219 | 220 | $client = new Client([ 221 | 'userName' => 'oleg', 222 | 'password' => 'qwerty123', 223 | 'httpClient' => $httpClient 224 | ]); 225 | 226 | $this->expectException(ActionException::class); 227 | $this->expectExceptionMessage('Error!'); 228 | 229 | $client->execute('testAction'); 230 | } 231 | 232 | public function provideErredResponses(): iterable 233 | { 234 | yield [[200, \json_encode(['errorCode' => 100, 'errorMessage' => 'Error!'])]]; 235 | yield [[200, \json_encode(['ErrorCode' => 100, 'ErrorMessage' => 'Error!'])]]; 236 | yield [[200, \json_encode(['error' => ['code' => 100, 'message' => 'Error!']])]]; 237 | yield [[200, \json_encode(['error' => ['code' => 100, 'description' => 'Error!']])]]; 238 | } 239 | 240 | public function testRegistersANewOrder() 241 | { 242 | $httpClient = $this->getHttpClientToTestSendingData( 243 | '/payment/rest/register.do', 244 | 'POST', 245 | [ 'Content-Type' => 'application/x-www-form-urlencoded' ], 246 | 'currency=330&orderNumber=eee-eee-eee&amount=1200&returnUrl=https%3A%2F%2Fgithub.com%2Fvoronkovich%2Fsberbank-acquiring-client&token=abrakadabra' 247 | ); 248 | 249 | $client = new Client([ 250 | 'token' => 'abrakadabra', 251 | 'httpClient' => $httpClient, 252 | ]); 253 | 254 | $client->registerOrder('eee-eee-eee', 1200, 'https://github.com/voronkovich/sberbank-acquiring-client', ['currency' => 330]); 255 | } 256 | 257 | public function testRegistersANewOrderWithCustomPrefix() 258 | { 259 | $httpClient = $this->getHttpClientToTestSendingData( 260 | '/other/prefix/register.do', 261 | 'POST', 262 | [ 'Content-Type' => 'application/x-www-form-urlencoded' ], 263 | 'currency=330&orderNumber=eee-eee-eee&amount=1200&returnUrl=https%3A%2F%2Fgithub.com%2Fvoronkovich%2Fsberbank-acquiring-client&token=abrakadabra' 264 | ); 265 | 266 | $client = new Client([ 267 | 'token' => 'abrakadabra', 268 | 'httpClient' => $httpClient, 269 | 'prefixDefault'=>'/other/prefix/' 270 | ]); 271 | 272 | $client->registerOrder('eee-eee-eee', 1200, 'https://github.com/voronkovich/sberbank-acquiring-client', ['currency' => 330]); 273 | } 274 | 275 | public function testRegisterANewPreAuthorizedOrder() 276 | { 277 | $httpClient = $this->getHttpClientToTestSendingData( 278 | '/payment/rest/registerPreAuth.do', 279 | 'POST', 280 | [ 'Content-Type' => 'application/x-www-form-urlencoded' ], 281 | 'currency=330&orderNumber=eee-eee-eee&amount=1200&returnUrl=https%3A%2F%2Fgithub.com%2Fvoronkovich%2Fsberbank-acquiring-client&token=abrakadabra' 282 | ); 283 | 284 | $client = new Client([ 285 | 'token' => 'abrakadabra', 286 | 'httpClient' => $httpClient, 287 | ]); 288 | 289 | $client->registerOrderPreAuth('eee-eee-eee', 1200, 'https://github.com/voronkovich/sberbank-acquiring-client', ['currency' => 330]); 290 | } 291 | 292 | public function testRegistersANewCreditOrder() 293 | { 294 | $httpClient = $this->getHttpClientToTestSendingData( 295 | '/sbercredit/register.do', 296 | 'POST', 297 | [ 'Content-Type' => 'application/x-www-form-urlencoded' ], 298 | 'currency=643&orderNumber=aaa-aaa-aaa&amount=50000&returnUrl=https%3A%2F%2Fgithub.com%2Fvoronkovich%2Fsberbank-acquiring-client&token=secret' 299 | ); 300 | 301 | $client = new Client([ 302 | 'token' => 'secret', 303 | 'httpClient' => $httpClient, 304 | ]); 305 | 306 | $client->registerCreditOrder('aaa-aaa-aaa', 50000, 'https://github.com/voronkovich/sberbank-acquiring-client', [ 307 | 'currency' => 643 308 | ]); 309 | } 310 | 311 | public function testRegistersANewCreditOrderWithCustomPrefix() 312 | { 313 | $httpClient = $this->getHttpClientToTestSendingData( 314 | '/custom/credit/register.do', 315 | 'POST', 316 | [ 'Content-Type' => 'application/x-www-form-urlencoded' ], 317 | 'currency=643&orderNumber=aaa-aaa-aaa&amount=50000&returnUrl=https%3A%2F%2Fgithub.com%2Fvoronkovich%2Fsberbank-acquiring-client&token=secret' 318 | ); 319 | 320 | $client = new Client([ 321 | 'token' => 'secret', 322 | 'httpClient' => $httpClient, 323 | 'prefixCredit'=> '/custom/credit/', 324 | ]); 325 | 326 | $client->registerCreditOrder('aaa-aaa-aaa', 50000, 'https://github.com/voronkovich/sberbank-acquiring-client', [ 327 | 'currency' => 643 328 | ]); 329 | } 330 | 331 | public function testRegistersANewPreAuthorizedCreditOrder() 332 | { 333 | $httpClient = $this->getHttpClientToTestSendingData( 334 | '/sbercredit/registerPreAuth.do', 335 | 'POST', 336 | [ 'Content-Type' => 'application/x-www-form-urlencoded' ], 337 | 'currency=643&orderNumber=aaa-aaa-aaa&amount=50000&returnUrl=https%3A%2F%2Fgithub.com%2Fvoronkovich%2Fsberbank-acquiring-client&token=secret' 338 | ); 339 | 340 | $client = new Client([ 341 | 'token' => 'secret', 342 | 'httpClient' => $httpClient, 343 | ]); 344 | 345 | $client->registerCreditOrderPreAuth('aaa-aaa-aaa', 50000, 'https://github.com/voronkovich/sberbank-acquiring-client', [ 346 | 'currency' => 643 347 | ]); 348 | } 349 | 350 | /** 351 | * @testdox Throws an exception if a "jsonParams" is not an array. 352 | */ 353 | public function testThrowsAnExceptionIfAJsonParamsIsNotAnArray() 354 | { 355 | $client = new Client(['userName' => 'oleg', 'password' => 'qwerty123']); 356 | 357 | $this->expectException(\InvalidArgumentException::class); 358 | $this->expectExceptionMessage('The "jsonParams" parameter must be an array.'); 359 | 360 | $client->registerOrder(1, 1, 'returnUrl', ['jsonParams' => '{}']); 361 | 362 | $this->expectException(\InvalidArgumentException::class); 363 | $this->expectExceptionMessage('The "jsonParams" parameter must be an array.'); 364 | 365 | $client->registerOrderPreAuth(1, 1, 'returnUrl', ['jsonParams' => '{}']); 366 | } 367 | 368 | /** 369 | * @testdox Encodes to JSON a "jsonParams" parameter. 370 | */ 371 | public function testEncodesToJSONAJsonParamsParameter() 372 | { 373 | $httpClient = $this->getHttpClientToTestSendingData( 374 | '/payment/rest/register.do', 375 | 'POST', 376 | [ 'Content-Type' => 'application/x-www-form-urlencoded' ], 377 | 'jsonParams=%7B%22showApplePay%22%3Atrue%2C%22showGooglePay%22%3Atrue%7D&orderNumber=1&amount=1&returnUrl=returnUrl&token=abc' 378 | ); 379 | 380 | $client = new Client([ 381 | 'token' => 'abc', 382 | 'httpClient' => $httpClient, 383 | ]); 384 | 385 | $client->registerOrder(1, 1, 'returnUrl', [ 386 | 'jsonParams' => [ 387 | 'showApplePay' => true, 388 | 'showGooglePay' => true, 389 | ], 390 | ]); 391 | } 392 | 393 | /** 394 | * @testdox Encodes to JSON an "orderBundle" parameter. 395 | */ 396 | public function testEncodesToJSONAnOrderBundleParameter() 397 | { 398 | $httpClient = $this->getHttpClientToTestSendingData( 399 | '/payment/rest/register.do', 400 | 'POST', 401 | [ 'Content-Type' => 'application/x-www-form-urlencoded' ], 402 | 'orderBundle=%7B%22items%22%3A%5B%22item1%22%2C%22item2%22%5D%7D&orderNumber=1&amount=1&returnUrl=returnUrl&token=abc' 403 | ); 404 | 405 | $client = new Client([ 406 | 'token' => 'abc', 407 | 'httpClient' => $httpClient, 408 | ]); 409 | 410 | $client->registerOrder(1, 1, 'returnUrl', [ 411 | 'orderBundle' => [ 412 | 'items' => [ 413 | 'item1', 414 | 'item2', 415 | ], 416 | ], 417 | ]); 418 | } 419 | 420 | public function testDepositsAPreAuthorizedOrder() 421 | { 422 | $httpClient = $this->getHttpClientToTestSendingData( 423 | '/payment/rest/deposit.do', 424 | 'POST', 425 | [ 'Content-Type' => 'application/x-www-form-urlencoded' ], 426 | 'currency=810&orderId=aaa-bbb-yyy&amount=1000&token=abrakadabra' 427 | ); 428 | 429 | $client = new Client([ 430 | 'token' => 'abrakadabra', 431 | 'httpClient' => $httpClient, 432 | ]); 433 | 434 | $client->deposit('aaa-bbb-yyy', 1000, ['currency' => 810]); 435 | } 436 | 437 | public function testReversesAnOrder() 438 | { 439 | $httpClient = $this->getHttpClientToTestSendingData( 440 | '/payment/rest/reverse.do', 441 | 'POST', 442 | [ 'Content-Type' => 'application/x-www-form-urlencoded' ], 443 | 'currency=480&orderId=aaa-bbb-yyy&token=abrakadabra' 444 | ); 445 | 446 | $client = new Client([ 447 | 'token' => 'abrakadabra', 448 | 'httpClient' => $httpClient, 449 | ]); 450 | 451 | $client->reverseOrder('aaa-bbb-yyy', ['currency' => 480]); 452 | } 453 | 454 | public function testRefundsAnOrder() 455 | { 456 | $httpClient = $this->getHttpClientToTestSendingData( 457 | '/payment/rest/refund.do', 458 | 'POST', 459 | [ 'Content-Type' => 'application/x-www-form-urlencoded' ], 460 | 'currency=456&orderId=aaa-bbb-yyy&amount=5050&token=abrakadabra' 461 | ); 462 | 463 | $client = new Client([ 464 | 'token' => 'abrakadabra', 465 | 'httpClient' => $httpClient, 466 | ]); 467 | 468 | $client->refundOrder('aaa-bbb-yyy', 5050, ['currency' => 456]); 469 | } 470 | 471 | public function testGetsAnOrderStatus() 472 | { 473 | $httpClient = $this->getHttpClientToTestSendingData( 474 | '/rest/getOrderStatusExtended.do', 475 | 'POST', 476 | [ 'Content-Type' => 'application/x-www-form-urlencoded' ], 477 | 'currency=100&orderId=aaa-bbb-yyy&token=abrakadabra' 478 | ); 479 | 480 | $client = new Client([ 481 | 'token' => 'abrakadabra', 482 | 'httpClient' => $httpClient, 483 | ]); 484 | 485 | $client->getOrderStatus('aaa-bbb-yyy', ['currency' => 100]); 486 | } 487 | 488 | public function testGetsAnOrderStatusByOwnId() 489 | { 490 | $httpClient = $this->getHttpClientToTestSendingData( 491 | '/rest/getOrderStatusExtended.do', 492 | 'POST', 493 | [ 'Content-Type' => 'application/x-www-form-urlencoded' ], 494 | 'currency=100&orderNumber=111&token=abrakadabra' 495 | ); 496 | 497 | $client = new Client([ 498 | 'token' => 'abrakadabra', 499 | 'httpClient' => $httpClient, 500 | ]); 501 | 502 | $client->getOrderStatusByOwnId(111, ['currency' => 100]); 503 | } 504 | 505 | public function testVerifiesACardEnrollment() 506 | { 507 | $httpClient = $this->getHttpClientToTestSendingData( 508 | '/payment/rest/verifyEnrollment.do', 509 | 'POST', 510 | [ 'Content-Type' => 'application/x-www-form-urlencoded' ], 511 | 'currency=200&pan=aaazzz&token=abrakadabra' 512 | ); 513 | 514 | $client = new Client([ 515 | 'token' => 'abrakadabra', 516 | 'httpClient' => $httpClient, 517 | ]); 518 | 519 | $client->verifyEnrollment('aaazzz', ['currency' => 200]); 520 | } 521 | 522 | /** 523 | * @testdox Updates an SSL card list 524 | */ 525 | public function testUpdatesAnSSLCardList() 526 | { 527 | $httpClient = $this->getHttpClientToTestSendingData( 528 | '/payment/rest/updateSSLCardList.do', 529 | 'POST', 530 | [ 'Content-Type' => 'application/x-www-form-urlencoded' ], 531 | 'mdorder=aaazzz&token=abrakadabra' 532 | ); 533 | 534 | $client = new Client([ 535 | 'token' => 'abrakadabra', 536 | 'httpClient' => $httpClient, 537 | ]); 538 | 539 | $client->updateSSLCardList('aaazzz'); 540 | } 541 | 542 | public function testPaysAnOrderUsingBinding() 543 | { 544 | $httpClient = $this->getHttpClientToTestSendingData( 545 | '/payment/rest/paymentOrderBinding.do', 546 | 'POST', 547 | [ 'Content-Type' => 'application/x-www-form-urlencoded' ], 548 | 'language=en&mdOrder=xxx-yyy-zzz&bindingId=600&token=abrakadabra' 549 | ); 550 | 551 | $client = new Client([ 552 | 'token' => 'abrakadabra', 553 | 'httpClient' => $httpClient, 554 | ]); 555 | 556 | $client->paymentOrderBinding('xxx-yyy-zzz', '600', ['language' => 'en']); 557 | } 558 | 559 | public function testBindsACard() 560 | { 561 | $httpClient = $this->getHttpClientToTestSendingData( 562 | '/payment/rest/bindCard.do', 563 | 'POST', 564 | [ 'Content-Type' => 'application/x-www-form-urlencoded' ], 565 | 'language=ru&bindingId=bbb000&token=abrakadabra' 566 | ); 567 | 568 | $client = new Client([ 569 | 'token' => 'abrakadabra', 570 | 'httpClient' => $httpClient, 571 | ]); 572 | 573 | $client->bindCard('bbb000', ['language' => 'ru']); 574 | } 575 | 576 | public function testUnbindsACard() 577 | { 578 | $httpClient = $this->getHttpClientToTestSendingData( 579 | '/payment/rest/unBindCard.do', 580 | 'POST', 581 | [ 'Content-Type' => 'application/x-www-form-urlencoded' ], 582 | 'language=en&bindingId=uuu800&token=abrakadabra' 583 | ); 584 | 585 | $client = new Client([ 586 | 'token' => 'abrakadabra', 587 | 'httpClient' => $httpClient, 588 | ]); 589 | 590 | $client->unBindCard('uuu800', ['language' => 'en']); 591 | } 592 | 593 | public function testExtendsABinding() 594 | { 595 | $httpClient = $this->getHttpClientToTestSendingData( 596 | '/payment/rest/extendBinding.do', 597 | 'POST', 598 | [ 'Content-Type' => 'application/x-www-form-urlencoded' ], 599 | 'language=ru&bindingId=eeeB00&newExpiry=203009&token=abrakadabra' 600 | ); 601 | 602 | $client = new Client([ 603 | 'token' => 'abrakadabra', 604 | 'httpClient' => $httpClient, 605 | ]); 606 | 607 | $client->extendBinding('eeeB00', new \DateTime('2030-09'), ['language' => 'ru']); 608 | } 609 | 610 | public function testGetsBindings() 611 | { 612 | $httpClient = $this->getHttpClientToTestSendingData( 613 | '/payment/rest/getBindings.do', 614 | 'POST', 615 | [ 'Content-Type' => 'application/x-www-form-urlencoded' ], 616 | 'language=ru&clientId=clientIDABC&token=abrakadabra' 617 | ); 618 | 619 | $client = new Client([ 620 | 'token' => 'abrakadabra', 621 | 'httpClient' => $httpClient, 622 | ]); 623 | 624 | $client->getBindings('clientIDABC', ['language' => 'ru']); 625 | } 626 | 627 | public function testGetsARepceiptStatus() 628 | { 629 | $httpClient = $this->getHttpClientToTestSendingData( 630 | '/payment/rest/getReceiptStatus.do', 631 | 'POST', 632 | [ 'Content-Type' => 'application/x-www-form-urlencoded' ], 633 | 'uuid=ffff&language=ru&token=abrakadabra' 634 | ); 635 | 636 | $client = new Client([ 637 | 'token' => 'abrakadabra', 638 | 'httpClient' => $httpClient, 639 | ]); 640 | 641 | $client->getReceiptStatus(['uuid' => 'ffff', 'language' => 'ru']); 642 | } 643 | 644 | /** 645 | * @testdox Pays with an "Apple Pay" 646 | */ 647 | public function testPaysWithAnApplePay() 648 | { 649 | $httpClient = $this->getHttpClientToTestSendingData( 650 | '/payment/applepay/payment.do', 651 | 'POST', 652 | [ 'Content-Type' => 'application/json' ], 653 | '{"language":"en","orderNumber":"eee-eee","merchant":"my_merchant","paymentToken":"token_zzz"}' 654 | ); 655 | 656 | $client = new Client([ 657 | 'token' => 'abrakadabra', 658 | 'httpClient' => $httpClient, 659 | ]); 660 | 661 | $client->payWithApplePay('eee-eee', 'my_merchant', 'token_zzz', ['language' => 'en']); 662 | } 663 | 664 | /** 665 | * @testdox Pays with an "Apple Pay" with custom prefix 666 | */ 667 | public function testPaysWithAnApplePayWithCustomPrefix() 668 | { 669 | $httpClient = $this->getHttpClientToTestSendingData( 670 | '/other/prefix/payment.do', 671 | 'POST', 672 | [ 'Content-Type' => 'application/json' ], 673 | '{"language":"en","orderNumber":"eee-eee","merchant":"my_merchant","paymentToken":"token_zzz"}' 674 | ); 675 | 676 | $client = new Client([ 677 | 'token' => 'abrakadabra', 678 | 'httpClient' => $httpClient, 679 | 'prefixApple'=>'/other/prefix/' 680 | ]); 681 | 682 | $client->payWithApplePay('eee-eee', 'my_merchant', 'token_zzz', ['language' => 'en']); 683 | } 684 | 685 | /** 686 | * @testdox Pays with a "Google Pay" 687 | */ 688 | public function testPaysWithAGooglePay() 689 | { 690 | $httpClient = $this->getHttpClientToTestSendingData( 691 | '/payment/google/payment.do', 692 | 'POST', 693 | [ 'Content-Type' => 'application/json' ], 694 | '{"language":"en","orderNumber":"eee-eee","merchant":"my_merchant","paymentToken":"token_zzz"}' 695 | ); 696 | 697 | $client = new Client([ 698 | 'token' => 'abrakadabra', 699 | 'httpClient' => $httpClient, 700 | ]); 701 | 702 | $client->payWithGooglePay('eee-eee', 'my_merchant', 'token_zzz', ['language' => 'en']); 703 | } 704 | 705 | /** 706 | * @testdox Pays with a "Google Pay" with custom prefix 707 | */ 708 | public function testPaysWithAGooglePayWithCustomPrefix() 709 | { 710 | $httpClient = $this->getHttpClientToTestSendingData( 711 | '/other/prefix/google/payment.do', 712 | 'POST', 713 | [ 'Content-Type' => 'application/json' ], 714 | '{"language":"en","orderNumber":"eee-eee","merchant":"my_merchant","paymentToken":"token_zzz"}' 715 | ); 716 | 717 | $client = new Client([ 718 | 'token' => 'abrakadabra', 719 | 'httpClient' => $httpClient, 720 | 'prefixGoogle'=>'/other/prefix/google/' 721 | ]); 722 | 723 | $client->payWithGooglePay('eee-eee', 'my_merchant', 'token_zzz', ['language' => 'en']); 724 | } 725 | 726 | /** 727 | * @testdox Pays with a "Samsung Pay" 728 | */ 729 | public function testPaysWithASamsungPay() 730 | { 731 | $httpClient = $this->getHttpClientToTestSendingData( 732 | '/payment/samsung/payment.do', 733 | 'POST', 734 | [ 'Content-Type' => 'application/json' ], 735 | '{"language":"en","orderNumber":"eee-eee","merchant":"my_merchant","paymentToken":"token_zzz"}' 736 | ); 737 | 738 | $client = new Client([ 739 | 'token' => 'abrakadabra', 740 | 'httpClient' => $httpClient, 741 | ]); 742 | 743 | $client->payWithSamsungPay('eee-eee', 'my_merchant', 'token_zzz', ['language' => 'en']); 744 | } 745 | 746 | /** 747 | * @testdox Pays with a "Samsung Pay" with custom prefix 748 | */ 749 | public function testPaysWithASamsungPayWithCustomPrefix() 750 | { 751 | $httpClient = $this->getHttpClientToTestSendingData( 752 | '/other/prefix/sumsung/payment.do', 753 | 'POST', 754 | [ 'Content-Type' => 'application/json' ], 755 | '{"language":"en","orderNumber":"eee-eee","merchant":"my_merchant","paymentToken":"token_zzz"}' 756 | ); 757 | 758 | $client = new Client([ 759 | 'token' => 'abrakadabra', 760 | 'httpClient' => $httpClient, 761 | 'prefixSamsung' => '/other/prefix/sumsung/', 762 | ]); 763 | 764 | $client->payWithSamsungPay('eee-eee', 'my_merchant', 'token_zzz', ['language' => 'en']); 765 | } 766 | 767 | public function testGetSbpDynamicQrThrowsAnExceptionIfPrefixSbpQrOptionIsUnspecified() 768 | { 769 | $client = new Client(['token' => 'abrakadabra']); 770 | 771 | $this->expectException(\RuntimeException::class); 772 | $this->expectExceptionMessage('The "prefixSbpQr" option is unspecified.'); 773 | 774 | $client->getSbpDynamicQr('xxx-yyy-zzz', ['qrHeight' => 100, 'qrWidth' => 100, 'qrFormat' => 'image']); 775 | } 776 | 777 | public function testGetsSbpDynamicQrWithCustomPrefix() 778 | { 779 | $httpClient = $this->getHttpClientToTestSendingData( 780 | '/payment/rest/sbp/c2b/qr/dynamic/get.do', 781 | 'POST', 782 | [ 'Content-Type' => 'application/x-www-form-urlencoded' ], 783 | 'qrHeight=100&qrWidth=100&qrFormat=image&mdOrder=xxx-yyy-zzz&token=abrakadabra' 784 | ); 785 | 786 | $client = new Client([ 787 | 'token' => 'abrakadabra', 788 | 'httpClient' => $httpClient, 789 | 'prefixSbpQr' => '/payment/rest/sbp/c2b/qr/', 790 | ]); 791 | 792 | $client->getSbpDynamicQr('xxx-yyy-zzz', ['qrHeight' => 100, 'qrWidth' => 100, 'qrFormat' => 'image']); 793 | } 794 | 795 | public function testGetSbpQrStatusThrowsAnExceptionIfPrefixSbpQrOptionIsUnspecified() 796 | { 797 | $client = new Client(['token' => 'abrakadabra']); 798 | 799 | $this->expectException(\RuntimeException::class); 800 | $this->expectExceptionMessage('The "prefixSbpQr" option is unspecified.'); 801 | 802 | $client->getSbpQrStatus('xxx-yyy-zzz', 'meh'); 803 | } 804 | 805 | public function testGetsSbpQrStatusWithCustomPrefix() 806 | { 807 | $httpClient = $this->getHttpClientToTestSendingData( 808 | '/payment/rest/sbp/c2b/qr/status.do', 809 | 'POST', 810 | [ 'Content-Type' => 'application/x-www-form-urlencoded' ], 811 | 'mdOrder=xxx-yyy-zzz&qrId=meh&token=abrakadabra' 812 | ); 813 | 814 | $client = new Client([ 815 | 'token' => 'abrakadabra', 816 | 'httpClient' => $httpClient, 817 | 'prefixSbpQr' => '/payment/rest/sbp/c2b/qr/', 818 | ]); 819 | 820 | $client->getSbpQrStatus('xxx-yyy-zzz', 'meh'); 821 | } 822 | 823 | public function testAddsASpecialPrefixToActionForBackwardCompatibility() 824 | { 825 | $httpClient = $this->mockHttpClient(); 826 | 827 | $httpClient 828 | ->expects($this->atLeastOnce()) 829 | ->method('request') 830 | ->with($this->equalTo(Client::API_URI.'/payment/rest/getOrderStatusExtended.do')) 831 | ; 832 | 833 | $client = new Client([ 834 | 'token' => 'abrakadabra', 835 | 'httpClient' => $httpClient, 836 | 'apiUri' => Client::API_URI, 837 | ]); 838 | 839 | $client->execute('getOrderStatusExtended.do'); 840 | } 841 | 842 | public function testSupportsEcomProtocol() 843 | { 844 | $httpClient = $this->getHttpClientToTestSendingData( 845 | 'https://ecommerce.sberbank.ru/ecomm/gw/partner/api/v1/register.do', 846 | 'POST', 847 | [ 'Content-Type' => 'application/json' ], 848 | '{"currency":"978","jsonParams":{"web2app":true},"orderBundle":{"ffdVersion":"1.2"},"orderNumber":"111111","amount":19900,"returnUrl":"https:\/\/github.com\/voronkovich\/sberbank-acquiring-client","userName":"testUserName","password":"testPassword"}' 849 | ); 850 | 851 | $client = new Client([ 852 | 'apiUri' => 'https://ecommerce.sberbank.ru', 853 | 'prefixDefault' => '/ecomm/gw/partner/api/v1/', 854 | 'userName' => 'testUserName', 855 | 'password' => 'testPassword', 856 | 'ecom' => true, 857 | 'httpClient' => $httpClient, 858 | ]); 859 | 860 | $client->registerOrder(111111, 19900, 'https://github.com/voronkovich/sberbank-acquiring-client', [ 861 | 'currency' => Currency::EUR, 862 | 'jsonParams' => [ 863 | 'web2app' => true, 864 | ], 865 | 'orderBundle' => [ 866 | 'ffdVersion' => '1.2', 867 | ], 868 | ]); 869 | } 870 | 871 | public function testUsesDifferentUnbindCardEndpointForEcomProtocol() 872 | { 873 | $httpClient = $this->getHttpClientToTestSendingData( 874 | 'https://ecommerce.sberbank.ru/ecomm/gw/partner/api/v1/unbindCard.do', 875 | 'POST', 876 | [ 'Content-Type' => 'application/json' ], 877 | '{"bindingId":"fdbbc879-c171-4cff-b636-ceab16fd6fce","userName":"testUserName","password":"testPassword"}' 878 | ); 879 | 880 | $client = new Client([ 881 | 'apiUri' => 'https://ecommerce.sberbank.ru', 882 | 'prefixDefault' => '/ecomm/gw/partner/api/v1/', 883 | 'userName' => 'testUserName', 884 | 'password' => 'testPassword', 885 | 'ecom' => true, 886 | 'httpClient' => $httpClient, 887 | ]); 888 | 889 | $client->unBindCard('fdbbc879-c171-4cff-b636-ceab16fd6fce'); 890 | } 891 | 892 | private function mockHttpClient(?array $response = null) 893 | { 894 | $httpClient = $this->createMock(HttpClientInterface::class); 895 | 896 | if (null === $response) { 897 | $response = [200, \json_encode(['errorCode' => 0, 'errorMessage' => 'No error.'])]; 898 | } 899 | 900 | $httpClient 901 | ->method('request') 902 | ->willReturn($response) 903 | ; 904 | 905 | return $httpClient; 906 | } 907 | 908 | private function getHttpClientToTestSendingData(string $uri, string $method, array $headers = [], string $data = '') 909 | { 910 | $httpClient = $this->mockHttpClient(); 911 | 912 | $httpClient 913 | ->expects($this->once()) 914 | ->method('request') 915 | ->with( 916 | $this->stringEndsWith($uri), 917 | $this->equalTo($method), 918 | $this->equalTo($headers), 919 | $this->equalTo($data) 920 | ) 921 | ; 922 | 923 | return $httpClient; 924 | } 925 | } 926 | --------------------------------------------------------------------------------