├── .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 | [](https://app.travis-ci.com/github/voronkovich/sberbank-acquiring-client)
4 | [](https://packagist.org/packages/voronkovich/sberbank-acquiring-client)
5 | [](https://packagist.org/packages/voronkovich/sberbank-acquiring-client/stats)
6 | [](./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 |
--------------------------------------------------------------------------------