From 3229c92ae797dc49afde4c263869fe0976c912d4 Mon Sep 17 00:00:00 2001 From: liquetsoft <49859130+liquetsoft@users.noreply.github.com> Date: Tue, 12 Jul 2022 11:31:34 +0200 Subject: [PATCH] Setup (#27) --- .github/workflows/php.yml | 50 ++ .gitignore | 12 + .php-cs-fixer.dist.php | 35 + LICENSE | 21 + Makefile | 31 + README.md | 185 ++++++ composer.json | 45 ++ docker/docker-compose.yml | 12 + docker/php/Dockerfile | 31 + phpunit.xml.dist | 13 + psalm.xml | 30 + src/CbrfDaily.php | 619 ++++++++++++++++++ src/CbrfException.php | 12 + src/CbrfSoapService.php | 129 ++++ src/DataHelper.php | 238 +++++++ src/Entity/Currency.php | 19 + src/Entity/CurrencyEnum.php | 73 +++ src/Entity/CurrencyRate.php | 66 ++ src/Entity/DepoRate.php | 34 + src/Entity/InternationalReserve.php | 34 + src/Entity/InternationalReserveWeek.php | 34 + src/Entity/KeyRate.php | 34 + src/Entity/Mkr.php | 82 +++ src/Entity/OstatDepoRate.php | 42 ++ src/Entity/OstatRate.php | 46 ++ src/Entity/PreciousMetalRate.php | 47 ++ src/Entity/ReutersCurrencyRate.php | 73 +++ src/Entity/RuoniaBid.php | 50 ++ src/Entity/RuoniaIndex.php | 58 ++ src/Entity/Saldo.php | 34 + src/Entity/SwapRate.php | 66 ++ tests/BaseTestCase.php | 169 +++++ tests/CbrfDailyTest.php | 822 ++++++++++++++++++++++++ tests/CbrfSoapServiceTest.php | 94 +++ tests/DataHelperTest.php | 588 +++++++++++++++++ 35 files changed, 3928 insertions(+) create mode 100644 .github/workflows/php.yml create mode 100644 .gitignore create mode 100644 .php-cs-fixer.dist.php create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 composer.json create mode 100644 docker/docker-compose.yml create mode 100644 docker/php/Dockerfile create mode 100644 phpunit.xml.dist create mode 100644 psalm.xml create mode 100644 src/CbrfDaily.php create mode 100644 src/CbrfException.php create mode 100644 src/CbrfSoapService.php create mode 100644 src/DataHelper.php create mode 100644 src/Entity/Currency.php create mode 100644 src/Entity/CurrencyEnum.php create mode 100644 src/Entity/CurrencyRate.php create mode 100644 src/Entity/DepoRate.php create mode 100644 src/Entity/InternationalReserve.php create mode 100644 src/Entity/InternationalReserveWeek.php create mode 100644 src/Entity/KeyRate.php create mode 100644 src/Entity/Mkr.php create mode 100644 src/Entity/OstatDepoRate.php create mode 100644 src/Entity/OstatRate.php create mode 100644 src/Entity/PreciousMetalRate.php create mode 100644 src/Entity/ReutersCurrencyRate.php create mode 100644 src/Entity/RuoniaBid.php create mode 100644 src/Entity/RuoniaIndex.php create mode 100644 src/Entity/Saldo.php create mode 100644 src/Entity/SwapRate.php create mode 100644 tests/BaseTestCase.php create mode 100644 tests/CbrfDailyTest.php create mode 100644 tests/CbrfSoapServiceTest.php create mode 100644 tests/DataHelperTest.php diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml new file mode 100644 index 0000000..a333e8a --- /dev/null +++ b/.github/workflows/php.yml @@ -0,0 +1,50 @@ +name: cbrf_service + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + + name: CBRF service (PHP ${{ matrix.php-versions }}) + runs-on: ubuntu-latest + strategy: + fail-fast: true + matrix: + php-versions: ['7.4', '8.0', '8.1'] + + steps: + - uses: actions/checkout@v2 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + + - name: Check PHP Version + run: php -v + + - name: Validate composer.json and composer.lock + run: composer validate + + - name: Cache Composer packages + id: composer-cache + uses: actions/cache@v2 + with: + path: vendor + key: ${{ runner.os }}-php-${{ matrix.php-versions }}-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-php- + + - name: Install dependencies + if: steps.composer-cache.outputs.cache-hit != 'true' + run: composer install --prefer-dist --no-progress --no-suggest + + - name: Run test suite + run: composer run-script test + + - name: Run linters + run: composer run-script linter diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d4de4bd --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +.idea +vendor +composer.lock +.php_cs.cache +.phpunit.result.cache +.php-cs-fixer.cache + +tests/coverage +docker/.env +test.php + +.~lock.* diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..baf45b0 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,35 @@ +in(__DIR__); + +$rules = [ + '@Symfony' => true, + 'new_with_braces' => true, + 'concat_space' => ['spacing' => 'one'], + 'array_syntax' => ['syntax' => 'short'], + 'yoda_style' => false, + 'phpdoc_no_empty_return' => false, + 'no_superfluous_phpdoc_tags' => false, + 'single_line_throw' => false, + 'array_indentation' => true, + 'declare_strict_types' => true, + 'void_return' => true, + 'non_printable_character' => true, + 'modernize_types_casting' => true, + 'ordered_interfaces' => ['order' => 'alpha', 'direction' => 'ascend'], + 'date_time_immutable' => true, + 'native_constant_invocation' => true, + 'combine_nested_dirname' => true, + 'native_function_invocation' => ['include' => ['@compiler_optimized'], 'scope' => 'namespaced', 'strict' => true], + 'php_unit_construct' => true, + 'php_unit_dedicate_assert' => true, + 'php_unit_expectation' => true, + 'php_unit_internal_class' => true, + 'php_unit_mock_short_will_return' => true, + 'php_unit_strict' => true, + 'strict_comparison' => true, +]; + +return (new Config())->setRules($rules)->setFinder($finder); diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..dabc729 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 liquetsoft + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..528bb04 --- /dev/null +++ b/Makefile @@ -0,0 +1,31 @@ +#!/usr/bin/make + +user_id := $(shell id -u) +docker_compose_bin := $(shell command -v docker-compose 2> /dev/null) --file "docker/docker-compose.yml" +php_container_bin := $(docker_compose_bin) run --rm -u "$(user_id)" "php" +php_composer_script := $(php_container_bin) composer run-script + +.DEFAULT_GOAL := build + +# --- [ Development tasks ] ------------------------------------------------------------------------------------------- + +build: ## Build container and install composer libs + $(docker_compose_bin) build --force-rm + +install: ## Install all data + $(php_container_bin) composer update + +shell: ## Runs shell in container + $(php_container_bin) bash + +fixer: ## Run fixer to fix code style + $(php_composer_script) fixer + +linter: ## Run linter to check project + $(php_composer_script) linter + +test: ## Run tests + $(php_composer_script) test + +coverage: ## Run tests with coverage + $(php_composer_script) coverage diff --git a/README.md b/README.md index e69de29..bdd5b84 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,185 @@ +Php курсы валют +=============== + +[![Latest Stable Version](https://poser.pugx.org/liquetsoft/cbrfservice/v/stable.png)](https://packagist.org/packages/liquetsoft/cbrfservice) +[![Total Downloads](https://poser.pugx.org/liquetsoft/cbrfservice/downloads.png)](https://packagist.org/packages/liquetsoft/cbrfservice) +[![License](https://poser.pugx.org/liquetsoft/cbrfservice/license.svg)](https://packagist.org/packages/liquetsoft/cbrfservice) +[![Build Status](https://github.com/liquetsoft/cbrfservice/workflows/cbrf_service/badge.svg)](https://github.com/liquetsoft/cbrfservice/actions?query=workflow%3A%22cbrf_service%22) + +Php обертка для [сервиса Центробанка РФ](https://www.cbr.ru/development/DWS/). + + + +Установка +--------- + +Добавьте библиотеку в проект с помощью [Composer](https://getcomposer.org/doc/00-intro.md): + +```bash +composer req liquetsoft/cbrfservice +``` + + + +Использование +------------- + +```php +//инициируем новый объект сервиса +$cbrf = new \Liquetsoft\CbrfService\CbrfDaily(); +``` + +```php +//получаем курсы всех валют +$rates = $cbrf->getCursOnDate(new \DateTimeImmutable()); + +//получаем курс валюты по ее буквенному коду +$rateEur = $cbrf->getCursOnDateByCharCode(new \DateTimeImmutable(), 'EUR'); + +//получаем курс валюты по ее цифровому коду +$rate978 = $cbrf->getCursOnDateByNumericCode(new \DateTimeImmutable(), 978); +``` + +```php +//получаем словарь всех доступных валют +$currencies = $cbrf->enumValutes(); + +//получаем описание валюты из словаря по буквенному коду +$enumEur = $cbrf->enumValuteByCharCode('EUR'); + +//получаем описание валюты из словаря по цифровому коду +$enum978 = $cbrf->enumValuteByNumericCode(978); + +//получаем динамику курса для указанной валюты за последний месяц +$dynamic = $cbrf->getCursDynamic( + new \DateTimeImmutable('-1 month'), + new \DateTimeImmutable(), + $enumEur +); +``` + +```php +//получаем динамику ключевой ставки за последний месяц +$keyRate = $cbrf->keyRate( + new \DateTimeImmutable('-1 month'), + new \DateTimeImmutable() +); +``` + +```php +//получаем динамику цен на драгоценные металлы за последний месяц +$metalsPrices = $cbrf->dragMetDynamic( + new \DateTimeImmutable('-1 month'), + new \DateTimeImmutable() +); +``` + +```php +//получаем валютный своп за последний месяц +$swap = $cbrf->swapDynamic( + new \DateTimeImmutable('-1 month'), + new \DateTimeImmutable() +); +``` + +```php +//получаем динамику ставок привлечения средств по депозитным операциям за последний месяц +$depo = $cbrf->depoDynamic( + new \DateTimeImmutable('-1 month'), + new \DateTimeImmutable() +); +``` + +```php +//получаем динамику сведений об остатках средств на корреспондентских счетах кредитных организаций +$leftovers = $cbrf->ostatDynamic( + new \DateTimeImmutable('-1 month'), + new \DateTimeImmutable() +); +``` + +```php +//получаем депозиты банков в Банке России +$leftovers = $cbrf->ostatDepo( + new \DateTimeImmutable('-1 month'), + new \DateTimeImmutable() +); +``` + +```php +//получаем международные резервы Российской Федерации, ежемесячные значения +$mrrf = $cbrf->mrrf( + new \DateTimeImmutable('-1 month'), + new \DateTimeImmutable() +); +``` + +```php +//получаем международные резервы Российской Федерации, еженедельные значения +$mrrf = $cbrf->mrrf7d( + new \DateTimeImmutable('-1 month'), + new \DateTimeImmutable() +); +``` + +```php +//получаем сальдо операций ЦБ РФ по предоставлению/абсорбированию ликвидности +$saldo = $cbrf->saldo( + new \DateTimeImmutable('-1 month'), + new \DateTimeImmutable() +); +``` + +```php +//получаем индекс и срочную версию RUONIA +$saldo = $cbrf->ruoniaSV( + new \DateTimeImmutable('-1 month'), + new \DateTimeImmutable() +); +``` + +```php +//получаем ставку RUONIA +$saldo = $cbrf->ruonia( + new \DateTimeImmutable('-1 month'), + new \DateTimeImmutable() +); +``` + +```php +//получаем ставки межбанковского кредитного рынка +$mkr = $cbrf->mkr( + new \DateTimeImmutable('-1 month'), + new \DateTimeImmutable() +); +``` + +В случае, если необходимо передать сконфигурированный заранее транспорт, например для использования proxy: + +```php +//инициируем новый объект SoapClient +$client = new SoapClient( + \Liquetsoft\CbrfService\CbrfSoapService::DEFAULT_WSDL, + [ + 'proxy_host' => 'localhost', + 'proxy_port' => 8080 + ] +); + +//инициируем новый объект сервиса +$cbrf = new \Liquetsoft\CbrfService\CbrfDaily($client); +``` + + + +Обработка ошибок +---------------- + +Все ошибки, которые будут перехвачены при запросах, будут выброшены как исключение `\Liquetsoft\CbrfService\CbrfException`. Если `\SoapClient` будет сконфигурирован с отключенными исключениями, то обработка ошибок остается на стороне клиентского скрипта. + + + +Методы +------ + +Описание методов вы можете найти на [сайте банка России](https://www.cbr.ru/development/DWS/). diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..ea14a28 --- /dev/null +++ b/composer.json @@ -0,0 +1,45 @@ +{ + "name": "liquetsoft/cbrfservice", + "description": "Component for bank of Russia daily web service", + "type": "library", + "keywords": ["php", "banks", "currency rate", "soap", "service"], + "license": "MIT", + "require": { + "php": ">=7.4", + "ext-soap": "*", + "ext-SimpleXML": "*" + }, + "require-dev": { + "phpunit/phpunit": "^9.0", + "friendsofphp/php-cs-fixer": "^2.11|^3.0", + "sebastian/phpcpd": "^6.0", + "vimeo/psalm": "^4.0", + "psalm/plugin-phpunit": "^0.15.1" + }, + "autoload": { + "psr-4": { + "Liquetsoft\\CbrfService\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Liquetsoft\\CbrfService\\Tests\\": "tests/" + } + }, + "scripts": { + "test": "vendor/bin/phpunit --configuration phpunit.xml.dist", + "coverage": "vendor/bin/phpunit --configuration phpunit.xml.dist --coverage-html=tests/coverage", + "fixer": "vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -vv --allow-risky=yes", + "linter": [ + "vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php -vv --allow-risky=yes --dry-run --stop-on-violation", + "vendor/bin/phpcpd ./ --exclude vendor --exclude tests", + "vendor/bin/psalm --show-info=true --php-version=$(php -r \"echo phpversion();\")" + ] + }, + "repositories": [ + { + "type": "git", + "url": "https://github.com/liquetsoft/cbrfservice" + } + ] +} diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..22d8d59 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,12 @@ +version: '3.7' + + +# container to run tests and other developer activities +services: + php: + container_name: cbrf-service-php + image: cbrf-service-php + build: + context: php + volumes: + - ./../:/var/app:cached diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile new file mode 100644 index 0000000..7cbef9c --- /dev/null +++ b/docker/php/Dockerfile @@ -0,0 +1,31 @@ +FROM php:8.1-cli-alpine + + +RUN set -xe && apk update && apk add --no-cache \ + libzip \ + bash \ + curl \ + libmcrypt-dev \ + libxml2-dev \ + libzip-dev \ + pcre-dev \ + git \ + autoconf \ + g++ \ + make + + +RUN docker-php-ext-install zip soap \ + && docker-php-source extract \ + && pecl install xdebug \ + && docker-php-ext-enable xdebug \ + && docker-php-source delete \ + && echo 'date.timezone=Europe/London' >> /usr/local/etc/php/conf.d/php-date.ini \ + && echo 'xdebug.mode=coverage' >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini + + +RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer --version=2.3.7 \ + && mkdir -p /.composer && chmod -Rf 777 /.composer + + +WORKDIR /var/app \ No newline at end of file diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..aa1f2c8 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,13 @@ + + + + + ./src + + + + + ./tests + + + diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..9566bf6 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/CbrfDaily.php b/src/CbrfDaily.php new file mode 100644 index 0000000..3471cc0 --- /dev/null +++ b/src/CbrfDaily.php @@ -0,0 +1,619 @@ +soapClient = new CbrfSoapService($client); + } + + /** + * Returns list of rates for all currencies for set date. + * + * @param DateTimeInterface $onDate + * + * @return CurrencyRate[] + * + * @throws CbrfException + */ + public function getCursOnDate(DateTimeInterface $date): array + { + $soapResult = $this->soapClient->query( + 'GetCursOnDate', + [ + 'On_date' => $date->format(CbrfSoapService::DATE_TIME_FORMAT), + ] + ); + + $immutableDate = DataHelper::createImmutableDateTime($date); + $list = DataHelper::array('ValuteData.ValuteCursOnDate', $soapResult); + $callback = fn (array $item): CurrencyRate => new CurrencyRate($item, $immutableDate); + + return array_map($callback, $list); + } + + /** + * Returns rate for currency with set char code. + * + * @param DateTimeInterface $onDate + * @param string $charCode + * + * @return CurrencyRate|null + * + * @throws CbrfException + */ + public function getCursOnDateByCharCode(DateTimeInterface $date, string $charCode): ?CurrencyRate + { + $list = $this->getCursOnDate($date); + + $return = null; + foreach ($list as $item) { + if (strcasecmp($charCode, $item->getCharCode()) === 0) { + $return = $item; + break; + } + } + + return $return; + } + + /** + * Returns rate for currency with set numeric code. + * + * @param DateTimeInterface $onDate + * @param int $numericCode + * + * @return CurrencyRate|null + * + * @throws CbrfException + */ + public function getCursOnDateByNumericCode(DateTimeInterface $date, int $numericCode): ?CurrencyRate + { + $list = $this->getCursOnDate($date); + + $return = null; + foreach ($list as $item) { + if ($item->getNumericCode() === $numericCode) { + $return = $item; + break; + } + } + + return $return; + } + + /** + * List of all currencies that allowed on cbrf service. + * + * @param bool $seld + * + * @return CurrencyEnum[] + * + * @throws CbrfException + */ + public function enumValutes(bool $seld = false): array + { + $soapResult = $this->soapClient->query( + 'EnumValutes', + [ + 'Seld' => $seld, + ] + ); + + $list = DataHelper::array('ValuteData.EnumValutes', $soapResult); + $callback = fn (array $item): CurrencyEnum => new CurrencyEnum($item); + + return array_map($callback, $list); + } + + /** + * Returns enum for currency with set char code. + * + * @param string $charCode + * @param bool $seld + * + * @return CurrencyEnum|null + * + * @throws CbrfException + */ + public function enumValuteByCharCode(string $charCode, bool $seld = false): ?CurrencyEnum + { + $list = $this->enumValutes($seld); + + $return = null; + foreach ($list as $item) { + if (strcasecmp($charCode, $item->getCharCode()) === 0) { + $return = $item; + break; + } + } + + return $return; + } + + /** + * Returns enum for currency with set numeric code. + * + * @param int $numericCode + * @param bool $seld + * + * @return CurrencyEnum|null + * + * @throws CbrfException + */ + public function enumValuteByNumericCode(int $numericCode, bool $seld = false): ?CurrencyEnum + { + $list = $this->enumValutes($seld); + + $return = null; + foreach ($list as $item) { + if ($item->getNumericCode() === $numericCode) { + $return = $item; + break; + } + } + + return $return; + } + + /** + * Latest per day date and time of publication. + * + * @param string $format + * + * @return DateTimeInterface + * + * @throws CbrfException + */ + public function getLatestDateTime(): DateTimeInterface + { + $soapResult = $this->soapClient->query('GetLatestDateTime'); + + return DataHelper::dateTime('GetLatestDateTimeResult', $soapResult); + } + + /** + * Latest per day date and time of seld. + * + * @param string $format + * + * @return DateTimeInterface + * + * @throws CbrfException + */ + public function getLatestDateTimeSeld(): DateTimeInterface + { + $soapResult = $this->soapClient->query('GetLatestDateTimeSeld'); + + return DataHelper::dateTime('GetLatestDateTimeSeldResult', $soapResult); + } + + /** + * Latest per month date and time of publication. + * + * @param string $format + * + * @return DateTimeInterface + * + * @throws CbrfException + */ + public function getLatestDate(): DateTimeInterface + { + $soapResult = $this->soapClient->query('GetLatestDate'); + + return DataHelper::dateTime('GetLatestDateResult', $soapResult); + } + + /** + * Latest per month date and time of seld. + * + * @param string $format + * + * @return DateTimeInterface + * + * @throws CbrfException + */ + public function getLatestDateSeld(): DateTimeInterface + { + $soapResult = $this->soapClient->query('GetLatestDateSeld'); + + return DataHelper::dateTime('GetLatestDateSeldResult', $soapResult); + } + + /** + * Returns rate dynamic for set currency within set dates. + * + * @param DateTimeInterface $from + * @param DateTimeInterface $to + * @param CurrencyEnum $currency + * + * @return CurrencyRate[] + */ + public function getCursDynamic(DateTimeInterface $from, DateTimeInterface $to, CurrencyEnum $currency): array + { + $soapResult = $this->soapClient->query( + 'GetCursDynamic', + [ + 'FromDate' => $from->format(CbrfSoapService::DATE_TIME_FORMAT), + 'ToDate' => $to->format(CbrfSoapService::DATE_TIME_FORMAT), + 'ValutaCode' => $currency->getInternalCode(), + ] + ); + + $result = []; + $list = DataHelper::array('ValuteData.ValuteCursDynamic', $soapResult); + foreach ($list as $item) { + $date = DataHelper::dateTime('CursDate', $item); + $item['Vname'] = $currency->getName(); + $item['VchCode'] = $currency->getCharCode(); + $item['Vcode'] = $currency->getNumericCode(); + $result[] = new CurrencyRate($item, $date); + } + + return $result; + } + + /** + * Returns key rate dynamic within set dates. + * + * @param DateTimeInterface $from + * @param DateTimeInterface $to + * + * @return KeyRate[] + */ + public function keyRate(DateTimeInterface $from, DateTimeInterface $to): array + { + $soapResult = $this->soapClient->query( + 'KeyRate', + [ + 'fromDate' => $from->format(CbrfSoapService::DATE_TIME_FORMAT), + 'ToDate' => $to->format(CbrfSoapService::DATE_TIME_FORMAT), + ] + ); + + $list = DataHelper::array('KeyRate.KR', $soapResult); + $callback = fn (array $item): KeyRate => new KeyRate($item); + + return array_map($callback, $list); + } + + /** + * Returns list of presious metals prices within set dates. + * + * @param DateTimeInterface $from + * @param DateTimeInterface $to + * + * @return PreciousMetalRate[] + */ + public function dragMetDynamic(DateTimeInterface $from, DateTimeInterface $to): array + { + $soapResult = $this->soapClient->query( + 'DragMetDynamic', + [ + 'fromDate' => $from->format(CbrfSoapService::DATE_TIME_FORMAT), + 'ToDate' => $to->format(CbrfSoapService::DATE_TIME_FORMAT), + ] + ); + + $list = DataHelper::array('DragMetall.DrgMet', $soapResult); + $callback = fn (array $item): PreciousMetalRate => new PreciousMetalRate($item); + + return array_map($callback, $list); + } + + /** + * Returns list of swap rates within set dates. + * + * @param DateTimeInterface $from + * @param DateTimeInterface $to + * + * @return SwapRate[] + */ + public function swapDynamic(DateTimeInterface $from, DateTimeInterface $to): array + { + $soapResult = $this->soapClient->query( + 'SwapDynamic', + [ + 'fromDate' => $from->format(CbrfSoapService::DATE_TIME_FORMAT), + 'ToDate' => $to->format(CbrfSoapService::DATE_TIME_FORMAT), + ] + ); + + $list = DataHelper::array('SwapDynamic.Swap', $soapResult); + $callback = fn (array $item): SwapRate => new SwapRate($item); + + return array_map($callback, $list); + } + + /** + * Returns list depo dynamic items within set dates. + * + * @param DateTimeInterface $from + * @param DateTimeInterface $to + * + * @return DepoRate[] + */ + public function depoDynamic(DateTimeInterface $from, DateTimeInterface $to): array + { + $soapResult = $this->soapClient->query( + 'DepoDynamic', + [ + 'fromDate' => $from->format(CbrfSoapService::DATE_TIME_FORMAT), + 'ToDate' => $to->format(CbrfSoapService::DATE_TIME_FORMAT), + ] + ); + + $list = DataHelper::array('DepoDynamic.Depo', $soapResult); + $callback = fn (array $item): DepoRate => new DepoRate($item); + + return array_map($callback, $list); + } + + /** + * Returns the dynamic of balances of funds items within set dates. + * + * @param DateTimeInterface $from + * @param DateTimeInterface $to + * + * @return OstatRate[] + */ + public function ostatDynamic(DateTimeInterface $from, DateTimeInterface $to): array + { + $soapResult = $this->soapClient->query( + 'OstatDynamic', + [ + 'fromDate' => $from->format(CbrfSoapService::DATE_TIME_FORMAT), + 'ToDate' => $to->format(CbrfSoapService::DATE_TIME_FORMAT), + ] + ); + + $list = DataHelper::array('OstatDynamic.Ostat', $soapResult); + $callback = fn (array $item): OstatRate => new OstatRate($item); + + return array_map($callback, $list); + } + + /** + * Returns the banks deposites at bank of Russia. + * + * @param DateTimeInterface $from + * @param DateTimeInterface $to + * + * @return OstatDepoRate[] + */ + public function ostatDepo(DateTimeInterface $from, DateTimeInterface $to): array + { + $soapResult = $this->soapClient->query( + 'OstatDepo', + [ + 'fromDate' => $from->format(CbrfSoapService::DATE_TIME_FORMAT), + 'ToDate' => $to->format(CbrfSoapService::DATE_TIME_FORMAT), + ] + ); + + $list = DataHelper::array('OD.odr', $soapResult); + $callback = fn (array $item): OstatDepoRate => new OstatDepoRate($item); + + return array_map($callback, $list); + } + + /** + * Returns international valute reseves of Russia for month. + * + * @param DateTimeInterface $from + * @param DateTimeInterface $to + * + * @return InternationalReserve[] + */ + public function mrrf(DateTimeInterface $from, DateTimeInterface $to): array + { + $soapResult = $this->soapClient->query( + 'mrrf', + [ + 'fromDate' => $from->format(CbrfSoapService::DATE_TIME_FORMAT), + 'ToDate' => $to->format(CbrfSoapService::DATE_TIME_FORMAT), + ] + ); + + $list = DataHelper::array('mmrf.mr', $soapResult); + $callback = fn (array $item): InternationalReserve => new InternationalReserve($item); + + return array_map($callback, $list); + } + + /** + * Returns international valute reseves of Russia for week. + * + * @param DateTimeInterface $from + * @param DateTimeInterface $to + * + * @return InternationalReserveWeek[] + */ + public function mrrf7d(DateTimeInterface $from, DateTimeInterface $to): array + { + $soapResult = $this->soapClient->query( + 'mrrf7D', + [ + 'fromDate' => $from->format(CbrfSoapService::DATE_TIME_FORMAT), + 'ToDate' => $to->format(CbrfSoapService::DATE_TIME_FORMAT), + ] + ); + + $list = DataHelper::array('mmrf7d.mr', $soapResult); + $callback = fn (array $item): InternationalReserveWeek => new InternationalReserveWeek($item); + + return array_map($callback, $list); + } + + /** + * Returns operations saldo. + * + * @param DateTimeInterface $from + * @param DateTimeInterface $to + * + * @return Saldo[] + */ + public function saldo(DateTimeInterface $from, DateTimeInterface $to): array + { + $soapResult = $this->soapClient->query( + 'Saldo', + [ + 'fromDate' => $from->format(CbrfSoapService::DATE_TIME_FORMAT), + 'ToDate' => $to->format(CbrfSoapService::DATE_TIME_FORMAT), + ] + ); + + $list = DataHelper::array('Saldo.So', $soapResult); + $callback = fn (array $item): Saldo => new Saldo($item); + + return array_map($callback, $list); + } + + /** + * Returns Ruonia index. + * + * @param DateTimeInterface $from + * @param DateTimeInterface $to + * + * @return RuoniaIndex[] + */ + public function ruoniaSV(DateTimeInterface $from, DateTimeInterface $to): array + { + $soapResult = $this->soapClient->query( + 'RuoniaSV', + [ + 'fromDate' => $from->format(CbrfSoapService::DATE_TIME_FORMAT), + 'ToDate' => $to->format(CbrfSoapService::DATE_TIME_FORMAT), + ] + ); + + $list = DataHelper::array('RuoniaSV.ra', $soapResult); + $callback = fn (array $item): RuoniaIndex => new RuoniaIndex($item); + + return array_map($callback, $list); + } + + /** + * Returns Ruonia bid. + * + * @param DateTimeInterface $from + * @param DateTimeInterface $to + * + * @return RuoniaBid[] + */ + public function ruonia(DateTimeInterface $from, DateTimeInterface $to): array + { + $soapResult = $this->soapClient->query( + 'Ruonia', + [ + 'fromDate' => $from->format(CbrfSoapService::DATE_TIME_FORMAT), + 'ToDate' => $to->format(CbrfSoapService::DATE_TIME_FORMAT), + ] + ); + + $list = DataHelper::array('Ruonia.ro', $soapResult); + $callback = fn (array $item): RuoniaBid => new RuoniaBid($item); + + return array_map($callback, $list); + } + + /** + * Returns inter banks credit market bids. + * + * @param DateTimeInterface $from + * @param DateTimeInterface $to + * + * @return Mkr[] + */ + public function mkr(DateTimeInterface $from, DateTimeInterface $to): array + { + $soapResult = $this->soapClient->query( + 'MKR', + [ + 'fromDate' => $from->format(CbrfSoapService::DATE_TIME_FORMAT), + 'ToDate' => $to->format(CbrfSoapService::DATE_TIME_FORMAT), + ] + ); + + $list = DataHelper::array('mkr_base.MKR', $soapResult); + $callback = fn (array $item): Mkr => new Mkr($item); + + return array_map($callback, $list); + } + + /** + * Returns list of Reuters rates for all currencies for set date. + * + * @param DateTimeInterface $onDate + * + * @return ReutersCurrencyRate[] + * + * @throws CbrfException + */ + public function getReutersCursOnDate(DateTimeInterface $date): array + { + $enumSoapResults = $this->soapClient->query( + 'EnumReutersValutes', + [ + 'On_date' => $date->format(CbrfSoapService::DATE_TIME_FORMAT), + ] + ); + + $enumCurrencies = []; + foreach ($enumSoapResults['ReutersValutesList']['EnumRValutes'] as $enumSoapResult) { + $enumCurrencies[$enumSoapResult['num_code']] = $enumSoapResult; + } + + $soapValutesResults = $this->soapClient->query( + 'GetReutersCursOnDate', + [ + 'On_date' => $date->format(CbrfSoapService::DATE_TIME_FORMAT), + ] + ); + + foreach ($soapValutesResults['ReutersValutesData']['Currency'] as $soapValutesResult) { + $enumCurrencies[$soapValutesResult['num_code']] = array_merge($enumCurrencies[$soapValutesResult['num_code']], $soapValutesResult); + } + + $results = []; + $immutableDate = DataHelper::createImmutableDateTime($date); + + foreach ($enumCurrencies as $item) { + if (\is_array($item)) { + $results[] = new ReutersCurrencyRate($item, $immutableDate); + } + } + + return $results; + } +} diff --git a/src/CbrfException.php b/src/CbrfException.php new file mode 100644 index 0000000..c7c6105 --- /dev/null +++ b/src/CbrfException.php @@ -0,0 +1,12 @@ +client = $client; + } else { + $this->wsdl = $client; + } + } + + /** + * Makes a soap call. + * + * @param string $method + * @param array $params + * + * @return array + * + * @throws CbrfException + */ + public function query(string $method, array $params = []): array + { + try { + return $this->queryInternal($method, $params); + } catch (Throwable $e) { + $message = sprintf("Fail on '%s': '%s'.", $method, $e->getMessage()); + throw new CbrfException($message, 0, $e); + } + } + + /** + * Makes an internal soap call. + * + * @param string $method + * @param array $params + * + * @return array + */ + private function queryInternal(string $method, array $params = []): array + { + // need to do this because every params list are nested to parameters object + if (!empty($params)) { + $params = [$params]; + } + + $soapCallResult = $this->getSoapClient()->__soapCall($method, $params); + + $resName = $method . 'Result'; + if (!empty($soapCallResult->$resName->any)) { + $xml = simplexml_load_string( + $soapCallResult->$resName->any, + 'SimpleXMLElement', + \LIBXML_NOCDATA + ); + $parsedResult = $this->xml2array($xml); + } else { + $parsedResult = (array) $soapCallResult; + } + + return $parsedResult; + } + + /** + * Converts SimpleXMLElement to an associative array,. + * + * @param mixed $xmlObject + * + * @return array + */ + private function xml2array($xmlObject): array + { + $out = []; + + $xmlArray = (array) $xmlObject; + foreach ($xmlArray as $index => $node) { + if (\is_object($node) || \is_array($node)) { + $out[$index] = $this->xml2array($node); + } else { + $out[$index] = $node; + } + } + + return $out; + } + + /** + * Returns a SoapClient instance for soap requests. + * + * @return SoapClient + */ + private function getSoapClient() + { + if ($this->client !== null) { + return $this->client; + } + + $this->client = new SoapClient( + $this->wsdl, + [ + 'exception' => true, + ] + ); + + return $this->client; + } +} diff --git a/src/DataHelper.php b/src/DataHelper.php new file mode 100644 index 0000000..cab51de --- /dev/null +++ b/src/DataHelper.php @@ -0,0 +1,238 @@ +format(\DATE_ATOM), + $date->getTimezone() + ); + } else { + $immutableDate = new DateTimeImmutable($date); + } + } catch (Throwable $e) { + throw new CbrfException($e->getMessage(), 0, $e); + } + + return $immutableDate; + } + + /** + * Returns array from the set path. + * + * @param string $path + * @param mixed $data + * + * @return array + */ + public static function array(string $path, $data): array + { + $item = self::get($path, $data); + + if ($item === null) { + $item = []; + } elseif (!\is_array($item)) { + $message = sprintf("Can't find an array by '%s' path.", $path); + throw new CbrfException($message); + } + + return $item; + } + + /** + * Returns DateTimeInterface from the set path. + * + * @param string $path + * @param mixed $data + * + * @return DateTimeInterface + */ + public static function dateTime(string $path, $data): DateTimeInterface + { + $item = self::get($path, $data); + + if (!\is_string($item) || empty($item)) { + $message = sprintf("Can't find a date by '%s' path.", $path); + throw new CbrfException($message); + } + + return self::createImmutableDateTime($item); + } + + /** + * Returns string from the set path. + * + * @param string $path + * @param mixed $data + * @param string|null $default + * + * @return string + */ + public static function string(string $path, $data, ?string $default = null): string + { + $item = self::get($path, $data); + + if ($item === null) { + if ($default !== null) { + $return = $default; + } else { + $message = sprintf("Can't find a string by '%s' path.", $path); + throw new CbrfException($message); + } + } else { + $return = trim((string) $item); + } + + return $return; + } + + /** + * Returns float from the set path. + * + * @param string $path + * @param mixed $data + * @param float|null $default + * + * @return float + */ + public static function float(string $path, $data, ?float $default = null): float + { + $item = self::get($path, $data); + + if ($item === null) { + if ($default !== null) { + $return = $default; + } else { + $message = sprintf("Can't find a float by '%s' path.", $path); + throw new CbrfException($message); + } + } else { + $return = (float) trim((string) $item); + } + + return $return; + } + + /** + * Returns float from the set path or null is there is no data. + * + * @param string $path + * @param mixed $data + * + * @return float|null + */ + public static function floatOrNull(string $path, $data): ?float + { + $item = self::get($path, $data); + + if ($item === null) { + $return = null; + } else { + $return = (float) trim((string) $item); + } + + return $return; + } + + /** + * Returns int from the set path. + * + * @param string $path + * @param mixed $data + * @param int|null $default + * + * @return int + */ + public static function int(string $path, $data, ?int $default = null): int + { + $item = self::get($path, $data); + + if ($item === null) { + if ($default !== null) { + $return = $default; + } else { + $message = sprintf("Can't find an int by '%s' path.", $path); + throw new CbrfException($message); + } + } else { + $return = (int) trim((string) $item); + } + + return $return; + } + + /** + * Returns char code from the set path. + * + * @param string $path + * @param mixed $data + * @param string|null $default + * + * @return string + */ + public static function charCode(string $path, $data, ?string $default = null): string + { + $item = self::get($path, $data); + + if ($item === null) { + if ($default !== null) { + $return = $default; + } else { + $message = sprintf("Can't find a char code by '%s' path.", $path); + throw new CbrfException($message); + } + } else { + $return = trim((string) $item); + } + + return strtoupper($return); + } + + /** + * Returns data from the set path. + * + * @param string $path + * @param mixed $data + * + * @return mixed + */ + private static function get(string $path, $data) + { + $arPath = explode('.', trim($path, " \n\r\t\v\0.")); + + $item = $data; + foreach ($arPath as $chainItem) { + if (\is_array($item) && \array_key_exists($chainItem, $item)) { + $item = $item[$chainItem]; + } elseif (\is_object($item) && property_exists($item, $chainItem)) { + $item = $item->$chainItem; + } else { + $item = null; + break; + } + } + + return $item; + } +} diff --git a/src/Entity/Currency.php b/src/Entity/Currency.php new file mode 100644 index 0000000..95c7cc6 --- /dev/null +++ b/src/Entity/Currency.php @@ -0,0 +1,19 @@ +internalCode = DataHelper::string('Vcode', $item, ''); + $this->name = DataHelper::string('Vname', $item, ''); + $this->engName = DataHelper::string('VEngname', $item, ''); + $this->nom = DataHelper::int('Vnom', $item, 0); + $this->commonCode = DataHelper::string('VcommonCode', $item, ''); + $this->numericCode = DataHelper::int('VnumCode', $item, 0); + $this->charCode = DataHelper::charCode('VcharCode', $item, ''); + } + + public function getInternalCode(): string + { + return $this->internalCode; + } + + public function getName(): string + { + return $this->name; + } + + public function getEngName(): string + { + return $this->engName; + } + + public function getNom(): int + { + return $this->nom; + } + + public function getCommonCode(): string + { + return $this->commonCode; + } + + public function getNumericCode(): int + { + return $this->numericCode; + } + + public function getCharCode(): string + { + return $this->charCode; + } +} diff --git a/src/Entity/CurrencyRate.php b/src/Entity/CurrencyRate.php new file mode 100644 index 0000000..1a47336 --- /dev/null +++ b/src/Entity/CurrencyRate.php @@ -0,0 +1,66 @@ +charCode = DataHelper::charCode('VchCode', $item, ''); + $this->name = DataHelper::string('Vname', $item, ''); + $this->numericCode = DataHelper::int('Vcode', $item, 0); + $this->rate = DataHelper::float('Vcurs', $item, .0); + $this->nom = DataHelper::int('Vnom', $item, 0); + $this->date = $date; + } + + public function getCharCode(): string + { + return $this->charCode; + } + + public function getName(): string + { + return $this->name; + } + + public function getNumericCode(): int + { + return $this->numericCode; + } + + public function getRate(): float + { + return $this->rate; + } + + public function getNom(): int + { + return $this->nom; + } + + public function getDate(): DateTimeInterface + { + return $this->date; + } +} diff --git a/src/Entity/DepoRate.php b/src/Entity/DepoRate.php new file mode 100644 index 0000000..d43e9ca --- /dev/null +++ b/src/Entity/DepoRate.php @@ -0,0 +1,34 @@ +rate = DataHelper::float('Overnight', $item, .0); + $this->date = DataHelper::dateTime('DateDepo', $item); + } + + public function getRate(): float + { + return $this->rate; + } + + public function getDate(): DateTimeInterface + { + return $this->date; + } +} diff --git a/src/Entity/InternationalReserve.php b/src/Entity/InternationalReserve.php new file mode 100644 index 0000000..0f3d854 --- /dev/null +++ b/src/Entity/InternationalReserve.php @@ -0,0 +1,34 @@ +date = DataHelper::dateTime('D0', $item); + $this->value = DataHelper::float('p1', $item, .0); + } + + public function getValue(): float + { + return $this->value; + } + + public function getDate(): DateTimeInterface + { + return $this->date; + } +} diff --git a/src/Entity/InternationalReserveWeek.php b/src/Entity/InternationalReserveWeek.php new file mode 100644 index 0000000..98ebcd8 --- /dev/null +++ b/src/Entity/InternationalReserveWeek.php @@ -0,0 +1,34 @@ +date = DataHelper::dateTime('D0', $item); + $this->value = DataHelper::float('val', $item, .0); + } + + public function getValue(): float + { + return $this->value; + } + + public function getDate(): DateTimeInterface + { + return $this->date; + } +} diff --git a/src/Entity/KeyRate.php b/src/Entity/KeyRate.php new file mode 100644 index 0000000..ad87af2 --- /dev/null +++ b/src/Entity/KeyRate.php @@ -0,0 +1,34 @@ +date = DataHelper::dateTime('DT', $item); + $this->rate = DataHelper::float('Rate', $item, .0); + } + + public function getRate(): float + { + return $this->rate; + } + + public function getDate(): DateTimeInterface + { + return $this->date; + } +} diff --git a/src/Entity/Mkr.php b/src/Entity/Mkr.php new file mode 100644 index 0000000..55d66da --- /dev/null +++ b/src/Entity/Mkr.php @@ -0,0 +1,82 @@ +date = DataHelper::dateTime('CDate', $item); + $this->p1 = DataHelper::int('p1', $item, 0); + $this->d1 = DataHelper::floatOrNull('d1', $item); + $this->d7 = DataHelper::floatOrNull('d7', $item); + $this->d30 = DataHelper::floatOrNull('d30', $item); + $this->d90 = DataHelper::floatOrNull('d90', $item); + $this->d180 = DataHelper::floatOrNull('d180', $item); + $this->d360 = DataHelper::floatOrNull('d360', $item); + } + + public function getDate(): DateTimeInterface + { + return $this->date; + } + + public function getP1(): int + { + return $this->p1; + } + + public function getD1(): ?float + { + return $this->d1; + } + + public function getD7(): ?float + { + return $this->d7; + } + + public function getD30(): ?float + { + return $this->d30; + } + + public function getD90(): ?float + { + return $this->d90; + } + + public function getD180(): ?float + { + return $this->d180; + } + + public function getD360(): ?float + { + return $this->d360; + } +} diff --git a/src/Entity/OstatDepoRate.php b/src/Entity/OstatDepoRate.php new file mode 100644 index 0000000..756a986 --- /dev/null +++ b/src/Entity/OstatDepoRate.php @@ -0,0 +1,42 @@ +date = DataHelper::dateTime('D0', $item); + $this->days1to7 = DataHelper::float('D1_7', $item, .0); + $this->total = DataHelper::float('total', $item, .0); + } + + public function getDate(): DateTimeInterface + { + return $this->date; + } + + public function getDays1to7(): float + { + return $this->days1to7; + } + + public function getTotal(): float + { + return $this->total; + } +} diff --git a/src/Entity/OstatRate.php b/src/Entity/OstatRate.php new file mode 100644 index 0000000..2eeb6a7 --- /dev/null +++ b/src/Entity/OstatRate.php @@ -0,0 +1,46 @@ +>>>>>> origin/master + */ +class OstatRate +{ + private float $moscow = .0; + + private float $russia = .0; + + private DateTimeInterface $date; + + public function __construct(array $item) + { + $this->date = DataHelper::dateTime('DateOst', $item); + $this->moscow = DataHelper::float('InMoscow', $item, .0); + $this->russia = DataHelper::float('InRuss', $item, .0); + } + + public function getDate(): DateTimeInterface + { + return $this->date; + } + + public function getMoscow(): float + { + return $this->moscow; + } + + public function getRussia(): float + { + return $this->russia; + } +} diff --git a/src/Entity/PreciousMetalRate.php b/src/Entity/PreciousMetalRate.php new file mode 100644 index 0000000..e349bda --- /dev/null +++ b/src/Entity/PreciousMetalRate.php @@ -0,0 +1,47 @@ +date = DataHelper::dateTime('DateMet', $item); + $this->code = DataHelper::int('CodMet', $item, 0); + $this->price = DataHelper::float('price', $item, .0); + } + + public function getDate(): DateTimeInterface + { + return $this->date; + } + + public function getCode(): int + { + return $this->code; + } + + public function getPrice(): float + { + return $this->price; + } +} diff --git a/src/Entity/ReutersCurrencyRate.php b/src/Entity/ReutersCurrencyRate.php new file mode 100644 index 0000000..be038fa --- /dev/null +++ b/src/Entity/ReutersCurrencyRate.php @@ -0,0 +1,73 @@ +chCode = strtoupper(trim($item['char_code'] ?? '')); + $this->nameRu = trim($item['Title_ru'] ?? ''); + $this->nameEn = trim($item['Title_en'] ?? ''); + $this->code = (int) ($item['num_code'] ?? 0); + $this->curs = (float) ($item['val'] ?? .0); + $this->dir = (int) ($item['dir'] ?? 0); + $this->date = $date; + } + + public function getChCode(): string + { + return $this->chCode; + } + + public function getNameRu(): string + { + return $this->nameRu; + } + + public function getNameEn(): string + { + return $this->nameEn; + } + + public function getCode(): int + { + return $this->code; + } + + public function getCurs(): float + { + return $this->curs; + } + + public function getDir(): int + { + return $this->dir; + } + + public function getDate(): DateTimeInterface + { + return $this->date; + } +} diff --git a/src/Entity/RuoniaBid.php b/src/Entity/RuoniaBid.php new file mode 100644 index 0000000..9df3d08 --- /dev/null +++ b/src/Entity/RuoniaBid.php @@ -0,0 +1,50 @@ +date = DataHelper::dateTime('D0', $item); + $this->bid = DataHelper::float('ruo', $item, .0); + $this->dealsVolume = DataHelper::float('vol', $item, .0); + $this->dateUpdate = DataHelper::dateTime('DateUpdate', $item); + } + + public function getDate(): DateTimeInterface + { + return $this->date; + } + + public function getBid(): float + { + return $this->bid; + } + + public function getDealsVolume(): float + { + return $this->dealsVolume; + } + + public function getDateUpdate(): DateTimeInterface + { + return $this->dateUpdate; + } +} diff --git a/src/Entity/RuoniaIndex.php b/src/Entity/RuoniaIndex.php new file mode 100644 index 0000000..f05f9c3 --- /dev/null +++ b/src/Entity/RuoniaIndex.php @@ -0,0 +1,58 @@ +date = DataHelper::dateTime('DT', $item); + $this->index = DataHelper::float('RUONIA_Index', $item, .0); + $this->average1Month = DataHelper::float('RUONIA_AVG_1M', $item, .0); + $this->average3Month = DataHelper::float('RUONIA_AVG_3M', $item, .0); + $this->average6Month = DataHelper::float('RUONIA_AVG_6M', $item, .0); + } + + public function getDate(): DateTimeInterface + { + return $this->date; + } + + public function getIndex(): float + { + return $this->index; + } + + public function getAverage1Month(): float + { + return $this->average1Month; + } + + public function getAverage3Month(): float + { + return $this->average3Month; + } + + public function getAverage6Month(): float + { + return $this->average6Month; + } +} diff --git a/src/Entity/Saldo.php b/src/Entity/Saldo.php new file mode 100644 index 0000000..5cfe5b6 --- /dev/null +++ b/src/Entity/Saldo.php @@ -0,0 +1,34 @@ +date = DataHelper::dateTime('Dt', $item); + $this->value = DataHelper::float('DEADLINEBS', $item, .0); + } + + public function getDate(): DateTimeInterface + { + return $this->date; + } + + public function getValue(): float + { + return $this->value; + } +} diff --git a/src/Entity/SwapRate.php b/src/Entity/SwapRate.php new file mode 100644 index 0000000..22f39fc --- /dev/null +++ b/src/Entity/SwapRate.php @@ -0,0 +1,66 @@ +dateBuy = DataHelper::dateTime('DateBuy', $item); + $this->dateSell = DataHelper::dateTime('DateSell', $item); + $this->baseRate = DataHelper::float('BaseRate', $item, .0); + $this->tir = DataHelper::float('TIR', $item, .0); + $this->rate = DataHelper::float('Stavka', $item, .0); + $this->currency = DataHelper::int('Currency', $item, 0); + } + + public function getDateBuy(): DateTimeInterface + { + return $this->dateBuy; + } + + public function getDateSell(): DateTimeInterface + { + return $this->dateSell; + } + + public function getBaseRate(): float + { + return $this->baseRate; + } + + public function getTIR(): float + { + return $this->tir; + } + + public function getRate(): float + { + return $this->rate; + } + + public function getCurrency(): int + { + return $this->currency; + } +} diff --git a/tests/BaseTestCase.php b/tests/BaseTestCase.php new file mode 100644 index 0000000..62e941a --- /dev/null +++ b/tests/BaseTestCase.php @@ -0,0 +1,169 @@ +assertSame( + $date1->getTimestamp(), + $date2->getTimestamp(), + "Date objects don't contain same dates" + ); + } + + /** + * @param string $method + * @param array|null $params + * @param mixed $result + * + * @return SoapClient + */ + protected function createSoapCallMock(string $method, ?array $params, $result = null): SoapClient + { + /** @var MockObject&SoapClient */ + $soapClient = $this->getMockBuilder(SoapClient::class) + ->disableOriginalConstructor() + ->getMock(); + + if ($params === null) { + $soapClient->expects($this->once()) + ->method('__soapCall') + ->with( + $this->identicalTo($method) + ) + ->willReturn($result) + ; + } else { + $soapClient->expects($this->once()) + ->method('__soapCall') + ->with( + $this->identicalTo($method), + $this->identicalTo([$params]) + ) + ->willReturn($result) + ; + } + + return $soapClient; + } + + /** + * Creates full fixture. + * + * @param array $schema + * + * @return array + */ + protected function createFixture(array $schema): array + { + $data = $this->createFixtureData($schema['schema'] ?? []); + $response = $this->createFixtureResponse($schema['path'] ?? '', $data); + + return [$data, $response]; + } + + /** + * Cretaes array of fixtures by schema. + * + * @param array $schema + * @param int $count + * + * @return array> + */ + protected function createFixtureData(array $schema, int $count = 4): array + { + $data = []; + for ($i = 0; $i < $count; ++$i) { + $datum = []; + foreach ($schema as $name => $type) { + switch ($type) { + case self::FIXTURE_TYPE_STRING: + $value = "{$name}_{$i}"; + break; + case self::FIXTURE_TYPE_FLOAT: + $value = (float) (mt_rand(101, 999) / 100); + break; + case self::FIXTURE_TYPE_INT: + $value = $i * 10 + mt_rand(0, 9); + break; + case self::FIXTURE_TYPE_DATE: + $value = '2010-10-1' . mt_rand(0, 9); + break; + default: + $message = sprintf("Can't recognize field type '%s'.", $type); + throw new RuntimeException($message); + } + $datum[$name] = $value; + } + $data[] = $datum; + } + + return $data; + } + + /** + * Returns fixture allowed for xml response. + * + * @param string $xmlPath + * @param array> $data + * + * @return object + */ + protected function createFixtureResponse(string $xmlPath, array $data): object + { + $arPath = explode('.any.', $xmlPath); + if (\count($arPath) !== 2 || empty($arPath[0]) || empty($arPath[1])) { + $message = sprintf("Incorrect XML path '%s'.", $xmlPath); + throw new RuntimeException($message); + } + [$beforeAny, $afterAny] = $arPath; + + $arAfterAny = explode('.', $afterAny); + $lastItem = array_pop($arAfterAny); + $any = ''; + foreach ($arAfterAny as $nodeName) { + $any .= "<{$nodeName} xmlns=\"\">"; + } + foreach ($data as $datum) { + $any .= "<{$lastItem} xmlns=\"\">"; + foreach ($datum as $key => $value) { + $any .= "<{$key}>{$value}"; + } + $any .= ""; + } + foreach (array_reverse($arAfterAny) as $nodeName) { + $any .= ""; + } + $any .= ''; + + $arBeforeAny = explode('.', $beforeAny); + $latest = $soapResponse = new stdClass(); + foreach ($arBeforeAny as $objectNode) { + $newNode = new stdClass(); + $latest->{$objectNode} = $newNode; + $latest = $newNode; + } + $latest->any = $any; + + return $soapResponse; + } +} diff --git a/tests/CbrfDailyTest.php b/tests/CbrfDailyTest.php new file mode 100644 index 0000000..711944c --- /dev/null +++ b/tests/CbrfDailyTest.php @@ -0,0 +1,822 @@ + [ + 'schema' => [ + 'VchCode' => self::FIXTURE_TYPE_STRING, + 'Vname' => self::FIXTURE_TYPE_STRING, + 'Vcode' => self::FIXTURE_TYPE_INT, + 'Vcurs' => self::FIXTURE_TYPE_FLOAT, + 'Vnom' => self::FIXTURE_TYPE_INT, + ], + 'path' => 'GetCursOnDateResult.any.ValuteData.ValuteCursOnDate', + ], + 'EnumValutes' => [ + 'schema' => [ + 'Vcode' => self::FIXTURE_TYPE_STRING, + 'Vname' => self::FIXTURE_TYPE_STRING, + 'VEngname' => self::FIXTURE_TYPE_STRING, + 'Vnom' => self::FIXTURE_TYPE_INT, + 'VcommonCode' => self::FIXTURE_TYPE_STRING, + 'VnumCode' => self::FIXTURE_TYPE_INT, + 'VcharCode' => self::FIXTURE_TYPE_STRING, + ], + 'path' => 'EnumValutesResult.any.ValuteData.EnumValutes', + ], + 'CursDynamic' => [ + 'schema' => [ + 'CursDate' => self::FIXTURE_TYPE_DATE, + 'Vcurs' => self::FIXTURE_TYPE_FLOAT, + 'Vcode' => self::FIXTURE_TYPE_STRING, + 'Vnom' => self::FIXTURE_TYPE_INT, + ], + 'path' => 'GetCursDynamicResult.any.ValuteData.ValuteCursDynamic', + ], + 'KeyRate' => [ + 'schema' => [ + 'DT' => self::FIXTURE_TYPE_DATE, + 'Rate' => self::FIXTURE_TYPE_FLOAT, + ], + 'path' => 'KeyRateResult.any.KeyRate.KR', + ], + 'DragMetDynamic' => [ + 'schema' => [ + 'DateMet' => self::FIXTURE_TYPE_DATE, + 'price' => self::FIXTURE_TYPE_FLOAT, + 'CodMet' => self::FIXTURE_TYPE_INT, + ], + 'path' => 'DragMetDynamicResult.any.DragMetall.DrgMet', + ], + 'SwapDynamic' => [ + 'schema' => [ + 'DateBuy' => self::FIXTURE_TYPE_DATE, + 'DateSell' => self::FIXTURE_TYPE_DATE, + 'BaseRate' => self::FIXTURE_TYPE_FLOAT, + 'TIR' => self::FIXTURE_TYPE_FLOAT, + 'Stavka' => self::FIXTURE_TYPE_FLOAT, + 'Currency' => self::FIXTURE_TYPE_INT, + ], + 'path' => 'SwapDynamicResult.any.SwapDynamic.Swap', + ], + 'DepoDynamic' => [ + 'schema' => [ + 'Overnight' => self::FIXTURE_TYPE_FLOAT, + 'DateDepo' => self::FIXTURE_TYPE_DATE, + ], + 'path' => 'DepoDynamicResult.any.DepoDynamic.Depo', + ], + 'OstatDynamic' => [ + 'schema' => [ + 'DateOst' => self::FIXTURE_TYPE_DATE, + 'InMoscow' => self::FIXTURE_TYPE_FLOAT, + 'InRuss' => self::FIXTURE_TYPE_FLOAT, + ], + 'path' => 'OstatDynamicResult.any.OstatDynamic.Ostat', + ], + 'OstatDepo' => [ + 'schema' => [ + 'D0' => self::FIXTURE_TYPE_DATE, + 'D1_7' => self::FIXTURE_TYPE_FLOAT, + 'total' => self::FIXTURE_TYPE_FLOAT, + ], + 'path' => 'OstatDepoResult.any.OD.odr', + ], + 'Mrrf' => [ + 'schema' => [ + 'D0' => self::FIXTURE_TYPE_DATE, + 'p1' => self::FIXTURE_TYPE_FLOAT, + ], + 'path' => 'mrrfResult.any.mmrf.mr', + ], + 'Mrrf7D' => [ + 'schema' => [ + 'D0' => self::FIXTURE_TYPE_DATE, + 'val' => self::FIXTURE_TYPE_FLOAT, + ], + 'path' => 'mrrf7DResult.any.mmrf7d.mr', + ], + 'Saldo' => [ + 'schema' => [ + 'Dt' => self::FIXTURE_TYPE_DATE, + 'DEADLINEBS' => self::FIXTURE_TYPE_FLOAT, + ], + 'path' => 'SaldoResult.any.Saldo.So', + ], + 'RuoniaSV' => [ + 'schema' => [ + 'DT' => self::FIXTURE_TYPE_DATE, + 'RUONIA_Index' => self::FIXTURE_TYPE_FLOAT, + 'RUONIA_AVG_1M' => self::FIXTURE_TYPE_FLOAT, + 'RUONIA_AVG_3M' => self::FIXTURE_TYPE_FLOAT, + 'RUONIA_AVG_6M' => self::FIXTURE_TYPE_FLOAT, + ], + 'path' => 'RuoniaSVResult.any.RuoniaSV.ra', + ], + 'Ruonia' => [ + 'schema' => [ + 'D0' => self::FIXTURE_TYPE_DATE, + 'ruo' => self::FIXTURE_TYPE_FLOAT, + 'vol' => self::FIXTURE_TYPE_FLOAT, + 'DateUpdate' => self::FIXTURE_TYPE_DATE, + ], + 'path' => 'RuoniaResult.any.Ruonia.ro', + ], + 'MKR' => [ + 'schema' => [ + 'CDate' => self::FIXTURE_TYPE_DATE, + 'p1' => self::FIXTURE_TYPE_INT, + 'd1' => self::FIXTURE_TYPE_FLOAT, + 'd7' => self::FIXTURE_TYPE_FLOAT, + 'd30' => self::FIXTURE_TYPE_FLOAT, + 'd90' => self::FIXTURE_TYPE_FLOAT, + 'd180' => self::FIXTURE_TYPE_FLOAT, + 'd360' => self::FIXTURE_TYPE_FLOAT, + ], + 'path' => 'MKRResult.any.mkr_base.MKR', + ], + ]; + + /** + * @test + */ + public function testGetCursOnDate(): void + { + [$courses, $response] = $this->createFixture(self::FIXTURES['CurrencyRate']); + $onDate = new DateTimeImmutable(); + + $soapClient = $this->createSoapCallMock( + 'GetCursOnDate', + [ + 'On_date' => $onDate->format(CbrfSoapService::DATE_TIME_FORMAT), + ], + $response + ); + + $service = new CbrfDaily($soapClient); + $list = $service->getCursOnDate($onDate); + + $this->assertCount(\count($courses), $list); + $this->assertContainsOnlyInstancesOf(CurrencyRate::class, $list); + foreach ($courses as $key => $course) { + $this->assertSame(strtoupper($course['VchCode']), $list[$key]->getCharCode()); + $this->assertSame($course['Vname'], $list[$key]->getName()); + $this->assertSame($course['Vcode'], $list[$key]->getNumericCode()); + $this->assertSame($course['Vcurs'], $list[$key]->getRate()); + $this->assertSame($course['Vnom'], $list[$key]->getNom()); + $this->assertSameDate($onDate, $list[$key]->getDate()); + } + } + + /** + * @test + */ + public function testGetCursOnDateByCharCode(): void + { + [$courses, $response] = $this->createFixture(self::FIXTURES['CurrencyRate']); + $charCode = $courses[0]['VchCode'] ?? ''; + $onDate = new DateTimeImmutable(); + + $soapClient = $this->createSoapCallMock( + 'GetCursOnDate', + [ + 'On_date' => $onDate->format(CbrfSoapService::DATE_TIME_FORMAT), + ], + $response + ); + + $service = new CbrfDaily($soapClient); + $item = $service->getCursOnDateByCharCode($onDate, $charCode); + + $this->assertInstanceOf(CurrencyRate::class, $item); + $this->assertSame(strtoupper($charCode), $item->getCharCode()); + } + + /** + * @test + */ + public function testGetCursOnDateByNumericCode(): void + { + [$courses, $response] = $this->createFixture(self::FIXTURES['CurrencyRate']); + $numericCode = $courses[0]['Vcode'] ?? 0; + $onDate = new DateTimeImmutable(); + + $soapClient = $this->createSoapCallMock( + 'GetCursOnDate', + [ + 'On_date' => $onDate->format(CbrfSoapService::DATE_TIME_FORMAT), + ], + $response + ); + + $service = new CbrfDaily($soapClient); + $item = $service->getCursOnDateByNumericCode($onDate, $numericCode); + + $this->assertInstanceOf(CurrencyRate::class, $item); + $this->assertSame($numericCode, $item->getNumericCode()); + } + + /** + * @test + */ + public function testEnumValutes(): void + { + [$currencies, $response] = $this->createFixture(self::FIXTURES['EnumValutes']); + $seld = false; + + $soapClient = $this->createSoapCallMock( + 'EnumValutes', + [ + 'Seld' => $seld, + ], + $response + ); + + $service = new CbrfDaily($soapClient); + $list = $service->enumValutes($seld); + + $this->assertCount(\count($currencies), $list); + $this->assertContainsOnlyInstancesOf(CurrencyEnum::class, $list); + foreach ($currencies as $key => $currency) { + $this->assertSame(strtoupper($currency['VcharCode']), $list[$key]->getCharCode()); + $this->assertSame($currency['Vname'], $list[$key]->getName()); + $this->assertSame($currency['Vcode'], $list[$key]->getInternalCode()); + $this->assertSame($currency['VEngname'], $list[$key]->getEngName()); + $this->assertSame($currency['Vnom'], $list[$key]->getNom()); + $this->assertSame($currency['VnumCode'], $list[$key]->getNumericCode()); + $this->assertSame($currency['VcommonCode'], $list[$key]->getCommonCode()); + } + } + + /** + * @test + */ + public function testEnumValuteByCharCode(): void + { + [$courses, $response] = $this->createFixture(self::FIXTURES['EnumValutes']); + $charCode = $courses[0]['VcharCode'] ?? ''; + $seld = false; + + $soapClient = $this->createSoapCallMock( + 'EnumValutes', + [ + 'Seld' => $seld, + ], + $response + ); + + $service = new CbrfDaily($soapClient); + $item = $service->enumValuteByCharCode($charCode, $seld); + + $this->assertInstanceOf(CurrencyEnum::class, $item); + $this->assertSame(strtoupper($charCode), $item->getCharCode()); + } + + /** + * @test + */ + public function testEnumValuteByNumericCode(): void + { + [$courses, $response] = $this->createFixture(self::FIXTURES['EnumValutes']); + $numericCode = $courses[0]['VnumCode'] ?? 0; + $seld = false; + + $soapClient = $this->createSoapCallMock( + 'EnumValutes', + [ + 'Seld' => $seld, + ], + $response + ); + + $service = new CbrfDaily($soapClient); + $item = $service->enumValuteByNumericCode($numericCode, $seld); + + $this->assertInstanceOf(CurrencyEnum::class, $item); + $this->assertSame($numericCode, $item->getNumericCode()); + } + + /** + * @test + */ + public function testGetLatestDateTime(): void + { + $date = new DateTimeImmutable(); + $response = new stdClass(); + $response->GetLatestDateTimeResult = $date->format(CbrfSoapService::DATE_TIME_FORMAT); + + $soapClient = $this->createSoapCallMock( + 'GetLatestDateTime', + null, + $response + ); + + $service = new CbrfDaily($soapClient); + $testDate = $service->getLatestDateTime(); + + $this->assertSameDate($date, $testDate); + } + + /** + * @test + */ + public function testGetLatestDateTimeSeld(): void + { + $date = new DateTimeImmutable(); + $response = new stdClass(); + $response->GetLatestDateTimeSeldResult = $date->format(CbrfSoapService::DATE_TIME_FORMAT); + + $soapClient = $this->createSoapCallMock( + 'GetLatestDateTimeSeld', + null, + $response + ); + + $service = new CbrfDaily($soapClient); + $testDate = $service->getLatestDateTimeSeld(); + + $this->assertSameDate($date, $testDate); + } + + /** + * @test + */ + public function testGetLatestDate(): void + { + $date = new DateTimeImmutable(); + $response = new stdClass(); + $response->GetLatestDateResult = $date->format(CbrfSoapService::DATE_TIME_FORMAT); + + $soapClient = $this->createSoapCallMock( + 'GetLatestDate', + null, + $response + ); + + $service = new CbrfDaily($soapClient); + $testDate = $service->getLatestDate(); + + $this->assertSameDate($date, $testDate); + } + + /** + * @test + */ + public function testGetLatestDateSeld(): void + { + $date = new DateTimeImmutable(); + $response = new stdClass(); + $response->GetLatestDateSeldResult = $date->format(CbrfSoapService::DATE_TIME_FORMAT); + + $soapClient = $this->createSoapCallMock( + 'GetLatestDateSeld', + null, + $response + ); + + $service = new CbrfDaily($soapClient); + $testDate = $service->getLatestDateSeld(); + + $this->assertSameDate($date, $testDate); + } + + /** + * @test + */ + public function testGetCursDynamic(): void + { + [$currencies, $response] = $this->createFixture(self::FIXTURES['CursDynamic']); + $from = new DateTimeImmutable('-1 month'); + $to = new DateTimeImmutable(); + $charCode = 'EUR'; + $numericCode = 978; + $name = 'Euro'; + $internalCode = 'test01'; + + /** @var MockObject&CurrencyEnum */ + $currencyEnum = $this->getMockBuilder(CurrencyEnum::class) + ->disableOriginalConstructor() + ->getMock(); + $currencyEnum->method('getInternalCode')->willReturn($internalCode); + $currencyEnum->method('getName')->willReturn($name); + $currencyEnum->method('getCharCode')->willReturn($charCode); + $currencyEnum->method('getNumericCode')->willReturn($numericCode); + + $soapClient = $this->createSoapCallMock( + 'GetCursDynamic', + [ + 'FromDate' => $from->format(CbrfSoapService::DATE_TIME_FORMAT), + 'ToDate' => $to->format(CbrfSoapService::DATE_TIME_FORMAT), + 'ValutaCode' => $internalCode, + ], + $response + ); + + $service = new CbrfDaily($soapClient); + $list = $service->getCursDynamic($from, $to, $currencyEnum); + + $this->assertCount(\count($currencies), $list); + $this->assertContainsOnlyInstancesOf(CurrencyRate::class, $list); + foreach ($currencies as $key => $currency) { + $this->assertSame($charCode, $list[$key]->getCharCode()); + $this->assertSame($name, $list[$key]->getName()); + $this->assertSame($numericCode, $list[$key]->getNumericCode()); + $this->assertSame($currency['Vcurs'], $list[$key]->getRate()); + $this->assertSame($currency['Vnom'], $list[$key]->getNom()); + $this->assertSameDate(new DateTimeImmutable($currency['CursDate']), $list[$key]->getDate()); + } + } + + /** + * @test + */ + public function testKeyRate(): void + { + [$rates, $response] = $this->createFixture(self::FIXTURES['KeyRate']); + $from = new DateTimeImmutable('-1 month'); + $to = new DateTimeImmutable(); + + $soapClient = $this->createSoapCallMock( + 'KeyRate', + [ + 'fromDate' => $from->format(CbrfSoapService::DATE_TIME_FORMAT), + 'ToDate' => $to->format(CbrfSoapService::DATE_TIME_FORMAT), + ], + $response + ); + + $service = new CbrfDaily($soapClient); + $list = $service->keyRate($from, $to); + + $this->assertCount(\count($rates), $list); + $this->assertContainsOnlyInstancesOf(KeyRate::class, $list); + foreach ($rates as $key => $rate) { + $this->assertSameDate(new DateTimeImmutable($rate['DT']), $list[$key]->getDate()); + $this->assertSame($rate['Rate'], $list[$key]->getRate()); + } + } + + /** + * @test + */ + public function testDragMetDynamic(): void + { + [$metals, $response] = $this->createFixture(self::FIXTURES['DragMetDynamic']); + $from = new DateTimeImmutable('-1 month'); + $to = new DateTimeImmutable(); + + $soapClient = $this->createSoapCallMock( + 'DragMetDynamic', + [ + 'fromDate' => $from->format(CbrfSoapService::DATE_TIME_FORMAT), + 'ToDate' => $to->format(CbrfSoapService::DATE_TIME_FORMAT), + ], + $response + ); + + $service = new CbrfDaily($soapClient); + $list = $service->dragMetDynamic($from, $to); + + $this->assertCount(\count($metals), $list); + $this->assertContainsOnlyInstancesOf(PreciousMetalRate::class, $list); + foreach ($metals as $key => $metal) { + $this->assertSameDate(new DateTimeImmutable($metal['DateMet']), $list[$key]->getDate()); + $this->assertSame($metal['CodMet'], $list[$key]->getCode()); + $this->assertSame($metal['price'], $list[$key]->getPrice()); + } + } + + /** + * @test + */ + public function testSwapDynamic(): void + { + [$swaps, $response] = $this->createFixture(self::FIXTURES['SwapDynamic']); + $from = new DateTimeImmutable('-1 month'); + $to = new DateTimeImmutable(); + + $soapClient = $this->createSoapCallMock( + 'SwapDynamic', + [ + 'fromDate' => $from->format(CbrfSoapService::DATE_TIME_FORMAT), + 'ToDate' => $to->format(CbrfSoapService::DATE_TIME_FORMAT), + ], + $response + ); + + $service = new CbrfDaily($soapClient); + $list = $service->swapDynamic($from, $to); + + $this->assertCount(\count($swaps), $list); + $this->assertContainsOnlyInstancesOf(SwapRate::class, $list); + foreach ($swaps as $key => $swap) { + $this->assertSameDate(new DateTimeImmutable($swap['DateBuy']), $list[$key]->getDateBuy()); + $this->assertSameDate(new DateTimeImmutable($swap['DateSell']), $list[$key]->getDateSell()); + $this->assertSame($swap['BaseRate'], $list[$key]->getBaseRate()); + $this->assertSame($swap['TIR'], $list[$key]->getTIR()); + $this->assertSame($swap['Stavka'], $list[$key]->getRate()); + $this->assertSame($swap['Currency'], $list[$key]->getCurrency()); + } + } + + /** + * @test + */ + public function testDepoDynamic(): void + { + [$depos, $response] = $this->createFixture(self::FIXTURES['DepoDynamic']); + $from = new DateTimeImmutable('-1 month'); + $to = new DateTimeImmutable(); + + $soapClient = $this->createSoapCallMock( + 'DepoDynamic', + [ + 'fromDate' => $from->format(CbrfSoapService::DATE_TIME_FORMAT), + 'ToDate' => $to->format(CbrfSoapService::DATE_TIME_FORMAT), + ], + $response + ); + + $service = new CbrfDaily($soapClient); + $list = $service->depoDynamic($from, $to); + + $this->assertCount(\count($depos), $list); + $this->assertContainsOnlyInstancesOf(DepoRate::class, $list); + foreach ($depos as $key => $depo) { + $this->assertSameDate(new DateTimeImmutable($depo['DateDepo']), $list[$key]->getDate()); + $this->assertSame($depo['Overnight'], $list[$key]->getRate()); + } + } + + /** + * @test + */ + public function testOstatDynamic(): void + { + [$depos, $response] = $this->createFixture(self::FIXTURES['OstatDynamic']); + $from = new DateTimeImmutable('-1 month'); + $to = new DateTimeImmutable(); + + $soapClient = $this->createSoapCallMock( + 'OstatDynamic', + [ + 'fromDate' => $from->format(CbrfSoapService::DATE_TIME_FORMAT), + 'ToDate' => $to->format(CbrfSoapService::DATE_TIME_FORMAT), + ], + $response + ); + + $service = new CbrfDaily($soapClient); + $list = $service->ostatDynamic($from, $to); + + $this->assertCount(\count($depos), $list); + $this->assertContainsOnlyInstancesOf(OstatRate::class, $list); + foreach ($depos as $key => $ostat) { + $this->assertSameDate(new DateTimeImmutable($ostat['DateOst']), $list[$key]->getDate()); + $this->assertSame($ostat['InMoscow'], $list[$key]->getMoscow()); + $this->assertSame($ostat['InRuss'], $list[$key]->getRussia()); + } + } + + /** + * @test + */ + public function testOstatDepo(): void + { + [$depos, $response] = $this->createFixture(self::FIXTURES['OstatDepo']); + $from = new DateTimeImmutable('-1 month'); + $to = new DateTimeImmutable(); + + $soapClient = $this->createSoapCallMock( + 'OstatDepo', + [ + 'fromDate' => $from->format(CbrfSoapService::DATE_TIME_FORMAT), + 'ToDate' => $to->format(CbrfSoapService::DATE_TIME_FORMAT), + ], + $response + ); + + $service = new CbrfDaily($soapClient); + $list = $service->ostatDepo($from, $to); + + $this->assertCount(\count($depos), $list); + $this->assertContainsOnlyInstancesOf(OstatDepoRate::class, $list); + foreach ($depos as $key => $ostat) { + $this->assertSameDate(new DateTimeImmutable($ostat['D0']), $list[$key]->getDate()); + $this->assertSame($ostat['D1_7'], $list[$key]->getDays1to7()); + $this->assertSame($ostat['total'], $list[$key]->getTotal()); + } + } + + /** + * @test + */ + public function testMrrf(): void + { + [$mrrfs, $response] = $this->createFixture(self::FIXTURES['Mrrf']); + $from = new DateTimeImmutable('-1 month'); + $to = new DateTimeImmutable(); + + $soapClient = $this->createSoapCallMock( + 'mrrf', + [ + 'fromDate' => $from->format(CbrfSoapService::DATE_TIME_FORMAT), + 'ToDate' => $to->format(CbrfSoapService::DATE_TIME_FORMAT), + ], + $response + ); + + $service = new CbrfDaily($soapClient); + $list = $service->mrrf($from, $to); + + $this->assertCount(\count($mrrfs), $list); + $this->assertContainsOnlyInstancesOf(InternationalReserve::class, $list); + foreach ($mrrfs as $key => $mrrf) { + $this->assertSameDate(new DateTimeImmutable($mrrf['D0']), $list[$key]->getDate()); + $this->assertSame($mrrf['p1'], $list[$key]->getValue()); + } + } + + /** + * @test + */ + public function testMrrf7d(): void + { + [$mrrfs, $response] = $this->createFixture(self::FIXTURES['Mrrf7D']); + $from = new DateTimeImmutable('-1 month'); + $to = new DateTimeImmutable(); + + $soapClient = $this->createSoapCallMock( + 'mrrf7D', + [ + 'fromDate' => $from->format(CbrfSoapService::DATE_TIME_FORMAT), + 'ToDate' => $to->format(CbrfSoapService::DATE_TIME_FORMAT), + ], + $response + ); + + $service = new CbrfDaily($soapClient); + $list = $service->mrrf7d($from, $to); + + $this->assertCount(\count($mrrfs), $list); + $this->assertContainsOnlyInstancesOf(InternationalReserveWeek::class, $list); + foreach ($mrrfs as $key => $mrrf) { + $this->assertSameDate(new DateTimeImmutable($mrrf['D0']), $list[$key]->getDate()); + $this->assertSame($mrrf['val'], $list[$key]->getValue()); + } + } + + /** + * @test + */ + public function testSaldo(): void + { + [$saldos, $response] = $this->createFixture(self::FIXTURES['Saldo']); + $from = new DateTimeImmutable('-1 month'); + $to = new DateTimeImmutable(); + + $soapClient = $this->createSoapCallMock( + 'Saldo', + [ + 'fromDate' => $from->format(CbrfSoapService::DATE_TIME_FORMAT), + 'ToDate' => $to->format(CbrfSoapService::DATE_TIME_FORMAT), + ], + $response + ); + + $service = new CbrfDaily($soapClient); + $list = $service->saldo($from, $to); + + $this->assertCount(\count($saldos), $list); + $this->assertContainsOnlyInstancesOf(Saldo::class, $list); + foreach ($saldos as $key => $saldo) { + $this->assertSameDate(new DateTimeImmutable($saldo['Dt']), $list[$key]->getDate()); + $this->assertSame($saldo['DEADLINEBS'], $list[$key]->getValue()); + } + } + + /** + * @test + */ + public function testRuoniaSV(): void + { + [$ruoniaIndexes, $response] = $this->createFixture(self::FIXTURES['RuoniaSV']); + $from = new DateTimeImmutable('-1 month'); + $to = new DateTimeImmutable(); + + $soapClient = $this->createSoapCallMock( + 'RuoniaSV', + [ + 'fromDate' => $from->format(CbrfSoapService::DATE_TIME_FORMAT), + 'ToDate' => $to->format(CbrfSoapService::DATE_TIME_FORMAT), + ], + $response + ); + + $service = new CbrfDaily($soapClient); + $list = $service->ruoniaSV($from, $to); + + $this->assertCount(\count($ruoniaIndexes), $list); + $this->assertContainsOnlyInstancesOf(RuoniaIndex::class, $list); + foreach ($ruoniaIndexes as $key => $ruoniaIndex) { + $this->assertSameDate(new DateTimeImmutable($ruoniaIndex['DT']), $list[$key]->getDate()); + $this->assertSame($ruoniaIndex['RUONIA_Index'], $list[$key]->getIndex()); + $this->assertSame($ruoniaIndex['RUONIA_AVG_1M'], $list[$key]->getAverage1Month()); + $this->assertSame($ruoniaIndex['RUONIA_AVG_3M'], $list[$key]->getAverage3Month()); + $this->assertSame($ruoniaIndex['RUONIA_AVG_6M'], $list[$key]->getAverage6Month()); + } + } + + /** + * @test + */ + public function testMKR(): void + { + [$mkrs, $response] = $this->createFixture(self::FIXTURES['MKR']); + $from = new DateTimeImmutable('-1 month'); + $to = new DateTimeImmutable(); + + $soapClient = $this->createSoapCallMock( + 'MKR', + [ + 'fromDate' => $from->format(CbrfSoapService::DATE_TIME_FORMAT), + 'ToDate' => $to->format(CbrfSoapService::DATE_TIME_FORMAT), + ], + $response + ); + + $service = new CbrfDaily($soapClient); + $list = $service->mkr($from, $to); + + $this->assertCount(\count($mkrs), $list); + $this->assertContainsOnlyInstancesOf(Mkr::class, $list); + foreach ($mkrs as $key => $mkr) { + $this->assertSameDate(new DateTimeImmutable($mkr['CDate']), $list[$key]->getDate()); + $this->assertSame($mkr['p1'], $list[$key]->getP1()); + $this->assertSame($mkr['d1'], $list[$key]->getD1()); + $this->assertSame($mkr['d7'], $list[$key]->getD7()); + $this->assertSame($mkr['d30'], $list[$key]->getD30()); + $this->assertSame($mkr['d180'], $list[$key]->getD180()); + $this->assertSame($mkr['d360'], $list[$key]->getD360()); + } + } + + /** + * @test + */ + public function testRuonia(): void + { + [$ruoniaBids, $response] = $this->createFixture(self::FIXTURES['Ruonia']); + $from = new DateTimeImmutable('-1 month'); + $to = new DateTimeImmutable(); + + $soapClient = $this->createSoapCallMock( + 'Ruonia', + [ + 'fromDate' => $from->format(CbrfSoapService::DATE_TIME_FORMAT), + 'ToDate' => $to->format(CbrfSoapService::DATE_TIME_FORMAT), + ], + $response + ); + + $service = new CbrfDaily($soapClient); + $list = $service->ruonia($from, $to); + + $this->assertCount(\count($ruoniaBids), $list); + $this->assertContainsOnlyInstancesOf(RuoniaBid::class, $list); + foreach ($ruoniaBids as $key => $ruoniaBid) { + $this->assertSameDate(new DateTimeImmutable($ruoniaBid['D0']), $list[$key]->getDate()); + $this->assertSame($ruoniaBid['ruo'], $list[$key]->getBid()); + $this->assertSame($ruoniaBid['vol'], $list[$key]->getDealsVolume()); + $this->assertSameDate(new DateTimeImmutable($ruoniaBid['DateUpdate']), $list[$key]->getDateUpdate()); + } + } +} diff --git a/tests/CbrfSoapServiceTest.php b/tests/CbrfSoapServiceTest.php new file mode 100644 index 0000000..e5b6226 --- /dev/null +++ b/tests/CbrfSoapServiceTest.php @@ -0,0 +1,94 @@ +getMockBuilder(SoapClient::class) + ->disableOriginalConstructor() + ->getMock(); + $soapClient->method('__soapCall') + ->with( + $this->identicalTo($method), + $this->identicalTo([$params]) + ) + ->willReturn($responseXml); + + $service = new CbrfSoapService($soapClient); + $testResponse = $service->query($method, $params); + + $this->assertSame($response, $testResponse); + } + + public function queryProvider(): array + { + $responseXml = new stdClass(); + $responseXml->TestSoapAnyXmlResult = new stdClass(); + $responseXml->TestSoapAnyXmlResult->any = ''; + $responseXml->TestSoapAnyXmlResult->any .= 'value'; + $responseXml->TestSoapAnyXmlResult->any .= ''; + + $regularResponse = new stdClass(); + $regularResponse->test = 'value'; + + return [ + "xml response inside 'any' parameter" => [ + 'TestSoapAnyXml', + [ + 'param_name' => 'param_value', + 'param_name_1' => 'param_value_1', + ], + $responseXml, + [ + 'test' => ['nested' => 'value'], + ], + ], + 'regular soap response' => [ + 'TestRegularSoap', + [ + 'param_name' => 'param_value', + 'param_name_1' => 'param_value_1', + ], + $regularResponse, + [ + 'test' => 'value', + ], + ], + ]; + } + + /** + * @test + */ + public function testQueryException(): void + { + $soapClient = $this->getMockBuilder(SoapClient::class) + ->disableOriginalConstructor() + ->getMock(); + $soapClient->method('__soapCall') + ->with($this->equalTo('EnumValutes')) + ->will($this->throwException(new Exception())); + + $service = new CbrfSoapService($soapClient); + + $this->expectException(CbrfException::class); + $service->query('test'); + } +} diff --git a/tests/DataHelperTest.php b/tests/DataHelperTest.php new file mode 100644 index 0000000..0916e73 --- /dev/null +++ b/tests/DataHelperTest.php @@ -0,0 +1,588 @@ +expectException(\get_class($result)); + } + + $testDateTime = DataHelper::createImmutableDateTime($input); + + if ($result instanceof DateTimeInterface) { + $this->assertInstanceOf(DateTimeImmutable::class, $testDateTime); + $this->assertSameDate($result, $testDateTime); + } + } + + public function createImmutableDateTimeProvider(): array + { + $dateTime = new DateTimeImmutable('-1 hour'); + $dateTimeTz = new DateTimeImmutable('+1 hour', new DateTimeZone('Asia/ShangHai')); + + return [ + 'dateTime instance' => [ + $dateTime, + $dateTime, + ], + 'dateTime instance with time zone' => [ + $dateTimeTz, + $dateTimeTz, + ], + 'string' => [ + $dateTime->format(\DATE_ATOM), + $dateTime, + ], + 'exception on incorrect date' => [ + 'test', + new CbrfException(), + ], + ]; + } + + /** + * @param string $path + * @param mixed $input + * @param array|Throwable $result + * + * @test + * @dataProvider arrayProvider + */ + public function testArray(string $path, $input, $result): void + { + if ($result instanceof Throwable) { + $this->expectException(\get_class($result)); + } + + $testArray = DataHelper::array($path, $input); + + if (\is_array($result)) { + $this->assertSame($result, $testArray); + } + } + + public function arrayProvider(): array + { + $path = 'test1.test2'; + $result = ['key' => 'value']; + + $object = new stdClass(); + $object->test1 = new stdClass(); + $object->test1->test2 = $result; + + $objectMixed = new stdClass(); + $objectMixed->test1 = [ + 'test2' => $result, + ]; + + return [ + 'search inside array' => [ + $path, + [ + 'test1' => [ + 'test2' => $result, + ], + ], + $result, + ], + 'search inside object' => [ + $path, + $object, + $result, + ], + 'mixed search in array and object' => [ + $path, + $objectMixed, + $result, + ], + 'nothing found' => [ + $path, + [], + [], + ], + 'wrong type exception' => [ + $path, + ['test1' => ['test2' => 'wqe']], + new CbrfException(), + ], + ]; + } + + /** + * @param string $path + * @param mixed $input + * @param DateTimeInterface|Throwable $result + * + * @test + * @dataProvider dateTimeProvider + */ + public function testDateTime(string $path, $input, $result): void + { + if ($result instanceof Throwable) { + $this->expectException(\get_class($result)); + } + + $testDateTime = DataHelper::dateTime($path, $input); + + if ($result instanceof DateTimeInterface) { + $this->assertInstanceOf(DateTimeImmutable::class, $testDateTime); + $this->assertSameDate($result, $testDateTime); + } + } + + public function dateTimeProvider(): array + { + $path = 'test1.test2'; + $result = new DateTimeImmutable(); + $date = $result->format(\DATE_ATOM); + + $object = new stdClass(); + $object->test1 = new stdClass(); + $object->test1->test2 = $date; + + $objectMixed = new stdClass(); + $objectMixed->test1 = [ + 'test2' => $date, + ]; + + return [ + 'search inside array' => [ + $path, + [ + 'test1' => [ + 'test2' => $date, + ], + ], + $result, + ], + 'search inside object' => [ + $path, + $object, + $result, + ], + 'mixed search in array and object' => [ + $path, + $objectMixed, + $result, + ], + 'serach by not trimmed path' => [ + " {$path} ", + $objectMixed, + $result, + ], + 'not found exception' => [ + $path, + [], + new CbrfException(), + ], + 'empty string exception' => [ + $path, + [ + 'test1' => [ + 'test2' => '', + ], + ], + new CbrfException(), + ], + 'non-string exception' => [ + $path, + [ + 'test1' => [ + 'test2' => false, + ], + ], + new CbrfException(), + ], + ]; + } + + /** + * @param string $path + * @param mixed $input + * @param string|Throwable $result + * @param string|null $default + * + * @test + * @dataProvider stringProvider + */ + public function testString(string $path, $input, $result, ?string $default = null): void + { + if ($result instanceof Throwable) { + $this->expectException(\get_class($result)); + } + + $testString = DataHelper::string($path, $input, $default); + + if (\is_string($result)) { + $this->assertSame($result, $testString); + } + } + + public function stringProvider(): array + { + $path = 'test1.test2'; + $string = ' test '; + $result = 'test'; + + $object = new stdClass(); + $object->test1 = new stdClass(); + $object->test1->test2 = $string; + + $objectMixed = new stdClass(); + $objectMixed->test1 = [ + 'test2' => $string, + ]; + + return [ + 'search inside array' => [ + $path, + [ + 'test1' => [ + 'test2' => $string, + ], + ], + $result, + ], + 'search inside object' => [ + $path, + $object, + $result, + ], + 'mixed search in array and object' => [ + $path, + $objectMixed, + $result, + ], + 'serach by not trimmed path' => [ + " {$path} ", + $objectMixed, + $result, + ], + 'not found exception' => [ + $path, + [], + new CbrfException(), + ], + 'test default' => [ + $path, + [], + $result, + $result, + ], + ]; + } + + /** + * @param string $path + * @param mixed $input + * @param float|Throwable $result + * @param float|null $default + * + * @test + * @dataProvider floatProvider + */ + public function testFloat(string $path, $input, $result, ?float $default = null): void + { + if ($result instanceof Throwable) { + $this->expectException(\get_class($result)); + } + + $testFloat = DataHelper::float($path, $input, $default); + + if (\is_float($result)) { + $this->assertSame($result, $testFloat); + } + } + + public function floatProvider(): array + { + $path = 'test1.test2'; + $result = 12.3; + $float = '12.3'; + + $object = new stdClass(); + $object->test1 = new stdClass(); + $object->test1->test2 = $float; + + $objectMixed = new stdClass(); + $objectMixed->test1 = [ + 'test2' => $float, + ]; + + return [ + 'search inside array' => [ + $path, + [ + 'test1' => [ + 'test2' => $float, + ], + ], + $result, + ], + 'search inside object' => [ + $path, + $object, + $result, + ], + 'mixed search in array and object' => [ + $path, + $objectMixed, + $result, + ], + 'serach by not trimmed path' => [ + " {$path} ", + $objectMixed, + $result, + ], + 'not found exception' => [ + $path, + [], + new CbrfException(), + ], + 'test default' => [ + $path, + [], + $result, + $result, + ], + ]; + } + + /** + * @param string $path + * @param mixed $input + * @param float|null $result + * + * @test + * @dataProvider floatOrNullProvider + */ + public function testFloatOrNull(string $path, $input, $result): void + { + $testFloat = DataHelper::floatOrNull($path, $input); + + $this->assertSame($result, $testFloat); + } + + public function floatOrNullProvider(): array + { + $path = 'test1.test2'; + $result = 12.3; + $float = '12.3'; + + $object = new stdClass(); + $object->test1 = new stdClass(); + $object->test1->test2 = $float; + + $objectMixed = new stdClass(); + $objectMixed->test1 = [ + 'test2' => $float, + ]; + + return [ + 'search inside array' => [ + $path, + [ + 'test1' => [ + 'test2' => $float, + ], + ], + $result, + ], + 'search inside object' => [ + $path, + $object, + $result, + ], + 'mixed search in array and object' => [ + $path, + $objectMixed, + $result, + ], + 'serach by not trimmed path' => [ + " {$path} ", + $objectMixed, + $result, + ], + 'not found' => [ + $path, + [], + null, + ], + ]; + } + + /** + * @param string $path + * @param mixed $input + * @param int|Throwable $result + * @param int|null $default + * + * @test + * @dataProvider intProvider + */ + public function testInt(string $path, $input, $result, ?int $default = null): void + { + if ($result instanceof Throwable) { + $this->expectException(\get_class($result)); + } + + $testInt = DataHelper::int($path, $input, $default); + + if (\is_int($result)) { + $this->assertSame($result, $testInt); + } + } + + public function intProvider(): array + { + $path = 'test1.test2'; + $result = 12; + $int = '12'; + + $object = new stdClass(); + $object->test1 = new stdClass(); + $object->test1->test2 = $int; + + $objectMixed = new stdClass(); + $objectMixed->test1 = [ + 'test2' => $int, + ]; + + return [ + 'search inside array' => [ + $path, + [ + 'test1' => [ + 'test2' => $int, + ], + ], + $result, + ], + 'search inside object' => [ + $path, + $object, + $result, + ], + 'mixed search in array and object' => [ + $path, + $objectMixed, + $result, + ], + 'serach by not trimmed path' => [ + " {$path} ", + $objectMixed, + $result, + ], + 'not found exception' => [ + $path, + [], + new CbrfException(), + ], + 'test default' => [ + $path, + [], + $result, + $result, + ], + ]; + } + + /** + * @param string $path + * @param mixed $input + * @param string|Throwable $result + * @param string|null $default + * + * @test + * @dataProvider charCodeProvider + */ + public function testCharCode(string $path, $input, $result, ?string $default = null): void + { + if ($result instanceof Throwable) { + $this->expectException(\get_class($result)); + } + + $testString = DataHelper::charCode($path, $input, $default); + + if (\is_string($result)) { + $this->assertSame($result, $testString); + } + } + + public function charCodeProvider(): array + { + $path = 'test1.test2'; + $string = ' TeSt '; + $result = 'TEST'; + + $object = new stdClass(); + $object->test1 = new stdClass(); + $object->test1->test2 = $string; + + $objectMixed = new stdClass(); + $objectMixed->test1 = [ + 'test2' => $string, + ]; + + return [ + 'search inside array' => [ + $path, + [ + 'test1' => [ + 'test2' => $string, + ], + ], + $result, + ], + 'search inside object' => [ + $path, + $object, + $result, + ], + 'mixed search in array and object' => [ + $path, + $objectMixed, + $result, + ], + 'serach by not trimmed path' => [ + " {$path} ", + $objectMixed, + $result, + ], + 'not found exception' => [ + $path, + [], + new CbrfException(), + ], + 'test default' => [ + $path, + [], + $result, + $result, + ], + ]; + } +}