diff --git a/src/Api/AtolClient.php b/src/Api/AtolClient.php new file mode 100644 index 0000000..81e58f0 --- /dev/null +++ b/src/Api/AtolClient.php @@ -0,0 +1,298 @@ +http = new Client(array_merge($config, [ + 'http_errors' => $config['http_errors'] ?? false, + ])); + $login && $this->setLogin($login); + $password && $this->setPassword($password); + } + + /** + * Возвращает установленный флаг тестового режима + * + * @return bool + */ + public function isTestMode(): bool + { + return $this->test_mode; + } + + /** + * Устанавливает флаг тестового режима + * + * @param bool $test_mode + * @return $this + */ + public function setTestMode(bool $test_mode): self + { + $this->test_mode = $test_mode; + return $this; + } + + /** + * Возвращает текущий токен авторизации + * + * @return string|null + */ + public function getToken(): ?string + { + return $this->token; + } + + /** + * Устанавливает токен авторизации + * + * @param string|null $token + * @return $this + */ + public function setToken(?string $token): AtolClient + { + $this->token = $token; + return $this; + } + + /** + * Возвращает последний ответ сервера + * + * @return KktResponse|null + */ + public function getResponse(): ?KktResponse + { + return $this->response; + } + + /** + * Возвращает логин доступа к API + * + * @return string|null + */ + protected function getLogin(): ?string + { + return $this->login; + } + + /** + * Устанавливает логин доступа к API + * + * @param string $login + * @return $this + * @throws EmptyLoginException + * @throws TooLongLoginException + */ + public function setLogin(string $login): self + { + $login = trim($login); + if (empty($login)) { + throw new EmptyLoginException(); + } elseif (mb_strlen($login) > Constraints::MAX_LENGTH_LOGIN) { + throw new TooLongLoginException($login, Constraints::MAX_LENGTH_LOGIN); + } + $this->login = $login; + return $this; + } + + /** + * Возвращает пароль доступа к API + * + * @return string|null + */ + protected function getPassword(): ?string + { + return $this->password; + } + + /** + * Устанавливает пароль доступа к API + * + * @param string $password + * @return $this + * @throws EmptyPasswordException Пароль ККТ не может быть пустым + * @throws TooLongPasswordException Слишком длинный пароль ККТ + */ + public function setPassword(string $password): self + { + if (empty($password)) { + throw new EmptyPasswordException(); + } elseif (mb_strlen($password) > Constraints::MAX_LENGTH_PASSWORD) { + throw new TooLongPasswordException($password, Constraints::MAX_LENGTH_PASSWORD); + } + $this->password = $password; + return $this; + } + + /** + * Возвращает набор заголовков для HTTP-запроса + * + * @return array + */ + private function getHeaders(): array + { + $headers['Content-type'] = 'application/json; charset=utf-8'; + if ($this->getToken()) { + $headers['Token'] = $this->getToken(); + } + return $headers; + } + + /** + * Возвращает полный URL для запроса + * + * @param string $method + * @return string + */ + protected function getUrlToMethod(string $method): string + { + return $this->getMainEndpoint() . '/' . trim($method); + } + + /** + * Отправляет авторизационный запрос на сервер АТОЛ и возвращает авторизационный токен + * + * @return string|null + * @throws AuthFailedException + * @throws EmptyPasswordException + * @throws EmptyLoginException + * @throws GuzzleException + */ + protected function doAuth(): ?string + { + $result = $this->sendRequest('POST', $this->getAuthEndpoint(), [ + 'login' => $this->getLogin() ?? throw new EmptyLoginException(), + 'pass' => $this->getPassword() ?? throw new EmptyPasswordException(), + ]); + if (!$result->isValid() || !$result->getContent()->token) { + throw new AuthFailedException($result); + } + return $result->getContent()?->token; + } + + /** + * Отправляет запрос и возвращает декодированный ответ + * + * @param string $http_method Метод HTTP + * @param string $url URL + * @param array|null $data Данные для передачи + * @param array|null $options Параметры Guzzle + * @return KktResponse + * @throws GuzzleException + * @see https://guzzle.readthedocs.io/en/latest/request-options.html + */ + protected function sendRequest( + string $http_method, + string $url, + ?array $data = null, + ?array $options = null + ): KktResponse { + $http_method = strtoupper(trim($http_method)); + $options['headers'] = array_merge($this->getHeaders(), $options['headers'] ?? []); + if ($http_method != 'GET') { + $options['json'] = $data; + } + $response = $this->http->request($http_method, $url, $options); + return $this->response = new KktResponse($response); + } + + /** + * Выполняет авторизацию на сервере АТОЛ + * + * Авторизация выолнится только если неизвестен токен + * + * @param string|null $login + * @param string|null $password + * @return bool + * @throws AuthFailedException + * @throws TooLongLoginException + * @throws EmptyLoginException + * @throws EmptyPasswordException + * @throws TooLongPasswordException + * @throws GuzzleException + */ + abstract public function auth(?string $login = null, ?string $password = null): bool; + + /** + * Возвращает URL для запроса авторизации + * + * @return string + */ + abstract protected function getAuthEndpoint(): string; + + /** + * Возвращает URL для запросов + * + * @return string + */ + abstract protected function getMainEndpoint(): string; +} diff --git a/src/Api/Kkt.php b/src/Api/Kkt.php index 7459c74..89921ff 100644 --- a/src/Api/Kkt.php +++ b/src/Api/Kkt.php @@ -17,15 +17,15 @@ use AtolOnline\{ Entities\Document, Exceptions\AuthFailedException, Exceptions\EmptyCorrectionInfoException, - Exceptions\EmptyKktLoginException, - Exceptions\EmptyKktPasswordException, + Exceptions\EmptyLoginException, + Exceptions\EmptyPasswordException, Exceptions\InvalidCallbackUrlException, Exceptions\InvalidDocumentTypeException, Exceptions\InvalidInnLengthException, Exceptions\InvalidUuidException, Exceptions\TooLongCallbackUrlException, - Exceptions\TooLongKktLoginException, - Exceptions\TooLongKktPasswordException, + Exceptions\TooLongLoginException, + Exceptions\TooLongPasswordException, Exceptions\TooLongPaymentAddressException, Exceptions\TooManyItemsException, Exceptions\TooManyVatsException, @@ -70,10 +70,6 @@ class Kkt extends Client * @param string|null $pass * @param bool $test_mode Флаг тестового режима * @param array $guzzle_config Конфигурация GuzzleHttp - * @throws EmptyKktLoginException Логин ККТ не может быть пустым - * @throws TooLongKktLoginException Слишком длинный логин ККТ - * @throws EmptyKktPasswordException Пароль ККТ не может быть пустым - * @throws TooLongKktPasswordException Слишком длинный пароль ККТ * @see https://guzzle.readthedocs.io/en/latest/request-options.html */ public function __construct( @@ -126,15 +122,15 @@ class Kkt extends Client * * @param string $login * @return $this - * @throws EmptyKktLoginException Логин ККТ не может быть пустым - * @throws TooLongKktLoginException Слишком длинный логин ККТ + * @throws EmptyLoginException Логин ККТ не может быть пустым + * @throws TooLongLoginException Слишком длинный логин ККТ */ public function setLogin(string $login): Kkt { if (empty($login)) { - throw new EmptyKktLoginException(); + throw new EmptyLoginException(); } elseif (mb_strlen($login) > Constraints::MAX_LENGTH_LOGIN) { - throw new TooLongKktLoginException($login, Constraints::MAX_LENGTH_LOGIN); + throw new TooLongLoginException($login, Constraints::MAX_LENGTH_LOGIN); } $this->kkt_config['prod']['login'] = $login; return $this; @@ -155,15 +151,15 @@ class Kkt extends Client * * @param string $password * @return $this - * @throws EmptyKktPasswordException Пароль ККТ не может быть пустым - * @throws TooLongKktPasswordException Слишком длинный пароль ККТ + * @throws EmptyPasswordException Пароль ККТ не может быть пустым + * @throws TooLongPasswordException Слишком длинный пароль ККТ */ public function setPassword(string $password): Kkt { if (empty($password)) { - throw new EmptyKktPasswordException(); + throw new EmptyPasswordException(); } elseif (mb_strlen($password) > Constraints::MAX_LENGTH_PASSWORD) { - throw new TooLongKktPasswordException($password, Constraints::MAX_LENGTH_PASSWORD); + throw new TooLongPasswordException($password, Constraints::MAX_LENGTH_PASSWORD); } $this->kkt_config['prod']['pass'] = $password; return $this; @@ -207,16 +203,6 @@ class Kkt extends Client { return $this->kkt_config[$this->isTestMode() ? 'test' : 'prod']['callback_url']; } - - /** - * Возвращает последний ответ сервера - * - * @return KktResponse|null - */ - public function getLastResponse(): ?KktResponse - { - return $this->last_response; - } /** * Возвращает флаг тестового режима diff --git a/src/Api/KktMonitor.php b/src/Api/KktMonitor.php new file mode 100644 index 0000000..8ea7037 --- /dev/null +++ b/src/Api/KktMonitor.php @@ -0,0 +1,124 @@ +isTestMode() + ? 'https://testonline.atol.ru/api/auth/v1/gettoken' + : 'https://online.atol.ru/api/auth/v1/gettoken'; + } + + /** + * @inheritDoc + */ + protected function getMainEndpoint(): string + { + return $this->isTestMode() + ? 'https://testonline.atol.ru/api/kkt/v1' + : 'https://online.atol.ru/api/kkt/v1'; + } + + /** + * @inheritDoc + */ + public function auth(?string $login = null, ?string $password = null): bool + { + if (empty($this->getToken())) { + $login && $this->setLogin($login); + $password && $this->setPassword($password); + if ($token = $this->doAuth()) { + $this->setToken($token); + } + } + return !empty($this->getToken()); + } + + /** + * Получает от API информацию обо всех ККТ и ФН в рамках группы + * + * @param int|null $limit + * @param int|null $offset + * @return KktResponse + * @throws GuzzleException + * @see https://online.atol.ru/files/API_service_information.pdf Документация, стр 9 + */ + protected function fetchAll(?int $limit = null, ?int $offset = null): KktResponse + { + $params = []; + $limit && $params['limit'] = $limit; + $offset && $params['offset'] = $offset; + return $this->sendRequest('GET', self::getUrlToMethod('cash-registers'), $params); + } + + /** + * Возвращает информацию обо всех ККТ и ФН в рамках группы + * + * @param int|null $limit + * @param int|null $offset + * @return KktResponse + * @throws GuzzleException + * @see https://online.atol.ru/files/API_service_information.pdf Документация, стр 9 + */ + public function getAll(?int $limit = null, ?int $offset = null): KktResponse + { + return $this->fetchAll($limit, $offset); + } + + /** + * Получает от API информацию о конкретной ККТ по её серийному номеру + * + * @param string $serial_number + * @return KktResponse + * @throws GuzzleException + * @see https://online.atol.ru/files/API_service_information.pdf Документация, стр 11 + */ + protected function fetchOne(string $serial_number): KktResponse + { + return $this->sendRequest( + 'GET', + self::getUrlToMethod('cash-registers') . '/' . trim($serial_number), + options: [ + 'headers' => [ + 'Accept' => 'application/hal+json', + ], + ] + ); + } + + /** + * Возвращает информацию о конкретной ККТ по её серийному номеру + * + * @todo кастовать к отдельному классу со своими геттерами + * @param string $serial_number + * @return stdClass + * @throws GuzzleException + * @see https://online.atol.ru/files/API_service_information.pdf Документация, стр 11 + */ + public function getOne(string $serial_number): stdClass + { + return $this->fetchOne($serial_number)->getContent()->data; + } +} diff --git a/src/Api/KktResponse.php b/src/Api/KktResponse.php index 7fe359a..796df1a 100644 --- a/src/Api/KktResponse.php +++ b/src/Api/KktResponse.php @@ -14,6 +14,7 @@ namespace AtolOnline\Api; use JsonSerializable; use Psr\Http\Message\ResponseInterface; use stdClass; +use Stringable; /** * Класс AtolResponse, описывающий ответ от ККТ @@ -21,7 +22,7 @@ use stdClass; * @property mixed $error * @package AtolOnline\Api */ -class KktResponse implements JsonSerializable +class KktResponse implements JsonSerializable, Stringable { /** * @var int Код ответа сервера @@ -29,9 +30,9 @@ class KktResponse implements JsonSerializable protected int $code; /** - * @var stdClass Содержимое ответа сервера + * @var stdClass|array|null Содержимое ответа сервера */ - protected $content; + protected stdClass|array|null $content; /** * @var array Заголовки ответа @@ -66,9 +67,9 @@ class KktResponse implements JsonSerializable * @param $name * @return mixed */ - public function __get($name) + public function __get($name): mixed { - return $this->getContent()->$name; + return $this->getContent()?->$name; } /** @@ -84,9 +85,9 @@ class KktResponse implements JsonSerializable /** * Возвращает объект результата запроса * - * @return stdClass|null + * @return mixed */ - public function getContent(): ?stdClass + public function getContent(): mixed { return $this->content; } @@ -107,7 +108,7 @@ class KktResponse implements JsonSerializable /** * Возвращает текстовое представление */ - public function __toString() + public function __toString(): string { return json_encode($this->jsonSerialize(), JSON_UNESCAPED_UNICODE); } @@ -115,7 +116,7 @@ class KktResponse implements JsonSerializable /** * @inheritDoc */ - public function jsonSerialize() + public function jsonSerialize(): array { return [ 'code' => $this->code, diff --git a/src/Exceptions/AuthFailedException.php b/src/Exceptions/AuthFailedException.php index 9c2c76a..e213f6b 100644 --- a/src/Exceptions/AuthFailedException.php +++ b/src/Exceptions/AuthFailedException.php @@ -16,26 +16,26 @@ use Exception; use Throwable; /** - * Исключение, возникающее при работе с АТОЛ Онлайн + * Исключение, возникающее при неудачной авторизации */ class AuthFailedException extends Exception { /** * Конструктор * - * @param KktResponse $last_response + * @param KktResponse $response * @param string $message * @param int $code * @param Throwable|null $previous */ - public function __construct(KktResponse $last_response, $message = "", $code = 0, Throwable $previous = null) + public function __construct(KktResponse $response, $message = "", $code = 0, Throwable $previous = null) { - $message = $last_response->isValid() + $message = $response->isValid() ? $message - : '[' . $last_response->error->code . '] ' . $last_response->error->text . - '. ERROR_ID: ' . $last_response->error->error_id . - '. TYPE: ' . $last_response->error->type; - $code = $last_response->isValid() ? $code : $last_response->error->code; + : '[' . $response->error->code . '] ' . $response->error->text . + '. ERROR_ID: ' . $response->error->error_id . + '. TYPE: ' . $response->error->type; + $code = $response->isValid() ? $code : $response->error->code; parent::__construct($message, $code, $previous); } } diff --git a/src/Exceptions/EmptyKktLoginException.php b/src/Exceptions/EmptyLoginException.php similarity index 91% rename from src/Exceptions/EmptyKktLoginException.php rename to src/Exceptions/EmptyLoginException.php index 112b799..fbd4b5e 100644 --- a/src/Exceptions/EmptyKktLoginException.php +++ b/src/Exceptions/EmptyLoginException.php @@ -14,7 +14,7 @@ namespace AtolOnline\Exceptions; /** * Исключение, возникающее при попытке указать пустой логин ККТ */ -class EmptyKktLoginException extends AtolException +class EmptyLoginException extends AtolException { /** * @var string Сообщение об ошибке diff --git a/src/Exceptions/EmptyKktPasswordException.php b/src/Exceptions/EmptyPasswordException.php similarity index 91% rename from src/Exceptions/EmptyKktPasswordException.php rename to src/Exceptions/EmptyPasswordException.php index aba4aa9..d3370b8 100644 --- a/src/Exceptions/EmptyKktPasswordException.php +++ b/src/Exceptions/EmptyPasswordException.php @@ -14,7 +14,7 @@ namespace AtolOnline\Exceptions; /** * Исключение, возникающее при попытке указать пустой пароль ККТ */ -class EmptyKktPasswordException extends AtolException +class EmptyPasswordException extends AtolException { /** * @var string Сообщение об ошибке diff --git a/src/Exceptions/TooLongKktLoginException.php b/src/Exceptions/TooLongLoginException.php similarity index 90% rename from src/Exceptions/TooLongKktLoginException.php rename to src/Exceptions/TooLongLoginException.php index 7d35711..51170e9 100644 --- a/src/Exceptions/TooLongKktLoginException.php +++ b/src/Exceptions/TooLongLoginException.php @@ -14,7 +14,7 @@ namespace AtolOnline\Exceptions; /** * Исключение, возникающее при попытке указать слишком длинный логин ККТ */ -class TooLongKktLoginException extends BasicTooLongException +class TooLongLoginException extends BasicTooLongException { /** * @var string Сообщение об ошибке diff --git a/src/Exceptions/TooLongKktPasswordException.php b/src/Exceptions/TooLongPasswordException.php similarity index 90% rename from src/Exceptions/TooLongKktPasswordException.php rename to src/Exceptions/TooLongPasswordException.php index 756767f..c9fb51e 100644 --- a/src/Exceptions/TooLongKktPasswordException.php +++ b/src/Exceptions/TooLongPasswordException.php @@ -14,7 +14,7 @@ namespace AtolOnline\Exceptions; /** * Исключение, возникающее при попытке указать слишком длинный пароль ККТ */ -class TooLongKktPasswordException extends BasicTooLongException +class TooLongPasswordException extends BasicTooLongException { /** * @var string Сообщение об ошибке diff --git a/tests/AtolClientTest.php b/tests/AtolClientTest.php new file mode 100644 index 0000000..b5f387d --- /dev/null +++ b/tests/AtolClientTest.php @@ -0,0 +1,48 @@ +