diff --git a/.gitignore b/.gitignore index 9fa7c878..ba666ac2 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ spec/dummy/tmp/ *.log .yardoc/ .byebug_history +.claude/settings.local.json +.worktrees diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 00000000..6a81b4c8 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +2.7.8 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..4b70aa82 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,210 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Gera is a Rails engine for generating and managing currency exchange rates for crypto changers and markets. It collects rates from external sources, builds currency rate matrices, and calculates final rates for payment systems with commissions. + +## Core Architecture + +### Rate Flow Hierarchy +1. **ExternalRate** - Raw rates from external sources (EXMO, Bitfinex, Binance, CBR, etc.) +2. **CurrencyRate** - Basic currency rates calculated from external rates using different modes (direct, inverse, cross) +3. **DirectionRate** - Final rates for specific payment system pairs with commissions applied +4. **ExchangeRate** - Configuration for commissions between payment systems + +### Key Models +- **RateSource** - External rate providers with STI subclasses (RateSourceExmo, RateSourceBitfinex, etc.) +- **PaymentSystem** - Payment systems with currencies and commissions +- **CurrencyPair** - Utility class for currency pair operations +- **Universe** - Central repository pattern for accessing rate data + +### Worker Architecture +- **RatesWorker** concern for fetching external rates +- Individual workers for each rate source (ExmoRatesWorker, BitfinexRatesWorker, etc.) +- **CurrencyRatesWorker** - Builds currency rate matrix from external rates +- **DirectionsRatesWorker** - Calculates final direction rates with commissions +- **CreateHistory_intervalsWorker** - Aggregates historical data + +## Development Commands + +### Running Tests +```bash +# Run all tests +bundle exec rake spec + +# Run specific test file +bundle exec rspec spec/models/gera/currency_rate_spec.rb + +# Run with focus +bundle exec rspec --tag focus +``` + +### Building and Development +```bash +# Install dependencies +bundle install + +# Run dummy app for testing +cd spec/dummy && rails server + +# Generate documentation +bundle exec yard + +# Clean database between tests (uses DatabaseRewinder) +``` + +### Code Quality +```bash +# Lint code +bundle exec rubocop + +# Auto-correct linting issues +bundle exec rubocop -a +``` + +## Configuration + +Create `./config/initializers/gera.rb`: +```ruby +Gera.configure do |config| + config.cross_pairs = { kzt: :rub, eur: :rub } + config.default_cross_currency = :usd +end +``` + +## Key Business Logic + +### Rate Calculation Modes +- **direct** - Direct rate from external source +- **inverse** - Inverted rate (1/rate) +- **same** - Same currency (rate = 1) +- **cross** - Calculated through intermediate currency + +### Supported Currencies +RUB, USD, BTC, LTC, ETH, DSH, KZT, XRP, ETC, XMR, BCH, EUR, NEO, ZEC + +### External Rate Sources +- EXMO, Bitfinex, Binance, GarantexIO +- Russian Central Bank (CBR) +- Manual rates and FF (fixed/float) sources + +## Testing Notes + +- Uses dummy Rails app in `spec/dummy/` +- Factory Bot for test data in `factories/` +- VCR for HTTP request mocking +- Database Rewinder for fast test cleanup +- Sidekiq testing inline enabled + +### Запуск изолированных тестов автокурсов + +Для тестов автокурсов (PositionAware, Legacy калькуляторы) используются изолированные тесты, +которые не загружают Rails и spec_helper. Это позволяет быстро тестировать логику без полной +загрузки приложения. + +```bash +# Переименовать .rspec чтобы не загружался spec_helper +mv .rspec .rspec.bak + +# Запустить изолированные тесты +mise exec -- bundle exec rspec spec/services/gera/autorate_calculators/isolated_spec.rb --no-color + +# Вернуть .rspec обратно +mv .rspec.bak .rspec +``` + +Или используйте Makefile (требует БД): +```bash +make test # запускает isolated_spec.rb и exchange_rate_spec.rb +``` + +**Важно:** Файл `isolated_spec.rb` самодостаточен и содержит все необходимые стабы для Gera модуля. + +## File Organization + +- `app/models/gera/` - Core domain models +- `app/workers/gera/` - Background job workers +- `lib/gera/` - Core engine logic and utilities +- `lib/builders/` - Rate calculation builders +- `spec/` - Test suite with dummy app + + +## Серверы и логи + +### Stage сервер + +На stage сервере логи находятся тут: + +```bash +scp kassa@89.248.193.193:/home/kassa/admin.kassa.cc/current/log/* . +``` + +### Production сервер + +```bash +ssh kassa@185.132.176.44 +cd /home/kassa/admin.kassa.cc/current/log +``` + +## Логи калькулятора PositionAware + +Калькулятор `PositionAwareCalculator` логирует с тегом `[PositionAware]` на уровне WARN. + +### Команды для анализа логов + +```bash +# SSH на production +ssh kassa@185.132.176.44 +cd /home/kassa/admin.kassa.cc/current/log + +# Все логи PositionAware (последние записи) +grep -i 'PositionAware' production.log | tail -100 + +# Финальные результаты расчётов +grep 'FINAL RESULT' production.log | grep PositionAware | tail -50 + +# Пустые target rates (edge case когда мало курсов) +grep 'Target position rates.*\[\]' production.log + +# Случаи когда не найден rate_above +grep 'NO rate_above found' production.log | tail -50 + +# Полный контекст одного расчёта (10 строк до и после FINAL) +grep -B10 -A2 'FINAL RESULT' production.log | grep PositionAware | tail -100 +``` + +### Структура логов калькулятора + +``` +[PositionAware] START position_from=X position_to=Y # Начало расчёта +[PositionAware] autorate_from=A autorate_to=B # Границы авторейта +[PositionAware] Filtered rates count: N, our_exchanger_id: 522 +[PositionAware] Target position rates [X..Y]: [...] # Курсы на целевых позициях +[PositionAware] Target rate: Z # Целевой курс +[PositionAware] calculate_adaptive_gap: ... # Расчёт GAP +[PositionAware] adjust_for_position_above: ... # Корректировка +[PositionAware] FINAL RESULT: X.XXXX # Финальный результат +``` + +### Особенности интерпретации + +- **Отрицательные комиссии** (FINAL RESULT: -0.888) - это нормально +- **find_non_anomalous_rate_above: result = nil** - часто встречается, не аномалия +- **Пустые Target position rates []** - когда настроенная позиция > количества курсов + +### Ключевые файлы калькулятора + +```bash +# Поиск по имени класса +grep -r "PositionAware" --include="*.rb" . + +# Калькулятор вызывается из: +# gera/app/models/gera/exchange_rate.rb:162 - метод rate_comission_calculator +``` + +# Requirements Management + +- **spreadsheet_id:** 1bY_cH5XpuO47qnPsYEdxjkQpwvPNYXHAms_ohkca15A +- **spreadsheet_url:** https://docs.google.com/spreadsheets/d/1bY_cH5XpuO47qnPsYEdxjkQpwvPNYXHAms_ohkca15A diff --git a/Gemfile b/Gemfile index b6570d93..dbc9f486 100644 --- a/Gemfile +++ b/Gemfile @@ -6,7 +6,7 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" } # development dependencies will be added by default to the :development group. gemspec -gem 'rails', '~> 5.2.2.1' +gem 'rails', '~> 6.0.6' gem 'dapi-archivable', '~> 0.1.2', require: 'archivable' gem 'active_link_to', github: 'BrandyMint/active_link_to' gem 'noty_flash', github: 'BrandyMint/noty_flash' diff --git a/Gemfile.lock b/Gemfile.lock index f7f621ab..883fb25a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -8,14 +8,14 @@ GIT GIT remote: https://github.com/BrandyMint/noty_flash.git - revision: 8d4a5b618367f0dfbe2a32d82cad134393d93f60 + revision: 9f2d93b8192c52122b691f8a14953c35613dfdc1 specs: noty_flash (0.1.2) PATH remote: . specs: - gera (0.3.3) + gera (0.4.6) active_link_to authority auto_logger (~> 0.1.4) @@ -23,14 +23,14 @@ PATH breadcrumbs_on_rails business_time dapi-archivable - draper (~> 3.0.1) + draper (~> 3.1.0) kaminari money money-rails noty_flash percentable - psych - rails (~> 5.2.1) + psych (~> 3.1.0) + rails (~> 6.0.6) request_store require_all rest-client (~> 2.0) @@ -41,58 +41,71 @@ PATH GEM remote: https://rubygems.org/ specs: - actioncable (5.2.2.1) - actionpack (= 5.2.2.1) + actioncable (6.0.6.1) + actionpack (= 6.0.6.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailer (5.2.2.1) - actionpack (= 5.2.2.1) - actionview (= 5.2.2.1) - activejob (= 5.2.2.1) + actionmailbox (6.0.6.1) + actionpack (= 6.0.6.1) + activejob (= 6.0.6.1) + activerecord (= 6.0.6.1) + activestorage (= 6.0.6.1) + activesupport (= 6.0.6.1) + mail (>= 2.7.1) + actionmailer (6.0.6.1) + actionpack (= 6.0.6.1) + actionview (= 6.0.6.1) + activejob (= 6.0.6.1) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (5.2.2.1) - actionview (= 5.2.2.1) - activesupport (= 5.2.2.1) - rack (~> 2.0) + actionpack (6.0.6.1) + actionview (= 6.0.6.1) + activesupport (= 6.0.6.1) + rack (~> 2.0, >= 2.0.8) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.2.2.1) - activesupport (= 5.2.2.1) + rails-html-sanitizer (~> 1.0, >= 1.2.0) + actiontext (6.0.6.1) + actionpack (= 6.0.6.1) + activerecord (= 6.0.6.1) + activestorage (= 6.0.6.1) + activesupport (= 6.0.6.1) + nokogiri (>= 1.8.5) + actionview (6.0.6.1) + activesupport (= 6.0.6.1) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.3) - activejob (5.2.2.1) - activesupport (= 5.2.2.1) + rails-html-sanitizer (~> 1.1, >= 1.2.0) + activejob (6.0.6.1) + activesupport (= 6.0.6.1) globalid (>= 0.3.6) - activemodel (5.2.2.1) - activesupport (= 5.2.2.1) - activemodel-serializers-xml (1.0.2) - activemodel (> 5.x) - activesupport (> 5.x) + activemodel (6.0.6.1) + activesupport (= 6.0.6.1) + activemodel-serializers-xml (1.0.3) + activemodel (>= 5.0.0.a) + activesupport (>= 5.0.0.a) builder (~> 3.1) - activerecord (5.2.2.1) - activemodel (= 5.2.2.1) - activesupport (= 5.2.2.1) - arel (>= 9.0) - activestorage (5.2.2.1) - actionpack (= 5.2.2.1) - activerecord (= 5.2.2.1) - marcel (~> 0.3.1) - activesupport (5.2.2.1) + activerecord (6.0.6.1) + activemodel (= 6.0.6.1) + activesupport (= 6.0.6.1) + activestorage (6.0.6.1) + actionpack (= 6.0.6.1) + activejob (= 6.0.6.1) + activerecord (= 6.0.6.1) + marcel (~> 1.0) + activesupport (6.0.6.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 0.7, < 2) minitest (~> 5.1) tzinfo (~> 1.1) - addressable (2.6.0) - public_suffix (>= 2.0.2, < 4.0) - arel (9.0.0) - ast (2.4.0) + zeitwerk (~> 2.2, >= 2.2.2) + addressable (2.8.4) + public_suffix (>= 2.0.2, < 6.0) + ast (2.4.2) authority (3.3.0) activesupport (>= 3.0.0) - auto_logger (0.1.4) + auto_logger (0.1.8) activesupport beautiful-log awesome_print (1.8.0) @@ -100,57 +113,58 @@ GEM descendants_tracker (~> 0.0.4) ice_nine (~> 0.11.0) thread_safe (~> 0.3, >= 0.3.1) + base64 (0.3.0) beautiful-log (0.2.2) awesome_print (~> 1.8.0) colorize (~> 0.8.1) best_in_place (3.1.1) actionpack (>= 3.2) railties (>= 3.2) - breadcrumbs_on_rails (3.0.1) - builder (3.2.3) - business_time (0.9.3) + breadcrumbs_on_rails (4.1.0) + railties (>= 5.0) + builder (3.2.4) + business_time (0.13.0) activesupport (>= 3.2.0) tzinfo - byebug (11.0.0) - coderay (1.1.2) + byebug (11.1.3) + coderay (1.1.3) coercible (1.0.0) descendants_tracker (~> 0.0.1) colorize (0.8.1) - concurrent-ruby (1.1.5) - connection_pool (2.2.2) - crack (0.4.3) - safe_yaml (~> 1.0.0) - crass (1.0.4) + concurrent-ruby (1.2.2) + connection_pool (2.5.5) + crack (0.4.5) + rexml + crass (1.0.6) dapi-archivable (0.1.3) activerecord activesupport - database_rewinder (0.9.1) + database_rewinder (0.9.8) + date (3.3.3) descendants_tracker (0.0.4) thread_safe (~> 0.3, >= 0.3.1) - diff-lcs (1.3) - domain_name (0.5.20180417) - unf (>= 0.0.5, < 1.0.0) - draper (3.0.1) - actionpack (~> 5.0) - activemodel (~> 5.0) - activemodel-serializers-xml (~> 1.0) - activesupport (~> 5.0) - request_store (~> 1.0) - equalizer (0.0.11) - erubi (1.8.0) - factory_bot (5.0.2) - activesupport (>= 4.2.0) - ffi (1.10.0) - formatador (0.2.5) - globalid (0.4.2) - activesupport (>= 4.2.0) - guard (2.15.0) + diff-lcs (1.5.0) + domain_name (0.6.20240107) + draper (3.1.0) + actionpack (>= 5.0) + activemodel (>= 5.0) + activemodel-serializers-xml (>= 1.0) + activesupport (>= 5.0) + request_store (>= 1.0) + erubi (1.12.0) + factory_bot (6.2.1) + activesupport (>= 5.0.0) + ffi (1.15.5) + formatador (1.1.0) + globalid (1.1.0) + activesupport (>= 5.0) + guard (2.18.0) formatador (>= 0.2.4) listen (>= 2.7, < 4.0) lumberjack (>= 1.0.12, < 2.0) nenv (~> 0.1) notiffany (~> 0.0) - pry (>= 0.9.12) + pry (>= 0.13.0) shellany (~> 0.0) thor (>= 0.18.1) guard-bundler (2.2.1) @@ -165,203 +179,226 @@ GEM guard (~> 2.1) guard-compat (~> 1.1) rspec (>= 2.99.0, < 4.0) - guard-rubocop (1.3.0) + guard-rubocop (1.5.0) guard (~> 2.0) - rubocop (~> 0.20) - hashdiff (0.3.8) - http-cookie (1.0.3) + rubocop (< 2.0) + hashdiff (1.0.1) + http-accept (1.7.0) + http-cookie (1.1.0) domain_name (~> 0.5) - i18n (1.6.0) + i18n (1.13.0) concurrent-ruby (~> 1.0) ice_nine (0.11.2) - jaro_winkler (1.5.2) - kaminari (1.1.1) + json (2.6.3) + kaminari (1.2.2) activesupport (>= 4.1.0) - kaminari-actionview (= 1.1.1) - kaminari-activerecord (= 1.1.1) - kaminari-core (= 1.1.1) - kaminari-actionview (1.1.1) + kaminari-actionview (= 1.2.2) + kaminari-activerecord (= 1.2.2) + kaminari-core (= 1.2.2) + kaminari-actionview (1.2.2) actionview - kaminari-core (= 1.1.1) - kaminari-activerecord (1.1.1) + kaminari-core (= 1.2.2) + kaminari-activerecord (1.2.2) activerecord - kaminari-core (= 1.1.1) - kaminari-core (1.1.1) - listen (3.1.5) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) - ruby_dep (~> 1.2) - loofah (2.2.3) + kaminari-core (= 1.2.2) + kaminari-core (1.2.2) + listen (3.8.0) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + logger (1.7.0) + loofah (2.21.3) crass (~> 1.0.2) - nokogiri (>= 1.5.9) - lumberjack (1.0.13) - mail (2.7.1) + nokogiri (>= 1.12.0) + lumberjack (1.2.8) + mail (2.8.1) mini_mime (>= 0.1.1) - marcel (0.3.3) - mimemagic (~> 0.3.2) - method_source (0.9.2) - mime-types (3.2.2) - mime-types-data (~> 3.2015) - mime-types-data (3.2018.0812) - mimemagic (0.3.3) - mini_mime (1.0.1) - mini_portile2 (2.4.0) - minitest (5.11.3) - monetize (1.9.1) + net-imap + net-pop + net-smtp + marcel (1.0.2) + method_source (1.0.0) + mime-types (3.7.0) + logger + mime-types-data (~> 3.2025, >= 3.2025.0507) + mime-types-data (3.2025.0924) + mini_mime (1.1.2) + mini_portile2 (2.8.2) + minitest (5.18.0) + monetize (1.13.0) money (~> 6.12) - money (6.13.2) + money (6.19.0) i18n (>= 0.6.4, <= 2) - money-rails (1.13.1) + money-rails (1.15.0) activesupport (>= 3.0) - monetize (~> 1.9.0) - money (~> 6.13.0) + monetize (~> 1.9) + money (~> 6.13) railties (>= 3.0) - mysql2 (0.5.2) + mysql2 (0.5.5) nenv (0.3.0) + net-imap (0.3.4) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.1) + timeout + net-smtp (0.3.3) + net-protocol netrc (0.11.0) - nio4r (2.3.1) - nokogiri (1.10.1) - mini_portile2 (~> 2.4.0) - notiffany (0.1.1) + nio4r (2.5.9) + nokogiri (1.15.2) + mini_portile2 (~> 2.8.2) + racc (~> 1.4) + notiffany (0.1.3) nenv (~> 0.1) shellany (~> 0.0) - parallel (1.14.0) - parser (2.6.0.0) - ast (~> 2.4.0) + parallel (1.23.0) + parser (3.2.2.1) + ast (~> 2.4.1) percentable (1.1.2) - pg (1.1.4) - powerpack (0.1.2) - pry (0.12.2) - coderay (~> 1.1.0) - method_source (~> 0.9.0) - pry-byebug (3.7.0) + pg (1.5.3) + pry (0.14.2) + coderay (~> 1.1) + method_source (~> 1.0) + pry-byebug (3.10.1) byebug (~> 11.0) - pry (~> 0.10) - pry-doc (1.0.0) + pry (>= 0.13, < 0.15) + pry-doc (1.4.0) pry (~> 0.11) yard (~> 0.9.11) pry-rails (0.3.9) pry (>= 0.10.4) psych (3.1.0) - public_suffix (3.0.3) - rack (2.0.6) - rack-protection (2.0.5) - rack - rack-test (1.1.0) - rack (>= 1.0, < 3) - rails (5.2.2.1) - actioncable (= 5.2.2.1) - actionmailer (= 5.2.2.1) - actionpack (= 5.2.2.1) - actionview (= 5.2.2.1) - activejob (= 5.2.2.1) - activemodel (= 5.2.2.1) - activerecord (= 5.2.2.1) - activestorage (= 5.2.2.1) - activesupport (= 5.2.2.1) + public_suffix (5.0.1) + racc (1.6.2) + rack (2.2.7) + rack-test (2.1.0) + rack (>= 1.3) + rails (6.0.6.1) + actioncable (= 6.0.6.1) + actionmailbox (= 6.0.6.1) + actionmailer (= 6.0.6.1) + actionpack (= 6.0.6.1) + actiontext (= 6.0.6.1) + actionview (= 6.0.6.1) + activejob (= 6.0.6.1) + activemodel (= 6.0.6.1) + activerecord (= 6.0.6.1) + activestorage (= 6.0.6.1) + activesupport (= 6.0.6.1) bundler (>= 1.3.0) - railties (= 5.2.2.1) + railties (= 6.0.6.1) sprockets-rails (>= 2.0.0) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) - rails-html-sanitizer (1.0.4) - loofah (~> 2.2, >= 2.2.2) - railties (5.2.2.1) - actionpack (= 5.2.2.1) - activesupport (= 5.2.2.1) + rails-html-sanitizer (1.5.0) + loofah (~> 2.19, >= 2.19.1) + railties (6.0.6.1) + actionpack (= 6.0.6.1) + activesupport (= 6.0.6.1) method_source rake (>= 0.8.7) - thor (>= 0.19.0, < 2.0) - rainbow (3.0.0) - rake (12.3.2) - rb-fsevent (0.10.3) - rb-inotify (0.10.0) + thor (>= 0.20.3, < 2.0) + rainbow (3.1.1) + rake (13.0.6) + rb-fsevent (0.11.2) + rb-inotify (0.10.1) ffi (~> 1.0) - redis (4.1.0) - request_store (1.4.1) + redis-client (0.26.2) + connection_pool + regexp_parser (2.8.0) + request_store (1.7.0) rack (>= 1.4) - require_all (2.0.0) - rest-client (2.0.2) + require_all (3.0.0) + rest-client (2.1.0) + http-accept (>= 1.7.0, < 2.0) http-cookie (>= 1.0.2, < 2.0) mime-types (>= 1.16, < 4.0) netrc (~> 0.8) - rspec (3.8.0) - rspec-core (~> 3.8.0) - rspec-expectations (~> 3.8.0) - rspec-mocks (~> 3.8.0) - rspec-core (3.8.0) - rspec-support (~> 3.8.0) - rspec-expectations (3.8.2) + rexml (3.2.5) + rspec (3.9.0) + rspec-core (~> 3.9.0) + rspec-expectations (~> 3.9.0) + rspec-mocks (~> 3.9.0) + rspec-core (3.9.3) + rspec-support (~> 3.9.3) + rspec-expectations (3.9.4) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.8.0) - rspec-mocks (3.8.0) + rspec-support (~> 3.9.0) + rspec-mocks (3.9.1) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.8.0) - rspec-rails (3.8.2) + rspec-support (~> 3.9.0) + rspec-rails (3.9.1) actionpack (>= 3.0) activesupport (>= 3.0) railties (>= 3.0) - rspec-core (~> 3.8.0) - rspec-expectations (~> 3.8.0) - rspec-mocks (~> 3.8.0) - rspec-support (~> 3.8.0) - rspec-support (3.8.0) - rubocop (0.65.0) - jaro_winkler (~> 1.5.1) + rspec-core (~> 3.9.0) + rspec-expectations (~> 3.9.0) + rspec-mocks (~> 3.9.0) + rspec-support (~> 3.9.0) + rspec-support (3.9.4) + rubocop (1.51.0) + json (~> 2.3) parallel (~> 1.10) - parser (>= 2.5, != 2.5.1.1) - powerpack (~> 0.1) - psych (>= 3.1.0) + parser (>= 3.2.0.0) rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.28.0, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (~> 1.4.0) - rubocop-rspec (1.32.0) - rubocop (>= 0.60.0) - ruby-progressbar (1.10.0) - ruby_dep (1.5.0) - safe_yaml (1.0.5) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.28.1) + parser (>= 3.2.1.0) + rubocop-capybara (2.18.0) + rubocop (~> 1.41) + rubocop-factory_bot (2.23.1) + rubocop (~> 1.33) + rubocop-rspec (2.22.0) + rubocop (~> 1.33) + rubocop-capybara (~> 2.17) + rubocop-factory_bot (~> 2.22) + ruby-progressbar (1.13.0) shellany (0.0.1) - sidekiq (5.2.5) - connection_pool (~> 2.2, >= 2.2.2) - rack (>= 1.5.0) - rack-protection (>= 1.5.0) - redis (>= 3.3.5, < 5) - simple_form (4.1.0) - actionpack (>= 5.0) - activemodel (>= 5.0) - sprockets (3.7.2) + sidekiq (7.3.10) + base64 + connection_pool (>= 2.3.0, < 3) + logger + rack (>= 2.2.4, < 3.3) + redis-client (>= 0.23.0, < 1) + simple_form (5.3.1) + actionpack (>= 5.2) + activemodel (>= 5.2) + sprockets (4.2.0) concurrent-ruby (~> 1.0) - rack (> 1, < 3) - sprockets-rails (3.2.1) - actionpack (>= 4.0) - activesupport (>= 4.0) + rack (>= 2.2.4, < 4) + sprockets-rails (3.4.2) + actionpack (>= 5.2) + activesupport (>= 5.2) sprockets (>= 3.0.0) - thor (0.20.3) + thor (1.2.2) thread_safe (0.3.6) - timecop (0.9.1) - tzinfo (1.2.5) + timecop (0.9.6) + timeout (0.3.2) + tzinfo (1.2.11) thread_safe (~> 0.1) - unf (0.1.4) - unf_ext - unf_ext (0.0.7.5) - unicode-display_width (1.4.1) - vcr (4.0.0) - virtus (1.0.5) + unicode-display_width (2.4.2) + vcr (6.1.0) + virtus (2.0.0) axiom-types (~> 0.1) coercible (~> 1.0) descendants_tracker (~> 0.0, >= 0.0.3) - equalizer (~> 0.0, >= 0.0.9) - webmock (3.5.1) - addressable (>= 2.3.6) + webmock (3.18.1) + addressable (>= 2.8.0) crack (>= 0.3.2) - hashdiff - websocket-driver (0.7.0) + hashdiff (>= 0.4.0, < 2.0.0) + websocket-driver (0.7.5) websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.3) - yard (0.9.18) + websocket-extensions (0.1.5) + yard (0.9.34) yard-rspec (0.1) yard + zeitwerk (2.6.8) PLATFORMS ruby @@ -384,7 +421,7 @@ DEPENDENCIES pry-byebug pry-doc pry-rails - rails (~> 5.2.2.1) + rails (~> 6.0.6) rspec-rails (~> 3.7) rubocop rubocop-rspec diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..35972611 --- /dev/null +++ b/Makefile @@ -0,0 +1,25 @@ +.PHONY: test test-all db-up db-down + +# Run calculator and exchange_rate tests (the ones we fixed) +test: + mise exec -- bundle exec rspec \ + spec/services/gera/autorate_calculators/isolated_spec.rb \ + spec/models/gera/exchange_rate_spec.rb \ + --no-color + +# Run all tests +test-all: + mise exec -- bundle exec rspec --no-color + +# Start MySQL for testing +db-up: + docker-compose up -d + @echo "Waiting for MySQL..." + @for i in $$(seq 1 30); do \ + docker exec gera-legacy-mysql-1 mysqladmin ping -h localhost -u root -p1111 2>/dev/null && break || sleep 2; \ + done + @echo "MySQL is ready" + +# Stop MySQL +db-down: + docker-compose down diff --git a/README.md b/README.md index 61b3a975..7e8032e2 100644 --- a/README.md +++ b/README.md @@ -34,12 +34,35 @@ $ gem install gera Add `./config/initializers/gera.rb` with this content: -``` +```ruby Gera.configure do |config| config.cross_pairs = { kzt: :rub, eur: :rub } + + # Автокурс: ID нашего обменника в BestChange (для исключения из расчёта позиции) + config.our_exchanger_id = 999 + + # Автокурс: Порог аномальной комиссии для защиты от манипуляторов (по умолчанию 50%) + config.anomaly_threshold_percent = 50.0 end ``` +### Autorate Calculator Types + +Для каждого направления обмена (`ExchangeRate`) можно выбрать тип калькулятора автокурса: + +```ruby +# Legacy (по умолчанию) - старый алгоритм +exchange_rate.update!(calculator_type: 'legacy') + +# PositionAware - новый алгоритм с защитой от перепрыгивания позиций +exchange_rate.update!(calculator_type: 'position_aware') +``` + +**PositionAware** гарантирует, что обменник займёт позицию внутри целевого диапазона (`position_from..position_to`), а не перепрыгнет выше. Поддерживает: +- Адаптивный GAP для плотных рейтингов +- Исключение своего обменника из расчёта +- Защиту от манипуляторов с аномальными курсами + ## Supported external sources of basic rates * EXMO, Russian Central Bank, Bitfinex, Manual diff --git a/app/controllers/gera/external_rate_snapshots_controller.rb b/app/controllers/gera/external_rate_snapshots_controller.rb index 686e6cd9..b0aa450e 100644 --- a/app/controllers/gera/external_rate_snapshots_controller.rb +++ b/app/controllers/gera/external_rate_snapshots_controller.rb @@ -5,7 +5,7 @@ module Gera class ExternalRateSnapshotsController < ApplicationController authorize_actions_for ExchangeRate - PER_PAGE = 200 + PER_PAGE = 25 helper_method :rate_source def index diff --git a/app/models/concerns/gera/currency_rate_mode_builder_support.rb b/app/models/concerns/gera/currency_rate_mode_builder_support.rb index 3f8837df..6707a9c0 100644 --- a/app/models/concerns/gera/currency_rate_mode_builder_support.rb +++ b/app/models/concerns/gera/currency_rate_mode_builder_support.rb @@ -21,9 +21,9 @@ def build_currency_rate! def builder case mode when 'auto' - CurrencyRateAutoBuilder.new currency_pair: currency_pair + CurrencyRateAutoBuilder.new(currency_pair: currency_pair) when 'cross' - CurrencyRateCrossBuilder.new currency_pair: currency_pair, cross_rate_modes: cross_rate_modes + CurrencyRateCrossBuilder.new(currency_pair: currency_pair, cross_rate_modes: cross_rate_modes) else source = RateSource.find_by_key(mode) raise "not supported mode #{mode} for #{currency_pair}" unless source.present? diff --git a/app/models/gera/currency_rate.rb b/app/models/gera/currency_rate.rb index 92ce2b32..6c67c5bf 100644 --- a/app/models/gera/currency_rate.rb +++ b/app/models/gera/currency_rate.rb @@ -22,7 +22,7 @@ class CurrencyRate < ApplicationRecord enum mode: %i[direct inverse same cross], _prefix: true before_save do - raise("У кросс-курса (#{currency_pair}) должно быть несколько external_rates (#{external_rates.count})") if mode_cross? && !external_rates.many? + raise("У кросс-курса (#{currency_pair}) должен быть минимум 1 external_rates (#{external_rates.count})") if mode_cross? && external_rates.blank? self.metadata ||= {} end diff --git a/app/models/gera/currency_rate_mode.rb b/app/models/gera/currency_rate_mode.rb index bf764bf2..44627759 100644 --- a/app/models/gera/currency_rate_mode.rb +++ b/app/models/gera/currency_rate_mode.rb @@ -16,5 +16,17 @@ class CurrencyRateMode < ApplicationRecord accepts_nested_attributes_for :cross_rate_modes, reject_if: :all_blank, allow_destroy: true delegate :to_s, to: :mode + + def self.default_for_pair(pair) + new(currency_pair: pair, mode: :auto) + end + + def to_s + new_record? && mode.auto? ? "default" : mode + end + + def mode + super.inquiry + end end end diff --git a/app/models/gera/currency_rate_mode_snapshot.rb b/app/models/gera/currency_rate_mode_snapshot.rb index c8c1ce5c..2ea55969 100644 --- a/app/models/gera/currency_rate_mode_snapshot.rb +++ b/app/models/gera/currency_rate_mode_snapshot.rb @@ -14,7 +14,7 @@ class CurrencyRateModeSnapshot < ApplicationRecord self.title = Time.zone.now.to_s if title.blank? end - validates :title, presence: true, uniqueness: true + validates :title, presence: true, uniqueness: { case_sensitive: false } def create_modes! CurrencyPair.all.each do |pair| diff --git a/app/models/gera/direction_rate.rb b/app/models/gera/direction_rate.rb index 5dc62cd8..cc6da967 100644 --- a/app/models/gera/direction_rate.rb +++ b/app/models/gera/direction_rate.rb @@ -111,25 +111,35 @@ def dump end def exchange_notification + by_income = ExchangeNotification.find_by( + income_payment_system_id: income_payment_system_id, + outcome_payment_system_id: nil + ) + + by_outcome = ExchangeNotification.find_by( + income_payment_system_id: nil, + outcome_payment_system_id: outcome_payment_system_id + ) + + return ExchangeNotification.new( + income_payment_system_id: income_payment_system_id, + outcome_payment_system_id: outcome_payment_system_id, + body_ru: [by_income.body_ru, by_outcome.body_ru].join('

'), + body_en: [by_income.body_en, by_outcome.body_en].join('

'), + body_cs: [by_income.body_cs, by_outcome.body_cs].join('

') + ) if by_income && by_outcome + ExchangeNotification.find_by( income_payment_system_id: income_payment_system_id, outcome_payment_system_id: outcome_payment_system_id - ) || - ExchangeNotification.find_by( - income_payment_system_id: income_payment_system_id, - outcome_payment_system_id: nil - ) || - ExchangeNotification.find_by( - income_payment_system_id: nil, - outcome_payment_system_id: outcome_payment_system_id - ) + ) || by_income || by_outcome end def calculate_rate self.base_rate_value = currency_rate.rate_value raise UnknownExchangeRate, "No exchange_rate for #{ps_from}->#{ps_to}" unless exchange_rate - self.rate_percent = exchange_rate.comission_percents + self.rate_percent = exchange_rate.final_rate_percents self.rate_value = calculate_finite_rate base_rate_value, rate_percent unless rate_percent.nil? end end diff --git a/app/models/gera/exchange_rate.rb b/app/models/gera/exchange_rate.rb index 692ee920..bd5a5444 100644 --- a/app/models/gera/exchange_rate.rb +++ b/app/models/gera/exchange_rate.rb @@ -17,12 +17,17 @@ class ExchangeRate < ApplicationRecord include Authority::Abilities DEFAULT_COMISSION = 50 + MIN_COMISSION = -9.9 + + CALCULATOR_TYPES = %w[legacy position_aware].freeze include Mathematic include DirectionSupport belongs_to :payment_system_from, foreign_key: :income_payment_system_id, class_name: 'Gera::PaymentSystem' belongs_to :payment_system_to, foreign_key: :outcome_payment_system_id, class_name: 'Gera::PaymentSystem' + has_one :target_autorate_setting, class_name: 'TargetAutorateSetting' + has_one :exchange_rate_limit, class_name: 'Gera::ExchangeRateLimit' scope :ordered, -> { order :id } scope :enabled, -> { where is_enabled: true } @@ -36,10 +41,16 @@ class ExchangeRate < ApplicationRecord with_payment_systems .enabled .where("#{PaymentSystem.table_name}.income_enabled and payment_system_tos_gera_exchange_rates.outcome_enabled") - .where("#{table_name}.income_payment_system_id <> #{table_name}.outcome_payment_system_id") } - after_commit :update_direction_rates, if: -> { previous_changes.key?('value') } + # Legacy alias — previously excluded same-to-same directions (income_ps_id <> outcome_ps_id), + # but available no longer has that restriction (PIR-076 / issue #2291). + scope :available_for_parser, -> { available } + + scope :with_auto_rates, -> { where(auto_rate: true) } + + # ОТКЛЮЧЕНО: Roman Tershak (1f04791) — "Не обновлять глобальные курсы после апдейта exchange_rate" + # after_commit :update_direction_rates, if: -> { previous_changes.key?('value') } before_create do self.in_cur = payment_system_from.currency.to_s @@ -48,9 +59,21 @@ class ExchangeRate < ApplicationRecord end validates :commission, presence: true + validates :commission, numericality: { greater_than_or_equal_to: MIN_COMISSION } + validates :calculator_type, inclusion: { in: CALCULATOR_TYPES } delegate :rate, :currency_rate, to: :direction_rate + delegate :auto_comission_by_reserve, :comission_by_base_rate, :auto_rate_by_base_from, + :auto_rate_by_base_to, :auto_rate_by_reserve_from, :auto_rate_by_reserve_to, + :current_base_rate, :average_base_rate, :auto_comission_from, + :auto_comission_to, :bestchange_delta, to: :rate_comission_calculator + + delegate :position_from, :position_to, + :autorate_from, :autorate_to, to: :target_autorate_setting, allow_nil: true + + delegate :min_amount, :max_amount, to: :exchange_rate_limit, allow_nil: true + alias_attribute :ps_from_id, :income_payment_system_id alias_attribute :ps_to_id, :outcome_payment_system_id alias_attribute :payment_system_from_id, :income_payment_system_id @@ -59,10 +82,14 @@ class ExchangeRate < ApplicationRecord alias_attribute :comission, :value alias_attribute :commission, :value alias_attribute :comission_percents, :value + alias_attribute :fixed_comission, :value alias_attribute :income_payment_system, :payment_system_from alias_attribute :outcome_payment_system, :payment_system_to + monetize :minamount_cents, as: :minamount + monetize :maxamount_cents, as: :maxamount + def self.list_rates order('id asc').each_with_object({}) do |er, h| h[er.income_payment_system_id] ||= {} @@ -75,7 +102,10 @@ def available? end def update_finite_rate!(finite_rate) - update! comission: calculate_comission(finite_rate, currency_rate.rate_value) + logger = Logger.new("#{Rails.root}/log/call_exchange_rate_updater_worker.log") + logger.info("Calls perform_async from update_finite_rate Gera::ExchangeRate") + + ExchangeRateUpdaterWorker.perform_async(id, { comission: calculate_comission(finite_rate, currency_rate.rate_value) }) end def custom_inspect @@ -121,10 +151,39 @@ def direction_rate Universe.direction_rates_repository.find_direction_rate_by_exchange_rate_id id end - private + def final_rate_percents + @final_rate_percents ||= auto_rate? ? rate_comission_calculator.auto_comission : rate_comission_calculator.fixed_comission + end def update_direction_rates DirectionsRatesWorker.perform_async(exchange_rate_id: id) end + + def rate_comission_calculator + @rate_comission_calculator ||= RateComissionCalculator.new(exchange_rate: self, external_rates: external_rates) + end + + def external_rates + @external_rates ||= BestChange::Service.new(exchange_rate: self).rows_without_kassa + end + + def flexible_rate + FlexibleRateConfig.active.where("JSON_CONTAINS(exchange_rate_ids, :id)", id: id.to_json).present? + end + + def flexible_rate? + flexible_rate + end + + def autorate_calculator_class + case calculator_type + when 'legacy', nil + AutorateCalculators::Legacy + when 'position_aware' + AutorateCalculators::PositionAware + else + raise ArgumentError, "Unknown calculator_type: #{calculator_type}" + end + end end end diff --git a/app/models/gera/exchange_rate_limit.rb b/app/models/gera/exchange_rate_limit.rb new file mode 100644 index 00000000..aed9349e --- /dev/null +++ b/app/models/gera/exchange_rate_limit.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Gera + class ExchangeRateLimit < ApplicationRecord + belongs_to :exchange_rate, class_name: 'Gera::ExchangeRate' + end +end diff --git a/app/models/gera/external_rate_snapshot.rb b/app/models/gera/external_rate_snapshot.rb index a999ef56..cf9fc852 100644 --- a/app/models/gera/external_rate_snapshot.rb +++ b/app/models/gera/external_rate_snapshot.rb @@ -4,7 +4,7 @@ module Gera class ExternalRateSnapshot < ApplicationRecord belongs_to :rate_source - has_many :external_rates, foreign_key: :snapshot_id + has_many :external_rates, foreign_key: :snapshot_id, dependent: :destroy scope :ordered, -> { order 'actual_for desc' } scope :last_actuals_by_rate_sources, -> { where id: group(:rate_source_id).maximum(:id).values } diff --git a/app/models/gera/payment_system.rb b/app/models/gera/payment_system.rb index 38812bea..8430d391 100644 --- a/app/models/gera/payment_system.rb +++ b/app/models/gera/payment_system.rb @@ -15,7 +15,7 @@ class PaymentSystem < ApplicationRecord enum total_computation_method: %i[regular_fee reverse_fee] enum transfer_comission_payer: %i[user shop], _prefix: :transfer_comission_payer - validates :name, presence: true, uniqueness: true + validates :name, presence: true, uniqueness: { case_sensitive: true } validates :currency, presence: true before_create do diff --git a/app/models/gera/rate_source.rb b/app/models/gera/rate_source.rb index 21e898d9..f8a0263a 100644 --- a/app/models/gera/rate_source.rb +++ b/app/models/gera/rate_source.rb @@ -16,7 +16,7 @@ class RateSource < ApplicationRecord scope :enabled_for_cross_rates, -> { enabled } - validates :key, presence: true, uniqueness: true + validates :key, presence: true, uniqueness: { case_sensitive: false } before_create do self.priority ||= RateSource.maximum(:priority).to_i + 1 diff --git a/app/models/gera/rate_source_binance.rb b/app/models/gera/rate_source_binance.rb new file mode 100644 index 00000000..a91fec2c --- /dev/null +++ b/app/models/gera/rate_source_binance.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Gera + class RateSourceBinance < RateSource + def self.supported_currencies + %i[BTC BCH DSH ETH ETC LTC XRP XMR ZEC NEO EOS ADA XEM WAVES TRX DOGE BNB XLM DOT USDT UNI LINK SOL USDC MATIC AVAX].map { |m| Money::Currency.find! m } + end + end +end diff --git a/app/models/gera/rate_source_bitfinex.rb b/app/models/gera/rate_source_bitfinex.rb index 5c36d8c7..e508aa1f 100644 --- a/app/models/gera/rate_source_bitfinex.rb +++ b/app/models/gera/rate_source_bitfinex.rb @@ -3,7 +3,7 @@ module Gera class RateSourceBitfinex < RateSource def self.supported_currencies - %i[NEO BTC ETH EUR USD].map { |m| Money::Currency.find! m } + %i[NEO BTC ETH EUR USD XMR].map { |m| Money::Currency.find! m } end end end diff --git a/app/models/gera/rate_source_bybit.rb b/app/models/gera/rate_source_bybit.rb new file mode 100644 index 00000000..4d228001 --- /dev/null +++ b/app/models/gera/rate_source_bybit.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Gera + class RateSourceBybit < RateSource + def self.supported_currencies + %i[USDT RUB].map { |m| Money::Currency.find! m } + end + end +end diff --git a/app/models/gera/rate_source_cbr.rb b/app/models/gera/rate_source_cbr.rb index bf71df1c..3e32a05a 100644 --- a/app/models/gera/rate_source_cbr.rb +++ b/app/models/gera/rate_source_cbr.rb @@ -1,13 +1,13 @@ # frozen_string_literal: true module Gera - class RateSourceCBR < RateSource + class RateSourceCbr < RateSource def self.supported_currencies - %i[RUB KZT USD EUR UAH].map { |m| Money::Currency.find! m } + %i[RUB KZT USD EUR UAH UZS AZN BYN TRY THB IDR CNY INR].map { |m| Money::Currency.find! m } end def self.available_pairs - ['KZT/RUB', 'USD/RUB', 'EUR/RUB', 'UAH/RUB'].map { |cp| Gera::CurrencyPair.new cp }.freeze + ['KZT/RUB', 'USD/RUB', 'EUR/RUB', 'UAH/RUB', 'UZS/RUB', 'AZN/RUB', 'BYN/RUB', 'TRY/RUB', 'THB/RUB', 'IDR/RUB', 'CNY/RUB', 'INR/RUB'].map { |cp| Gera::CurrencyPair.new cp }.freeze end end end diff --git a/app/models/gera/rate_source_cbr_avg.rb b/app/models/gera/rate_source_cbr_avg.rb index 4e37304d..c1ab365d 100644 --- a/app/models/gera/rate_source_cbr_avg.rb +++ b/app/models/gera/rate_source_cbr_avg.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true module Gera - class RateSourceCBRAvg < RateSourceCBR + class RateSourceCbrAvg < RateSourceCbr end end diff --git a/app/models/gera/rate_source_cryptomus.rb b/app/models/gera/rate_source_cryptomus.rb new file mode 100644 index 00000000..fe6fa5c3 --- /dev/null +++ b/app/models/gera/rate_source_cryptomus.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Gera + class RateSourceCryptomus < RateSource + def self.supported_currencies + %i[RUB USD BTC LTC ETH DSH KZT XMR BCH EUR USDT UAH TRX DOGE BNB TON UZS AZN BYN SOL USDC TRY MATIC].map { |m| Money::Currency.find! m } + end + end +end diff --git a/app/models/gera/rate_source_exmo.rb b/app/models/gera/rate_source_exmo.rb index 5b89cda5..11c70648 100644 --- a/app/models/gera/rate_source_exmo.rb +++ b/app/models/gera/rate_source_exmo.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true module Gera - class RateSourceEXMO < RateSource + class RateSourceExmo < RateSource def self.supported_currencies - %i[BTC BCH DSH ETH ETC LTC XRP XMR USD RUB ZEC EUR USDT NEO EOS ADA XEM WAVES TRX DOGE].map { |m| Money::Currency.find! m } + %i[BTC BCH DSH ETH ETC LTC XRP XMR USD RUB ZEC EUR USDT NEO EOS ADA XEM WAVES TRX DOGE TON].map { |m| Money::Currency.find! m } end end end diff --git a/app/models/gera/rate_source_ff_fixed.rb b/app/models/gera/rate_source_ff_fixed.rb new file mode 100644 index 00000000..e5a96984 --- /dev/null +++ b/app/models/gera/rate_source_ff_fixed.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Gera + class RateSourceFfFixed < RateSource + def self.supported_currencies + %i[BTC BCH DSH ETH ETC LTC XRP XMR ZEC NEO EOS ADA XEM WAVES TRX DOGE BNB XLM DOT USDT UNI LINK SOL USDC MATIC TON].map { |m| Money::Currency.find! m } + end + end +end diff --git a/app/models/gera/rate_source_ff_float.rb b/app/models/gera/rate_source_ff_float.rb new file mode 100644 index 00000000..aa0ba899 --- /dev/null +++ b/app/models/gera/rate_source_ff_float.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Gera + class RateSourceFfFloat < RateSource + def self.supported_currencies + %i[BTC BCH DSH ETH ETC LTC XRP XMR ZEC NEO EOS ADA XEM WAVES TRX DOGE BNB XLM DOT USDT UNI LINK SOL USDC MATIC TON].map { |m| Money::Currency.find! m } + end + end +end diff --git a/app/models/gera/rate_source_garantexio.rb b/app/models/gera/rate_source_garantexio.rb new file mode 100644 index 00000000..89e6fdb8 --- /dev/null +++ b/app/models/gera/rate_source_garantexio.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Gera + class RateSourceGarantexio < RateSource + def self.supported_currencies + %i[USDT BTC RUB].map { |m| Money::Currency.find! m } + end + end +end diff --git a/app/models/gera/target_autorate_setting.rb b/app/models/gera/target_autorate_setting.rb new file mode 100644 index 00000000..9cef13c0 --- /dev/null +++ b/app/models/gera/target_autorate_setting.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Gera + class TargetAutorateSetting < ApplicationRecord + belongs_to :exchange_rate, class_name: 'Gera::ExchangeRate' + + def could_be_calculated? + position_from.present? && position_to.present? && autorate_from.present? && autorate_to.present? + end + end +end diff --git a/app/services/gera/autorate_calculators/base.rb b/app/services/gera/autorate_calculators/base.rb new file mode 100644 index 00000000..1b911ced --- /dev/null +++ b/app/services/gera/autorate_calculators/base.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'active_support/core_ext/module/delegation' +require 'active_support/core_ext/object/blank' + +module Gera + module AutorateCalculators + class Base + include Virtus.model strict: true + + AUTO_COMISSION_GAP = 0.0001 + # Количество знаков после запятой для комиссии + COMMISSION_PRECISION = 4 + + attribute :exchange_rate + attribute :external_rates + + delegate :position_from, :position_to, :autorate_from, :autorate_to, + to: :exchange_rate + + def call + raise NotImplementedError, "#{self.class}#call must be implemented" + end + + protected + + # Округление комиссии до заданной точности + # @param value [Numeric] значение комиссии + # @raise [ArgumentError] если value nil + def round_commission(value) + raise ArgumentError, "Cannot round nil commission value" if value.nil? + + value.round(COMMISSION_PRECISION) + end + + def could_be_calculated? + !external_rates.nil? && exchange_rate.target_autorate_setting&.could_be_calculated? + end + + def external_rates_in_target_position + return nil unless external_rates.present? + + external_rates[(position_from - 1)..(position_to - 1)] + end + + def external_rates_in_target_comission + return [] unless external_rates_in_target_position.present? + + external_rates_in_target_position.compact.select do |rate| + (autorate_from..autorate_to).include?(rate.target_rate_percent) + end + end + end + end +end diff --git a/app/services/gera/autorate_calculators/legacy.rb b/app/services/gera/autorate_calculators/legacy.rb new file mode 100644 index 00000000..d2b21d02 --- /dev/null +++ b/app/services/gera/autorate_calculators/legacy.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Gera + module AutorateCalculators + # Legacy калькулятор - сохраняет текущее поведение. + # Вычитает фиксированный GAP из комиссии первого конкурента в диапазоне. + # Может "перепрыгивать" позиции выше целевого диапазона. + class Legacy < Base + def call + return 0 unless could_be_calculated? + return autorate_from unless external_rates_in_target_position.present? + return autorate_from if external_rates_in_target_comission.empty? + + external_rates_in_target_comission.first.target_rate_percent - AUTO_COMISSION_GAP + end + end + end +end diff --git a/app/services/gera/autorate_calculators/position_aware.rb b/app/services/gera/autorate_calculators/position_aware.rb new file mode 100644 index 00000000..f61f98b4 --- /dev/null +++ b/app/services/gera/autorate_calculators/position_aware.rb @@ -0,0 +1,238 @@ +# frozen_string_literal: true + +module Gera + module AutorateCalculators + # Калькулятор с учётом позиций выше целевого диапазона. + # Гарантирует, что обменник займёт позицию внутри диапазона position_from..position_to, + # а не перепрыгнет выше. + # + # Поддерживает: + # - UC-6: Адаптивный GAP для плотных рейтингов + # - UC-8: Исключение своего обменника из расчёта + # - UC-12: Не вычитать GAP при одинаковых курсах (для любого position_from) + # - UC-13: Защита от перепрыгивания позиции position_from - 1 + # - UC-14: Fallback на первую целевую позицию при отсутствии rate_above (issue #83) + # + # ОТМЕНЕНО: + # - UC-9: Защита от аномалий по медиане (не работает с отрицательными курсами) + class PositionAware < Base + # Минимальный GAP (используется когда разница между позициями меньше стандартного) + # Должен быть меньше AUTO_COMISSION_GAP чтобы адаптивная логика работала + MIN_GAP = 0.00001 + + def call + debug_log("START position_from=#{position_from} position_to=#{position_to}") + debug_log("autorate_from=#{autorate_from} autorate_to=#{autorate_to}") + + unless could_be_calculated? + warn_log("SKIP: could_be_calculated?=false, exchange_rate_id=#{exchange_rate&.id}") + return 0 + end + + # UC-8: Фильтрация своего обменника + filtered = filtered_external_rates + debug_log("Filtered rates count: #{filtered&.size}, our_exchanger_id: #{Gera.our_exchanger_id}") + + unless filtered.present? + debug_log("RETURN autorate_from (no filtered rates)") + return autorate_from + end + + rates_in_target_position = filtered[(position_from - 1)..(position_to - 1)] + debug_log("Target position rates [#{position_from - 1}..#{position_to - 1}]: #{rates_in_target_position&.compact&.map(&:target_rate_percent)&.first(5)}") + + unless rates_in_target_position.present? + debug_log("RETURN autorate_from (no rates in target position)") + return autorate_from + end + + valid_rates = rates_in_target_position.compact.select do |rate| + (autorate_from..autorate_to).include?(rate.target_rate_percent) + end + + if valid_rates.empty? + debug_log("RETURN autorate_from (no valid rates in commission range)") + return autorate_from + end + + target_rate = valid_rates.first + debug_log("Target rate: #{target_rate.target_rate_percent}") + + # UC-12: При одинаковых курсах не вычитаем GAP (для любого position_from) + if should_skip_gap?(filtered, target_rate) + debug_log("UC-12: Skipping GAP, RETURN #{target_rate.target_rate_percent}") + return round_commission(target_rate.target_rate_percent) + end + + # UC-6: Адаптивный GAP + gap = calculate_adaptive_gap(filtered, target_rate) + debug_log("Calculated GAP: #{gap}") + + target_comission = round_commission(target_rate.target_rate_percent - gap) + debug_log("Target comission after GAP: #{target_comission}") + + # Проверяем, не перепрыгнем ли мы позицию выше position_from + adjusted_comission = adjust_for_position_above(target_comission, target_rate, filtered) + debug_log("Adjusted comission: #{adjusted_comission}") + + result = round_commission(adjusted_comission) + debug_log("FINAL RESULT: #{result}") + result + end + + private + + # Условное логирование (включается через Gera.autorate_debug_enabled). + # Использует уровень warn для видимости в production-логах. + def debug_log(message) + return unless Gera.autorate_debug_enabled + + log_message(message) + end + + # Постоянное логирование важных бизнес-событий (не зависит от autorate_debug_enabled). + # Используется для аварийных путей (fallback failures), требующих внимания. + def warn_log(message) + log_message(message) + end + + # Общий метод логирования с fallback в STDERR когда Rails недоступен + def log_message(message) + formatted = "[PositionAware] #{message}" + if defined?(Rails) && Rails.logger + Rails.logger.warn { formatted } + else + warn formatted + end + end + + # UC-12: Проверяем, нужно ли пропустить вычитание GAP + # Если курс на целевой позиции равен курсу на соседней позиции - не вычитаем GAP + def should_skip_gap?(rates, target_rate) + if position_from == 1 + # Для position_from=1: сравниваем с позицией НИЖЕ (следующей) + return false if rates.size < 2 + + next_rate = rates[1] + next_rate && next_rate.target_rate_percent == target_rate.target_rate_percent + else + # Для position_from>1: сравниваем с позицией ВЫШЕ (предыдущей) + return false if rates.size < position_from + + rate_above = rates[position_from - 2] + rate_above && rate_above.target_rate_percent == target_rate.target_rate_percent + end + end + + # UC-8: Фильтрация своего обменника + # @raise [ArgumentError] если external_rates nil (должен был быть отфильтрован в could_be_calculated?) + def filtered_external_rates + if external_rates.nil? + raise ArgumentError, "external_rates is nil - should have been caught by could_be_calculated?" + end + + return external_rates unless Gera.our_exchanger_id.present? + + external_rates.reject { |rate| rate&.exchanger_id == Gera.our_exchanger_id } + end + + # UC-6: Адаптивный GAP + def calculate_adaptive_gap(rates, target_rate) + if position_from <= 1 + debug_log("calculate_adaptive_gap: position_from <= 1, using AUTO_COMISSION_GAP") + return AUTO_COMISSION_GAP + end + + rate_above = rates[position_from - 2] + debug_log("calculate_adaptive_gap: rate_above[#{position_from - 2}] = #{rate_above&.target_rate_percent}") + + unless rate_above + debug_log("calculate_adaptive_gap: no rate_above, using AUTO_COMISSION_GAP") + return AUTO_COMISSION_GAP + end + + diff = target_rate.target_rate_percent - rate_above.target_rate_percent + debug_log("calculate_adaptive_gap: diff = #{diff} (target #{target_rate.target_rate_percent} - above #{rate_above.target_rate_percent})") + + # Если разница между позициями меньше стандартного GAP, + # используем половину разницы (но не меньше MIN_GAP) + if diff.positive? && diff < AUTO_COMISSION_GAP + gap = [diff / 2.0, MIN_GAP].max + debug_log("calculate_adaptive_gap: using adaptive gap = #{gap}") + gap + else + debug_log("calculate_adaptive_gap: using AUTO_COMISSION_GAP = #{AUTO_COMISSION_GAP}") + AUTO_COMISSION_GAP + end + end + + # UC-13: Защита от перепрыгивания позиции position_from - 1 + # Если после вычитания GAP наш курс станет лучше чем у позиции выше — корректируем + # + # UC-14: Если position_from > 1, но позиции выше нет (rate_above = nil) — занимаем + # первую целевую позицию если она в допустимом диапазоне autorate_from..autorate_to + def adjust_for_position_above(target_comission, target_rate, rates) + if position_from <= 1 + debug_log("adjust_for_position_above: position_from <= 1, no adjustment") + return target_comission + end + + # Берём ближайшую позицию выше целевого диапазона + rate_above = rates[position_from - 2] + debug_log("adjust_for_position_above: rate_above[#{position_from - 2}] = #{rate_above&.target_rate_percent}") + + # UC-14: Если позиции выше нет — занимаем первую целевую позицию + unless rate_above + debug_log("adjust_for_position_above: no rate_above, using fallback") + # Постоянное логирование для fallback (важное бизнес-событие) + warn_log("Fallback: no rate_above for position_from=#{position_from}, exchange_rate_id=#{exchange_rate.id}") + return fallback_to_first_target_position(rates) + end + + rate_above_comission = rate_above.target_rate_percent + debug_log("adjust_for_position_above: comparing target_comission (#{target_comission}) < rate_above_comission (#{rate_above_comission}) = #{target_comission < rate_above_comission}") + + # Если после вычитания GAP комиссия станет меньше (выгоднее) чем у позиции выше - + # мы перепрыгнём её. Нужно скорректировать. + if target_comission < rate_above_comission + # Устанавливаем комиссию равную позиции выше (не перепрыгиваем) + debug_log("adjust_for_position_above: ADJUSTING to rate_above_comission = #{rate_above_comission}") + return rate_above_comission + end + + debug_log("adjust_for_position_above: no adjustment needed") + target_comission + end + + # UC-14 (issue #83): Fallback на первую целевую позицию при отсутствии позиций выше. + # При position_from > 1 и rate_above = nil — ВСЕГДА используем курс первой целевой позиции, + # если он в допустимом диапазоне autorate_from..autorate_to. Иначе — autorate_from. + # + # Edge cases: + # 1. SUCCESS: first_target_rate существует и в диапазоне → round_commission(target) + # 2. FAIL: first_target_rate = nil (нет данных на позиции) → autorate_from + warn_log + # 3. FAIL: курс вне диапазона autorate_from..autorate_to → autorate_from + warn_log + def fallback_to_first_target_position(rates) + first_target_rate = rates[position_from - 1] + + unless first_target_rate + warn_log("Fallback FAILED: first_target_rate is nil for position_from=#{position_from}, exchange_rate_id=#{exchange_rate.id}") + return autorate_from + end + + first_target_comission = first_target_rate.target_rate_percent + debug_log("fallback: first_target_rate[#{position_from - 1}] = #{first_target_comission}") + + # Проверяем что первая целевая позиция в допустимом диапазоне + unless (autorate_from..autorate_to).include?(first_target_comission) + warn_log("Fallback FAILED: first_target=#{first_target_comission} out of range [#{autorate_from}..#{autorate_to}], exchange_rate_id=#{exchange_rate.id}") + return autorate_from + end + + # Всегда возвращаем курс первой целевой позиции (с округлением для консистентности) + debug_log("fallback: using first_target_comission = #{first_target_comission}") + round_commission(first_target_comission) + end + end + end +end diff --git a/app/services/gera/rate_comission_calculator.rb b/app/services/gera/rate_comission_calculator.rb new file mode 100644 index 00000000..f23c13f7 --- /dev/null +++ b/app/services/gera/rate_comission_calculator.rb @@ -0,0 +1,182 @@ +# frozen_string_literal: true + +module Gera + class RateComissionCalculator + include Virtus.model strict: true + + # AUTO_COMISSION_GAP теперь определён в AutorateCalculators::Base + NOT_ALLOWED_COMISSION_RANGE = (0.7..1.4) + EXCLUDED_PS_IDS = [54, 56] + + attribute :exchange_rate + attribute :external_rates + + delegate :in_currency, :payment_system_from, :payment_system_to, + :out_currency, :fixed_comission, :position_from, + :position_to, :autorate_from, :autorate_to, to: :exchange_rate + + def auto_comission + calculate_allowed_comission(commission) + end + + def auto_comission_by_reserve + average(auto_rate_by_reserve_from, auto_rate_by_reserve_to) + end + + def comission_by_base_rate + average(auto_rate_by_base_from, auto_rate_by_base_to) + end + + def auto_rate_by_base_from + return 0.0 unless auto_rates_by_base_rate_ready? + + calculate_auto_rate_by_base_rate_min_boundary + end + + def auto_rate_by_base_to + return 0.0 unless auto_rates_by_base_rate_ready? + + calculate_auto_rate_by_base_rate_max_boundary + end + + def auto_rate_by_reserve_from + return 0.0 unless auto_rates_by_reserve_ready? + + calculate_auto_rate_by_reserve_min_boundary + end + + def auto_rate_by_reserve_to + return 0.0 unless auto_rates_by_reserve_ready? + + calculate_auto_rate_by_reserve_max_boundary + end + + def current_base_rate + return 1.0 if same_currencies? + + @current_base_rate ||= Gera::CurrencyRateHistoryInterval.where(cur_from_id: in_currency.local_id, cur_to_id: out_currency.local_id).last.avg_rate + end + + def average_base_rate + return 1.0 if same_currencies? + + @average_base_rate ||= Gera::CurrencyRateHistoryInterval.where('interval_from > ?', DateTime.now.utc - 24.hours).where(cur_from_id: in_currency.local_id, cur_to_id: out_currency.local_id).average(:avg_rate) + end + + def auto_comission_from + @auto_comission_from ||= auto_rate_by_reserve_from + auto_rate_by_base_from + end + + def auto_comission_to + @auto_comission_to_boundary ||= auto_rate_by_reserve_to + auto_rate_by_base_to + end + + def bestchange_delta + auto_comission_by_external_comissions + end + + private + + def auto_rates_by_reserve_ready? + income_auto_rate_setting&.reserves_positive? && outcome_auto_rate_setting&.reserves_positive? && income_reserve_checkpoint.present? && outcome_reserve_checkpoint.present? + end + + def auto_rates_by_base_rate_ready? + income_base_rate_checkpoint.present? && outcome_base_rate_checkpoint.present? + end + + def income_auto_rate_setting + @income_auto_rate_setting ||= payment_system_from.auto_rate_settings.detect { |s| s.direction == 'income' } + end + + def outcome_auto_rate_setting + @outcome_auto_rate_setting ||= payment_system_to.auto_rate_settings.detect { |s| s.direction == 'outcome' } + end + + def income_reserve_checkpoint + @income_reserve_checkpoint ||= income_auto_rate_setting && + income_auto_rate_setting.checkpoint( + base_value: income_auto_rate_setting&.reserve, + additional_value: income_auto_rate_setting&.base, + type: 'reserve' + ) + end + + def outcome_reserve_checkpoint + @outcome_reserve_checkpoint ||= outcome_auto_rate_setting && outcome_auto_rate_setting.checkpoint( + base_value: outcome_auto_rate_setting&.reserve, + additional_value: outcome_auto_rate_setting&.base, + type: 'reserve' + ) + end + + def income_base_rate_checkpoint + @income_base_rate_checkpoint ||= income_auto_rate_setting && income_auto_rate_setting.checkpoint( + base_value: current_base_rate, + additional_value: average_base_rate, + type: 'by_base_rate' + ) + end + + def outcome_base_rate_checkpoint + @outcome_base_rate_checkpoint ||= outcome_auto_rate_setting && outcome_auto_rate_setting.checkpoint( + base_value: current_base_rate, + additional_value: average_base_rate, + type: 'by_base_rate' + ) + end + + def calculate_auto_rate_by_reserve_min_boundary + average(income_reserve_checkpoint.min_boundary, outcome_reserve_checkpoint.min_boundary) + end + + def calculate_auto_rate_by_reserve_max_boundary + average(income_reserve_checkpoint.max_boundary, outcome_reserve_checkpoint.max_boundary) + end + + def calculate_auto_rate_by_base_rate_min_boundary + average(income_base_rate_checkpoint.min_boundary, outcome_base_rate_checkpoint.min_boundary) + end + + def calculate_auto_rate_by_base_rate_max_boundary + average(income_base_rate_checkpoint.max_boundary, outcome_base_rate_checkpoint.max_boundary) + end + + def average(a, b) + ((a + b) / 2.0).round(2) + end + + def commission + @commission ||= auto_comission_by_external_comissions + auto_comission_by_reserve + comission_by_base_rate + end + + def auto_commision_range + @auto_commision_range ||= (auto_comission_from..auto_comission_to) + end + + def auto_comission_by_external_comissions + @auto_comission_by_external_comissions ||= begin + calculator = exchange_rate.autorate_calculator_class.new( + exchange_rate: exchange_rate, + external_rates: external_rates + ) + calculator.call + rescue StandardError => e + Rails.logger.error do + "[RateComissionCalculator] Calculator failed for exchange_rate_id=#{exchange_rate.id}, " \ + "calculator=#{exchange_rate.autorate_calculator_class}, error=#{e.class}: #{e.message}\n" \ + "Backtrace:\n#{e.backtrace&.first(10)&.join("\n")}" + end + exchange_rate.autorate_from || 0 + end + end + + def calculate_allowed_comission(comission) + comission + end + + def same_currencies? + in_currency == out_currency + end + end +end diff --git a/app/workers/concerns/gera/rates_worker.rb b/app/workers/concerns/gera/rates_worker.rb index 38bd82a9..4abc664a 100644 --- a/app/workers/concerns/gera/rates_worker.rb +++ b/app/workers/concerns/gera/rates_worker.rb @@ -4,35 +4,22 @@ require 'rest-client' module Gera - # Import rates from all sources - # module RatesWorker - Error = Class.new StandardError + Error = Class.new(StandardError) def perform - # Alternative approach is `Model.uncached do` + logger.debug "RatesWorker: before perform for #{rate_source.class.name}" ActiveRecord::Base.connection.clear_query_cache - rates # Load before a translaction - - rate_source.with_lock do - create_snapshot - rates.each do |pair, data| - save_rate pair, data - end - rate_source.update actual_snapshot_id: snapshot.id - end - - CurrencyRatesWorker.new.perform - - snapshot.id - - # EXMORatesWorker::Error: Error 40016: Maintenance work in progress + @rates = load_rates + create_rate_source_snapshot + save_all_rates + rate_source_snapshot.id rescue ActiveRecord::RecordNotUnique, RestClient::TooManyRequests => error raise error if Rails.env.test? logger.error error - Bugsnag.notify error do |b| + Bugsnag.notify(error) do |b| b.severity = :warning b.meta_data = { error: error } end @@ -40,48 +27,34 @@ def perform private - attr_reader :snapshot - - delegate :actual_for, to: :snapshot - - def create_snapshot - @snapshot ||= rate_source.snapshots.create! actual_for: Time.zone.now - end + attr_reader :rate_source_snapshot, :rates + delegate :actual_for, to: :rate_source_snapshot - def rates - @rates ||= load_rates + def create_rate_source_snapshot + @rate_source_snapshot ||= rate_source.snapshots.create!(actual_for: Time.zone.now) end - def create_external_rates(currency_pair, data, sell_price:, buy_price:) - logger.warn "Ignore #{currency_pair}" unless CurrencyPair.all.include? currency_pair - - logger.info "save_rate_for_date #{actual_for}, #{currency_pair} #{data}" - ExternalRate.create!( - currency_pair: currency_pair, - snapshot: snapshot, - source: rate_source, - rate_value: buy_price.to_f - ) - ExternalRate.create!( - currency_pair: currency_pair.inverse, - snapshot: snapshot, - source: rate_source, - rate_value: 1.0 / sell_price.to_f + def save_all_rates + batched_rates = rates.each_with_object({}) do |(pair, data), hash| + buy_key, sell_key = rate_keys.values_at(:buy, :sell) + + buy_price = data.is_a?(Array) ? data[buy_key] : data[buy_key.to_s] + sell_price = data.is_a?(Array) ? data[sell_key] : data[sell_key.to_s] + + next unless buy_price && sell_price + + hash[pair] = { buy: buy_price.to_f, sell: sell_price.to_f } + end + + ExternalRatesBatchWorker.perform_async( + rate_source_snapshot.id, + rate_source.id, + batched_rates ) - rescue ActiveRecord::RecordNotUnique => err - raise error if Rails.env.test? + end - if err.message.include? 'external_rates_unique_index' - logger.debug "save_rate_for_date: #{actual_for} , #{currency_pair} -> #{err}" - if defined? Bugsnag - Bugsnag.notify 'Try to rewrite rates' do |b| - b.meta_data = { actual_for: actual_for, snapshot_id: snapshot.id, currency_pair: currency_pair } - end - end - else - logger.error "save_rate_for_date: #{actual_for} , #{pair} -> #{err}" - raise error - end + def rate_keys + raise NotImplementedError, 'You must define #rate_keys in your worker' end end end diff --git a/app/workers/gera/binance_rates_worker.rb b/app/workers/gera/binance_rates_worker.rb new file mode 100644 index 00000000..77cdec70 --- /dev/null +++ b/app/workers/gera/binance_rates_worker.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Gera + class BinanceRatesWorker + include Sidekiq::Worker + include AutoLogger + include RatesWorker + + sidekiq_options lock: :until_executed + + private + + def rate_source + @rate_source ||= RateSourceBinance.get! + end + + def load_rates + BinanceFetcher.new.perform + end + + def rate_keys + { buy: 'bidPrice', sell: 'askPrice' } + end + end +end diff --git a/app/workers/gera/bitfinex_rates_worker.rb b/app/workers/gera/bitfinex_rates_worker.rb index 08e30ce7..58e3174e 100644 --- a/app/workers/gera/bitfinex_rates_worker.rb +++ b/app/workers/gera/bitfinex_rates_worker.rb @@ -6,33 +6,7 @@ module Gera class BitfinexRatesWorker include Sidekiq::Worker include AutoLogger - - prepend RatesWorker - - # Stolen from: https://api.bitfinex.com/v1/symbols - AVAILABLE_TICKETS = %i[btcusd ltcusd ltcbtc ethusd ethbtc etcbtc etcusd rrtusd rrtbtc zecusd zecbtc xmrusd xmrbtc dshusd dshbtc btceur btcjpy xrpusd - xrpbtc iotusd iotbtc ioteth eosusd eosbtc eoseth sanusd sanbtc saneth omgusd omgbtc omgeth neousd neobtc neoeth etpusd etpbtc - etpeth qtmusd qtmbtc qtmeth avtusd avtbtc avteth edousd edobtc edoeth btgusd btgbtc datusd datbtc dateth qshusd qshbtc qsheth - yywusd yywbtc yyweth gntusd gntbtc gnteth sntusd sntbtc snteth ioteur batusd batbtc bateth mnausd mnabtc mnaeth funusd funbtc - funeth zrxusd zrxbtc zrxeth tnbusd tnbbtc tnbeth spkusd spkbtc spketh trxusd trxbtc trxeth rcnusd rcnbtc rcneth rlcusd rlcbtc - rlceth aidusd aidbtc aideth sngusd sngbtc sngeth repusd repbtc repeth elfusd elfbtc elfeth btcgbp etheur ethjpy ethgbp neoeur - neojpy neogbp eoseur eosjpy eosgbp iotjpy iotgbp iosusd iosbtc ioseth aiousd aiobtc aioeth requsd reqbtc reqeth rdnusd rdnbtc - rdneth lrcusd lrcbtc lrceth waxusd waxbtc waxeth daiusd daibtc daieth agiusd agibtc agieth bftusd bftbtc bfteth mtnusd mtnbtc - mtneth odeusd odebtc odeeth antusd antbtc anteth dthusd dthbtc dtheth mitusd mitbtc miteth stjusd stjbtc stjeth xlmusd xlmeur - xlmjpy xlmgbp xlmbtc xlmeth xvgusd xvgeur xvgjpy xvggbp xvgbtc xvgeth bciusd bcibtc mkrusd mkrbtc mkreth kncusd kncbtc knceth - poausd poabtc poaeth lymusd lymbtc lymeth utkusd utkbtc utketh veeusd veebtc veeeth dadusd dadbtc dadeth orsusd orsbtc orseth - aucusd aucbtc auceth poyusd poybtc poyeth fsnusd fsnbtc fsneth cbtusd cbtbtc cbteth zcnusd zcnbtc zcneth senusd senbtc seneth - ncausd ncabtc ncaeth cndusd cndbtc cndeth ctxusd ctxbtc ctxeth paiusd paibtc seeusd seebtc seeeth essusd essbtc esseth atmusd - atmbtc atmeth hotusd hotbtc hoteth dtausd dtabtc dtaeth iqxusd iqxbtc iqxeos wprusd wprbtc wpreth zilusd zilbtc zileth bntusd - bntbtc bnteth absusd abseth xrausd xraeth manusd maneth bbnusd bbneth niousd nioeth dgxusd dgxeth vetusd vetbtc veteth utnusd - utneth tknusd tkneth gotusd goteur goteth xtzusd xtzbtc cnnusd cnneth boxusd boxeth trxeur trxgbp trxjpy mgousd mgoeth rteusd - rteeth yggusd yggeth mlnusd mlneth wtcusd wtceth csxusd csxeth omnusd omnbtc intusd inteth drnusd drneth pnkusd pnketh dgbusd - dgbbtc bsvusd bsvbtc babusd babbtc wlousd wloxlm vldusd vldeth enjusd enjeth onlusd onleth rbtusd rbtbtc ustusd euteur eutusd - gsdusd udcusd tsdusd paxusd rifusd rifbtc pasusd paseth vsyusd vsybtc zrxdai mkrdai omgdai bttusd bttbtc btcust ethust clousd - clobtc].freeze - - # NOTE: formar tickers neousd neobtc neoeth neoeur - TICKERS = %i[].freeze + include RatesWorker private @@ -40,26 +14,14 @@ def rate_source @rate_source ||= RateSourceBitfinex.get! end - # {"mid":"8228.25", - # "bid":"8228.2", - # "ask":"8228.3", - # "last_price":"8228.3", - # "low":"8055.0", - # "high":"8313.3", - # "volume":"13611.826947359996", - # "timestamp":"1532874580.9087598"} - def save_rate(ticker, data) - currency_pair = pair_from_ticker ticker - create_external_rates currency_pair, data, sell_price: data['high'], buy_price: data['low'] + def load_rates + BitfinexFetcher.new.perform end - def pair_from_ticker(ticker) - ticker = ticker.to_s - CurrencyPair.new ticker[0, 3], ticker[3, 3] - end + # ["tXMRBTC", 0.0023815, 1026.97384923, 0.0023839, 954.7667526, -0.0000029, -0.00121619, 0.0023816, 3944.20608752, 0.0024229, 0.0022927] - def load_rates - TICKERS.each_with_object({}) { |ticker, ag| ag[ticker] = BitfinexFetcher.new(ticker: ticker).perform } + def rate_keys + { buy: 7, sell: 7 } end end end diff --git a/app/workers/gera/bybit_rates_worker.rb b/app/workers/gera/bybit_rates_worker.rb new file mode 100644 index 00000000..cb994876 --- /dev/null +++ b/app/workers/gera/bybit_rates_worker.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Gera + # Import rates from Bybit + # + class BybitRatesWorker + include Sidekiq::Worker + include AutoLogger + include RatesWorker + + private + + def rate_source + @rate_source ||= RateSourceBybit.get! + end + + def load_rates + BybitFetcher.new.perform + end + + def rate_keys + { buy: 'price', sell: 'price' } + end + end +end diff --git a/app/workers/gera/cbr_avg_rates_worker.rb b/app/workers/gera/cbr_avg_rates_worker.rb index ebf82330..b86db088 100644 --- a/app/workers/gera/cbr_avg_rates_worker.rb +++ b/app/workers/gera/cbr_avg_rates_worker.rb @@ -1,24 +1,26 @@ # frozen_string_literal: true module Gera - class CBRAvgRatesWorker + class CbrAvgRatesWorker include Sidekiq::Worker include AutoLogger + sidekiq_options lock: :until_executed + def perform ActiveRecord::Base.connection.clear_query_cache - source.with_lock do + ActiveRecord::Base.transaction do source.available_pairs.each do |pair| create_rate pair end - source.update_attribute :actual_snapshot_id, snapshot.id end + source.update_column :actual_snapshot_id, snapshot.id end private - + def source - @source ||= RateSourceCBRAvg.get! + @source ||= RateSourceCbrAvg.get! end def snapshot diff --git a/app/workers/gera/cbr_rates_worker.rb b/app/workers/gera/cbr_rates_worker.rb index 871997de..f8a0f0fb 100644 --- a/app/workers/gera/cbr_rates_worker.rb +++ b/app/workers/gera/cbr_rates_worker.rb @@ -7,17 +7,27 @@ module Gera # Import rates from Russian Central Bank # http://www.cbr.ru/scripts/XML_daily.asp?date_req=08/04/2018 # - class CBRRatesWorker + class CbrRatesWorker include Sidekiq::Worker include AutoLogger - CURRENCIES = %w[USD KZT EUR UAH].freeze + # sidekiq_options lock: :until_executed + + CURRENCIES = %w[USD KZT EUR UAH UZS AZN BYN TRY THB IDR CNY INR].freeze CBR_IDS = { 'USD' => 'R01235', 'KZT' => 'R01335', 'EUR' => 'R01239', - 'UAH' => 'R01720' + 'UAH' => 'R01720', + 'UZS' => 'R01717', + 'AZN' => 'R01020A', + 'BYN' => 'R01090B', + 'TRY' => 'R01700J', + 'THB' => 'R01675', + 'IDR' => 'R01280', + 'CNY' => 'R01375', + 'INR' => 'R01270' }.freeze ROUND = 15 @@ -25,17 +35,21 @@ class CBRRatesWorker Error = Class.new StandardError WrongDate = Class.new Error - URL = 'http://www.cbr.ru/scripts/XML_daily.asp' + URL = 'https://pay.hub.pp.ru/api/cbr' def perform + logger.debug 'CbrRatesWorker: before perform' ActiveRecord::Base.connection.clear_query_cache - cbr.with_lock do - days.each do |date| - fetch_and_save_rate date + rates_by_date = load_rates + logger.debug 'CbrRatesWorker: before transaction' + ActiveRecord::Base.transaction do + rates_by_date.each do |date, rates| + save_rates(date, rates) end - - make_snapshot end + logger.debug 'CbrRatesWorker: after transaction' + make_snapshot + logger.debug 'CbrRatesWorker: after perform' end private @@ -53,13 +67,14 @@ def avg_snapshot end def make_snapshot - save_snapshot_rate USD, RUB - save_snapshot_rate KZT, RUB - save_snapshot_rate EUR, RUB - save_snapshot_rate UAH, RUB + save_snapshot_rates + + cbr.update_column :actual_snapshot_id, snapshot.id + cbr_avg.update_column :actual_snapshot_id, avg_snapshot.id + end - cbr.update_attribute :actual_snapshot_id, snapshot.id - cbr_avg.update_attribute :actual_snapshot_id, avg_snapshot.id + def save_snapshot_rates + CURRENCIES.each { |cur_from| save_snapshot_rate(cur_from.constantize, RUB) } end def save_snapshot_rate(cur_from, cur_to) @@ -105,6 +120,14 @@ def save_snapshot_rate(cur_from, cur_to) ) end + def cbr_avg + @cbr_avg ||= RateSourceCbrAvg.get! + end + + def cbr + @cbr ||= RateSourceCbr.get! + end + def days today = Date.today logger.info "Start import for #{today}" @@ -119,37 +142,23 @@ def days ].uniq.sort end - def fetch_and_save_rate(date) - fetch_rates date - rescue WrongDate => err - logger.warn err - - # HTTP redirection loop: http://www.cbr.ru/scripts/XML_daily.asp?date_req=09/01/2019 - rescue RuntimeError => err - raise err unless err.message.include? 'HTTP redirection loop' - - logger.error err - end - - def cbr_avg - @cbr_avg ||= RateSourceCBRAvg.get! - end - - def cbr - @cbr ||= RateSourceCBR.get! - end - - def fetch_rates(date) - return if CbrExternalRate.where(date: date, cur_from: currencies.map(&:iso_code)).count == currencies.count - - root = build_root date - - currencies.each do |cur| - save_rate get_rate(root, CBR_IDS[cur.iso_code]), cur, date unless CbrExternalRate.where(date: date, cur_from: cur.iso_code).exists? + def load_rates + rates_by_date = {} + days.each do |date| + rates_by_date[date] = fetch_rates(date) + rescue WrongDate => err + logger.warn err + + # HTTP redirection loop: http://www.cbr.ru/scripts/XML_daily.asp?date_req=09/01/2019 + rescue RuntimeError => err + raise err unless err.message.include? 'HTTP redirection loop' + + logger.error err end + rates_by_date end - def build_root(date) + def fetch_rates(date) uri = URI.parse URL uri.query = 'date_req=' + date.strftime('%d/%m/%Y') @@ -166,6 +175,14 @@ def build_root(date) raise WrongDate, "Request and response dates are different #{uri}: #{validate_date} <> #{root_date}" end + def save_rates(date, rates) + return if CbrExternalRate.where(date: date, cur_from: currencies.map(&:iso_code)).count == currencies.count + + currencies.each do |cur| + save_rate get_rate(rates, CBR_IDS[cur.iso_code]), cur, date unless CbrExternalRate.where(date: date, cur_from: cur.iso_code).exists? + end + end + def get_rate(root, id) valute = root.xpath("Valute[@ID=\"#{id}\"]") original_rate = valute.xpath('Value').text.sub(',', '.').to_f diff --git a/app/workers/gera/create_history_intervals_worker.rb b/app/workers/gera/create_history_intervals_worker.rb index 63d1b6d3..9ccd89ac 100644 --- a/app/workers/gera/create_history_intervals_worker.rb +++ b/app/workers/gera/create_history_intervals_worker.rb @@ -5,6 +5,8 @@ class CreateHistoryIntervalsWorker include Sidekiq::Worker include AutoLogger + sidekiq_options lock: :until_executed + MAXIMAL_DATE = 30.minutes MINIMAL_DATE = Time.parse('13-07-2018 18:00') diff --git a/app/workers/gera/cryptomus_rates_worker.rb b/app/workers/gera/cryptomus_rates_worker.rb new file mode 100644 index 00000000..94a5a58c --- /dev/null +++ b/app/workers/gera/cryptomus_rates_worker.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Gera + class CryptomusRatesWorker + include Sidekiq::Worker + include AutoLogger + include RatesWorker + + private + + def rate_source + @rate_source ||= RateSourceCryptomus.get! + end + + def load_rates + CryptomusFetcher.new.perform + end + + def rate_keys + { buy: 'course', sell: 'course' } + end + end +end diff --git a/app/workers/gera/currency_rates_worker.rb b/app/workers/gera/currency_rates_worker.rb index b9a254c7..0c805041 100644 --- a/app/workers/gera/currency_rates_worker.rb +++ b/app/workers/gera/currency_rates_worker.rb @@ -12,53 +12,49 @@ class CurrencyRatesWorker def perform logger.info 'start' - CurrencyRate.transaction do - @snapshot = create_snapshot - - Gera::CurrencyPair.all.each do |pair| - create_rate pair - end + snapshot = create_snapshot + CurrencyPair.all.each { |pair| create_rate(pair: pair, snapshot: snapshot) } end - logger.info 'finish' - DirectionsRatesWorker.perform_async - true end private - attr_reader :snapshot - def create_snapshot - CurrencyRateSnapshot.create! currency_rate_mode_snapshot: Universe.currency_rate_modes_repository.snapshot + CurrencyRateSnapshot.create!(currency_rate_mode_snapshot: currency_rates.snapshot) end - def create_rate(pair) - crm = Universe.currency_rate_modes_repository.find_currency_rate_mode_by_pair pair - - logger.debug "build_rate(#{pair}, #{crm || :default})" - - crm ||= CurrencyRateMode.new(currency_pair: pair, mode: :auto).freeze - - cr = crm.build_currency_rate + def currency_rates + Universe.currency_rate_modes_repository + end - raise Error, "Can not calculate rate of #{pair} for mode '#{crm.try :mode}'" unless cr.present? + def create_rate(pair:, snapshot:) + currency_rate_mode = find_currency_rate_mode_by_pair(pair) + logger.debug "build_rate(#{pair}, #{currency_rate_mode})" + currency_rate = currency_rate_mode.build_currency_rate + raise Error, "Unable to calculate rate for #{pair} and mode '#{currency_rate_mode.mode}'" unless currency_rate.present? - cr.snapshot = snapshot - cr.save! + currency_rate.snapshot = snapshot + currency_rate.save! + rescue RateSource::RateNotFound => err + logger.error err rescue StandardError => err raise err if !err.is_a?(Error) && Rails.env.test? - logger.error err - Rails.logger.error err if Rails.env.development? + if defined? Bugsnag Bugsnag.notify err do |b| b.meta_data = { pair: pair } end end end + + def find_currency_rate_mode_by_pair(pair) + currency_rates.find_currency_rate_mode_by_pair(pair) || + CurrencyRateMode.default_for_pair(pair).freeze + end end end diff --git a/app/workers/gera/directions_rates_worker.rb b/app/workers/gera/directions_rates_worker.rb index b206b380..38457803 100644 --- a/app/workers/gera/directions_rates_worker.rb +++ b/app/workers/gera/directions_rates_worker.rb @@ -8,7 +8,7 @@ class DirectionsRatesWorker Error = Class.new StandardError - sidekiq_options queue: :critical + sidekiq_options queue: :critical, lock: :until_executed define_callbacks :perform # exchange_rate_id - ID of changes exchange_rate @@ -16,11 +16,36 @@ class DirectionsRatesWorker def perform(*_args) # exchange_rate_id: nil) logger.info 'start' - run_callbacks :perform do - DirectionRate.transaction do - ExchangeRate.includes(:payment_system_from, :payment_system_to).find_each do |exchange_rate| - safe_create(exchange_rate) + DirectionRateSnapshot.transaction do + rates = ExchangeRate.includes(:target_autorate_setting, payment_system_from: { auto_rate_settings: :auto_rate_checkpoints }, payment_system_to: { auto_rate_settings: :auto_rate_checkpoints }).map do |exchange_rate| + rate_value = Universe.currency_rates_repository.find_currency_rate_by_pair(exchange_rate.currency_pair) + + next unless rate_value + + base_rate_value = rate_value.rate_value + rate_percent = exchange_rate.final_rate_percents + current_time = Time.current + { + ps_from_id: exchange_rate.payment_system_from_id, + ps_to_id: exchange_rate.payment_system_to_id, + snapshot_id: snapshot.id, + exchange_rate_id: exchange_rate.id, + currency_rate_id: rate_value.id, + created_at: current_time, + base_rate_value: base_rate_value, + rate_percent: rate_percent, + rate_value: calculate_finite_rate(base_rate_value, rate_percent) + } + rescue CurrencyRatesRepository::UnknownPair, DirectionRate::UnknownExchangeRate => e + logger.warn "[DirectionsRatesWorker] Skipped exchange_rate_id=#{exchange_rate.id}: #{e.class}" + nil + end.compact + + if rates.present? + DirectionRate.insert_all(rates) + else + logger.warn '[DirectionsRatesWorker] No rates to insert — all exchange_rates were skipped or had no currency_rate' end end end @@ -35,16 +60,11 @@ def snapshot @snapshot ||= DirectionRateSnapshot.create! end - def safe_create(exchange_rate) - direction_rates.create!( - snapshot: snapshot, - exchange_rate: exchange_rate, - currency_rate: Universe.currency_rates_repository.find_currency_rate_by_pair(exchange_rate.currency_pair) - ) - rescue DirectionRate::UnknownExchangeRate, ActiveRecord::RecordInvalid, CurrencyRatesRepository::UnknownPair => err - logger.error err - Bugsnag.notify err do |b| - b.meta_data = { exchange_rate_id: exchange_rate.id, currency_rate_id: currency_rate.try(:id) } + def calculate_finite_rate(base_rate, comission) + if base_rate <= 1 + base_rate.to_f * (1.0 - comission.to_f/100) + else + base_rate - comission.to_percent end end end diff --git a/app/workers/gera/exchange_rate_updater_worker.rb b/app/workers/gera/exchange_rate_updater_worker.rb new file mode 100644 index 00000000..c729411f --- /dev/null +++ b/app/workers/gera/exchange_rate_updater_worker.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Gera + class ExchangeRateUpdaterWorker + include Sidekiq::Worker + include AutoLogger + + sidekiq_options queue: :exchange_rates + + def perform(exchange_rate_id, attributes) + increment_exchange_rate_touch_metric + ExchangeRate.where(id: exchange_rate_id).update_all(attributes) + end + + private + + def increment_exchange_rate_touch_metric + Yabeda.exchange.exchange_rate_touch_count.increment({ + action: 'update', + source: 'Gera::ExchangeRateUpdaterWorker' + }) + end + end +end diff --git a/app/workers/gera/exmo_rates_worker.rb b/app/workers/gera/exmo_rates_worker.rb index 6cf6981d..be2a9989 100644 --- a/app/workers/gera/exmo_rates_worker.rb +++ b/app/workers/gera/exmo_rates_worker.rb @@ -1,62 +1,23 @@ # frozen_string_literal: true module Gera - # Import rates from EXMO - # - class EXMORatesWorker + class ExmoRatesWorker include Sidekiq::Worker include AutoLogger - - prepend RatesWorker - - URL1 = 'https://api.exmo.com/v1/ticker/' - URL2 = 'https://api.exmo.me/v1/ticker/' - URL = URL2 + include RatesWorker private def rate_source - @rate_source ||= RateSourceEXMO.get! - end - - # data contains - # {"buy_price"=>"8734.99986728", - # "sell_price"=>"8802.299431", - # "last_trade"=>"8789.71226599", - # "high"=>"9367.055011", - # "low"=>"8700.00000001", - # "avg"=>"8963.41293922", - # "vol"=>"330.70358291", - # "vol_curr"=>"2906789.33918745", - # "updated"=>1520415288}, - - def save_rate(raw_pair, data) - # TODO: Best way to move this into ExmoRatesWorker - # - cf, ct = raw_pair.split('_') .map { |c| c == 'DASH' ? 'DSH' : c } - - cur_from = Money::Currency.find cf - unless cur_from - logger.warn "Not supported currency #{cf}" - return - end - - cur_to = Money::Currency.find ct - unless cur_to - logger.warn "Not supported currency #{ct}" - return - end - - currency_pair = CurrencyPair.new cur_from, cur_to - create_external_rates currency_pair, data, sell_price: data['sell_price'], buy_price: data['buy_price'] + @rate_source ||= RateSourceExmo.get! end def load_rates - result = JSON.parse open(URI.parse(URL)).read - raise Error, 'Result is not a hash' unless result.is_a? Hash - raise Error, result['error'] if result['error'].present? + ExmoFetcher.new.perform + end - result + def rate_keys + { buy: 'buy_price', sell: 'sell_price' } end end end diff --git a/app/workers/gera/external_rate_saver_worker.rb b/app/workers/gera/external_rate_saver_worker.rb new file mode 100644 index 00000000..b6112de0 --- /dev/null +++ b/app/workers/gera/external_rate_saver_worker.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module Gera + class ExternalRateSaverWorker + include Sidekiq::Worker + include AutoLogger + + sidekiq_options queue: :external_rates + + def perform(currency_pair, snapshot_id, rate, source_rates_count) + rate_source = find_rate_source(rate) + snapshot = ExternalRateSnapshot.find(snapshot_id) + create_external_rate( + rate_source: rate_source, + snapshot: snapshot, + currency_pair: CurrencyPair.new(currency_pair), + rate_value: rate['value'] + ) + update_actual_snapshot( + rate_source: rate_source, + snapshot: snapshot, + ) if snapshot_filled_up?(snapshot: snapshot, source_rates_count: source_rates_count) + rescue ActiveRecord::RecordNotUnique => err + raise err if Rails.env.test? + end + + private + + def find_rate_source(rate) + rate['source_class_name'].constantize.find(rate['source_id']) + end + + def create_external_rate(rate_source:, snapshot:, currency_pair:, rate_value:) + ExternalRate.create!( + currency_pair: currency_pair, + snapshot: snapshot, + source: rate_source, + rate_value: rate_value + ) + end + + def update_actual_snapshot(rate_source:, snapshot:) + update_actual_snapshot(snapshot: snapshot, rate_source: rate_source) + end + + def snapshot_filled_up?(snapshot:, source_rates_count:) + snapshot.external_rates.count == source_rates_count * 2 + end + + def update_actual_snapshot(snapshot:, rate_source:) + rate_source.update!(actual_snapshot_id: snapshot.id) + end + end +end diff --git a/app/workers/gera/external_rates_batch_worker.rb b/app/workers/gera/external_rates_batch_worker.rb new file mode 100644 index 00000000..5b0c6c7f --- /dev/null +++ b/app/workers/gera/external_rates_batch_worker.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Gera + class ExternalRatesBatchWorker + include Sidekiq::Worker + + def perform(snapshot_id, rate_source_id, rates) + snapshot = ExternalRateSnapshot.find(snapshot_id) + rate_source = RateSource.find(rate_source_id) + + values = rates.flat_map do |pair, prices| + cur_from, cur_to = pair.split('/') + + buy = prices[:buy] || prices['buy'] + sell = prices[:sell] || prices['sell'] + + next if buy.nil? || sell.nil? + + buy = buy.to_f + sell = sell.to_f + next if buy <= 0 || sell <= 0 + + [ + { + snapshot_id: snapshot.id, + source_id: rate_source.id, + cur_from: cur_from, + cur_to: cur_to, + rate_value: buy + }, + { + snapshot_id: snapshot.id, + source_id: rate_source.id, + cur_from: cur_to, + cur_to: cur_from, + rate_value: (1.0 / sell) + } + ] + end.compact + + ExternalRate.insert_all(values) if values.any? + rate_source.update!(actual_snapshot_id: snapshot.id) + end + end +end diff --git a/app/workers/gera/ff_fixed_rates_worker.rb b/app/workers/gera/ff_fixed_rates_worker.rb new file mode 100644 index 00000000..8307b06b --- /dev/null +++ b/app/workers/gera/ff_fixed_rates_worker.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Gera + class FfFixedRatesWorker + include Sidekiq::Worker + include AutoLogger + include RatesWorker + + private + + def rate_source + @rate_source ||= RateSourceFfFixed.get! + end + + def load_rates + FfFixedFetcher.new.perform + end + + def rate_keys + { buy: 'out', sell: 'out' } + end + end +end diff --git a/app/workers/gera/ff_float_rates_worker.rb b/app/workers/gera/ff_float_rates_worker.rb new file mode 100644 index 00000000..606ab6d4 --- /dev/null +++ b/app/workers/gera/ff_float_rates_worker.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Gera + class FfFloatRatesWorker + include Sidekiq::Worker + include AutoLogger + include RatesWorker + + private + + def rate_source + @rate_source ||= RateSourceFfFloat.get! + end + + def load_rates + FfFloatFetcher.new.perform + end + + def rate_keys + { buy: 'out', sell: 'out' } + end + end +end diff --git a/app/workers/gera/garantexio_rates_worker.rb b/app/workers/gera/garantexio_rates_worker.rb new file mode 100644 index 00000000..b71c9f09 --- /dev/null +++ b/app/workers/gera/garantexio_rates_worker.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Gera + class GarantexioRatesWorker + include Sidekiq::Worker + include AutoLogger + include RatesWorker + + private + + def rate_source + @rate_source ||= RateSourceGarantexio.get! + end + + def load_rates + GarantexioFetcher.new.perform + end + + def rate_keys + { buy: 'last_price', sell: 'last_price' } + end + end +end diff --git a/app/workers/gera/purge_currency_rates_worker.rb b/app/workers/gera/purge_currency_rates_worker.rb deleted file mode 100644 index d127151e..00000000 --- a/app/workers/gera/purge_currency_rates_worker.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -module Gera - class PurgeCurrencyRatesWorker - include Sidekiq::Worker - - sidekiq_options queue: :purgers, retry: false - - KEEP_PERIOD = 3.hours - - def perform - currency_rate_snapshots.batch_purge batch_size: 100 - end - - private - - def currency_rate_snapshots - CurrencyRateSnapshot.where.not(id: CurrencyRateSnapshot.last).where('created_at < ?', KEEP_PERIOD.ago) - end - end -end diff --git a/app/workers/gera/purge_direction_rates_worker.rb b/app/workers/gera/purge_direction_rates_worker.rb deleted file mode 100644 index d0303085..00000000 --- a/app/workers/gera/purge_direction_rates_worker.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -module Gera - class PurgeDirectionRatesWorker - include Sidekiq::Worker - - sidekiq_options queue: :purgers, retry: false - - KEEP_PERIOD = 3.hours - - def perform - direction_rate_snapshots.batch_purge - direction_rates.batch_purge - end - - private - - def lock_timeout - 7.days * 1000 - end - - def direction_rate_snapshots - DirectionRateSnapshot.where.not(id: DirectionRateSnapshot.last).where('created_at < ?', KEEP_PERIOD.ago) - end - - def direction_rates - DirectionRate.where.not(id: DirectionRateSnapshot.last.direction_rates.pluck(:id)).where('created_at < ?', KEEP_PERIOD.ago) - end - end -end diff --git a/config/currencies.yml b/config/currencies.yml index 6eda21a9..32e54775 100644 --- a/config/currencies.yml +++ b/config/currencies.yml @@ -537,8 +537,8 @@ doge: name: Dogecoin symbol: Ð alternate_symbols: [] - subunit: Microdoge - subunit_to_unit: 1000000 + subunit: Nanodoge + subunit_to_unit: 1000000000 symbol_first: true html_entity: '' decimal_mark: "." @@ -557,3 +557,493 @@ doge: # минимальная сумма валюты на выдачу (из minGetSumOut) minimal_output_value: 100 + +bnb: + priority: 22 + iso_code: BNB + name: Binance Coin + symbol: ¤ + alternate_symbols: [] + subunit: Gwei + subunit_to_unit: 1000000000 + symbol_first: false + html_entity: '' + decimal_mark: "." + thousands_separator: "," + iso_numeric: '' + smallest_denomination: 1 + authorized_round: 6 + is_crypto: true + + # Местные настройки + # + # Идентфикатор в type_cy + local_id: 24 + + # минимальная сумма валюты на прием (из minGetSum) + minimal_input_value: 10 + + # минимальная сумма валюты на выдачу (из minGetSumOut) + minimal_output_value: 10 + +xlm: + priority: 23 + iso_code: XLM + name: Stellar + symbol: "*" + alternate_symbols: [] + subunit: Stroop + subunit_to_unit: 10000000 + symbol_first: false + html_entity: '' + decimal_mark: "." + thousands_separator: "," + iso_numeric: '' + smallest_denomination: 1 + is_crypto: true + + # Местные настройки + # + # Идентфикатор в type_cy + local_id: 25 + + # минимальная сумма валюты на прием (из minGetSum) + minimal_input_value: 0.0000001 + + # минимальная сумма валюты на выдачу (из minGetSumOut) + minimal_output_value: 1.22 + +dot: + priority: 24 + iso_code: DOT + name: Polkadot + symbol: "P" + alternate_symbols: [] + subunit: Dot + subunit_to_unit: 10000000000 + symbol_first: false + html_entity: '' + decimal_mark: "." + thousands_separator: "," + iso_numeric: '' + smallest_denomination: 1 + is_crypto: true + + # Местные настройки + # + # Идентфикатор в type_cy + local_id: 26 + + # минимальная сумма валюты на прием (из minGetSum) + minimal_input_value: 0.00000001 + + # минимальная сумма валюты на выдачу (из minGetSumOut) + minimal_output_value: 0.012 + +uni: + priority: 25 + iso_code: UNI + name: Uniswap + symbol: + alternate_symbols: [] + subunit: Gwei + subunit_to_unit: 1000000000 + symbol_first: false + html_entity: '' + decimal_mark: "." + thousands_separator: "," + iso_numeric: '' + smallest_denomination: 1 + authorized_round: 6 + is_crypto: true + + # Местные настройки + # + # Идентфикатор в type_cy + local_id: 27 + + # минимальная сумма валюты на прием (из minGetSum) + minimal_input_value: 0.01 + + # минимальная сумма валюты на выдачу (из minGetSumOut) + minimal_output_value: 0.01 + +link: + priority: 26 + iso_code: LINK + name: Chainlink + symbol: + alternate_symbols: [] + subunit: Gwei + subunit_to_unit: 1000000000 + symbol_first: false + html_entity: '' + decimal_mark: "." + thousands_separator: "," + iso_numeric: '' + smallest_denomination: 1 + authorized_round: 6 + is_crypto: true + + # Местные настройки + # + # Идентфикатор в type_cy + local_id: 28 + + # минимальная сумма валюты на прием (из minGetSum) + minimal_input_value: 0.01 + + # минимальная сумма валюты на выдачу (из minGetSumOut) + minimal_output_value: 0.01 + +ton: + priority: 27 + iso_code: TON + name: Ton + symbol: + alternate_symbols: [] + subunit: Nanoton + subunit_to_unit: 1000000000 + symbol_first: false + html_entity: '' + decimal_mark: "." + thousands_separator: "," + iso_numeric: '' + smallest_denomination: 1 + authorized_round: 6 + is_crypto: true + + # Местные настройки + # + # Идентфикатор в type_cy + local_id: 29 + + # минимальная сумма валюты на прием (из minGetSum) + minimal_input_value: 0.01 + + # минимальная сумма валюты на выдачу (из minGetSumOut) + minimal_output_value: 0.01 + +uzs: + priority: 28 + iso_code: UZS + name: Uzbekistan Som + symbol: "so'm" + alternate_symbols: [] + subunit: Tiyin + subunit_to_unit: 100 + symbol_first: false + html_entity: '' + decimal_mark: "," + thousands_separator: "." + iso_numeric: '4217' + smallest_denomination: 1 + is_crypto: false + + # Местные настройки + # + # Идентфикатор в type_cy + local_id: 30 + + # минимальная сумма валюты на прием (из minGetSum) + minimal_input_value: 50000 + + # минимальная сумма валюты на выдачу (из minGetSumOut) + minimal_output_value: 50000 + +azn: + priority: 29 + iso_code: AZN + name: Azerbaijani manat + symbol: 'm' + alternate_symbols: [] + subunit: Gapik + subunit_to_unit: 100 + symbol_first: false + html_entity: '' + decimal_mark: "," + thousands_separator: "." + iso_numeric: '944' + smallest_denomination: 1 + is_crypto: false + + # Местные настройки + # + # Идентфикатор в type_cy + local_id: 31 + + # минимальная сумма валюты на прием (из minGetSum) + minimal_input_value: 5 + + # минимальная сумма валюты на выдачу (из minGetSumOut) + minimal_output_value: 5 + +byn: + priority: 30 + iso_code: BYN + name: Belarusian Ruble + symbol: 'Rbl‎' + alternate_symbols: [] + subunit: Kopeck + subunit_to_unit: 100 + symbol_first: false + html_entity: '' + decimal_mark: "," + thousands_separator: "." + iso_numeric: '4217' + smallest_denomination: 1 + is_crypto: false + + # Местные настройки + # + # Идентфикатор в type_cy + local_id: 32 + + # минимальная сумма валюты на прием (из minGetSum) + minimal_input_value: 4 + + # минимальная сумма валюты на выдачу (из minGetSumOut) + minimal_output_value: 4 + +sol: + priority: 31 + iso_code: SOL + name: Solana + symbol: + alternate_symbols: [] + subunit: Lamport + subunit_to_unit: 1000000000 + symbol_first: false + html_entity: '' + decimal_mark: "," + thousands_separator: "." + iso_numeric: + smallest_denomination: 1 + is_crypto: true + + # Местные настройки + # + # Идентфикатор в type_cy + local_id: 33 + + # минимальная сумма валюты на прием (из minGetSum) + minimal_input_value: 0.01 + + # минимальная сумма валюты на выдачу (из minGetSumOut) + minimal_output_value: 0.01 + +usdc: + priority: 32 + iso_code: USDC + name: USD Coin + symbol: + alternate_symbols: [] + subunit: USDC Cent + subunit_to_unit: 100 + symbol_first: false + html_entity: "$" + decimal_mark: "," + thousands_separator: "." + iso_numeric: + smallest_denomination: 1 + is_crypto: true + + # Местные настройки + # + # Идентфикатор в type_cy + local_id: 34 + + # минимальная сумма валюты на прием (из minGetSum) + minimal_input_value: 1 + + # минимальная сумма валюты на выдачу (из minGetSumOut) + minimal_output_value: 1 + +try: + priority: 33 + iso_code: TRY + name: Turkish Lira + symbol: '₺' + alternate_symbols: [] + subunit: Kurus + subunit_to_unit: 100 + symbol_first: false + html_entity: '' + decimal_mark: "," + thousands_separator: "." + iso_numeric: '949' + smallest_denomination: 1 + is_crypto: false + + # Местные настройки + # + # Идентфикатор в type_cy + local_id: 35 + + # минимальная сумма валюты на прием (из minGetSum) + minimal_input_value: 50 + + # минимальная сумма валюты на выдачу (из minGetSumOut) + minimal_output_value: 50 + +matic: + priority: 34 + iso_code: MATIC + name: Polygon Matic + symbol: + alternate_symbols: [] + subunit: Gwei + subunit_to_unit: 1000000000 + symbol_first: false + html_entity: '' + decimal_mark: "," + thousands_separator: "." + iso_numeric: + smallest_denomination: 1 + is_crypto: true + + # Местные настройки + # + # Идентфикатор в type_cy + local_id: 36 + + # минимальная сумма валюты на прием (из minGetSum) + minimal_input_value: 1 + + # минимальная сумма валюты на выдачу (из minGetSumOut) + minimal_output_value: 1 + +thb: + priority: 35 + iso_code: THB + name: Thai baht + symbol: '฿' + alternate_symbols: [] + subunit: Satang + subunit_to_unit: 100 + symbol_first: false + html_entity: '' + decimal_mark: "," + thousands_separator: "." + iso_numeric: '764' + smallest_denomination: 1 + is_crypto: false + + # Местные настройки + # + # Идентфикатор в type_cy + local_id: 37 + + # минимальная сумма валюты на прием (из minGetSum) + minimal_input_value: 10 + + # минимальная сумма валюты на выдачу (из minGetSumOut) + minimal_output_value: 10 + +idr: + priority: 36 + iso_code: IDR + name: Indonesian rupiah + symbol: 'Rp' + alternate_symbols: [] + subunit: Sen + subunit_to_unit: 100 + symbol_first: false + html_entity: '' + decimal_mark: "," + thousands_separator: "." + iso_numeric: '360' + smallest_denomination: 1 + is_crypto: false + + # Местные настройки + # + # Идентфикатор в type_cy + local_id: 38 + + # минимальная сумма валюты на прием (из minGetSum) + minimal_input_value: 1000 + + # минимальная сумма валюты на выдачу (из minGetSumOut) + minimal_output_value: 1000 + +cny: + priority: 37 + iso_code: CNY + name: Chinese yuan + symbol: "¥" + alternate_symbols: [] + subunit: fēn + subunit_to_unit: 100 + symbol_first: false + html_entity: '' + decimal_mark: "," + thousands_separator: "." + iso_numeric: '4217' + smallest_denomination: 1 + is_crypto: false + + # Местные настройки + # + # Идентфикатор в type_cy + local_id: 39 + + # минимальная сумма валюты на прием (из minGetSum) + minimal_input_value: 10 + + # минимальная сумма валюты на выдачу (из minGetSumOut) + minimal_output_value: 10 + +inr: + priority: 38 + iso_code: INR + name: Indian rupee + symbol: "₹" + alternate_symbols: [] + subunit: paisa + subunit_to_unit: 100 + symbol_first: false + html_entity: '' + decimal_mark: "," + thousands_separator: "." + iso_numeric: '356' + smallest_denomination: 1 + is_crypto: false + + # Местные настройки + # + # Идентфикатор в type_cy + local_id: 40 + + # минимальная сумма валюты на прием (из minGetSum) + minimal_input_value: 100 + + # минимальная сумма валюты на выдачу (из minGetSumOut) + minimal_output_value: 100 + +avax: + priority: 39 + iso_code: AVAX + name: Avalanche + symbol: + alternate_symbols: [] + subunit: Gwei + subunit_to_unit: 1000000000 + symbol_first: false + html_entity: '' + decimal_mark: "," + thousands_separator: "." + iso_numeric: + smallest_denomination: 1 + is_crypto: true + + # Местные настройки + # + # Идентфикатор в type_cy + local_id: 41 + + # минимальная сумма валюты на прием (из minGetSum) + minimal_input_value: 0.1 + + # минимальная сумма валюты на выдачу (из minGetSumOut) + minimal_output_value: 0.1 diff --git a/db/migrate/20251224134401_add_calculator_type_to_exchange_rates.rb b/db/migrate/20251224134401_add_calculator_type_to_exchange_rates.rb new file mode 100644 index 00000000..ef11007b --- /dev/null +++ b/db/migrate/20251224134401_add_calculator_type_to_exchange_rates.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class AddCalculatorTypeToExchangeRates < ActiveRecord::Migration[6.0] + def change + add_column :gera_exchange_rates, :calculator_type, :string, default: 'legacy', null: false + add_index :gera_exchange_rates, :calculator_type + end +end diff --git a/db/migrate/20251224145000_add_amount_columns_to_exchange_rates.rb b/db/migrate/20251224145000_add_amount_columns_to_exchange_rates.rb new file mode 100644 index 00000000..aa804764 --- /dev/null +++ b/db/migrate/20251224145000_add_amount_columns_to_exchange_rates.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class AddAmountColumnsToExchangeRates < ActiveRecord::Migration[6.0] + def change + return if column_exists?(:gera_exchange_rates, :minamount_cents) + + add_column :gera_exchange_rates, :minamount_cents, :bigint, default: 0, null: false + add_column :gera_exchange_rates, :minamount_currency, :string, default: 'RUB', null: false + add_column :gera_exchange_rates, :maxamount_cents, :bigint, default: 0, null: false + add_column :gera_exchange_rates, :maxamount_currency, :string, default: 'RUB', null: false + end +end diff --git a/doc/Gera.html b/doc/Gera.html deleted file mode 100644 index 49a620dc..00000000 --- a/doc/Gera.html +++ /dev/null @@ -1,172 +0,0 @@ - - - - - - - Module: Gera - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Module: Gera - - - -

-
- - - - -
-
Extended by:
-
Configuration
-
- - - - - - - - -
-
Defined in:
-
app/models/gera/exchange_rate.rb,
- app/models/gera/direction.rb,
app/models/gera/rate_source.rb,
app/jobs/gera/application_job.rb,
app/models/gera/currency_rate.rb,
app/models/gera/external_rate.rb,
app/models/gera/direction_rate.rb,
app/models/gera/payment_system.rb,
app/models/gera/cross_rate_mode.rb,
app/models/gera/rate_source_cbr.rb,
app/models/gera/rate_source_auto.rb,
app/models/gera/rate_source_exmo.rb,
app/models/gera/cbr_external_rate.rb,
app/workers/gera/cbr_rates_worker.rb,
app/models/gera/application_record.rb,
app/models/gera/currency_rate_mode.rb,
app/models/gera/rate_source_manual.rb,
app/workers/gera/exmo_rates_worker.rb,
app/helpers/gera/application_helper.rb,
app/models/gera/rate_source_cbr_avg.rb,
app/models/gera/rate_source_bitfinex.rb,
app/workers/gera/cbr_avg_rates_worker.rb,
app/models/gera/currency_rate_snapshot.rb,
app/models/gera/external_rate_snapshot.rb,
app/workers/concerns/gera/rates_worker.rb,
app/workers/gera/bitfinex_rates_worker.rb,
app/workers/gera/currency_rates_worker.rb,
app/models/gera/direction_rate_snapshot.rb,
app/workers/gera/directions_rates_worker.rb,
app/models/concerns/gera/direction_support.rb,
app/controllers/gera/application_controller.rb,
app/models/gera/currency_rate_mode_snapshot.rb,
app/workers/gera/purge_currency_rates_worker.rb,
app/workers/gera/purge_direction_rates_worker.rb,
app/models/concerns/gera/currency_pair_support.rb,
app/models/gera/currency_rate_history_interval.rb,
app/models/gera/direction_rate_history_interval.rb,
app/models/concerns/gera/currency_pair_generator.rb,
app/workers/gera/create_history_intervals_worker.rb,
app/models/concerns/gera/history_interval_concern.rb,
app/models/gera/direction_rate_snapshot_to_record.rb,
app/models/concerns/gera/currency_rate_mode_builder_support.rb,
lib/gera.rb,
lib/gera/rate.rb,
lib/gera/engine.rb,
lib/gera/numeric.rb,
lib/gera/railtie.rb,
lib/gera/version.rb,
lib/gera/mathematic.rb,
lib/gera/configuration.rb,
lib/gera/currency_pair.rb,
lib/gera/money_support.rb,
lib/gera/bitfinex_fetcher.rb,
lib/gera/currencies_purger.rb,
lib/banks/currency_exchange.rb,
lib/gera/repositories/universe.rb,
lib/gera/rate_from_multiplicator.rb,
lib/builders/currency_rate_builder.rb,
lib/builders/currency_rate_auto_builder.rb,
lib/builders/currency_rate_cross_builder.rb,
lib/builders/currency_rate_direct_builder.rb,
lib/gera/repositories/currency_rates_repository.rb,
lib/gera/repositories/exchange_rates_repository.rb,
lib/gera/repositories/direction_rates_repository.rb,
lib/gera/repositories/payment_systems_repository.rb,
lib/gera/repositories/currency_rate_modes_repository.rb,
spec/lib/mathematic_spec.rb,
spec/lib/currency_pair_spec.rb,
spec/lib/bitfinex_fetcher_spec.rb,
spec/models/gera/exchange_rate_spec.rb,
spec/workers/gera/cbr_rates_worker_spec.rb,
spec/workers/gera/exmo_rates_worker_spec.rb,
spec/workers/gera/currency_rates_worker_spec.rb,
spec/lib/builders/currency_rate_cross_builder_spec.rb
-
-
- -
- -

Overview

-
- -

Банк для Money

- - -
-
-
- - -

Defined Under Namespace

-

- - - Modules: ApplicationHelper, Configuration, CurrenciesPurger, CurrencyPairGenerator, CurrencyPairSupport, CurrencyRateModeBuilderSupport, DirectionSupport, HistoryIntervalConcern, Mathematic, MoneySupport, Numeric, RatesWorker - - - - Classes: ApplicationController, ApplicationJob, ApplicationRecord, BitfinexFetcher, BitfinexRatesWorker, CBRAvgRatesWorker, CBRRatesWorker, CbrExternalRate, CreateHistoryIntervalsWorker, CrossRateMode, CurrencyExchange, CurrencyPair, CurrencyRate, CurrencyRateAutoBuilder, CurrencyRateBuilder, CurrencyRateCrossBuilder, CurrencyRateDirectBuilder, CurrencyRateHistoryInterval, CurrencyRateMode, CurrencyRateModeSnapshot, CurrencyRateModesRepository, CurrencyRateSnapshot, CurrencyRatesRepository, CurrencyRatesWorker, Direction, DirectionRate, DirectionRateHistoryInterval, DirectionRateSnapshot, DirectionRateSnapshotToRecord, DirectionRatesRepository, DirectionsRatesWorker, EXMORatesWorker, Engine, ExchangeRate, ExchangeRatesRepository, ExternalRate, ExternalRateSnapshot, PaymentSystem, PaymentSystemsRepository, PurgeCurrencyRatesWorker, PurgeDirectionRatesWorker, Railtie, Rate, RateFromMultiplicator, RateSource, RateSourceAuto, RateSourceBitfinex, RateSourceCBR, RateSourceCBRAvg, RateSourceEXMO, RateSourceManual, Universe - - -

- - -

- Constant Summary - collapse -

- -
- -
FACTORY_PATH = - -
-
File.expand_path("../factories", __dir__)
- -
CURRENCIES_PATH = - -
-
File.expand_path("../config/currencies.yml", __dir__)
- -
VERSION = - -
-
'0.1.0'
- -
- - - - - - - - - - - - - - - -

Method Summary

- -

Methods included from Configuration

-

configure, cross_pairs, default_cross_currency

- - -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/ApplicationController.html b/doc/Gera/ApplicationController.html deleted file mode 100644 index 7fe97278..00000000 --- a/doc/Gera/ApplicationController.html +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - - Class: Gera::ApplicationController - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::ApplicationController - - - -

-
- -
-
Inherits:
-
- ActionController::Base - -
    -
  • Object
  • - - - - - -
- show all - -
-
- - - - - - - - - - - -
-
Defined in:
-
app/controllers/gera/application_controller.rb
-
- -
- - - - - - - - - - - -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/ApplicationHelper.html b/doc/Gera/ApplicationHelper.html deleted file mode 100644 index b4d69a8d..00000000 --- a/doc/Gera/ApplicationHelper.html +++ /dev/null @@ -1,105 +0,0 @@ - - - - - - - Module: Gera::ApplicationHelper - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Module: Gera::ApplicationHelper - - - -

-
- - - - - - - - - - - -
-
Defined in:
-
app/helpers/gera/application_helper.rb
-
- -
- - - - - - - - - - -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/ApplicationJob.html b/doc/Gera/ApplicationJob.html deleted file mode 100644 index df5afe7e..00000000 --- a/doc/Gera/ApplicationJob.html +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - - Class: Gera::ApplicationJob - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::ApplicationJob - - - -

-
- -
-
Inherits:
-
- ActiveJob::Base - -
    -
  • Object
  • - - - - - -
- show all - -
-
- - - - - - - - - - - -
-
Defined in:
-
app/jobs/gera/application_job.rb
-
- -
- - - - - - - - - - - -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/ApplicationRecord.html b/doc/Gera/ApplicationRecord.html deleted file mode 100644 index 852352cd..00000000 --- a/doc/Gera/ApplicationRecord.html +++ /dev/null @@ -1,141 +0,0 @@ - - - - - - - Class: Gera::ApplicationRecord - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::ApplicationRecord - Abstract - - -

-
- -
-
Inherits:
-
- ActiveRecord::Base - -
    -
  • Object
  • - - - - - -
- show all - -
-
- - - - - - - - - - - -
-
Defined in:
-
app/models/gera/application_record.rb
-
- -
- -

Overview

-
-
- This class is abstract. -
-
- - -
-
-
- - -
- - - - - - - - - - -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/BitfinexFetcher.html b/doc/Gera/BitfinexFetcher.html deleted file mode 100644 index a856e6fb..00000000 --- a/doc/Gera/BitfinexFetcher.html +++ /dev/null @@ -1,212 +0,0 @@ - - - - - - - Class: Gera::BitfinexFetcher - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::BitfinexFetcher - - - -

-
- -
-
Inherits:
-
- Object - -
    -
  • Object
  • - - - -
- show all - -
-
- - - - - - - - - - - -
-
Defined in:
-
lib/gera/bitfinex_fetcher.rb
-
- -
- - - -

- Constant Summary - collapse -

- -
- -
API_URL = - -
-
'https://api.bitfinex.com/v1/pubticker/'
- -
- - - - - - - - - -

- Instance Method Summary - collapse -

- - - - - - -
-

Instance Method Details

- - -
-

- - #performObject - - - - - -

- - - - -
-
-
-
-15
-16
-17
-18
-19
-20
-
-
# File 'lib/gera/bitfinex_fetcher.rb', line 15
-
-def perform
-  response = RestClient::Request.execute url: url, method: :get, verify_ssl: false
-
-  raise response.code unless response.code == 200
-  JSON.parse response.body
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/BitfinexRatesWorker.html b/doc/Gera/BitfinexRatesWorker.html deleted file mode 100644 index 267ef64b..00000000 --- a/doc/Gera/BitfinexRatesWorker.html +++ /dev/null @@ -1,167 +0,0 @@ - - - - - - - Class: Gera::BitfinexRatesWorker - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::BitfinexRatesWorker - - - -

-
- -
-
Inherits:
-
- Object - -
    -
  • Object
  • - - - -
- show all - -
-
- - - - - - -
-
Includes:
-
AutoLogger, Sidekiq::Worker
-
- - - - - - -
-
Defined in:
-
app/workers/gera/bitfinex_rates_worker.rb
-
- -
- -

Overview

-
- -

Загрузка курсов из EXMO

- - -
-
-
- - -
- -

- Constant Summary - collapse -

- -
- -
TICKERS = -
-
- -

Stolen from: api.bitfinex.com/v1/symbols

- - -
-
-
- - -
-
-
%i(neousd neobtc neoeth neoeur)
- -
- - - - - - - - - - - - -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/CBRAvgRatesWorker.html b/doc/Gera/CBRAvgRatesWorker.html deleted file mode 100644 index 81df59c2..00000000 --- a/doc/Gera/CBRAvgRatesWorker.html +++ /dev/null @@ -1,209 +0,0 @@ - - - - - - - Class: Gera::CBRAvgRatesWorker - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::CBRAvgRatesWorker - - - -

-
- -
-
Inherits:
-
- Object - -
    -
  • Object
  • - - - -
- show all - -
-
- - - - - - -
-
Includes:
-
AutoLogger, Sidekiq::Worker
-
- - - - - - -
-
Defined in:
-
app/workers/gera/cbr_avg_rates_worker.rb
-
- -
- - - - - - - - - -

- Instance Method Summary - collapse -

- - - - - - - - -
-

Instance Method Details

- - -
-

- - #performObject - - - - - -

- - - - -
-
-
-
-6
-7
-8
-9
-10
-11
-12
-13
-14
-
-
# File 'app/workers/gera/cbr_avg_rates_worker.rb', line 6
-
-def perform
-  ActiveRecord::Base.connection.clear_query_cache
-  source.with_lock do
-    source.available_pairs.each do |pair|
-      create_rate pair
-    end
-    source.update_attribute :actual_snapshot_id, snapshot.id
-  end
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/CBRRatesWorker.html b/doc/Gera/CBRRatesWorker.html deleted file mode 100644 index dc73dfde..00000000 --- a/doc/Gera/CBRRatesWorker.html +++ /dev/null @@ -1,268 +0,0 @@ - - - - - - - Class: Gera::CBRRatesWorker - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::CBRRatesWorker - - - -

-
- -
-
Inherits:
-
- Object - -
    -
  • Object
  • - - - -
- show all - -
-
- - - - - - -
-
Includes:
-
AutoLogger, Sidekiq::Worker
-
- - - - - - -
-
Defined in:
-
app/workers/gera/cbr_rates_worker.rb
-
- -
- -

Overview

-
- -

Загрузчик курсов из ЦБ РФ по адресу www.cbr.ru/scripts/XML_daily.asp?date_req=08/04/2018

- - -
-
-
- - -
- -

- Constant Summary - collapse -

- -
- -
CURRENCIES = - -
-
%w(USD KZT EUR)
- -
CBR_IDS = - -
-
{
-  'USD' => 'R01235'.freeze,
-  'KZT' => 'R01335'.freeze,
-  'EUR' => 'R01239'.freeze
-}
- -
ROUND = - -
-
15
- -
Error = - -
-
Class.new StandardError
- -
WrongDate = - -
-
Class.new Error
- -
URL = - -
-
'http://www.cbr.ru/scripts/XML_daily.asp'
- -
- - - - - - - - - -

- Instance Method Summary - collapse -

- - - - - - - - -
-

Instance Method Details

- - -
-

- - #performObject - - - - - -

- - - - -
-
-
-
-28
-29
-30
-31
-32
-33
-34
-35
-36
-37
-
-
# File 'app/workers/gera/cbr_rates_worker.rb', line 28
-
-def perform
-  ActiveRecord::Base.connection.clear_query_cache
-  cbr.with_lock do
-    days.each do |date|
-      fetch_and_save_rate date
-    end
-
-    make_snapshot
-  end
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/CbrExternalRate.html b/doc/Gera/CbrExternalRate.html deleted file mode 100644 index 66a34d9e..00000000 --- a/doc/Gera/CbrExternalRate.html +++ /dev/null @@ -1,201 +0,0 @@ - - - - - - - Class: Gera::CbrExternalRate - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::CbrExternalRate - - - -

-
- -
-
Inherits:
-
- ApplicationRecord - -
    -
  • Object
  • - - - - - - - -
- show all - -
-
- - - - - - - - - - - -
-
Defined in:
-
app/models/gera/cbr_external_rate.rb
-
- -
- - - - - - - - - -

- Instance Method Summary - collapse -

- - - - - - - - - - - - - -
-

Instance Method Details

- - -
-

- - #<=>(other) ⇒ Object - - - - - -

- - - - -
-
-
-
-9
-10
-11
-
-
# File 'app/models/gera/cbr_external_rate.rb', line 9
-
-def <=>(other)
-  rate <=> other.rate
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/Configuration.html b/doc/Gera/Configuration.html deleted file mode 100644 index 986806f0..00000000 --- a/doc/Gera/Configuration.html +++ /dev/null @@ -1,390 +0,0 @@ - - - - - - - Module: Gera::Configuration - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Module: Gera::Configuration - - - -

-
- - - - - - - - - -
-
Included in:
-
Gera
-
- - - -
-
Defined in:
-
lib/gera/configuration.rb
-
- -
- -

Overview

-
- -

Gera configuration module. This is extended by Gera to provide -configuration settings.

- - -
-
-
- - -
- -

- Constant Summary - collapse -

- -
- -
@@default_cross_currency = - -
-
:usd
- -
@@cross_pairs = -
-
- -

В данном примере курс к KZT считать через RUB

- - -
-
-
- - -
-
-
{ kzt: :rub }
- -
- - - - - - - - - -

- Instance Method Summary - collapse -

- - - - - - -
-

Instance Method Details

- - -
-

- - #configure {|_self| ... } ⇒ Object - - - - - -

-
- -

Start a Gera configuration block in an initializer.

- -

example: Provide a default currency for the application

- -
Gera.configure do |config|
-  config.default_currency = :eur
-end
-
- - -
-
-
- -

Yields:

-
    - -
  • - - - (_self) - - - -
  • - -
-

Yield Parameters:

-
    - -
  • - - _self - - - (Gera::Configuration) - - - - — -
    -

    the object that the method was called on

    -
    - -
  • - -
- -
- - - - -
-
-
-
-12
-13
-14
-
-
# File 'lib/gera/configuration.rb', line 12
-
-def configure
-	yield self
-end
-
-
- -
-

- - #cross_pairsObject - - - - - -

- - - - -
-
-
-
-30
-31
-32
-33
-34
-35
-36
-
-
# File 'lib/gera/configuration.rb', line 30
-
-def cross_pairs
-  h = {}
-  @@cross_pairs.each do |k, v|
-    h[Money::Currency.find!(k)] = Money::Currency.find! v
-  end
-  h
-end
-
-
- -
-

- - #default_cross_currencyObject - - - - - -

- - - - -
-
-
-
-20
-21
-22
-23
-
-
# File 'lib/gera/configuration.rb', line 20
-
-def default_cross_currency
-  return @@default_cross_currency if @@default_cross_currency.is_a? Money::Currency
-  Money::Currency.find! @@default_cross_currency
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/CreateHistoryIntervalsWorker.html b/doc/Gera/CreateHistoryIntervalsWorker.html deleted file mode 100644 index 869ab983..00000000 --- a/doc/Gera/CreateHistoryIntervalsWorker.html +++ /dev/null @@ -1,220 +0,0 @@ - - - - - - - Class: Gera::CreateHistoryIntervalsWorker - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::CreateHistoryIntervalsWorker - - - -

-
- -
-
Inherits:
-
- Object - -
    -
  • Object
  • - - - -
- show all - -
-
- - - - - - -
-
Includes:
-
AutoLogger, Sidekiq::Worker
-
- - - - - - -
-
Defined in:
-
app/workers/gera/create_history_intervals_worker.rb
-
- -
- - - -

- Constant Summary - collapse -

- -
- -
MAXIMAL_DATE = - -
-
30.minutes
- -
MINIMAL_DATE = - -
-
Time.parse('13-07-2018 18:00')
- -
- - - - - - - - - -

- Instance Method Summary - collapse -

- - - - - - - - -
-

Instance Method Details

- - -
-

- - #performObject - - - - - -

- - - - -
-
-
-
-9
-10
-11
-12
-
-
# File 'app/workers/gera/create_history_intervals_worker.rb', line 9
-
-def perform
-  save_direction_rate_history_intervals
-  save_currency_rate_history_intervals
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/CrossRateMode.html b/doc/Gera/CrossRateMode.html deleted file mode 100644 index 8c678ed1..00000000 --- a/doc/Gera/CrossRateMode.html +++ /dev/null @@ -1,218 +0,0 @@ - - - - - - - Class: Gera::CrossRateMode - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::CrossRateMode - - - -

-
- -
-
Inherits:
-
- ApplicationRecord - - - show all - -
-
- - - - - - -
-
Includes:
-
CurrencyPairSupport
-
- - - - - - -
-
Defined in:
-
app/models/gera/cross_rate_mode.rb
-
- -
- - - - - - - - - -

- Instance Method Summary - collapse -

- - - - - - - - - - - - - -

Methods included from CurrencyPairSupport

-

#currency_from, #currency_pair, #currency_pair=, #currency_to

- - - - - - - - - - -
-

Instance Method Details

- - -
-

- - #titleObject - - - - - -

- - - - -
-
-
-
-9
-10
-11
-
-
# File 'app/models/gera/cross_rate_mode.rb', line 9
-
-def title
-  "#{currency_pair}(#{rate_source || 'auto'})"
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/CurrenciesPurger.html b/doc/Gera/CurrenciesPurger.html deleted file mode 100644 index f1beac26..00000000 --- a/doc/Gera/CurrenciesPurger.html +++ /dev/null @@ -1,216 +0,0 @@ - - - - - - - Module: Gera::CurrenciesPurger - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Module: Gera::CurrenciesPurger - - - -

-
- - - - - - - - - - - -
-
Defined in:
-
lib/gera/currencies_purger.rb
-
- -
- - - - - - - - - -

- Class Method Summary - collapse -

- - - - - - -
-

Class Method Details

- - -
-

- - .purge_all(env) ⇒ Object - - - - - -

- - - - -
-
-
-
-3
-4
-5
-6
-7
-8
-9
-10
-11
-12
-13
-14
-15
-16
-17
-18
-19
-20
-21
-22
-23
-24
-25
-26
-
-
# File 'lib/gera/currencies_purger.rb', line 3
-
-def self.purge_all(env)
-  raise unless env == Rails.env
-
-  if Rails.env.prodiction?
-    puts 'Disable all sidekiqs'
-    Sidekiq::Cron::Job.all.each(&:disable!)
-    sleep 2
-  end
-
-  DirectionRateSnapshot.batch_purge if DirectionRateSnapshot.table_exists?
-  DirectionRate.batch_purge
-
-  ExternalRate.batch_purge
-  ExternalRateSnapshot.batch_purge
-
-  CurrencyRate.batch_purge
-  RateSource.update_all actual_snapshot_id: nil
-  CurrencyRateSnapshot.batch_purge
-
-  if Rails.env.prodiction?
-    puts 'Enable all sidekiqs'
-    Sidekiq::Cron::Job.all.each(&:enable!)
-  end
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/CurrencyExchange.html b/doc/Gera/CurrencyExchange.html deleted file mode 100644 index 70e035c5..00000000 --- a/doc/Gera/CurrencyExchange.html +++ /dev/null @@ -1,194 +0,0 @@ - - - - - - - Class: Gera::CurrencyExchange - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::CurrencyExchange - - - -

-
- -
-
Inherits:
-
- Object - -
    -
  • Object
  • - - - -
- show all - -
-
- - - - - - - - - - - -
-
Defined in:
-
lib/banks/currency_exchange.rb
-
- -
- - - - - - - - - -

- Class Method Summary - collapse -

- - - - - - -
-

Class Method Details

- - -
-

- - .get_rate(cur_from, cur_to) ⇒ Object - - - - - -

- - - - -
-
-
-
-6
-7
-8
-9
-10
-
-
# File 'lib/banks/currency_exchange.rb', line 6
-
-def self.get_rate(cur_from, cur_to)
-  pair = CurrencyPair.new(cur_from, cur_to)
-  cr = Universe.currency_rates_repository.find_currency_rate_by_pair(pair) || raise("Отсутсвует текущий курс для #{pair}")
-  cr.rate_value
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/CurrencyPair.html b/doc/Gera/CurrencyPair.html deleted file mode 100644 index e883490d..00000000 --- a/doc/Gera/CurrencyPair.html +++ /dev/null @@ -1,899 +0,0 @@ - - - - - - - Class: Gera::CurrencyPair - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::CurrencyPair - - - -

-
- -
-
Inherits:
-
- Object - -
    -
  • Object
  • - - - -
- show all - -
-
- - - - - - - - - - - -
-
Defined in:
-
lib/gera/currency_pair.rb
-
- -
- -

Overview

-
- -

Валютная пара

- - -
-
-
- - -
- - - - - - - -

- Class Method Summary - collapse -

- - - -

- Instance Method Summary - collapse -

- - - - -
-

Constructor Details

- -
-

- - #initialize(*args) ⇒ CurrencyPair - - - - - -

-
- -

Варианты:

- -

new cur_from: :rub, cur_to: usd new :rub, :usd new 'rub/usd'

- - -
-
-
- - -
- - - - -
-
-
-
-20
-21
-22
-23
-24
-25
-26
-27
-28
-29
-30
-31
-32
-33
-
-
# File 'lib/gera/currency_pair.rb', line 20
-
-def initialize(*args)
-  if args.first.is_a? Hash
-    super(args.first).freeze
-
-  elsif args.count ==1
-    initialize(*args.first.split(/[\/_-]/)).freeze
-
-  elsif args.count == 2
-    super(cur_from: args[0], cur_to: args[1]).freeze
-
-  else
-    raise "WTF? #{args}"
-  end
-end
-
-
- -
- - -
-

Class Method Details

- - -
-

- - .allObject - - - - - -

- - - - -
-
-
-
-37
-38
-39
-40
-41
-
-
# File 'lib/gera/currency_pair.rb', line 37
-
-def self.all
-  @all ||= Money::Currency.all.each_with_object([]) { |cur_from, list|
-    Money::Currency.all.each { |cur_to| list << CurrencyPair.new(cur_from, cur_to) }
-  }.freeze
-end
-
-
- -
- -
-

Instance Method Details

- - -
-

- - #change_from(cur) ⇒ Object - - - - - -

- - - - -
-
-
-
-71
-72
-73
-
-
# File 'lib/gera/currency_pair.rb', line 71
-
-def change_from(cur)
-  CurrencyPair.new cur, cur_to
-end
-
-
- -
-

- - #change_to(cur) ⇒ Object - - - - - -

- - - - -
-
-
-
-67
-68
-69
-
-
# File 'lib/gera/currency_pair.rb', line 67
-
-def change_to(cur)
-  CurrencyPair.new cur_from, cur
-end
-
-
- -
-

- - #cur_from=(value) ⇒ Object - - - - - -

- - - - -
-
-
-
-59
-60
-61
-62
-63
-64
-65
-
-
# File 'lib/gera/currency_pair.rb', line 59
-
-def cur_from=(value)
-  if value.is_a? Money::Currency
-    super value
-  else
-    super Money::Currency.find(value) || raise("Не известная валюта #{value} в cur_from")
-  end
-end
-
-
- -
-

- - #cur_to=(value) ⇒ Object - - - - - -

- - - - -
-
-
-
-51
-52
-53
-54
-55
-56
-57
-
-
# File 'lib/gera/currency_pair.rb', line 51
-
-def cur_to=(value)
-  if value.is_a? Money::Currency
-    super value
-  else
-    super Money::Currency.find(value) || raise("Не известная валюта #{value} в cur_to")
-  end
-end
-
-
- -
-

- - #inspectObject - - - - - -

- - - - -
-
-
-
-43
-44
-45
-
-
# File 'lib/gera/currency_pair.rb', line 43
-
-def inspect
-  to_s
-end
-
-
- -
-

- - #inverseObject - - - - - -

- - - - -
-
-
-
-47
-48
-49
-
-
# File 'lib/gera/currency_pair.rb', line 47
-
-def inverse
-  self.class.new cur_to, cur_from
-end
-
-
- -
-

- - #keyObject - - - - - -

-
- -

Для машин

- - -
-
-
- - -
- - - - -
-
-
-
-89
-90
-91
-
-
# File 'lib/gera/currency_pair.rb', line 89
-
-def key
-  join '_'
-end
-
-
- -
-

- - #same?Boolean - - - - - -

-
- - -
-
-
- -

Returns:

-
    - -
  • - - - (Boolean) - - - -
  • - -
- -
- - - - -
-
-
-
-75
-76
-77
-
-
# File 'lib/gera/currency_pair.rb', line 75
-
-def same?
-  cur_from == cur_to
-end
-
-
- -
-

- - #to_aObject - - - - - -

- - - - -
-
-
-
-79
-80
-81
-
-
# File 'lib/gera/currency_pair.rb', line 79
-
-def to_a
-  [cur_from, cur_to]
-end
-
-
- -
-

- - #to_sObject - - - - - -

-
- -

Для людей

- - -
-
-
- - -
- - - - -
-
-
-
-84
-85
-86
-
-
# File 'lib/gera/currency_pair.rb', line 84
-
-def to_s
-  join '/'
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/CurrencyPairGenerator.html b/doc/Gera/CurrencyPairGenerator.html deleted file mode 100644 index 0b3aa109..00000000 --- a/doc/Gera/CurrencyPairGenerator.html +++ /dev/null @@ -1,189 +0,0 @@ - - - - - - - Module: Gera::CurrencyPairGenerator - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Module: Gera::CurrencyPairGenerator - - - -

-
- - - - - - - - - -
-
Included in:
-
RateSource
-
- - - -
-
Defined in:
-
app/models/concerns/gera/currency_pair_generator.rb
-
- -
- - - - - - - - - -

- Instance Method Summary - collapse -

- - - - - - -
-

Instance Method Details

- - -
-

- - #generate_pairs_from_currencies(currencies) ⇒ Object - - - - - -

- - - - -
-
-
-
-3
-4
-5
-6
-7
-8
-9
-10
-
-
# File 'app/models/concerns/gera/currency_pair_generator.rb', line 3
-
-def generate_pairs_from_currencies(currencies)
-  currencies = currencies.map { |c| Money::Currency.find c }
-
-  currencies.
-    map { |c1| currencies.reject { |c2| c2==c1 }.map { |c2| [c1,c2].join('/') } }.
-    flatten.compact.
-    map { |cp| Gera::CurrencyPair.new cp }.uniq
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/CurrencyPairSupport.html b/doc/Gera/CurrencyPairSupport.html deleted file mode 100644 index 25979b6a..00000000 --- a/doc/Gera/CurrencyPairSupport.html +++ /dev/null @@ -1,351 +0,0 @@ - - - - - - - Module: Gera::CurrencyPairSupport - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Module: Gera::CurrencyPairSupport - - - -

-
- - - - -
-
Extended by:
-
ActiveSupport::Concern
-
- - - - - - -
-
Included in:
-
CrossRateMode, CurrencyRate, CurrencyRateMode, ExternalRate
-
- - - -
-
Defined in:
-
app/models/concerns/gera/currency_pair_support.rb
-
- -
- - - - - - - - - -

- Instance Method Summary - collapse -

- - - - - - - -
-

Instance Method Details

- - -
-

- - #currency_fromObject - - - - - -

- - - - -
-
-
-
-32
-33
-34
-
-
# File 'app/models/concerns/gera/currency_pair_support.rb', line 32
-
-def currency_from
-  @currency_from ||= cur_from.is_a?(Money::Currency) ? cur_from : Money::Currency.find!(cur_from)
-end
-
-
- -
-

- - #currency_pairObject - - - - - -

- - - - -
-
-
-
-28
-29
-30
-
-
# File 'app/models/concerns/gera/currency_pair_support.rb', line 28
-
-def currency_pair
-  @currency_pair ||= Gera::CurrencyPair.new currency_from, currency_to
-end
-
-
- -
-

- - #currency_pair=(value) ⇒ Object - - - - - -

- - - - -
-
-
-
-19
-20
-21
-22
-23
-24
-25
-26
-
-
# File 'app/models/concerns/gera/currency_pair_support.rb', line 19
-
-def currency_pair=(value)
-  self.cur_from = value.cur_from
-  self.cur_to = value.cur_to
-  @currency_pair = nil
-  @currency_from = nil
-  @currency_to = nil
-  value
-end
-
-
- -
-

- - #currency_toObject - - - - - -

- - - - -
-
-
-
-36
-37
-38
-
-
# File 'app/models/concerns/gera/currency_pair_support.rb', line 36
-
-def currency_to
-  @currency_to ||= cur_to.is_a?(Money::Currency) ? cur_to : Money::Currency.find!(cur_to)
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/CurrencyRate.html b/doc/Gera/CurrencyRate.html deleted file mode 100644 index 8bc1f33d..00000000 --- a/doc/Gera/CurrencyRate.html +++ /dev/null @@ -1,601 +0,0 @@ - - - - - - - Class: Gera::CurrencyRate - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::CurrencyRate - - - -

-
- -
-
Inherits:
-
- ApplicationRecord - - - show all - -
-
- - - - - - -
-
Includes:
-
CurrencyPairSupport
-
- - - - - - -
-
Defined in:
-
app/models/gera/currency_rate.rb
-
- -
- -

Overview

-
- -

Базовый курс

- - -
-
-
- - -
- - - - - - - -

- Instance Method Summary - collapse -

- - - - - - - - - - - - - -

Methods included from CurrencyPairSupport

-

#currency_from, #currency_pair, #currency_pair=, #currency_to

- - - - - - - - - - -
-

Instance Method Details

- - -
-

- - #dumpObject - - - - - -

- - - - -
-
-
-
-53
-54
-55
-
-
# File 'app/models/gera/currency_rate.rb', line 53
-
-def dump
-  as_json(only: [:created_at, :cur_from, :cur_to, :mode, :rate_value, :metadata, :rate_source_id]).merge external_rates: external_rates.map(&:dump)
-end
-
-
- -
-

- - #external_ratesObject - - - - - -

- - - - -
-
-
-
-29
-30
-31
-
-
# File 'app/models/gera/currency_rate.rb', line 29
-
-def external_rates
-  [external_rate, external_rate1, external_rate2, external_rate3].compact
-end
-
-
- -
-

- - #external_rates=(rates) ⇒ Object - - - - - -

- - - - -
-
-
-
-25
-26
-27
-
-
# File 'app/models/gera/currency_rate.rb', line 25
-
-def external_rates=(rates)
-  self.external_rate, self.external_rate1, self.external_rate2, self.external_rate3 = rates
-end
-
-
- -
-

- - #humanized_rateObject - - - - - -

- - - - -
-
-
-
-45
-46
-47
-48
-49
-50
-51
-
-
# File 'app/models/gera/currency_rate.rb', line 45
-
-def humanized_rate
-  if rate_value < 1
-    "#{rate_value} (1/#{1.0/rate_value})"
-  else
-    rate_value
-  end
-end
-
-
- -
-

- - #metaObject - - - - - -

- - - - -
-
-
-
-57
-58
-59
-
-
# File 'app/models/gera/currency_rate.rb', line 57
-
-def meta
-  @meta ||= OpenStruct.new .deep_symbolize_keys
-end
-
-
- -
-

- - #rate_moneyObject - - - - - -

- - - - -
-
-
-
-37
-38
-39
-
-
# File 'app/models/gera/currency_rate.rb', line 37
-
-def rate_money
-  Money.from_amount(rate_value, cur_to)
-end
-
-
- -
-

- - #reverse_rate_moneyObject - - - - - -

- - - - -
-
-
-
-41
-42
-43
-
-
# File 'app/models/gera/currency_rate.rb', line 41
-
-def reverse_rate_money
-  Money.from_amount(1.0 / rate_value, cur_from)
-end
-
-
- -
-

- - #to_sObject - - - - - -

- - - - -
-
-
-
-33
-34
-35
-
-
# File 'app/models/gera/currency_rate.rb', line 33
-
-def to_s
-  "#{currency_pair}:#{humanized_rate}"
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/CurrencyRateAutoBuilder.html b/doc/Gera/CurrencyRateAutoBuilder.html deleted file mode 100644 index 421b66bf..00000000 --- a/doc/Gera/CurrencyRateAutoBuilder.html +++ /dev/null @@ -1,140 +0,0 @@ - - - - - - - Class: Gera::CurrencyRateAutoBuilder - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::CurrencyRateAutoBuilder - - - -

-
- -
-
Inherits:
-
- CurrencyRateBuilder - - - show all - -
-
- - - - - - - - - - - -
-
Defined in:
-
lib/builders/currency_rate_auto_builder.rb
-
- -
- - - - -

Constant Summary

- -

Constants inherited - from CurrencyRateBuilder

-

Gera::CurrencyRateBuilder::Error, Gera::CurrencyRateBuilder::Result

- - - - - - - - - - - - -

Method Summary

- -

Methods inherited from CurrencyRateBuilder

-

#build_currency_rate

- - -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/CurrencyRateBuilder.html b/doc/Gera/CurrencyRateBuilder.html deleted file mode 100644 index 6556df64..00000000 --- a/doc/Gera/CurrencyRateBuilder.html +++ /dev/null @@ -1,231 +0,0 @@ - - - - - - - Class: Gera::CurrencyRateBuilder - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::CurrencyRateBuilder - - - -

-
- -
-
Inherits:
-
- Object - -
    -
  • Object
  • - - - -
- show all - -
-
- - - - - - - - - - - -
-
Defined in:
-
lib/builders/currency_rate_builder.rb
-
- -
- - -

Defined Under Namespace

-

- - - - - Classes: ErrorResult, SuccessResult - - -

- - -

- Constant Summary - collapse -

- -
- -
Error = - -
-
Class.new StandardError
- -
Result = - -
-
Class.new
- -
- - - - - - - - - -

- Instance Method Summary - collapse -

- - - - - - -
-

Instance Method Details

- - -
-

- - #build_currency_rateObject - - - - - -

- - - - -
-
-
-
-39
-40
-41
-42
-43
-44
-
-
# File 'lib/builders/currency_rate_builder.rb', line 39
-
-def build_currency_rate
-  success build
-rescue => err
-  Rails.logger.error err unless err.is_a? Error
-  failure err
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/CurrencyRateBuilder/ErrorResult.html b/doc/Gera/CurrencyRateBuilder/ErrorResult.html deleted file mode 100644 index 6000e278..00000000 --- a/doc/Gera/CurrencyRateBuilder/ErrorResult.html +++ /dev/null @@ -1,338 +0,0 @@ - - - - - - - Class: Gera::CurrencyRateBuilder::ErrorResult - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::CurrencyRateBuilder::ErrorResult - - - -

-
- -
-
Inherits:
-
- Result - -
    -
  • Object
  • - - - -
- show all - -
-
- - - - - - - - - - - -
-
Defined in:
-
lib/builders/currency_rate_builder.rb
-
- -
- - - - - - - - - -

- Instance Method Summary - collapse -

- - - - - - -
-

Instance Method Details

- - -
-

- - #currency_rateObject - - - - - -

- - - - -
-
-
-
-22
-23
-24
-
-
# File 'lib/builders/currency_rate_builder.rb', line 22
-
-def currency_rate
-  nil
-end
-
-
- -
-

- - #error?Boolean - - - - - -

-
- - -
-
-
- -

Returns:

-
    - -
  • - - - (Boolean) - - - -
  • - -
- -
- - - - -
-
-
-
-30
-31
-32
-
-
# File 'lib/builders/currency_rate_builder.rb', line 30
-
-def error?
-  true
-end
-
-
- -
-

- - #success?Boolean - - - - - -

-
- - -
-
-
- -

Returns:

-
    - -
  • - - - (Boolean) - - - -
  • - -
- -
- - - - -
-
-
-
-26
-27
-28
-
-
# File 'lib/builders/currency_rate_builder.rb', line 26
-
-def success?
-  false
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/CurrencyRateBuilder/SuccessResult.html b/doc/Gera/CurrencyRateBuilder/SuccessResult.html deleted file mode 100644 index 5027f177..00000000 --- a/doc/Gera/CurrencyRateBuilder/SuccessResult.html +++ /dev/null @@ -1,286 +0,0 @@ - - - - - - - Class: Gera::CurrencyRateBuilder::SuccessResult - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::CurrencyRateBuilder::SuccessResult - - - -

-
- -
-
Inherits:
-
- Result - -
    -
  • Object
  • - - - -
- show all - -
-
- - - - - - - - - - - -
-
Defined in:
-
lib/builders/currency_rate_builder.rb
-
- -
- - - - - - - - - -

- Instance Method Summary - collapse -

- - - - - - -
-

Instance Method Details

- - -
-

- - #error?Boolean - - - - - -

-
- - -
-
-
- -

Returns:

-
    - -
  • - - - (Boolean) - - - -
  • - -
- -
- - - - -
-
-
-
-13
-14
-15
-
-
# File 'lib/builders/currency_rate_builder.rb', line 13
-
-def error?
-  false
-end
-
-
- -
-

- - #success?Boolean - - - - - -

-
- - -
-
-
- -

Returns:

-
    - -
  • - - - (Boolean) - - - -
  • - -
- -
- - - - -
-
-
-
-9
-10
-11
-
-
# File 'lib/builders/currency_rate_builder.rb', line 9
-
-def success?
-  true
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/CurrencyRateCrossBuilder.html b/doc/Gera/CurrencyRateCrossBuilder.html deleted file mode 100644 index 560d4a73..00000000 --- a/doc/Gera/CurrencyRateCrossBuilder.html +++ /dev/null @@ -1,140 +0,0 @@ - - - - - - - Class: Gera::CurrencyRateCrossBuilder - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::CurrencyRateCrossBuilder - - - -

-
- -
-
Inherits:
-
- CurrencyRateBuilder - - - show all - -
-
- - - - - - - - - - - -
-
Defined in:
-
lib/builders/currency_rate_cross_builder.rb
-
- -
- - - - -

Constant Summary

- -

Constants inherited - from CurrencyRateBuilder

-

Gera::CurrencyRateBuilder::Error, Gera::CurrencyRateBuilder::Result

- - - - - - - - - - - - -

Method Summary

- -

Methods inherited from CurrencyRateBuilder

-

#build_currency_rate

- - -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/CurrencyRateDirectBuilder.html b/doc/Gera/CurrencyRateDirectBuilder.html deleted file mode 100644 index 3e13bd59..00000000 --- a/doc/Gera/CurrencyRateDirectBuilder.html +++ /dev/null @@ -1,140 +0,0 @@ - - - - - - - Class: Gera::CurrencyRateDirectBuilder - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::CurrencyRateDirectBuilder - - - -

-
- -
-
Inherits:
-
- CurrencyRateBuilder - - - show all - -
-
- - - - - - - - - - - -
-
Defined in:
-
lib/builders/currency_rate_direct_builder.rb
-
- -
- - - - -

Constant Summary

- -

Constants inherited - from CurrencyRateBuilder

-

Gera::CurrencyRateBuilder::Error, Gera::CurrencyRateBuilder::Result

- - - - - - - - - - - - -

Method Summary

- -

Methods inherited from CurrencyRateBuilder

-

#build_currency_rate

- - -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/CurrencyRateHistoryInterval.html b/doc/Gera/CurrencyRateHistoryInterval.html deleted file mode 100644 index d73f8972..00000000 --- a/doc/Gera/CurrencyRateHistoryInterval.html +++ /dev/null @@ -1,249 +0,0 @@ - - - - - - - Class: Gera::CurrencyRateHistoryInterval - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::CurrencyRateHistoryInterval - - - -

-
- -
-
Inherits:
-
- ApplicationRecord - -
    -
  • Object
  • - - - - - - - -
- show all - -
-
- - - - - - -
-
Includes:
-
HistoryIntervalConcern
-
- - - - - - -
-
Defined in:
-
app/models/gera/currency_rate_history_interval.rb
-
- -
- - - - -

Constant Summary

- -

Constants included - from HistoryIntervalConcern

-

HistoryIntervalConcern::INTERVAL

- - - - - - -

- Class Method Summary - collapse -

- - - - - - - - - - - - - - - - - - - - -
-

Class Method Details

- - -
-

- - .create_by_interval!(interval_from, interval_to = nil) ⇒ Object - - - - - -

- - - - -
-
-
-
-6
-7
-8
-9
-10
-11
-12
-13
-14
-15
-16
-17
-18
-19
-20
-21
-22
-23
-
-
# File 'app/models/gera/currency_rate_history_interval.rb', line 6
-
-def self.create_by_interval!(interval_from, interval_to = nil)
-  interval_to ||= interval_from + INTERVAL
-  CurrencyRate.
-    where('created_at >= ? and created_at < ?', interval_from, interval_to).
-    group(:cur_from, :cur_to).
-    pluck(:cur_from, :cur_to, 'min(rate_value)', 'max(rate_value)').
-    each do |cur_from, cur_to, min_rate, max_rate|
-
-    next if cur_from == cur_to
-
-    create!(
-      cur_from_id: Money::Currency.find(cur_from).local_id,
-      cur_to_id: Money::Currency.find(cur_to).local_id,
-      min_rate: min_rate, max_rate: max_rate,
-      interval_from: interval_from, interval_to: interval_to
-    )
-  end
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/CurrencyRateMode.html b/doc/Gera/CurrencyRateMode.html deleted file mode 100644 index 329c4e79..00000000 --- a/doc/Gera/CurrencyRateMode.html +++ /dev/null @@ -1,160 +0,0 @@ - - - - - - - Class: Gera::CurrencyRateMode - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::CurrencyRateMode - - - -

-
- -
-
Inherits:
-
- ApplicationRecord - -
    -
  • Object
  • - - - - - - - -
- show all - -
-
- - - - - - -
-
Includes:
-
CurrencyPairSupport, CurrencyRateModeBuilderSupport
-
- - - - - - -
-
Defined in:
-
app/models/gera/currency_rate_mode.rb
-
- -
- - - - - - - - - - - - - - - -

Method Summary

- -

Methods included from CurrencyRateModeBuilderSupport

-

#build_currency_rate, #build_currency_rate!, #build_result, #builder

- - - - - - - - - -

Methods included from CurrencyPairSupport

-

#currency_from, #currency_pair, #currency_pair=, #currency_to

- - - - - - - - - - -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/CurrencyRateModeBuilderSupport.html b/doc/Gera/CurrencyRateModeBuilderSupport.html deleted file mode 100644 index 55808eed..00000000 --- a/doc/Gera/CurrencyRateModeBuilderSupport.html +++ /dev/null @@ -1,363 +0,0 @@ - - - - - - - Module: Gera::CurrencyRateModeBuilderSupport - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Module: Gera::CurrencyRateModeBuilderSupport - - - -

-
- - - - - - - - - -
-
Included in:
-
CurrencyRateMode
-
- - - -
-
Defined in:
-
app/models/concerns/gera/currency_rate_mode_builder_support.rb
-
- -
- - - - - - - - - -

- Instance Method Summary - collapse -

- - - - - - -
-

Instance Method Details

- - -
-

- - #build_currency_rateObject - - - - - -

- - - - -
-
-
-
-3
-4
-5
-6
-7
-8
-
-
# File 'app/models/concerns/gera/currency_rate_mode_builder_support.rb', line 3
-
-def build_currency_rate
-  @currency_rate ||= build_currency_rate!
-
-rescue CurrencyRateBuilder::Error
-  nil
-end
-
-
- -
-

- - #build_currency_rate!Object - - - - - -

- - - - -
-
-
-
-14
-15
-16
-17
-18
-
-
# File 'app/models/concerns/gera/currency_rate_mode_builder_support.rb', line 14
-
-def build_currency_rate!
-  raise build_result.error if build_result.error?
-
-  build_result.currency_rate
-end
-
-
- -
-

- - #build_resultObject - - - - - -

- - - - -
-
-
-
-10
-11
-12
-
-
# File 'app/models/concerns/gera/currency_rate_mode_builder_support.rb', line 10
-
-def build_result
-  @result ||= builder.build_currency_rate
-end
-
-
- -
-

- - #builderObject - - - - - -

- - - - -
-
-
-
-20
-21
-22
-23
-24
-25
-26
-27
-28
-29
-30
-31
-
-
# File 'app/models/concerns/gera/currency_rate_mode_builder_support.rb', line 20
-
-def builder
-  case mode
-  when 'auto'
-    CurrencyRateAutoBuilder.new currency_pair: currency_pair
-  when 'cross'
-    CurrencyRateCrossBuilder.new currency_pair: currency_pair, cross_rate_modes: cross_rate_modes
-  else
-    source = RateSource.find_by_key(mode)
-    raise "not supported mode #{mode} for #{currency_pair}" unless source.present?
-    CurrencyRateDirectBuilder.new currency_pair: currency_pair, source: source
-  end
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/CurrencyRateModeSnapshot.html b/doc/Gera/CurrencyRateModeSnapshot.html deleted file mode 100644 index 5ecd0851..00000000 --- a/doc/Gera/CurrencyRateModeSnapshot.html +++ /dev/null @@ -1,207 +0,0 @@ - - - - - - - Class: Gera::CurrencyRateModeSnapshot - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::CurrencyRateModeSnapshot - - - -

-
- -
-
Inherits:
-
- ApplicationRecord - -
    -
  • Object
  • - - - - - - - -
- show all - -
-
- - - - - - - - - - - -
-
Defined in:
-
app/models/gera/currency_rate_mode_snapshot.rb
-
- -
- - - - - - - - - -

- Instance Method Summary - collapse -

- - - - - - - - - - - - - -
-

Instance Method Details

- - -
-

- - #create_modes!Object - - - - - -

- - - - -
-
-
-
-19
-20
-21
-22
-23
-24
-
-
# File 'app/models/gera/currency_rate_mode_snapshot.rb', line 19
-
-def create_modes!
-  CurrencyPair.all.each do |pair|
-    currency_rate_modes.create! currency_pair: pair
-  end
-  self
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/CurrencyRateModesRepository.html b/doc/Gera/CurrencyRateModesRepository.html deleted file mode 100644 index fc56ebc5..00000000 --- a/doc/Gera/CurrencyRateModesRepository.html +++ /dev/null @@ -1,310 +0,0 @@ - - - - - - - Class: Gera::CurrencyRateModesRepository - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::CurrencyRateModesRepository - - - -

-
- -
-
Inherits:
-
- Object - -
    -
  • Object
  • - - - -
- show all - -
-
- - - - - - - - - - - -
-
Defined in:
-
lib/gera/repositories/currency_rate_modes_repository.rb
-
- -
- - - - - - - - - -

- Instance Method Summary - collapse -

- - - - - - -
-

Instance Method Details

- - -
-

- - #add_currency!(currency) ⇒ Object - - - - - -

- - - - -
-
-
-
-11
-12
-13
-14
-15
-16
-17
-18
-19
-20
-21
-
-
# File 'lib/gera/repositories/currency_rate_modes_repository.rb', line 11
-
-def add_currency!(currency)
-  snapshot.currency_rate_modes.group(:cur_from).count.keys.each do |cur_from|
-    snapshot.currency_rate_modes.create! cur_from: cur_from, cur_to: currency
-  end
-
-  snapshot.currency_rate_modes.group(:cur_to).count.keys.each do |cur_to|
-    snapshot.currency_rate_modes.create! cur_from: currency, cur_to: cur_to
-  end
-
-  @modes_by_pair = build_modes_by_pair
-end
-
-
- -
-

- - #find_currency_rate_mode_by_pair(pair) ⇒ Object - - - - - -

- - - - -
-
-
-
-7
-8
-9
-
-
# File 'lib/gera/repositories/currency_rate_modes_repository.rb', line 7
-
-def find_currency_rate_mode_by_pair pair
-  modes_by_pair[pair.key]
-end
-
-
- -
-

- - #snapshotObject - - - - - -

- - - - -
-
-
-
-3
-4
-5
-
-
# File 'lib/gera/repositories/currency_rate_modes_repository.rb', line 3
-
-def snapshot
-  @snapshot ||= find_or_create_active_snapshot
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/CurrencyRateSnapshot.html b/doc/Gera/CurrencyRateSnapshot.html deleted file mode 100644 index 46df930c..00000000 --- a/doc/Gera/CurrencyRateSnapshot.html +++ /dev/null @@ -1,201 +0,0 @@ - - - - - - - Class: Gera::CurrencyRateSnapshot - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::CurrencyRateSnapshot - - - -

-
- -
-
Inherits:
-
- ApplicationRecord - -
    -
  • Object
  • - - - - - - - -
- show all - -
-
- - - - - - - - - - - -
-
Defined in:
-
app/models/gera/currency_rate_snapshot.rb
-
- -
- - - - - - - - - -

- Instance Method Summary - collapse -

- - - - - - - - - - - - - -
-

Instance Method Details

- - -
-

- - #currency_ratesObject - - - - - -

- - - - -
-
-
-
-8
-9
-10
-
-
# File 'app/models/gera/currency_rate_snapshot.rb', line 8
-
-def currency_rates
-  rates
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/CurrencyRatesRepository.html b/doc/Gera/CurrencyRatesRepository.html deleted file mode 100644 index d7a6993a..00000000 --- a/doc/Gera/CurrencyRatesRepository.html +++ /dev/null @@ -1,314 +0,0 @@ - - - - - - - Class: Gera::CurrencyRatesRepository - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::CurrencyRatesRepository - - - -

-
- -
-
Inherits:
-
- Object - -
    -
  • Object
  • - - - -
- show all - -
-
- - - - - - - - - - - -
-
Defined in:
-
lib/gera/repositories/currency_rates_repository.rb
-
- -
- - - -

- Constant Summary - collapse -

- -
- -
UnknownPair = - -
-
Class.new StandardError
- -
- - - - - - - - - -

- Instance Method Summary - collapse -

- - - - - - -
-

Instance Method Details

- - -
-

- - #find_currency_rate_by_pair(pair) ⇒ Object - - - - - -

- - - - -
-
-
-
-9
-10
-11
-
-
# File 'lib/gera/repositories/currency_rates_repository.rb', line 9
-
-def find_currency_rate_by_pair pair
-  rates_by_pair[pair] || raise(UnknownPair, "Не найдена валютная пара #{pair} в базовых курсах")
-end
-
-
- -
-

- - #get_currency_rate_by_pair(pair) ⇒ Object - - - - - -

- - - - -
-
-
-
-13
-14
-15
-16
-17
-
-
# File 'lib/gera/repositories/currency_rates_repository.rb', line 13
-
-def get_currency_rate_by_pair pair
-  find_currency_rate_by_pair(pair)
-rescue UnknownPair
-  CurrencyRate.new(currency_pair: pair).freeze
-end
-
-
- -
-

- - #snapshotObject - - - - - -

- - - - -
-
-
-
-5
-6
-7
-
-
# File 'lib/gera/repositories/currency_rates_repository.rb', line 5
-
-def snapshot
-  @snapshot ||= CurrencyRateSnapshot.last || raise("Нет актуального snapshot-а")
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/CurrencyRatesWorker.html b/doc/Gera/CurrencyRatesWorker.html deleted file mode 100644 index 6658cab9..00000000 --- a/doc/Gera/CurrencyRatesWorker.html +++ /dev/null @@ -1,256 +0,0 @@ - - - - - - - Class: Gera::CurrencyRatesWorker - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::CurrencyRatesWorker - - - -

-
- -
-
Inherits:
-
- Object - -
    -
  • Object
  • - - - -
- show all - -
-
- - - - - - -
-
Includes:
-
AutoLogger, Sidekiq::Worker
-
- - - - - - -
-
Defined in:
-
app/workers/gera/currency_rates_worker.rb
-
- -
- -

Overview

-
- -

Строит текущие базовые курсы на основе источников и методов расчета

- - -
-
-
- - -
- -

- Constant Summary - collapse -

- -
- -
Error = - -
-
Class.new StandardError
- -
- - - - - - - - - -

- Instance Method Summary - collapse -

- - - - - - - - -
-

Instance Method Details

- - -
-

- - #performObject - - - - - -

- - - - -
-
-
-
-11
-12
-13
-14
-15
-16
-17
-18
-19
-20
-21
-22
-23
-24
-25
-26
-27
-28
-29
-
-
# File 'app/workers/gera/currency_rates_worker.rb', line 11
-
-def perform
-  logger.info 'start'
-
-  CurrencyRate.transaction do
-    @snapshot = create_snapshot
-
-    Gera::CurrencyPair.all.each do |pair|
-      create_rate pair
-    end
-  end
-
-  logger.info 'finish'
-
-  # Запускаем перерасчет конечных курсов
-  #
-  DirectionsRatesWorker.perform_async
-
-  true
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/Direction.html b/doc/Gera/Direction.html deleted file mode 100644 index 84229431..00000000 --- a/doc/Gera/Direction.html +++ /dev/null @@ -1,461 +0,0 @@ - - - - - - - Class: Gera::Direction - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::Direction - - - -

-
- -
-
Inherits:
-
- Object - -
    -
  • Object
  • - - - -
- show all - -
-
- - - - - - - - - - - -
-
Defined in:
-
app/models/gera/direction.rb
-
- -
- -

Overview

-
- -

Направление обмена

- - -
-
-
- - -
- - - - - - - -

- Instance Method Summary - collapse -

- - - - - - -
-

Instance Method Details

- - -
-

- - #currency_fromObject - - - - - -

- - - - -
-
-
-
-20
-21
-22
-
-
# File 'app/models/gera/direction.rb', line 20
-
-def currency_from
-  payment_system_from.currency
-end
-
-
- -
-

- - #currency_toObject - - - - - -

- - - - -
-
-
-
-24
-25
-26
-
-
# File 'app/models/gera/direction.rb', line 24
-
-def currency_to
-  payment_system_to.currency
-end
-
-
- -
-

- - #direction_rateObject - - - - - -

- - - - -
-
-
-
-40
-41
-42
-
-
# File 'app/models/gera/direction.rb', line 40
-
-def direction_rate
-  Universe.direction_rates_repository.find_by_direction self
-end
-
-
- -
-

- - #exchange_rateObject - - - - - -

- - - - -
-
-
-
-36
-37
-38
-
-
# File 'app/models/gera/direction.rb', line 36
-
-def exchange_rate
-  Universe.exchange_rates_repository.find_by_direction self
-end
-
-
- -
-

- - #inspectObject - - - - - -

- - - - -
-
-
-
-28
-29
-30
-
-
# File 'app/models/gera/direction.rb', line 28
-
-def inspect
-  to_s
-end
-
-
- -
-

- - #to_sObject - - - - - -

- - - - -
-
-
-
-32
-33
-34
-
-
# File 'app/models/gera/direction.rb', line 32
-
-def to_s
-  "direction:#{payment_system_from.try(:id) || '???'}-#{payment_system_to.try(:id) || '???'}"
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/DirectionRate.html b/doc/Gera/DirectionRate.html deleted file mode 100644 index 8e7b3909..00000000 --- a/doc/Gera/DirectionRate.html +++ /dev/null @@ -1,1090 +0,0 @@ - - - - - - - Class: Gera::DirectionRate - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::DirectionRate - - - -

-
- -
-
Inherits:
-
- ApplicationRecord - - - show all - -
-
- - - - - - -
-
Includes:
-
AutoLogger, DirectionSupport, Mathematic
-
- - - - - - -
-
Defined in:
-
app/models/gera/direction_rate.rb
-
- -
- -

Overview

-
- -

Конечный курс обмена по направлениями

- - -
-
-
- - -
- -

- Constant Summary - collapse -

- -
- -
UnknownExchangeRate = - -
-
Class.new StandardError
- -
- - - - - - - - - -

- Instance Method Summary - collapse -

- - - - - - - - - - - - - -

Methods included from DirectionSupport

-

#direction, #direction=

- - - - - - - - - - -

Methods included from Mathematic

-

#calculate_base_rate, #calculate_comission, #calculate_finite_rate, #calculate_profits, #diff_percents, #money_exchange, #money_reverse_exchange

- - - - - - - - - -
-

Instance Method Details

- - -
-

- - #base_rateObject - - - - - -

- - - - -
-
-
-
-90
-91
-92
-
-
# File 'app/models/gera/direction_rate.rb', line 90
-
-def base_rate
-  RateFromMultiplicator.new(base_rate_value).freeze
-end
-
-
- -
-

- - #currency_pairObject - - - - - -

- - - - -
-
-
-
-52
-53
-54
-
-
# File 'app/models/gera/direction_rate.rb', line 52
-
-def currency_pair
-  @currency_pair ||= CurrencyPair.new income_currency, outcome_currency
-end
-
-
- -
-

- - #dumpObject - - - - - -

- - - - -
-
-
-
-115
-116
-117
-118
-
-
# File 'app/models/gera/direction_rate.rb', line 115
-
-def dump
-  as_json(only: %i(id ps_from_id ps_to_id currency_rate_id rate_value base_rate_value rate_percent created_at))
-    .merge currency_rate: currency_rate.dump, dump_version: 1
-end
-
-
- -
-

- - #exchange(amount) ⇒ Object - - - - - -

- - - - -
-
-
-
-44
-45
-46
-
-
# File 'app/models/gera/direction_rate.rb', line 44
-
-def exchange(amount)
-  rate.exchange amount, outcome_currency
-end
-
-
- -
-

- - #exchange_notificationObject - - - - - -

- - - - -
-
-
-
-120
-121
-122
-123
-124
-125
-126
-127
-128
-129
-130
-131
-132
-133
-
-
# File 'app/models/gera/direction_rate.rb', line 120
-
-def exchange_notification
-  ExchangeNotification.find_by(
-    income_payment_system_id: income_payment_system_id,
-    outcome_payment_system_id: outcome_payment_system_id
-  ) ||
-  ExchangeNotification.find_by(
-    income_payment_system_id: income_payment_system_id,
-    outcome_payment_system_id: nil
-  ) ||
-  ExchangeNotification.find_by(
-    income_payment_system_id: nil,
-    outcome_payment_system_id: outcome_payment_system_id
-  )
-end
-
-
- -
-

- - #get_profit_result(income_amount) ⇒ Object - - - - - -

- - - - -
-
-
-
-98
-99
-100
-101
-102
-103
-104
-105
-106
-107
-108
-109
-110
-111
-112
-113
-
-
# File 'app/models/gera/direction_rate.rb', line 98
-
-def get_profit_result(income_amount)
-  res = calculate_profits(
-    base_rate: base_rate_value,
-    comission: rate_percent,
-    ps_interest: ps_comission,
-    income_amount: income_amount
-  )
-
-  diff = res.finite_rate.to_f.as_percentage_of(rate_value.to_f) - 100
-
-  if diff.abs > 0
-    logger.warn "direction_rate_id=#{id} Расчитанная конечная ставка (#{res.finite_rate}) не соответсвует текущей (#{rate_value}). Разница #{diff}"
-  end
-
-  res
-end
-
-
- -
-

- - #in_moneyObject - - - - - -

- - - - -
-
-
-
-72
-73
-74
-75
-
-
# File 'app/models/gera/direction_rate.rb', line 72
-
-def in_money
-  return 1 if rate_value < 1
-  rate_value
-end
-
-
- -
-

- - #income_currencyObject - - - - - -

- - - - -
-
-
-
-56
-57
-58
-
-
# File 'app/models/gera/direction_rate.rb', line 56
-
-def income_currency
-  ps_from.currency
-end
-
-
- -
-

- - #inverse_direction_rateObject - - - - - -

- - - - -
-
-
-
-94
-95
-96
-
-
# File 'app/models/gera/direction_rate.rb', line 94
-
-def inverse_direction_rate
-  Universe.direction_rates_repository.get_matrix[ps_to_id][ps_from_id]
-end
-
-
- -
-

- - #out_moneyObject - - - - - -

- - - - -
-
-
-
-77
-78
-79
-80
-
-
# File 'app/models/gera/direction_rate.rb', line 77
-
-def out_money
-  return 1.0 / rate_value if rate_value < 1
-  1
-end
-
-
- -
-

- - #outcome_currencyObject - - - - - -

- - - - -
-
-
-
-60
-61
-62
-
-
# File 'app/models/gera/direction_rate.rb', line 60
-
-def outcome_currency
-  ps_to.currency
-end
-
-
- -
-

- - #ps_comissionObject - - - - - -

- - - - -
-
-
-
-68
-69
-70
-
-
# File 'app/models/gera/direction_rate.rb', line 68
-
-def ps_comission
-  ps_to.commision
-end
-
-
- -
-

- - #rateObject - - - - - -

- - - - -
-
-
-
-82
-83
-84
-
-
# File 'app/models/gera/direction_rate.rb', line 82
-
-def rate
-  RateFromMultiplicator.new(rate_value).freeze
-end
-
-
- -
-

- - #rate_moneyObject - - - - - -

- - - - -
-
-
-
-86
-87
-88
-
-
# File 'app/models/gera/direction_rate.rb', line 86
-
-def rate_money
-  Money.from_amount(rate_value, currency_rate.currency_to)
-end
-
-
- -
-

- - #reverse_exchange(amount) ⇒ Object - - - - - -

- - - - -
-
-
-
-48
-49
-50
-
-
# File 'app/models/gera/direction_rate.rb', line 48
-
-def reverse_exchange(amount)
-  rate.reverse_exchange amount, income_currency
-end
-
-
- -
-

- - #snapshotObject - - - - - -

- - - - -
-
-
-
-64
-65
-66
-
-
# File 'app/models/gera/direction_rate.rb', line 64
-
-def snapshot
-  @snapshot ||= direction_rate_snapshots.last
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/DirectionRateHistoryInterval.html b/doc/Gera/DirectionRateHistoryInterval.html deleted file mode 100644 index 7c1d3feb..00000000 --- a/doc/Gera/DirectionRateHistoryInterval.html +++ /dev/null @@ -1,269 +0,0 @@ - - - - - - - Class: Gera::DirectionRateHistoryInterval - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::DirectionRateHistoryInterval - - - -

-
- -
-
Inherits:
-
- ApplicationRecord - -
    -
  • Object
  • - - - - - - - -
- show all - -
-
- - - - - - -
-
Includes:
-
HistoryIntervalConcern
-
- - - - - - -
-
Defined in:
-
app/models/gera/direction_rate_history_interval.rb
-
- -
- - - - -

Constant Summary

- -

Constants included - from HistoryIntervalConcern

-

HistoryIntervalConcern::INTERVAL

- - - - - - -

- Class Method Summary - collapse -

- - - - - - - - - - - - - - - - - - - - -
-

Class Method Details

- - -
-

- - .create_by_interval!(interval_from, interval_to = nil) ⇒ Object - - - - - -

-
- -

Их не надо подключать, потому что иначе при создании записи ActiveRercord -проверяет есить ли они в базе

- -

belongs_to :payment_system_from, class_name: 'PaymentSystem' -belongs_to :payment_system_to, class_name: 'PaymentSystem'

- - -
-
-
- - -
- - - - -
-
-
-
-13
-14
-15
-16
-17
-18
-19
-20
-21
-22
-23
-24
-25
-26
-27
-28
-29
-30
-31
-
-
# File 'app/models/gera/direction_rate_history_interval.rb', line 13
-
-def self.create_by_interval!(interval_from, interval_to = nil)
-  interval_to ||= interval_from + INTERVAL
-  DirectionRate.
-    where('created_at >= ? and created_at < ?', interval_from, interval_to).
-    group(:ps_from_id, :ps_to_id).
-    pluck(:ps_from_id, :ps_to_id, 'min(rate_value)', 'max(rate_value)', 'min(rate_percent)', 'max(rate_percent)').
-    each do |ps_from_id, ps_to_id, min_rate, max_rate, min_comission, max_comission|
-
-    next if ps_from_id == ps_to_id
-
-    create!(
-      payment_system_from_id: ps_from_id,
-      payment_system_to_id: ps_to_id,
-      min_rate: min_rate, max_rate: max_rate,
-      min_comission: min_comission, max_comission: max_comission,
-      interval_from: interval_from, interval_to: interval_to
-    )
-  end
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/DirectionRateSnapshot.html b/doc/Gera/DirectionRateSnapshot.html deleted file mode 100644 index 573c7f51..00000000 --- a/doc/Gera/DirectionRateSnapshot.html +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - Class: Gera::DirectionRateSnapshot - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::DirectionRateSnapshot - - - -

-
- -
-
Inherits:
-
- ApplicationRecord - -
    -
  • Object
  • - - - - - - - -
- show all - -
-
- - - - - - - - - - - -
-
Defined in:
-
app/models/gera/direction_rate_snapshot.rb
-
- -
- - - - - - - - - - - - - - - - - -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/DirectionRateSnapshotToRecord.html b/doc/Gera/DirectionRateSnapshotToRecord.html deleted file mode 100644 index 5bb720dd..00000000 --- a/doc/Gera/DirectionRateSnapshotToRecord.html +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - Class: Gera::DirectionRateSnapshotToRecord - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::DirectionRateSnapshotToRecord - - - -

-
- -
-
Inherits:
-
- ApplicationRecord - -
    -
  • Object
  • - - - - - - - -
- show all - -
-
- - - - - - - - - - - -
-
Defined in:
-
app/models/gera/direction_rate_snapshot_to_record.rb
-
- -
- - - - - - - - - - - - - - - - - -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/DirectionRatesRepository.html b/doc/Gera/DirectionRatesRepository.html deleted file mode 100644 index a9aab458..00000000 --- a/doc/Gera/DirectionRatesRepository.html +++ /dev/null @@ -1,471 +0,0 @@ - - - - - - - Class: Gera::DirectionRatesRepository - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::DirectionRatesRepository - - - -

-
- -
-
Inherits:
-
- Object - -
    -
  • Object
  • - - - -
- show all - -
-
- - - - - - - - - - - -
-
Defined in:
-
lib/gera/repositories/direction_rates_repository.rb
-
- -
- - - -

- Constant Summary - collapse -

- -
- -
FinitRateNotFound = - -
-
Class.new StandardError
- -
NoActualSnapshot = - -
-
Class.new StandardError
- -
- - - - - - - - - -

- Instance Method Summary - collapse -

- - - - - - -
-

Instance Method Details

- - -
-

- - #allObject - - - - - -

- - - - -
-
-
-
-10
-11
-12
-
-
# File 'lib/gera/repositories/direction_rates_repository.rb', line 10
-
-def all
-  snapshot.direction_rates
-end
-
-
- -
-

- - #find_by_direction(direction) ⇒ Object - - - - - -

- - - - -
-
-
-
-18
-19
-20
-
-
# File 'lib/gera/repositories/direction_rates_repository.rb', line 18
-
-def find_by_direction direction
-  get_by_direction direction
-end
-
-
- -
-

- - #find_direction_rate_by_exchange_rate_id(er_id) ⇒ Object - - - - - -

- - - - -
-
-
-
-14
-15
-16
-
-
# File 'lib/gera/repositories/direction_rates_repository.rb', line 14
-
-def find_direction_rate_by_exchange_rate_id er_id
-  rates_by_exchange_rate_id[er_id] || raise(FinitRateNotFound, "Не найден конечный курс обменя для exchange_rate_id=#{er_id} в direction_rate_snapshot_id=#{snapshot.id}")
-end
-
-
- -
-

- - #get_by_direction(direction) ⇒ Object - - - - - -

- - - - -
-
-
-
-22
-23
-24
-
-
# File 'lib/gera/repositories/direction_rates_repository.rb', line 22
-
-def get_by_direction direction
-  get_matrix[direction.ps_from_id][direction.ps_to_id]
-end
-
-
- -
-

- - #get_matrixObject - - - - - -

- - - - -
-
-
-
-26
-27
-28
-
-
# File 'lib/gera/repositories/direction_rates_repository.rb', line 26
-
-def get_matrix
-  @matrix ||= build_matrix
-end
-
-
- -
-

- - #snapshotObject - - - - - -

- - - - -
-
-
-
-6
-7
-8
-
-
# File 'lib/gera/repositories/direction_rates_repository.rb', line 6
-
-def snapshot
-  @snapshot ||= DirectionRateSnapshot.last || raise(NoActualSnapshot, "Нет актуального snapshot-а")
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/DirectionSupport.html b/doc/Gera/DirectionSupport.html deleted file mode 100644 index 57c417d5..00000000 --- a/doc/Gera/DirectionSupport.html +++ /dev/null @@ -1,233 +0,0 @@ - - - - - - - Module: Gera::DirectionSupport - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Module: Gera::DirectionSupport - - - -

-
- - - - - - - - - -
-
Included in:
-
DirectionRate, ExchangeRate
-
- - - -
-
Defined in:
-
app/models/concerns/gera/direction_support.rb
-
- -
- - - - - - - - - -

- Instance Method Summary - collapse -

- - - - - - -
-

Instance Method Details

- - -
-

- - #directionObject - - - - - -

- - - - -
-
-
-
-8
-9
-10
-
-
# File 'app/models/concerns/gera/direction_support.rb', line 8
-
-def direction
-  ::Gera::Direction.new(ps_from: payment_system_from, ps_to: payment_system_to).freeze
-end
-
-
- -
-

- - #direction=(value) ⇒ Object - - - - - -

- - - - -
-
-
-
-3
-4
-5
-6
-
-
# File 'app/models/concerns/gera/direction_support.rb', line 3
-
-def direction=(value)
-  self.payment_system_from = value.payment_system_from
-  self.payment_system_to = value.payment_system_to
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/DirectionsRatesWorker.html b/doc/Gera/DirectionsRatesWorker.html deleted file mode 100644 index 698942d2..00000000 --- a/doc/Gera/DirectionsRatesWorker.html +++ /dev/null @@ -1,244 +0,0 @@ - - - - - - - Class: Gera::DirectionsRatesWorker - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::DirectionsRatesWorker - - - -

-
- -
-
Inherits:
-
- Object - -
    -
  • Object
  • - - - -
- show all - -
-
- - - - - - -
-
Includes:
-
AutoLogger, Sidekiq::Worker
-
- - - - - - -
-
Defined in:
-
app/workers/gera/directions_rates_worker.rb
-
- -
- - - -

- Constant Summary - collapse -

- -
- -
Error = - -
-
Class.new StandardError
- -
- - - - - - - - - -

- Instance Method Summary - collapse -

- - - - - - - - -
-

Instance Method Details

- - -
-

- - #perform(*args) ⇒ Object - - - - - -

-
- -

exchange_rate_id - ID изменившегося направление фактически не используется

- - -
-
-
- - -
- - - - -
-
-
-
-13
-14
-15
-16
-17
-18
-19
-20
-21
-22
-23
-24
-
-
# File 'app/workers/gera/directions_rates_worker.rb', line 13
-
-def perform(*args) # exchange_rate_id: nil)
-  logger.info "start"
-
-  DirectionRate.transaction do
-    # Генерруем для всех, потому что так нужно старому пыху
-    # ExchangeRate.available.find_each do |er|
-    ExchangeRate.includes(:payment_system_from, :payment_system_to).find_each do |er|
-      safe_create er
-    end
-  end
-  logger.info "finish"
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/EXMORatesWorker.html b/doc/Gera/EXMORatesWorker.html deleted file mode 100644 index 29aeebec..00000000 --- a/doc/Gera/EXMORatesWorker.html +++ /dev/null @@ -1,165 +0,0 @@ - - - - - - - Class: Gera::EXMORatesWorker - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::EXMORatesWorker - - - -

-
- -
-
Inherits:
-
- Object - -
    -
  • Object
  • - - - -
- show all - -
-
- - - - - - -
-
Includes:
-
AutoLogger, Sidekiq::Worker
-
- - - - - - -
-
Defined in:
-
app/workers/gera/exmo_rates_worker.rb
-
- -
- -

Overview

-
- -

Загрузка курсов из EXMO

- - -
-
-
- - -
- -

- Constant Summary - collapse -

- -
- -
URL1 = - -
-
'https://api.exmo.com/v1/ticker/'.freeze
- -
URL2 = - -
-
'https://api.exmo.me/v1/ticker/'.freeze
- -
URL = - -
-
URL2
- -
- - - - - - - - - - - - -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/Engine.html b/doc/Gera/Engine.html deleted file mode 100644 index e1898ed4..00000000 --- a/doc/Gera/Engine.html +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - - Class: Gera::Engine - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::Engine - - - -

-
- -
-
Inherits:
-
- Rails::Engine - -
    -
  • Object
  • - - - - - -
- show all - -
-
- - - - - - - - - - - -
-
Defined in:
-
lib/gera/engine.rb
-
- -
- - - - - - - - - - - -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/ExchangeRate.html b/doc/Gera/ExchangeRate.html deleted file mode 100644 index b542a364..00000000 --- a/doc/Gera/ExchangeRate.html +++ /dev/null @@ -1,938 +0,0 @@ - - - - - - - Class: Gera::ExchangeRate - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::ExchangeRate - - - -

-
- -
-
Inherits:
-
- ApplicationRecord - - - show all - -
-
- - - - - - -
-
Includes:
-
DirectionSupport, Mathematic
-
- - - - - - -
-
Defined in:
-
app/models/gera/exchange_rate.rb
-
- -
- - - -

- Constant Summary - collapse -

- -
- -
DEFAULT_COMISSION = - -
-
50
- -
- - - - - - - - - -

- Class Method Summary - collapse -

- - - -

- Instance Method Summary - collapse -

- - - - - - - - - - - - - -

Methods included from DirectionSupport

-

#direction, #direction=

- - - - - - - - - -

Methods included from Mathematic

-

#calculate_base_rate, #calculate_comission, #calculate_finite_rate, #calculate_profits, #diff_percents, #money_exchange, #money_reverse_exchange

- - - - - - - - - -
-

Class Method Details

- - -
-

- - .list_ratesObject - - - - - -

- - - - -
-
-
-
-69
-70
-71
-72
-73
-74
-
-
# File 'app/models/gera/exchange_rate.rb', line 69
-
-def self.list_rates
-  order('id asc').each_with_object({}) do |er, h|
-    h[er.id_ps1] ||= {}
-    h[er.id_ps1][er.id_ps2] = h.value_ps
-  end
-end
-
-
- -
- -
-

Instance Method Details

- - -
-

- - #available?Boolean - - - - - -

-
- - -
-
-
- -

Returns:

-
    - -
  • - - - (Boolean) - - - -
  • - -
- -
- - - - -
-
-
-
-76
-77
-78
-
-
# File 'app/models/gera/exchange_rate.rb', line 76
-
-def available?
-  is_enabled?
-end
-
-
- -
-

- - #comission_percentsObject - - - - - -

-
- -

TODO rename to comission

- - -
-
-
- - -
- - - - -
-
-
-
-124
-125
-126
-
-
# File 'app/models/gera/exchange_rate.rb', line 124
-
-def comission_percents
-  value_ps
-end
-
-
- -
-

- - #currency_fromObject - - - - - -

- - - - -
-
-
-
-107
-108
-109
-
-
# File 'app/models/gera/exchange_rate.rb', line 107
-
-def currency_from
-  in_currency
-end
-
-
- -
-

- - #currency_pairObject - - - - - -

- - - - -
-
-
-
-95
-96
-97
-
-
# File 'app/models/gera/exchange_rate.rb', line 95
-
-def currency_pair
-  @currency_pair ||= CurrencyPair.new in_currency, out_currency
-end
-
-
- -
-

- - #currency_toObject - - - - - -

- - - - -
-
-
-
-103
-104
-105
-
-
# File 'app/models/gera/exchange_rate.rb', line 103
-
-def currency_to
-  out_currency
-end
-
-
- -
-

- - #custom_inspectObject - - - - - -

- - - - -
-
-
-
-84
-85
-86
-87
-88
-89
-90
-91
-92
-93
-
-
# File 'app/models/gera/exchange_rate.rb', line 84
-
-def custom_inspect
-  {
-    value_ps:            value_ps,
-    exchange_rate_id:    id,
-    payment_system_to:   payment_system_to.to_s,
-    payment_system_from: payment_system_from.to_s,
-    out_currency:        out_currency.to_s,
-    in_currency:         in_currency.to_s,
-  }.to_s
-end
-
-
- -
-

- - #direction_rateObject - - - - - -

- - - - -
-
-
-
-128
-129
-130
-
-
# File 'app/models/gera/exchange_rate.rb', line 128
-
-def direction_rate
-  Universe.direction_rates_repository.find_direction_rate_by_exchange_rate_id id
-end
-
-
- -
-

- - #finite_rateObject - - - - - -

- - - - -
-
-
-
-115
-116
-117
-
-
# File 'app/models/gera/exchange_rate.rb', line 115
-
-def finite_rate
-  direction_rate.rate
-end
-
-
- -
-

- - #in_currencyObject - - - - - -

- - - - -
-
-
-
-111
-112
-113
-
-
# File 'app/models/gera/exchange_rate.rb', line 111
-
-def in_currency
-  Money::Currency.find in_cur
-end
-
-
- -
-

- - #out_currencyObject - - - - - -

- - - - -
-
-
-
-99
-100
-101
-
-
# File 'app/models/gera/exchange_rate.rb', line 99
-
-def out_currency
-  Money::Currency.find out_cur
-end
-
-
- -
-

- - #to_sObject - - - - - -

- - - - -
-
-
-
-119
-120
-121
-
-
# File 'app/models/gera/exchange_rate.rb', line 119
-
-def to_s
-  [in_currency, out_currency].join '/'
-end
-
-
- -
-

- - #update_finite_rate!(finite_rate) ⇒ Object - - - - - -

- - - - -
-
-
-
-80
-81
-82
-
-
# File 'app/models/gera/exchange_rate.rb', line 80
-
-def update_finite_rate! finite_rate
-  update! comission: calculate_comission(finite_rate, currency_rate.rate_value)
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/ExchangeRatesRepository.html b/doc/Gera/ExchangeRatesRepository.html deleted file mode 100644 index ff34fbd1..00000000 --- a/doc/Gera/ExchangeRatesRepository.html +++ /dev/null @@ -1,242 +0,0 @@ - - - - - - - Class: Gera::ExchangeRatesRepository - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::ExchangeRatesRepository - - - -

-
- -
-
Inherits:
-
- Object - -
    -
  • Object
  • - - - -
- show all - -
-
- - - - - - - - - - - -
-
Defined in:
-
lib/gera/repositories/exchange_rates_repository.rb
-
- -
- - - - - - - - - -

- Instance Method Summary - collapse -

- - - - - - -
-

Instance Method Details

- - -
-

- - #find_by_direction(direction) ⇒ Object - - - - - -

- - - - -
-
-
-
-3
-4
-5
-
-
# File 'lib/gera/repositories/exchange_rates_repository.rb', line 3
-
-def find_by_direction direction
-  get_matrix[direction.ps_from_id][direction.ps_to_id]
-end
-
-
- -
-

- - #get_matrixObject - - - - - -

- - - - -
-
-
-
-7
-8
-9
-
-
# File 'lib/gera/repositories/exchange_rates_repository.rb', line 7
-
-def get_matrix
-  @matrix ||= build_matrix
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/ExternalRate.html b/doc/Gera/ExternalRate.html deleted file mode 100644 index a19acfd1..00000000 --- a/doc/Gera/ExternalRate.html +++ /dev/null @@ -1,270 +0,0 @@ - - - - - - - Class: Gera::ExternalRate - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::ExternalRate - - - -

-
- -
-
Inherits:
-
- ApplicationRecord - - - show all - -
-
- - - - - - -
-
Includes:
-
CurrencyPairSupport
-
- - - - - - -
-
Defined in:
-
app/models/gera/external_rate.rb
-
- -
- - - - - - - - - -

- Instance Method Summary - collapse -

- - - - - - - - - - - - - -

Methods included from CurrencyPairSupport

-

#currency_from, #currency_pair, #currency_pair=, #currency_to

- - - - - - - - - - -
-

Instance Method Details

- - -
-

- - #direction_rateObject - - - - - -

- - - - -
-
-
-
-24
-25
-26
-
-
# File 'app/models/gera/external_rate.rb', line 24
-
-def direction_rate
-  Universe.direction_rates_repository.find_direction_rate_by_exchange_rate_id id
-end
-
-
- -
-

- - #dumpObject - - - - - -

- - - - -
-
-
-
-28
-29
-30
-
-
# File 'app/models/gera/external_rate.rb', line 28
-
-def dump
-  as_json(only: [:id, :cur_from, :cur_to, :rate_value, :source_id, :created_at])
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/ExternalRateSnapshot.html b/doc/Gera/ExternalRateSnapshot.html deleted file mode 100644 index 2a456ac3..00000000 --- a/doc/Gera/ExternalRateSnapshot.html +++ /dev/null @@ -1,201 +0,0 @@ - - - - - - - Class: Gera::ExternalRateSnapshot - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::ExternalRateSnapshot - - - -

-
- -
-
Inherits:
-
- ApplicationRecord - -
    -
  • Object
  • - - - - - - - -
- show all - -
-
- - - - - - - - - - - -
-
Defined in:
-
app/models/gera/external_rate_snapshot.rb
-
- -
- - - - - - - - - -

- Instance Method Summary - collapse -

- - - - - - - - - - - - - -
-

Instance Method Details

- - -
-

- - #to_sObject - - - - - -

- - - - -
-
-
-
-16
-17
-18
-
-
# File 'app/models/gera/external_rate_snapshot.rb', line 16
-
-def to_s
-  "snapshot[#{id}]:#{rate_source}:#{actual_for}"
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/HistoryIntervalConcern.html b/doc/Gera/HistoryIntervalConcern.html deleted file mode 100644 index b3a75c84..00000000 --- a/doc/Gera/HistoryIntervalConcern.html +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - Module: Gera::HistoryIntervalConcern - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Module: Gera::HistoryIntervalConcern - - - -

-
- - - - -
-
Extended by:
-
ActiveSupport::Concern
-
- - - - - - -
-
Included in:
-
CurrencyRateHistoryInterval, DirectionRateHistoryInterval
-
- - - -
-
Defined in:
-
app/models/concerns/gera/history_interval_concern.rb
-
- -
- - - -

- Constant Summary - collapse -

- -
- -
INTERVAL = - -
-
5.minutes
- -
- - - - - - - - - - - -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/Mathematic.html b/doc/Gera/Mathematic.html deleted file mode 100644 index 8fe62000..00000000 --- a/doc/Gera/Mathematic.html +++ /dev/null @@ -1,686 +0,0 @@ - - - - - - - Module: Gera::Mathematic - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Module: Gera::Mathematic - - - -

-
- - - - - - - - - -
-
Included in:
-
DirectionRate, ExchangeRate, RateFromMultiplicator
-
- - - -
-
Defined in:
-
lib/gera/mathematic.rb
-
- -
- -

Defined Under Namespace

-

- - - - - Classes: Result - - -

- - - - - - - - -

- Instance Method Summary - collapse -

- - - - - - -
-

Instance Method Details

- - -
-

- - #calculate_base_rate(finite_rate, comission) ⇒ Object - - - - - -

-
- -

Отдает базовую тавку из конечной и комиссии

- - -
-
-
- - -
- - - - -
-
-
-
-51
-52
-53
-54
-55
-56
-57
-
-
# File 'lib/gera/mathematic.rb', line 51
-
-def calculate_base_rate(finite_rate, comission)
-  if finite_rate >= 1
-    100.0 * finite_rate / (100.0 - comission.to_f)
-  else
-    100.0 * finite_rate / (100.0 - comission.to_f)
-  end
-end
-
-
- -
-

- - #calculate_comission(finite_rate, base_rate) ⇒ Object - - - - - -

-
- -

Отдает комиссию исходя из конечной и базовой ставки

- - -
-
-
- - -
- - - - -
-
-
-
-61
-62
-63
-64
-65
-66
-67
-68
-
-
# File 'lib/gera/mathematic.rb', line 61
-
-def calculate_comission(finite_rate, base_rate)
-  if finite_rate <= 1
-    a = 1.0 / finite_rate.to_f - 1.0 / base_rate.to_f
-    (a.as_percentage_of(1.0 / finite_rate.to_f).to_f * 100)
-  else
-    (base_rate.to_f - finite_rate.to_f).as_percentage_of(base_rate.to_f).to_f * 100
-  end
-end
-
-
- -
-

- - #calculate_finite_rate(base_rate, comission) ⇒ Object - - - - - -

-
- -

Конечная ставка из базовой

- - -
-
-
- - -
- - - - -
-
-
-
-72
-73
-74
-75
-76
-77
-78
-
-
# File 'lib/gera/mathematic.rb', line 72
-
-def calculate_finite_rate(base_rate, comission)
-  if base_rate <= 1
-    base_rate.to_f * (1.0 - comission.to_f/100)
-  else
-    base_rate - comission.to_percent
-  end
-end
-
-
- -
-

- - #calculate_profits(base_rate:, comission:, ps_interest:, income_amount:) ⇒ Object - - - - - -

- - - - -
-
-
-
-24
-25
-26
-27
-28
-29
-30
-31
-32
-33
-34
-35
-36
-37
-38
-39
-40
-41
-42
-43
-44
-45
-46
-47
-
-
# File 'lib/gera/mathematic.rb', line 24
-
-def calculate_profits(base_rate:,
-                      comission:,
-                      ps_interest:,
-                      income_amount:
-                     )
-
-  finite_rate     = calculate_finite_rate base_rate, comission
-  finite_amount   = income_amount * finite_rate
-  ps_amount       = ps_interest.percent_of finite_amount
-  outcome_amount  = ps_amount + finite_amount
-
-  Result.new(
-    base_rate:       base_rate,
-    comission:       comission,
-    ps_interest:     ps_interest,
-    income_amount:   income_amount,
-
-    finite_rate:     finite_rate,
-    finite_amount:   finite_amount, # сумма которую получет пользователь на счет
-    ps_amount:       ps_amount,
-    outcome_amount:  outcome_amount, # сумма которую нужно перевести чтобы хватило и платежной системе и пользователю сколько обещали
-    profit_amount:   income_amount - outcome_amount / base_rate
-  ).freeze
-end
-
-
- -
-

- - #diff_percents(a, b) ⇒ Object - - - - - -

-
- -

На сколько абсолютных процентов отличается число b от числа a

- - -
-
-
- - -
- - - - -
-
-
-
-82
-83
-84
-85
-86
-87
-
-
# File 'lib/gera/mathematic.rb', line 82
-
-def diff_percents(a, b)
-  percent_value = (a.to_d / 100.0)
-  res = 100.0 - (b.to_d / percent_value)
-  res = -res if res < 0
-  res.to_percent
-end
-
-
- -
-

- - #money_exchange(rate, amount, to_currency) ⇒ Object - - - - - -

-
- -

Отдает сумму которую получим в результате обмена rate - курсовой -мультимликатор amount - входящая сумма to_currency - валютя в которой нужно -получить результат

- - -
-
-
- - -
- - - - -
-
-
-
-94
-95
-96
-97
-98
-99
-100
-101
-102
-103
-
-
# File 'lib/gera/mathematic.rb', line 94
-
-def money_exchange(rate, amount, to_currency)
-  fractional = BigDecimal(amount.fractional.to_s) / (
-    BigDecimal(amount.currency.subunit_to_unit.to_s) /
-    BigDecimal(to_currency.subunit_to_unit.to_s)
-  )
-
-  res = fractional * rate
-  res = res.round(0, BigDecimal::ROUND_DOWN)
-  Money.new(res, to_currency)
-end
-
-
- -
-

- - #money_reverse_exchange(rate, amount, to_currency) ⇒ Object - - - - - -

-
- -

Обратный обмен. Когда у нас есть курс и сумма которую мы хотим получить и -мы вычисляем из какой суммы на входе она получится

- - -
-
-
- - -
- - - - -
-
-
-
-108
-109
-110
-111
-112
-113
-114
-115
-116
-117
-
-
# File 'lib/gera/mathematic.rb', line 108
-
-def money_reverse_exchange(rate, amount, to_currency)
-  fractional = BigDecimal(amount.fractional.to_s) / (
-    BigDecimal(amount.currency.subunit_to_unit.to_s) /
-    BigDecimal(to_currency.subunit_to_unit.to_s)
-  )
-
-  res = fractional / rate
-  res = res.round(0, BigDecimal::ROUND_UP)
-  Money.new(res, to_currency)
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/Mathematic/Result.html b/doc/Gera/Mathematic/Result.html deleted file mode 100644 index b782687c..00000000 --- a/doc/Gera/Mathematic/Result.html +++ /dev/null @@ -1,121 +0,0 @@ - - - - - - - Class: Gera::Mathematic::Result - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::Mathematic::Result - - - -

-
- -
-
Inherits:
-
- Object - -
    -
  • Object
  • - - - -
- show all - -
-
- - - - - - - - - - - -
-
Defined in:
-
lib/gera/mathematic.rb
-
- -
- - - - - - - - - - -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/MoneySupport.html b/doc/Gera/MoneySupport.html deleted file mode 100644 index d024ba0b..00000000 --- a/doc/Gera/MoneySupport.html +++ /dev/null @@ -1,208 +0,0 @@ - - - - - - - Module: Gera::MoneySupport - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Module: Gera::MoneySupport - - - -

-
- - - - - - - - - - - -
-
Defined in:
-
lib/gera/money_support.rb
-
- -
- -

Defined Under Namespace

-

- - - Modules: CurrencyExtend - - - - -

- - - - - - - - -

- Class Method Summary - collapse -

- - - - - - -
-

Class Method Details

- - -
-

- - .initObject - - - - - -

- - - - -
-
-
-
-3
-4
-5
-6
-7
-8
-9
-10
-11
-12
-13
-14
-15
-16
-17
-
-
# File 'lib/gera/money_support.rb', line 3
-
-def self.init
-  # Убираем все валюты
-  Money::Currency.all.each do |cur|
-    Money::Currency.unregister cur.id.to_s
-  end
-
-  Psych.load( File.read CURRENCIES_PATH ).each { |key, cur| Money::Currency.register cur.symbolize_keys }
-
-  # Создают константы-валюты, типа RUB, USD и тп
-  Money::Currency.all.each do |cur|
-    Object.const_set cur.iso_code, cur
-  end
-
-  # Gera::Hooks.init
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/MoneySupport/CurrencyExtend.html b/doc/Gera/MoneySupport/CurrencyExtend.html deleted file mode 100644 index 4dd24263..00000000 --- a/doc/Gera/MoneySupport/CurrencyExtend.html +++ /dev/null @@ -1,548 +0,0 @@ - - - - - - - Module: Gera::MoneySupport::CurrencyExtend - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Module: Gera::MoneySupport::CurrencyExtend - - - -

-
- - - - - - - - - - - -
-
Defined in:
-
lib/gera/money_support.rb
-
- -
- - - - - -

Instance Attribute Summary collapse

- - - - - - -

- Instance Method Summary - collapse -

- - - - - -
-

Instance Attribute Details

- - - -
-

- - #authorized_roundObject (readonly) - - - - - -

-
- -

TODO отказаться

- - -
-
-
- - -
- - - - -
-
-
-
-24
-25
-26
-
-
# File 'lib/gera/money_support.rb', line 24
-
-def authorized_round
-  @authorized_round
-end
-
-
- - - -
-

- - #local_idObject (readonly) - - - - - -

-
- -

TODO Вынести в app

- - -
-
-
- - -
- - - - -
-
-
-
-21
-22
-23
-
-
# File 'lib/gera/money_support.rb', line 21
-
-def local_id
-  @local_id
-end
-
-
- -
- - -
-

Instance Method Details

- - -
-

- - #initialize_data!Object - - - - - -

- - - - -
-
-
-
-40
-41
-42
-43
-44
-45
-46
-47
-48
-49
-50
-
-
# File 'lib/gera/money_support.rb', line 40
-
-def initialize_data!
-  super
-
-  data = self.class.table[@id]
-
-  @is_crypto = data[:is_crypto]
-  @local_id = data[:local_id]
-  @minimal_input_value = data[:minimal_input_value]
-  @minimal_output_value = data[:minimal_output_value]
-  @authorized_round = data[:authorized_round]
-end
-
-
- -
-

- - #is_crypto?Boolean - - - - - -

-
- - -
-
-
- -

Returns:

-
    - -
  • - - - (Boolean) - - - -
  • - -
- -
- - - - -
-
-
-
-36
-37
-38
-
-
# File 'lib/gera/money_support.rb', line 36
-
-def is_crypto?
-  !!@is_crypto
-end
-
-
- -
-

- - #minimal_input_valueObject - - - - - -

-
- -

TODO Вынести в базу в app

- - -
-
-
- - -
- - - - -
-
-
-
-32
-33
-34
-
-
# File 'lib/gera/money_support.rb', line 32
-
-def minimal_input_value
-  Money.from_amount @minimal_input_value, self
-end
-
-
- -
-

- - #minimal_output_valueObject - - - - - -

-
- -

TODO Вынести в базу в app

- - -
-
-
- - -
- - - - -
-
-
-
-27
-28
-29
-
-
# File 'lib/gera/money_support.rb', line 27
-
-def minimal_output_value
-  Money.from_amount @minimal_output_value, self
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/Numeric.html b/doc/Gera/Numeric.html deleted file mode 100644 index 6427e0b4..00000000 --- a/doc/Gera/Numeric.html +++ /dev/null @@ -1,361 +0,0 @@ - - - - - - - Module: Gera::Numeric - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Module: Gera::Numeric - - - -

-
- - - - - - - - - -
-
Included in:
-
Numeric
-
- - - -
-
Defined in:
-
lib/gera/numeric.rb
-
- -
- - - - - - - - - -

- Instance Method Summary - collapse -

- - - - - - -
-

Instance Method Details

- - -
-

- - #as_percentage_of(value) ⇒ Object - - - - - -

-
- -

5.as_percentage_of(10) # => 50.0%

- - -
-
-
- - -
- - - - -
-
-
-
-17
-18
-19
-
-
# File 'lib/gera/numeric.rb', line 17
-
-def as_percentage_of(value)
-  (self.to_f * 100.0 / value).to_percent
-end
-
-
- -
-

- - #percent_of(value) ⇒ Object - - - - - -

-
- -

10.percent_of(100) # => (10/1)

- - -
-
-
- - -
- - - - -
-
-
-
-12
-13
-14
-
-
# File 'lib/gera/numeric.rb', line 12
-
-def percent_of(value)
-  value * to_percent
-end
-
-
- -
-

- - #percentsObject - - - - - -

- - - - -
-
-
-
-7
-8
-9
-
-
# File 'lib/gera/numeric.rb', line 7
-
-def percents
-  self.to_percent
-end
-
-
- -
-

- - #to_rateObject - - - - - -

- - - - -
-
-
-
-3
-4
-5
-
-
# File 'lib/gera/numeric.rb', line 3
-
-def to_rate
-  RateFromMultiplicator.new(self)
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/PaymentSystem.html b/doc/Gera/PaymentSystem.html deleted file mode 100644 index 0bcedfa9..00000000 --- a/doc/Gera/PaymentSystem.html +++ /dev/null @@ -1,382 +0,0 @@ - - - - - - - Class: Gera::PaymentSystem - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::PaymentSystem - - - -

-
- -
-
Inherits:
-
- ApplicationRecord - - - show all - -
-
- - - - - - -
-
Includes:
-
Archivable
-
- - - - - - -
-
Defined in:
-
app/models/gera/payment_system.rb
-
- -
- - - - - - - - - -

- Instance Method Summary - collapse -

- - - - - - - - - - - - - - -
-

Instance Method Details

- - -
-

- - #currencyObject - - - - - -

- - - - -
-
-
-
-29
-30
-31
-32
-
-
# File 'app/models/gera/payment_system.rb', line 29
-
-def currency
-  return unless type_cy
-  @currency ||= Money::Currency.find_by_local_id(type_cy) || raise("Не найдена валюта #{type_cy}")
-end
-
-
- -
-

- - #currency=(cur) ⇒ Object - - - - - -

- - - - -
-
-
-
-34
-35
-36
-37
-
-
# File 'app/models/gera/payment_system.rb', line 34
-
-def currency=(cur)
-  cur = Money::Currency.find cur unless cur.is_a? Money::Currency
-  self.type_cy = cur.is_a?(Money::Currency) ? cur.local_id : nil
-end
-
-
- -
-

- - #income_fee_percentsObject - - - - - -

-
- -

TODO: надо пособирать такие условия и может для каждой платежки сделать -json с настройками

- - -
-
-
- - -
- - - - -
-
-
-
-44
-45
-46
-
-
# File 'app/models/gera/payment_system.rb', line 44
-
-def income_fee_percents
-  income_fee.percents
-end
-
-
- -
-

- - #to_sObject - - - - - -

- - - - -
-
-
-
-39
-40
-41
-
-
# File 'app/models/gera/payment_system.rb', line 39
-
-def to_s
-  name
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/PaymentSystemsRepository.html b/doc/Gera/PaymentSystemsRepository.html deleted file mode 100644 index 73081b3d..00000000 --- a/doc/Gera/PaymentSystemsRepository.html +++ /dev/null @@ -1,294 +0,0 @@ - - - - - - - Class: Gera::PaymentSystemsRepository - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::PaymentSystemsRepository - - - -

-
- -
-
Inherits:
-
- Object - -
    -
  • Object
  • - - - -
- show all - -
-
- - - - - - - - - - - -
-
Defined in:
-
lib/gera/repositories/payment_systems_repository.rb
-
- -
- - - - - - - - - -

- Instance Method Summary - collapse -

- - - - - - -
-

Instance Method Details

- - -
-

- - #allObject - - - - - -

- - - - -
-
-
-
-11
-12
-13
-
-
# File 'lib/gera/repositories/payment_systems_repository.rb', line 11
-
-def all
-  @all ||= PaymentSystem.ordered.load
-end
-
-
- -
-

- - #availableObject - - - - - -

- - - - -
-
-
-
-7
-8
-9
-
-
# File 'lib/gera/repositories/payment_systems_repository.rb', line 7
-
-def available
-  @available ||= PaymentSystem.available.ordered.load
-end
-
-
- -
-

- - #find_by_id(id) ⇒ Object - - - - - -

- - - - -
-
-
-
-3
-4
-5
-
-
# File 'lib/gera/repositories/payment_systems_repository.rb', line 3
-
-def find_by_id id
-  cache_by_id[id]
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/PurgeCurrencyRatesWorker.html b/doc/Gera/PurgeCurrencyRatesWorker.html deleted file mode 100644 index 1bd3dad4..00000000 --- a/doc/Gera/PurgeCurrencyRatesWorker.html +++ /dev/null @@ -1,214 +0,0 @@ - - - - - - - Class: Gera::PurgeCurrencyRatesWorker - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::PurgeCurrencyRatesWorker - - - -

-
- -
-
Inherits:
-
- Object - -
    -
  • Object
  • - - - -
- show all - -
-
- - - - - - -
-
Includes:
-
Sidekiq::Worker
-
- - - - - - -
-
Defined in:
-
app/workers/gera/purge_currency_rates_worker.rb
-
- -
- - - -

- Constant Summary - collapse -

- -
- -
KEEP_PERIOD = - -
-
3.hours
- -
- - - - - - - - - -

- Instance Method Summary - collapse -

- - - - - - - -
-

Instance Method Details

- - -
-

- - #performObject - - - - - -

- - - - -
-
-
-
-9
-10
-11
-12
-
-
# File 'app/workers/gera/purge_currency_rates_worker.rb', line 9
-
-def perform
-  # Удаляем меньшими пачками, потому что каскадом удаляются прямая зависимость (currency_rates)
-  currency_rate_snapshots.batch_purge batch_size: 100
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/PurgeDirectionRatesWorker.html b/doc/Gera/PurgeDirectionRatesWorker.html deleted file mode 100644 index 936f1ef2..00000000 --- a/doc/Gera/PurgeDirectionRatesWorker.html +++ /dev/null @@ -1,224 +0,0 @@ - - - - - - - Class: Gera::PurgeDirectionRatesWorker - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::PurgeDirectionRatesWorker - - - -

-
- -
-
Inherits:
-
- Object - -
    -
  • Object
  • - - - -
- show all - -
-
- - - - - - -
-
Includes:
-
Sidekiq::Worker
-
- - - - - - -
-
Defined in:
-
app/workers/gera/purge_direction_rates_worker.rb
-
- -
- - - -

- Constant Summary - collapse -

- -
- -
KEEP_PERIOD = - -
-
3.hours
- -
- - - - - - - - - -

- Instance Method Summary - collapse -

- - - - - - - -
-

Instance Method Details

- - -
-

- - #performObject - - - - - -

- - - - -
-
-
-
-9
-10
-11
-12
-13
-14
-15
-16
-17
-
-
# File 'app/workers/gera/purge_direction_rates_worker.rb', line 9
-
-def perform
-  direction_rate_snapshots.batch_purge
-
-  # Удаляем отдельно, потому что они могут жить отдельно и связываются
-  # с direction_rate_snapshot через кросс-таблицу
-  direction_rates.batch_purge
-
-  # TODO Тут не плохо было бы добить direction_rates которые не входят в snapshot-ы и в actual
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/Railtie.html b/doc/Gera/Railtie.html deleted file mode 100644 index 0291d90e..00000000 --- a/doc/Gera/Railtie.html +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - - Class: Gera::Railtie - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::Railtie - - - -

-
- -
-
Inherits:
-
- Rails::Railtie - -
    -
  • Object
  • - - - - - -
- show all - -
-
- - - - - - - - - - - -
-
Defined in:
-
lib/gera/railtie.rb
-
- -
- - - - - - - - - - - -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/Rate.html b/doc/Gera/Rate.html deleted file mode 100644 index a7a66bc6..00000000 --- a/doc/Gera/Rate.html +++ /dev/null @@ -1,335 +0,0 @@ - - - - - - - Class: Gera::Rate - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::Rate - - - -

-
- -
-
Inherits:
-
- RateFromMultiplicator - - - show all - -
-
- - - - - - - - - - - -
-
Defined in:
-
lib/gera/rate.rb
-
- -
- - - - -

Constant Summary

- -

Constants inherited - from RateFromMultiplicator

-

Gera::RateFromMultiplicator::FORMAT_ROUND

- - - - -

Instance Attribute Summary

- -

Attributes inherited from RateFromMultiplicator

-

#value

- - - -

- Instance Method Summary - collapse -

- - - - - - - - - - - - - -

Methods inherited from RateFromMultiplicator

-

#==, #exchange, #format, #in_amount, #initialize, #out_amount, #reverse_exchange, #to_rate, #to_s

- - - - - - - - - -

Methods included from Mathematic

-

#calculate_base_rate, #calculate_comission, #calculate_finite_rate, #calculate_profits, #diff_percents, #money_exchange, #money_reverse_exchange

-
-

Constructor Details

- -

This class inherits a constructor from Gera::RateFromMultiplicator

- -
- - -
-

Instance Method Details

- - -
-

- - #reverseObject - - - - - -

- - - - -
-
-
-
-18
-19
-20
-
-
# File 'lib/gera/rate.rb', line 18
-
-def reverse
-  self.class.new(in_amount: out_amount, out_amount: in_amount).freeze
-end
-
-
- -
-

- - #to_dObject - - - - - -

- - - - -
-
-
-
-10
-11
-12
-
-
# File 'lib/gera/rate.rb', line 10
-
-def to_d
-  out_amount.to_d / in_amount.to_d
-end
-
-
- -
-

- - #to_fObject - - - - - -

- - - - -
-
-
-
-14
-15
-16
-
-
# File 'lib/gera/rate.rb', line 14
-
-def to_f
-  to_d.to_f
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/RateFromMultiplicator.html b/doc/Gera/RateFromMultiplicator.html deleted file mode 100644 index 10f7d1bd..00000000 --- a/doc/Gera/RateFromMultiplicator.html +++ /dev/null @@ -1,799 +0,0 @@ - - - - - - - Class: Gera::RateFromMultiplicator - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::RateFromMultiplicator - - - -

-
- -
-
Inherits:
-
- Object - -
    -
  • Object
  • - - - -
- show all - -
-
- - - - - - -
-
Includes:
-
Mathematic
-
- - - - - - -
-
Defined in:
-
lib/gera/rate_from_multiplicator.rb
-
- -
- -
-

Direct Known Subclasses

-

Rate

-
- - -

- Constant Summary - collapse -

- -
- -
FORMAT_ROUND = - -
-
3
- -
- - - - - -

Instance Attribute Summary collapse

- - - - - - -

- Instance Method Summary - collapse -

- - - - - - - - - - - - - -

Methods included from Mathematic

-

#calculate_base_rate, #calculate_comission, #calculate_finite_rate, #calculate_profits, #diff_percents, #money_exchange, #money_reverse_exchange

-
-

Constructor Details

- -
-

- - #initialize(value) ⇒ RateFromMultiplicator - - - - - -

-
- -

Returns a new instance of RateFromMultiplicator

- - -
-
-
- - -
- - - - -
-
-
-
-10
-11
-12
-
-
# File 'lib/gera/rate_from_multiplicator.rb', line 10
-
-def initialize(value)
-  @value = value
-end
-
-
- -
- -
-

Instance Attribute Details

- - - -
-

- - #valueObject (readonly) - - - - - -

-
- -

Returns the value of attribute value

- - -
-
-
- - -
- - - - -
-
-
-
-4
-5
-6
-
-
# File 'lib/gera/rate_from_multiplicator.rb', line 4
-
-def value
-  @value
-end
-
-
- -
- - -
-

Instance Method Details

- - -
-

- - #==(other) ⇒ Object - - - - - -

- - - - -
-
-
-
-14
-15
-16
-
-
# File 'lib/gera/rate_from_multiplicator.rb', line 14
-
-def ==(other)
-  value == other.value
-end
-
-
- -
-

- - #exchange(amount, currency) ⇒ Object - - - - - -

- - - - -
-
-
-
-26
-27
-28
-
-
# File 'lib/gera/rate_from_multiplicator.rb', line 26
-
-def exchange(amount, currency)
-  money_exchange to_d, amount, currency
-end
-
-
- -
-

- - #format(cur1 = '', cur2 = '') ⇒ Object - - - - - -

- - - - -
-
-
-
-40
-41
-42
-43
-44
-
-
# File 'lib/gera/rate_from_multiplicator.rb', line 40
-
-def format(cur1='', cur2='')
-  cur1 = " #{cur1}" if cur1.present?
-  cur2 = " #{cur2}" if cur2.present?
-  "#{in_amount.round FORMAT_ROUND}#{cur1}#{out_amount.round FORMAT_ROUND}#{cur2}"
-end
-
-
- -
-

- - #in_amountObject - - - - - -

- - - - -
-
-
-
-18
-19
-20
-
-
# File 'lib/gera/rate_from_multiplicator.rb', line 18
-
-def in_amount
-  value > 1 ? 1.0 : 1.0 / value
-end
-
-
- -
-

- - #out_amountObject - - - - - -

- - - - -
-
-
-
-22
-23
-24
-
-
# File 'lib/gera/rate_from_multiplicator.rb', line 22
-
-def out_amount
-  value > 1 ? value : 1.0
-end
-
-
- -
-

- - #reverseObject - - - - - -

- - - - -
-
-
-
-34
-35
-36
-
-
# File 'lib/gera/rate_from_multiplicator.rb', line 34
-
-def reverse
-  self.class.new(1.0 / value).freeze
-end
-
-
- -
-

- - #reverse_exchange(amount, currency) ⇒ Object - - - - - -

- - - - -
-
-
-
-30
-31
-32
-
-
# File 'lib/gera/rate_from_multiplicator.rb', line 30
-
-def reverse_exchange(amount, currency)
-  money_reverse_exchange to_d, amount, currency
-end
-
-
- -
-

- - #to_rateObject - - - - - -

- - - - -
-
-
-
-50
-51
-52
-
-
# File 'lib/gera/rate_from_multiplicator.rb', line 50
-
-def to_rate
-  self
-end
-
-
- -
-

- - #to_sObject - - - - - -

- - - - -
-
-
-
-46
-47
-48
-
-
# File 'lib/gera/rate_from_multiplicator.rb', line 46
-
-def to_s
-  format
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/RateSource.html b/doc/Gera/RateSource.html deleted file mode 100644 index 7b2e0523..00000000 --- a/doc/Gera/RateSource.html +++ /dev/null @@ -1,640 +0,0 @@ - - - - - - - Class: Gera::RateSource - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::RateSource - - - -

-
- -
-
Inherits:
-
- ApplicationRecord - - - show all - -
-
- - - - -
-
Extended by:
-
CurrencyPairGenerator
-
- - - - - - - - -
-
Defined in:
-
app/models/gera/rate_source.rb
-
- -
- - - - -

- Constant Summary - collapse -

- -
- -
RateNotFound = - -
-
Class.new StandardError
- -
- - - - - - - - - -

- Class Method Summary - collapse -

- - - -

- Instance Method Summary - collapse -

- - - - - - - - - - - - - -

Methods included from CurrencyPairGenerator

-

generate_pairs_from_currencies

- - - - - - - - - -
-

Class Method Details

- - -
-

- - .available_pairsObject - - - - - -

- - - - -
-
-
-
-34
-35
-36
-
-
# File 'app/models/gera/rate_source.rb', line 34
-
-def self.available_pairs
-  generate_pairs_from_currencies supported_currencies
-end
-
-
- -
-

- - .get!Object - - - - - -

- - - - -
-
-
-
-38
-39
-40
-
-
# File 'app/models/gera/rate_source.rb', line 38
-
-def self.get!
-  where(type: self.name).take!
-end
-
-
- -
-

- - .supported_currenciesObject - - - - - -

- - - - -
-
-
-
-30
-31
-32
-
-
# File 'app/models/gera/rate_source.rb', line 30
-
-def self.supported_currencies
-  raise 'not implemented'
-end
-
-
- -
- -
-

Instance Method Details

- - -
-

- - #actual_ratesObject - - - - - -

- - - - -
-
-
-
-54
-55
-56
-
-
# File 'app/models/gera/rate_source.rb', line 54
-
-def actual_rates
-  external_rates.where(snapshot_id: actual_snapshot_id)
-end
-
-
- -
-

- - #find_rate_by_currency_pair(pair) ⇒ Object - - - - - -

- - - - -
-
-
-
-46
-47
-48
-
-
# File 'app/models/gera/rate_source.rb', line 46
-
-def find_rate_by_currency_pair(pair)
-  actual_rates.find_by_currency_pair pair
-end
-
-
- -
-

- - #find_rate_by_currency_pair!(pair) ⇒ Object - - - - - -

- - - - -
-
-
-
-42
-43
-44
-
-
# File 'app/models/gera/rate_source.rb', line 42
-
-def find_rate_by_currency_pair!(pair)
-  find_rate_by_currency_pair(pair) || raise(RateNotFound, pair)
-end
-
-
- -
-

- - #is_currency_supported?(cur) ⇒ Boolean - - - - - -

-
- - -
-
-
- -

Returns:

-
    - -
  • - - - (Boolean) - - - -
  • - -
- -
- - - - -
-
-
-
-62
-63
-64
-65
-
-
# File 'app/models/gera/rate_source.rb', line 62
-
-def is_currency_supported?(cur)
-  cur = Money::Currency.find cur unless cur.is_a? Money::Currency
-  supported_currencies.include? cur
-end
-
-
- -
-

- - #to_sObject - - - - - -

- - - - -
-
-
-
-50
-51
-52
-
-
# File 'app/models/gera/rate_source.rb', line 50
-
-def to_s
-  name
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/RateSourceAuto.html b/doc/Gera/RateSourceAuto.html deleted file mode 100644 index 448641a5..00000000 --- a/doc/Gera/RateSourceAuto.html +++ /dev/null @@ -1,239 +0,0 @@ - - - - - - - Class: Gera::RateSourceAuto - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::RateSourceAuto - - - -

-
- -
-
Inherits:
-
- RateSource - - - show all - -
-
- - - - - - - - - - - -
-
Defined in:
-
app/models/gera/rate_source_auto.rb
-
- -
- - - - -

Constant Summary

- -

Constants inherited - from RateSource

-

Gera::RateSource::RateNotFound

- - - - - - -

- Instance Method Summary - collapse -

- - - - - - - - - - - - - -

Methods inherited from RateSource

-

#actual_rates, available_pairs, #find_rate_by_currency_pair, #find_rate_by_currency_pair!, get!, #is_currency_supported?, supported_currencies, #to_s

- - - - - - - - - -

Methods included from CurrencyPairGenerator

-

#generate_pairs_from_currencies

- - - - - - - - - -
-

Instance Method Details

- - -
-

- - #build_currency_rate(pair) ⇒ Object - - - - - -

- - - - -
-
-
-
-3
-4
-5
-6
-7
-8
-9
-
-
# File 'app/models/gera/rate_source_auto.rb', line 3
-
-def build_currency_rate(pair)
-  build_same(pair) ||
-    build_from_source(manual, pair) ||
-    build_from_source(cbr, pair) ||
-    build_from_source(exmo, pair) ||
-    build_cross(pair)
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/RateSourceBitfinex.html b/doc/Gera/RateSourceBitfinex.html deleted file mode 100644 index c2c81f92..00000000 --- a/doc/Gera/RateSourceBitfinex.html +++ /dev/null @@ -1,231 +0,0 @@ - - - - - - - Class: Gera::RateSourceBitfinex - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::RateSourceBitfinex - - - -

-
- -
-
Inherits:
-
- RateSource - - - show all - -
-
- - - - - - - - - - - -
-
Defined in:
-
app/models/gera/rate_source_bitfinex.rb
-
- -
- - - - -

Constant Summary

- -

Constants inherited - from RateSource

-

Gera::RateSource::RateNotFound

- - - - - - -

- Class Method Summary - collapse -

- - - - - - - - - - - - - -

Methods inherited from RateSource

-

#actual_rates, available_pairs, #find_rate_by_currency_pair, #find_rate_by_currency_pair!, get!, #is_currency_supported?, #to_s

- - - - - - - - - -

Methods included from CurrencyPairGenerator

-

#generate_pairs_from_currencies

- - - - - - - - - -
-

Class Method Details

- - -
-

- - .supported_currenciesObject - - - - - -

- - - - -
-
-
-
-3
-4
-5
-
-
# File 'app/models/gera/rate_source_bitfinex.rb', line 3
-
-def self.supported_currencies
-  %i(NEO BTC ETH EUR USD).map { |m| Money::Currency.find! m }
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/RateSourceCBR.html b/doc/Gera/RateSourceCBR.html deleted file mode 100644 index bb899bc3..00000000 --- a/doc/Gera/RateSourceCBR.html +++ /dev/null @@ -1,287 +0,0 @@ - - - - - - - Class: Gera::RateSourceCBR - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::RateSourceCBR - - - -

-
- -
-
Inherits:
-
- RateSource - - - show all - -
-
- - - - - - - - - - - -
-
Defined in:
-
app/models/gera/rate_source_cbr.rb
-
- -
- -
-

Direct Known Subclasses

-

RateSourceCBRAvg

-
- - - -

Constant Summary

- -

Constants inherited - from RateSource

-

Gera::RateSource::RateNotFound

- - - - - - -

- Class Method Summary - collapse -

- - - - - - - - - - - - - -

Methods inherited from RateSource

-

#actual_rates, #find_rate_by_currency_pair, #find_rate_by_currency_pair!, get!, #is_currency_supported?, #to_s

- - - - - - - - - -

Methods included from CurrencyPairGenerator

-

#generate_pairs_from_currencies

- - - - - - - - - -
-

Class Method Details

- - -
-

- - .available_pairsObject - - - - - -

- - - - -
-
-
-
-7
-8
-9
-
-
# File 'app/models/gera/rate_source_cbr.rb', line 7
-
-def self.available_pairs
-  [ 'KZT/RUB', 'USD/RUB', 'EUR/RUB' ].map { |cp| Gera::CurrencyPair.new cp }.freeze
-end
-
-
- -
-

- - .supported_currenciesObject - - - - - -

- - - - -
-
-
-
-3
-4
-5
-
-
# File 'app/models/gera/rate_source_cbr.rb', line 3
-
-def self.supported_currencies
-  %i(RUB KZT USD EUR).map { |m| Money::Currency.find! m }
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/RateSourceCBRAvg.html b/doc/Gera/RateSourceCBRAvg.html deleted file mode 100644 index 064d4051..00000000 --- a/doc/Gera/RateSourceCBRAvg.html +++ /dev/null @@ -1,175 +0,0 @@ - - - - - - - Class: Gera::RateSourceCBRAvg - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::RateSourceCBRAvg - - - -

-
- -
-
Inherits:
-
- RateSourceCBR - - - show all - -
-
- - - - - - - - - - - -
-
Defined in:
-
app/models/gera/rate_source_cbr_avg.rb
-
- -
- - - - -

Constant Summary

- -

Constants inherited - from RateSource

-

Gera::RateSource::RateNotFound

- - - - - - - - - - - - -

Method Summary

- -

Methods inherited from RateSourceCBR

-

available_pairs, supported_currencies

- - - - - - - - - -

Methods inherited from RateSource

-

#actual_rates, available_pairs, #find_rate_by_currency_pair, #find_rate_by_currency_pair!, get!, #is_currency_supported?, supported_currencies, #to_s

- - - - - - - - - -

Methods included from CurrencyPairGenerator

-

#generate_pairs_from_currencies

- - - - - - - - - -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/RateSourceEXMO.html b/doc/Gera/RateSourceEXMO.html deleted file mode 100644 index 4561f320..00000000 --- a/doc/Gera/RateSourceEXMO.html +++ /dev/null @@ -1,231 +0,0 @@ - - - - - - - Class: Gera::RateSourceEXMO - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::RateSourceEXMO - - - -

-
- -
-
Inherits:
-
- RateSource - - - show all - -
-
- - - - - - - - - - - -
-
Defined in:
-
app/models/gera/rate_source_exmo.rb
-
- -
- - - - -

Constant Summary

- -

Constants inherited - from RateSource

-

Gera::RateSource::RateNotFound

- - - - - - -

- Class Method Summary - collapse -

- - - - - - - - - - - - - -

Methods inherited from RateSource

-

#actual_rates, available_pairs, #find_rate_by_currency_pair, #find_rate_by_currency_pair!, get!, #is_currency_supported?, #to_s

- - - - - - - - - -

Methods included from CurrencyPairGenerator

-

#generate_pairs_from_currencies

- - - - - - - - - -
-

Class Method Details

- - -
-

- - .supported_currenciesObject - - - - - -

- - - - -
-
-
-
-3
-4
-5
-
-
# File 'app/models/gera/rate_source_exmo.rb', line 3
-
-def self.supported_currencies
-  %i(BTC BCH DSH ETH ETC LTC XRP XMR USD RUB ZEC EUR).map { |m| Money::Currency.find! m }
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/RateSourceManual.html b/doc/Gera/RateSourceManual.html deleted file mode 100644 index 18c1aa33..00000000 --- a/doc/Gera/RateSourceManual.html +++ /dev/null @@ -1,283 +0,0 @@ - - - - - - - Class: Gera::RateSourceManual - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::RateSourceManual - - - -

-
- -
-
Inherits:
-
- RateSource - - - show all - -
-
- - - - - - - - - - - -
-
Defined in:
-
app/models/gera/rate_source_manual.rb
-
- -
- - - - -

Constant Summary

- -

Constants inherited - from RateSource

-

Gera::RateSource::RateNotFound

- - - - - - -

- Class Method Summary - collapse -

- - - - - - - - - - - - - -

Methods inherited from RateSource

-

#actual_rates, #find_rate_by_currency_pair, #find_rate_by_currency_pair!, get!, #is_currency_supported?, #to_s

- - - - - - - - - -

Methods included from CurrencyPairGenerator

-

#generate_pairs_from_currencies

- - - - - - - - - -
-

Class Method Details

- - -
-

- - .available_pairsObject - - - - - -

- - - - -
-
-
-
-7
-8
-9
-
-
# File 'app/models/gera/rate_source_manual.rb', line 7
-
-def self.available_pairs
-  CurrencyPair.all
-end
-
-
- -
-

- - .supported_currenciesObject - - - - - -

- - - - -
-
-
-
-3
-4
-5
-
-
# File 'app/models/gera/rate_source_manual.rb', line 3
-
-def self.supported_currencies
-  Money::Currency.all
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/RatesWorker.html b/doc/Gera/RatesWorker.html deleted file mode 100644 index f93f2276..00000000 --- a/doc/Gera/RatesWorker.html +++ /dev/null @@ -1,251 +0,0 @@ - - - - - - - Module: Gera::RatesWorker - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Module: Gera::RatesWorker - - - -

-
- - - - - - - - - - - -
-
Defined in:
-
app/workers/concerns/gera/rates_worker.rb
-
- -
- -

Overview

-
- -

Сливает курсы со всех источников

- - -
-
-
- - -
- -

- Constant Summary - collapse -

- -
- -
Error = - -
-
Class.new StandardError
- -
- - - - - - - - - -

- Instance Method Summary - collapse -

- - - - - - -
-

Instance Method Details

- - -
-

- - #performObject - - - - - -

- - - - -
-
-
-
-10
-11
-12
-13
-14
-15
-16
-17
-18
-19
-20
-21
-22
-23
-24
-25
-26
-27
-28
-29
-30
-31
-32
-33
-34
-35
-36
-37
-
-
# File 'app/workers/concerns/gera/rates_worker.rb', line 10
-
-def perform
-  # Альтернативнвы подход: Model.uncached do
-  ActiveRecord::Base.connection.clear_query_cache
-
-  rates # Подгружаем до транзакции
-
-  rate_source.with_lock do
-    create_snapshot
-    rates.each do |pair, data|
-      save_rate pair, data
-    end
-    rate_source.update actual_snapshot_id: snapshot.id
-  end
-
-  # CurrencyRatesWorker.perform_async
-  CurrencyRatesWorker.new.perform
-
-  snapshot.id
-
-  # EXMORatesWorker::Error: Error 40016: Maintenance work in progress
-rescue ActiveRecord::RecordNotUnique, RestClient::TooManyRequests => error
-  raise error if Rails.env.test?
-  logger.error error
-  Bugsnag.notify error do |b|
-    b.severity = :warning
-    b. = { error: error }
-  end
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Gera/Universe.html b/doc/Gera/Universe.html deleted file mode 100644 index 42364cd2..00000000 --- a/doc/Gera/Universe.html +++ /dev/null @@ -1,597 +0,0 @@ - - - - - - - Class: Gera::Universe - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Gera::Universe - - - -

-
- -
-
Inherits:
-
- Object - -
    -
  • Object
  • - - - -
- show all - -
-
- - - - - - - - - - - -
-
Defined in:
-
lib/gera/repositories/universe.rb
-
- -
- - - - - -

Instance Attribute Summary collapse

- - - - - - -

- Class Method Summary - collapse -

- - - -

- Instance Method Summary - collapse -

- - - - - -
-

Instance Attribute Details

- - - -
-

- - #currency_rate_modes_repositoryObject (readonly) - - - - - -

-
- -

Returns the value of attribute currency_rate_modes_repository

- - -
-
-
- - -
- - - - -
-
-
-
-16
-17
-18
-
-
# File 'lib/gera/repositories/universe.rb', line 16
-
-def currency_rate_modes_repository
-  @currency_rate_modes_repository
-end
-
-
- - - -
-

- - #currency_rates_repositoryObject (readonly) - - - - - -

-
- -

Returns the value of attribute currency_rates_repository

- - -
-
-
- - -
- - - - -
-
-
-
-16
-17
-18
-
-
# File 'lib/gera/repositories/universe.rb', line 16
-
-def currency_rates_repository
-  @currency_rates_repository
-end
-
-
- - - -
-

- - #direction_rates_repositoryObject (readonly) - - - - - -

-
- -

Returns the value of attribute direction_rates_repository

- - -
-
-
- - -
- - - - -
-
-
-
-16
-17
-18
-
-
# File 'lib/gera/repositories/universe.rb', line 16
-
-def direction_rates_repository
-  @direction_rates_repository
-end
-
-
- -
- - -
-

Class Method Details

- - -
-

- - .instanceObject - - - - - -

- - - - -
-
-
-
-11
-12
-13
-
-
# File 'lib/gera/repositories/universe.rb', line 11
-
-def instance
-  RequestStore[:universe_repository] ||= new
-end
-
-
- -
- -
-

Instance Method Details

- - -
-

- - #clear!Object - - - - - -

- - - - -
-
-
-
-18
-19
-20
-21
-22
-23
-24
-25
-
-
# File 'lib/gera/repositories/universe.rb', line 18
-
-def clear!
-  @currency_rates_repository = nil
-  @currency_rate_modes_repository = nil
-  @direction_rates_repository = nil
-  @exchange_rates_repository = nil
-  @payment_systems = nil
-  @reserves = nil
-end
-
-
- -
-

- - #exchange_rates_repositoryObject - - - - - -

- - - - -
-
-
-
-43
-44
-45
-
-
# File 'lib/gera/repositories/universe.rb', line 43
-
-def exchange_rates_repository
-  @exchange_rates_repository ||= ExchangeRatesRepository.new
-end
-
-
- -
-

- - #payment_systemsObject - - - - - -

- - - - -
-
-
-
-27
-28
-29
-
-
# File 'lib/gera/repositories/universe.rb', line 27
-
-def payment_systems
-  @payment_systems ||= PaymentSystemsRepository.new
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Money.html b/doc/Money.html deleted file mode 100644 index b97841ab..00000000 --- a/doc/Money.html +++ /dev/null @@ -1,207 +0,0 @@ - - - - - - - Class: Money - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Money - - - -

-
- -
-
Inherits:
-
- Object - -
    -
  • Object
  • - - - -
- show all - -
-
- - - - - - - - - - - -
-
Defined in:
-
lib/gera/money_support.rb
-
- -
- - - - - - - - - -

- Instance Method Summary - collapse -

- - - - - - -
-

Instance Method Details

- - -
-

- - #authorized_roundObject - - - - - -

-
- -

TODO Отказаться Это сумма, до которой разрешено безопасное округление при -приеме суммы от клиента

- - -
-
-
- - -
- - - - -
-
-
-
-83
-84
-85
-86
-
-
# File 'lib/gera/money_support.rb', line 83
-
-def authorized_round
-  return self unless currency.authorized_round.is_a? Numeric
-  Money.from_amount to_f.round(currency.authorized_round), currency
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Money/Currency.html b/doc/Money/Currency.html deleted file mode 100644 index 9ad93fd0..00000000 --- a/doc/Money/Currency.html +++ /dev/null @@ -1,382 +0,0 @@ - - - - - - - Class: Money::Currency - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Money::Currency - - - -

-
- -
-
Inherits:
-
- Object - -
    -
  • Object
  • - - - -
- show all - -
-
- - - - - - - - - - - -
-
Defined in:
-
lib/gera/money_support.rb
-
- -
- - - - - - - - - -

- Class Method Summary - collapse -

- - - -

- Instance Method Summary - collapse -

- - - - - - -
-

Class Method Details

- - -
-

- - .all_cryptoObject - - - - - -

- - - - -
-
-
-
-70
-71
-72
-
-
# File 'lib/gera/money_support.rb', line 70
-
-def self.all_crypto
-  @all_crypto ||= all.select(&:is_crypto?)
-end
-
-
- -
-

- - .find!(query) ⇒ Object - - - - - -

- - - - -
-
-
-
-56
-57
-58
-
-
# File 'lib/gera/money_support.rb', line 56
-
-def self.find!(query)
-  find(query) || raise("No found currency (#{query.inspect})")
-end
-
-
- -
-

- - .find_by_local_id(local_id) ⇒ Object - - - - - -

-
- -

TODO Вынести в app

- - -
-
-
- - -
- - - - -
-
-
-
-62
-63
-64
-65
-66
-67
-68
-
-
# File 'lib/gera/money_support.rb', line 62
-
-def self.find_by_local_id(local_id)
-  local_id = local_id.to_i
-  id, _ = self.table.find{|key, currency| currency[:local_id] == local_id}
-  new(id)
-rescue UnknownCurrency
-  nil
-end
-
-
- -
- -
-

Instance Method Details

- - -
-

- - #zero_moneyObject - - - - - -

- - - - -
-
-
-
-74
-75
-76
-
-
# File 'lib/gera/money_support.rb', line 74
-
-def zero_money
-  Money.from_amount(0, self)
-end
-
-
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/Numeric.html b/doc/Numeric.html deleted file mode 100644 index aa10a528..00000000 --- a/doc/Numeric.html +++ /dev/null @@ -1,137 +0,0 @@ - - - - - - - Class: Numeric - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Class: Numeric - - - -

-
- -
-
Inherits:
-
- Object - -
    -
  • Object
  • - - - -
- show all - -
-
- - - - - - -
-
Includes:
-
Gera::Numeric
-
- - - - - - -
-
Defined in:
-
lib/gera/numeric.rb
-
- -
- - - - - - - - - - - - - - - -

Method Summary

- -

Methods included from Gera::Numeric

-

#as_percentage_of, #percent_of, #percents, #to_rate

- - -
- - - -
- - \ No newline at end of file diff --git a/doc/_index.html b/doc/_index.html deleted file mode 100644 index fbbbd74d..00000000 --- a/doc/_index.html +++ /dev/null @@ -1,702 +0,0 @@ - - - - - - - Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Documentation by YARD 0.9.16

-
-

Alphabetic Index

- -

File Listing

- - -
-

Namespace Listing A-Z

- - - - - - - - -
- - - - - - - - - - - - - - - - - -
    -
  • G
  • -
      - -
    • - Gera - -
    • - -
    -
- - - - - -
- - - - - - - - - - - - - - -
    -
  • S
  • - -
- - -
    -
  • U
  • - -
- -
- -
- -
- - - -
- - \ No newline at end of file diff --git a/doc/autorate_system.md b/doc/autorate_system.md new file mode 100644 index 00000000..e8466ae1 --- /dev/null +++ b/doc/autorate_system.md @@ -0,0 +1,523 @@ +# Система Автокурса (AutoRate) — Legacy + +> **Внимание:** Это документация **legacy-системы** расчёта курсов. Данная система используется в текущей production-версии, но планируется к замене. + +--- + +## Краткое описание для бизнес-аналитика + +### Что это +Система автоматически рассчитывает комиссию обменного направления, чтобы поддерживать конкурентную позицию в рейтинге BestChange. + +### Как формируется комиссия + +Итоговая комиссия = **Базовая** + **Корректировка по резервам** + **Корректировка по курсу** + +--- + +**1. Базовая комиссия (по позиции в BestChange)** + +Оператор задаёт: +- Целевой диапазон позиций (например, 3–5 место) +- Допустимый диапазон комиссии (например, 1–3%) + +Система смотрит, какую комиссию ставят конкуренты на этих позициях, и ставит **на 0.0001% ниже** первого подходящего — чтобы быть чуть выгоднее. + +--- + +**2. Корректировка по резервам** *(опционально)* + +Если резервов много → можно снизить комиссию (привлечь больше клиентов) +Если резервов мало → повысить комиссию (снизить нагрузку) + +Настраивается через контрольные точки: "при отклонении резерва на X% — изменить комиссию на Y%" + +> **Как включить:** Создать `AutoRateSetting` для платёжной системы + добавить `AutoRateCheckpoint` с типом `reserve`. Если настройки отсутствуют — корректировка = 0. + +--- + +**3. Корректировка по динамике курса** *(опционально)* + +Сравнивается текущий курс со средним за 24 часа. +При резких скачках курса комиссия корректируется для снижения рисков. + +> **Как включить:** Добавить `AutoRateCheckpoint` с типом `by_base_rate` для обеих платёжных систем направления. Если настройки отсутствуют — корректировка = 0. + +--- + +### Пример + +| Параметр | Значение | +|----------|----------| +| Целевая позиция | 3–5 место | +| Допустимая комиссия | 1–3% | +| Комиссия конкурента на 3 месте | 2.5% | +| **Наша комиссия** | **2.4999%** | + +Плюс корректировки по резервам и курсу, если настроены. + +--- + +### Итог + +Система позволяет автоматически удерживать заданную позицию в рейтинге, при этом учитывая внутренние ограничения (резервы) и рыночные условия (волатильность курса). + +--- + +## Техническое описание + +## Обзор + +Система автокурса автоматически рассчитывает комиссию обменного направления на основе: +1. Позиции в рейтинге BestChange +2. Текущих резервов платежных систем +3. Динамики базового курса валют + +--- + +## Структура данных + +### Основные модели + +#### `Gera::ExchangeRate` (таблица: `gera_exchange_rates`) + +Направление обмена между платежными системами. + +| Поле | Тип | Описание | +|------|-----|----------| +| `income_payment_system_id` | integer | Входящая ПС | +| `outcome_payment_system_id` | integer | Исходящая ПС | +| `value` (alias: `comission`) | float | Базовая комиссия направления (%) | +| `auto_rate` | boolean | Флаг включения автокурса | +| `source` | string | Источник курсов: `bestchange`, `manual` | +| `margin` | float | Наценка (%) | +| `is_enabled` | boolean | Направление включено | + +**Файл модели:** `vendor/gera → gera/app/models/gera/exchange_rate.rb` + +--- + +#### `Gera::TargetAutorateSetting` (таблица: `gera_target_autorate_settings`) + +Настройки автокурса для конкретного направления. + +| Поле | Тип | Описание | +|------|-----|----------| +| `exchange_rate_id` | integer | Связь с направлением | +| `position_from` | integer | Целевая позиция в BestChange (от) | +| `position_to` | integer | Целевая позиция в BestChange (до) | +| `autorate_from` | float | Целевая комиссия (от, %) | +| `autorate_to` | float | Целевая комиссия (до, %) | + +**Файл модели:** `vendor/gera → gera/app/models/gera/target_autorate_setting.rb` + +**Условие применимости:** +```ruby +def could_be_calculated? + position_from.present? && position_to.present? && autorate_from.present? && autorate_to.present? +end +``` + +--- + +#### `AutoRateSetting` (таблица: `auto_rate_settings`) + +Настройки автокурса для платежной системы. + +| Поле | Тип | Описание | +|------|-----|----------| +| `payment_system_id` | integer | Платежная система | +| `direction` | string | `'income'` или `'outcome'` | +| `min_fee_percents` | float | Минимальная комиссия (%) | +| `max_fee_percents` | float | Максимальная комиссия (%) | +| `base_reserve_cents` | bigint | Базовый резерв (в центах) | +| `base_reserve_currency` | string | Валюта резерва | +| `total_checkpoints` | integer | Количество контрольных точек | + +**Файл модели:** `app/models/auto_rate_setting.rb` + +--- + +#### `AutoRateCheckpoint` (таблица: `auto_rate_checkpoints`) + +Контрольные точки для расчета комиссии. + +| Поле | Тип | Описание | +|------|-----|----------| +| `auto_rate_setting_id` | integer | Связь с настройкой | +| `checkpoint_type` | string | `'reserve'` или `'by_base_rate'` | +| `direction` | string | `'plus'` или `'minus'` | +| `value_percents` | float | Порог срабатывания (%) | +| `min_boundary` | float | Минимальная граница комиссии | +| `max_boundary` | float | Максимальная граница комиссии | + +**Файл модели:** `app/models/auto_rate_checkpoint.rb` + +--- + +## Алгоритм расчета комиссии + +### Ключевой сервис: `Gera::RateComissionCalculator` + +**Файл:** `vendor/gera → gera/app/services/gera/rate_comission_calculator.rb` + +Итоговая комиссия складывается из трех компонентов: + +```ruby +def commission + auto_comission_by_external_comissions + auto_comission_by_reserve + comission_by_base_rate +end +``` + +--- + +### Компонент 1: Комиссия по позиции в BestChange + +**Метод:** `auto_comission_by_external_comissions` + +**Логика:** +1. Загружаются данные обменников из `BestChange::Repository` +2. Фильтруются по диапазону позиций `position_from..position_to` +3. Из них выбираются те, чья комиссия попадает в диапазон `autorate_from..autorate_to` +4. Берется первый обменник, из его комиссии вычитается `AUTO_COMISSION_GAP` (0.0001) + +```ruby +# Строки 161-172 +def auto_comission_by_external_comissions + return 0 unless could_be_calculated? + + external_rates_in_target_position = external_rates[(position_from - 1)..(position_to - 1)] + return autorate_from unless external_rates_in_target_position.present? + + external_rates_in_target_comission = external_rates_in_target_position.select { |rate| + ((autorate_from)..(autorate_to)).include?(rate.target_rate_percent) + } + return autorate_from if external_rates_in_target_comission.empty? + + target_comission = external_rates_in_target_comission.first.target_rate_percent - AUTO_COMISSION_GAP +end +``` + +--- + +### Компонент 2: Комиссия по резервам + +**Метод:** `auto_comission_by_reserve` + +**Логика:** +1. Для income и outcome платежных систем ищутся `AutoRateSetting` с `direction: 'income'/'outcome'` +2. Сравнивается текущий резерв (`reserve`) с базовым резервом (`base`) +3. Определяется направление: `'plus'` если резерв >= базы, иначе `'minus'` +4. Рассчитывается процент отклонения: `(max - min) / min * 100` +5. Ищется checkpoint с `checkpoint_type: 'reserve'` и подходящим `value_percents` +6. Усредняются `min_boundary` и `max_boundary` от обеих платежных систем + +```ruby +# Строки 96-110 +def income_reserve_checkpoint + income_auto_rate_setting.checkpoint( + base_value: income_auto_rate_setting.reserve, + additional_value: income_auto_rate_setting.base, + type: 'reserve' + ) +end + +# AutoRateSetting#checkpoint (app/models/auto_rate_setting.rb:18-21) +def checkpoint(base_value:, additional_value:, type:) + direction = base_value >= additional_value ? 'plus' : 'minus' + find_checkpoint(reserve_ratio: calculate_diff_in_percents(base_value, additional_value), direction: direction, type: type) +end +``` + +--- + +### Компонент 3: Комиссия по базовому курсу + +**Метод:** `comission_by_base_rate` + +**Логика:** +1. Получается текущий курс валют (`current_base_rate`) +2. Получается средний курс за последние 24 часа (`average_base_rate`) +3. Сравниваются значения +4. Ищется checkpoint с `checkpoint_type: 'by_base_rate'` +5. Усредняются границы + +```ruby +# Строки 54-63 +def current_base_rate + return 1.0 if same_currencies? + Gera::CurrencyRateHistoryInterval + .where(cur_from_id: in_currency.local_id, cur_to_id: out_currency.local_id) + .last.avg_rate +end + +def average_base_rate + return 1.0 if same_currencies? + Gera::CurrencyRateHistoryInterval + .where('interval_from > ?', DateTime.now.utc - 24.hours) + .where(cur_from_id: in_currency.local_id, cur_to_id: out_currency.local_id) + .average(:avg_rate) +end +``` + +--- + +## Применение комиссии к курсу + +### 1. Получение итоговой комиссии + +**Файл:** `gera/app/models/gera/exchange_rate.rb:159-161` + +```ruby +def final_rate_percents + @final_rate_percents ||= auto_rate? ? rate_comission_calculator.auto_comission : rate_comission_calculator.fixed_comission +end +``` + +### 2. Расчет конечного курса + +**Файл:** `gera/app/models/gera/direction_rate.rb:139-145` + +```ruby +def calculate_rate + self.base_rate_value = currency_rate.rate_value + raise UnknownExchangeRate unless exchange_rate + + self.rate_percent = exchange_rate.final_rate_percents # ← Берется auto или fixed комиссия + self.rate_value = calculate_finite_rate(base_rate_value, rate_percent) +end +``` + +### 3. Формула расчета конечного курса + +**Файл:** `gera/app/models/concerns/gera/mathematic.rb` + +```ruby +def calculate_finite_rate(base_rate, comission_percents) + base_rate * (1 - comission_percents / 100.0) +end +``` + +--- + +## Источники данных + +### BestChange::Repository + +Хранит рейтинг обменников из BestChange в Redis. + +**Файл:** `vendor/best_change → best_change/lib/best_change/repository.rb` + +```ruby +BestChange::Repository.getRows(bestchange_key) # Возвращает массив Row +``` + +### BestchangeCacheServer + +DRb-сервер для кеширования данных BestChange. + +**Файл:** `lib/bestchange_cache_server.rb` + +- URI: `druby://localhost:8787` +- TTL кеша: 10 секунд +- Автообновление в фоновом потоке + +### ReservesByPaymentSystems + +Репозиторий резервов по платежным системам. + +**Файл:** `app/repositories/reserves_by_payment_systems.rb` + +```ruby +ReservesByPaymentSystems.reserve_by_payment_system(payment_system_id) +``` + +Данные берутся из Redis: `Redis.new.get('final_reserves')` + +### CurrencyRateHistoryInterval + +История курсов валют (Gera gem). + +```ruby +Gera::CurrencyRateHistoryInterval + .where(cur_from_id: ..., cur_to_id: ...) + .last.avg_rate +``` + +--- + +## Фоновые задачи (Workers) + +| Worker | Очередь | Расписание | Назначение | +|--------|---------|------------|------------| +| `Gera::DirectionsRatesWorker` | critical | Периодически | Пересчитывает все `direction_rates` | +| `BestChange::LoadingWorker` | default | Периодически | Загружает данные из BestChange | +| `ExchangeRateCacheUpdaterWorker` | critical | Каждую минуту | Обновляет кеш позиций Kassa в BC | +| `Gera::ExchangeRateUpdaterJob` | exchange_rates | По событию | Обновляет `comission` в направлении | +| `RateUpdaterWorker` | critical | По событию | Обновляет курс для заказа | + +**Конфигурация:** `config/crontab_production.yml` + +--- + +## API управления + +### Обновление настроек автокурса + +**Endpoint:** `PUT /operator_api/exchange_rates/:id` + +**Параметры:** + +| Параметр | Тип | Описание | +|----------|-----|----------| +| `comission` | float | Ручная комиссия (%) | +| `is_enabled` | boolean | Включено направление | +| `position_from` | integer | Целевая позиция BC (от) | +| `position_to` | integer | Целевая позиция BC (до) | +| `autorate_from` | float | Целевая комиссия (от) | +| `autorate_to` | float | Целевая комиссия (до) | +| `source` | string | Источник курсов | +| `margin` | float | Наценка (%) | + +**Файл:** `app/api/operator_api/exchange_rates.rb` + +### Изменение позиции в BestChange + +**Endpoint:** `PUT /operator_api/bestchange/byExchangeRate/:exchange_rate_id/position` + +**Параметр:** `position` (integer) — целевая позиция (начинается с 0) + +**Логика:** `BestChange::PositionService#change_position!` + +--- + +## UI управления + +| URL | Описание | +|-----|----------| +| `/operator/auto_rate_settings` | Список настроек автокурса для ПС | +| `/operator/auto_rate_settings/:id/edit` | Редактирование настройки | +| `/operator/auto_rate_settings/:id/auto_rate_checkpoints` | Контрольные точки | +| `/operator/exchange_rates` | Матрица направлений | +| `/operator/autorate_managers` | Менеджер автокурса | + +--- + +## Схема работы + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ ИСТОЧНИКИ ДАННЫХ │ +├───────────────────┬───────────────────┬─────────────────────────────┤ +│ BestChange API │ Резервы (Redis) │ История курсов (MySQL) │ +│ (bm_*.dat файлы) │ final_reserves │ currency_rate_history_ │ +│ │ │ intervals │ +└─────────┬─────────┴─────────┬─────────┴──────────────┬──────────────┘ + │ │ │ + ▼ ▼ ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ RateComissionCalculator │ +│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ by_external │ │ by_reserve │ │ by_base_rate │ │ +│ │ (позиция в BC) │+│ (резервы ПС) │+│ (динамика курса) │ │ +│ │ │ │ │ │ │ │ +│ │ position_from/to │ │ AutoRateSetting │ │ current vs avg │ │ +│ │ autorate_from/to │ │ checkpoints │ │ за 24 часа │ │ +│ └──────────────────┘ └──────────────────┘ └──────────────────┘ │ +│ │ │ +│ ┌─────────▼─────────┐ │ +│ │ auto_comission │ │ +│ │ (итоговая %) │ │ +│ └─────────┬─────────┘ │ +└───────────────────────────────┼─────────────────────────────────────┘ + │ + ┌───────────▼───────────┐ + │ ExchangeRate │ + │ final_rate_percents │ + │ (auto или fixed) │ + └───────────┬───────────┘ + │ + ┌───────────▼───────────┐ + │ DirectionRate │ + │ base_rate_value │ ← Курс валют + │ rate_percent │ ← Комиссия + │ rate_value │ ← Конечный курс + └───────────────────────┘ +``` + +--- + +## Ключевые файлы + +### Mercury (основное приложение) + +| Файл | Описание | +|------|----------| +| `app/models/auto_rate_setting.rb` | Модель настроек для ПС | +| `app/models/auto_rate_checkpoint.rb` | Модель контрольных точек | +| `app/services/auto_rate_updater.rb` | Генерация checkpoint'ов | +| `app/repositories/reserves_by_payment_systems.rb` | Репозиторий резервов | +| `app/api/operator_api/exchange_rates.rb` | API управления | +| `app/workers/exchange_rate_cache_updater_worker.rb` | Обновление кеша | +| `lib/bestchange_cache_server.rb` | DRb-сервер кеша BC | +| `config/initializers/gera.rb` | Инициализация Gera | + +### Gera gem (`/home/danil/code/alfagen/gera`) + +| Файл | Описание | +|------|----------| +| `app/models/gera/exchange_rate.rb` | Модель направления | +| `app/models/gera/target_autorate_setting.rb` | Настройки автокурса | +| `app/models/gera/direction_rate.rb` | Конечный курс | +| `app/services/gera/rate_comission_calculator.rb` | **Расчет комиссии** | +| `app/jobs/gera/exchange_rate_updater_job.rb` | Обновление курса | + +### BestChange gem (`/home/danil/code/alfagen/best_change`) + +| Файл | Описание | +|------|----------| +| `lib/best_change/service.rb` | Сервис работы с BC | +| `lib/best_change/repository.rb` | Репозиторий данных BC | +| `lib/best_change/position_service.rb` | Изменение позиции | +| `lib/best_change/row.rb` | Строка рейтинга BC | +| `lib/best_change/record.rb` | Запись с расчетами | + +--- + +## Константы + +| Константа | Значение | Файл | Описание | +|-----------|----------|------|----------| +| `AUTO_COMISSION_GAP` | 0.0001 | autorate_calculators/base.rb | Отступ от комиссии конкурента | +| `NOT_ALLOWED_COMISSION_RANGE` | 0.7..1.4 | rate_comission_calculator.rb | Запрещенный диапазон (реферальная BC) | +| `EXCLUDED_PS_IDS` | [54, 56] | rate_comission_calculator.rb | Исключенные ПС | +| `STEP` | 0.005 | position_service.rb | Шаг изменения комиссии | +| `CACHE_TTL` | 10 | bestchange_cache_server.rb | TTL кеша BC (секунды) | + +--- + +## Пример расчета + +Допустим для направления QIWI RUB → BTC: + +1. **Настройки:** + - `position_from: 3`, `position_to: 5` + - `autorate_from: 1.0`, `autorate_to: 3.0` + +2. **Данные BestChange (позиции 2-4):** + - Позиция 3: комиссия 2.5% + - Позиция 4: комиссия 2.8% + - Позиция 5: комиссия 3.1% + +3. **Расчет `auto_comission_by_external_comissions`:** + - Фильтр по позиции: [2.5, 2.8, 3.1] + - Фильтр по комиссии (1.0-3.0): [2.5, 2.8] + - Первый: 2.5% + - Результат: 2.5 - 0.0001 = **2.4999%** + +4. **Добавляются корректировки по резервам и курсу (если настроены)** + +5. **Итоговая комиссия применяется к курсу:** + ``` + finite_rate = base_rate * (1 - 2.4999 / 100) + ``` diff --git a/doc/class_list.html b/doc/class_list.html deleted file mode 100644 index 8bcf3a72..00000000 --- a/doc/class_list.html +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - - - - - - Class List - - - -
-
-

Class List

- - - -
- - -
- - diff --git a/doc/controllers_brief.svg b/doc/controllers_brief.svg deleted file mode 100644 index d10ecaf8..00000000 --- a/doc/controllers_brief.svg +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - -controllers_diagram - - -_diagram_info -Controllers diagram -Date: Sep 22 2018 - 22:15 -Migration version: 20180913185640 -Generated by RailRoady 1.5.3 -http://railroady.prestonlee.com - - -ApplicationController - -ApplicationController - - - diff --git a/doc/controllers_complete.svg b/doc/controllers_complete.svg deleted file mode 100644 index e6e2cb38..00000000 --- a/doc/controllers_complete.svg +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - -controllers_diagram - - -_diagram_info -Controllers diagram -Date: Sep 22 2018 - 22:15 -Migration version: 20180913185640 -Generated by RailRoady 1.5.3 -http://railroady.prestonlee.com - - -ApplicationController - -ApplicationController - - - -_layout - - - diff --git a/doc/css/common.css b/doc/css/common.css deleted file mode 100644 index cf25c452..00000000 --- a/doc/css/common.css +++ /dev/null @@ -1 +0,0 @@ -/* Override this file with custom rules */ \ No newline at end of file diff --git a/doc/css/full_list.css b/doc/css/full_list.css deleted file mode 100644 index fa359824..00000000 --- a/doc/css/full_list.css +++ /dev/null @@ -1,58 +0,0 @@ -body { - margin: 0; - font-family: "Lucida Sans", "Lucida Grande", Verdana, Arial, sans-serif; - font-size: 13px; - height: 101%; - overflow-x: hidden; - background: #fafafa; -} - -h1 { padding: 12px 10px; padding-bottom: 0; margin: 0; font-size: 1.4em; } -.clear { clear: both; } -.fixed_header { position: fixed; background: #fff; width: 100%; padding-bottom: 10px; margin-top: 0; top: 0; z-index: 9999; height: 70px; } -#search { position: absolute; right: 5px; top: 9px; padding-left: 24px; } -#content.insearch #search, #content.insearch #noresults { background: url(data:image/gif;base64,R0lGODlhEAAQAPYAAP///wAAAPr6+pKSkoiIiO7u7sjIyNjY2J6engAAAI6OjsbGxjIyMlJSUuzs7KamppSUlPLy8oKCghwcHLKysqSkpJqamvT09Pj4+KioqM7OzkRERAwMDGBgYN7e3ujo6Ly8vCoqKjY2NkZGRtTU1MTExDw8PE5OTj4+PkhISNDQ0MrKylpaWrS0tOrq6nBwcKysrLi4uLq6ul5eXlxcXGJiYoaGhuDg4H5+fvz8/KKiohgYGCwsLFZWVgQEBFBQUMzMzDg4OFhYWBoaGvDw8NbW1pycnOLi4ubm5kBAQKqqqiQkJCAgIK6urnJyckpKSjQ0NGpqatLS0sDAwCYmJnx8fEJCQlRUVAoKCggICLCwsOTk5ExMTPb29ra2tmZmZmhoaNzc3KCgoBISEiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCAAAACwAAAAAEAAQAAAHaIAAgoMgIiYlg4kACxIaACEJCSiKggYMCRselwkpghGJBJEcFgsjJyoAGBmfggcNEx0flBiKDhQFlIoCCA+5lAORFb4AJIihCRbDxQAFChAXw9HSqb60iREZ1omqrIPdJCTe0SWI09GBACH5BAkIAAAALAAAAAAQABAAAAdrgACCgwc0NTeDiYozCQkvOTo9GTmDKy8aFy+NOBA7CTswgywJDTIuEjYFIY0JNYMtKTEFiRU8Pjwygy4ws4owPyCKwsMAJSTEgiQlgsbIAMrO0dKDGMTViREZ14kYGRGK38nHguHEJcvTyIEAIfkECQgAAAAsAAAAABAAEAAAB2iAAIKDAggPg4iJAAMJCRUAJRIqiRGCBI0WQEEJJkWDERkYAAUKEBc4Po1GiKKJHkJDNEeKig4URLS0ICImJZAkuQAhjSi/wQyNKcGDCyMnk8u5rYrTgqDVghgZlYjcACTA1sslvtHRgQAh+QQJCAAAACwAAAAAEAAQAAAHZ4AAgoOEhYaCJSWHgxGDJCQARAtOUoQRGRiFD0kJUYWZhUhKT1OLhR8wBaaFBzQ1NwAlkIszCQkvsbOHL7Y4q4IuEjaqq0ZQD5+GEEsJTDCMmIUhtgk1lo6QFUwJVDKLiYJNUd6/hoEAIfkECQgAAAAsAAAAABAAEAAAB2iAAIKDhIWGgiUlh4MRgyQkjIURGRiGGBmNhJWHm4uen4ICCA+IkIsDCQkVACWmhwSpFqAABQoQF6ALTkWFnYMrVlhWvIKTlSAiJiVVPqlGhJkhqShHV1lCW4cMqSkAR1ofiwsjJyqGgQAh+QQJCAAAACwAAAAAEAAQAAAHZ4AAgoOEhYaCJSWHgxGDJCSMhREZGIYYGY2ElYebi56fhyWQniSKAKKfpaCLFlAPhl0gXYNGEwkhGYREUywag1wJwSkHNDU3D0kJYIMZQwk8MjPBLx9eXwuETVEyAC/BOKsuEjYFhoEAIfkECQgAAAAsAAAAABAAEAAAB2eAAIKDhIWGgiUlh4MRgyQkjIURGRiGGBmNhJWHm4ueICImip6CIQkJKJ4kigynKaqKCyMnKqSEK05StgAGQRxPYZaENqccFgIID4KXmQBhXFkzDgOnFYLNgltaSAAEpxa7BQoQF4aBACH5BAkIAAAALAAAAAAQABAAAAdogACCg4SFggJiPUqCJSWGgkZjCUwZACQkgxGEXAmdT4UYGZqCGWQ+IjKGGIUwPzGPhAc0NTewhDOdL7Ykji+dOLuOLhI2BbaFETICx4MlQitdqoUsCQ2vhKGjglNfU0SWmILaj43M5oEAOwAAAAAAAAAAAA==) no-repeat center left; } -#full_list { padding: 0; list-style: none; margin-left: 0; margin-top: 80px; font-size: 1.1em; } -#full_list ul { padding: 0; } -#full_list li { padding: 0; margin: 0; list-style: none; } -#full_list li .item { padding: 5px 5px 5px 12px; } -#noresults { padding: 7px 12px; background: #fff; } -#content.insearch #noresults { margin-left: 7px; } -li.collapsed ul { display: none; } -li a.toggle { cursor: default; position: relative; left: -5px; top: 4px; text-indent: -999px; width: 10px; height: 9px; margin-left: -10px; display: block; float: left; background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAASCAYAAABb0P4QAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAK8AAACvABQqw0mAAAABx0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTM5jWRgMAAAAVdEVYdENyZWF0aW9uIFRpbWUAMy8xNC8wOeNZPpQAAAE2SURBVDiNrZTBccIwEEXfelIAHUA6CZ24BGaWO+FuzZAK4k6gg5QAdGAq+Bxs2Yqx7BzyL7Llp/VfzZeQhCTc/ezuGzKKnKSzpCxXJM8fwNXda3df5RZETlIt6YUzSQDs93sl8w3wBZxCCE10GM1OcWbWjB2mWgEH4Mfdyxm3PSepBHibgQE2wLe7r4HjEidpnXMYdQPKEMJcsZ4zs2POYQOcaPfwMVOo58zsAdMt18BuoVDPxUJRacELbXv3hUIX2vYmOUvi8C8ydz/ThjXrqKqqLbDIAdsCKBd+Wo7GWa7o9qzOQHVVVXeAbs+yHHCH4aTsaCOQqunmUy1yBUAXkdMIfMlgF5EXLo2OpV/c/Up7jG4hhHcYLgWzAZXUc2b2ixsfvc/RmNNfOXD3Q/oeL9axJE1yT9IOoUu6MGUkAAAAAElFTkSuQmCC) no-repeat bottom left; } -li.collapsed a.toggle { opacity: 0.5; cursor: default; background-position: top left; } -li { color: #888; cursor: pointer; } -li.deprecated { text-decoration: line-through; font-style: italic; } -li.odd { background: #f0f0f0; } -li.even { background: #fafafa; } -.item:hover { background: #ddd; } -li small:before { content: "("; } -li small:after { content: ")"; } -li small.search_info { display: none; } -a, a:visited { text-decoration: none; color: #05a; } -li.clicked > .item { background: #05a; color: #ccc; } -li.clicked > .item a, li.clicked > .item a:visited { color: #eee; } -li.clicked > .item a.toggle { opacity: 0.5; background-position: bottom right; } -li.collapsed.clicked a.toggle { background-position: top right; } -#search input { border: 1px solid #bbb; border-radius: 3px; } -#full_list_nav { margin-left: 10px; font-size: 0.9em; display: block; color: #aaa; } -#full_list_nav a, #nav a:visited { color: #358; } -#full_list_nav a:hover { background: transparent; color: #5af; } -#full_list_nav span:after { content: ' | '; } -#full_list_nav span:last-child:after { content: ''; } - -#content h1 { margin-top: 0; } -li { white-space: nowrap; cursor: normal; } -li small { display: block; font-size: 0.8em; } -li small:before { content: ""; } -li small:after { content: ""; } -li small.search_info { display: none; } -#search { width: 170px; position: static; margin: 3px; margin-left: 10px; font-size: 0.9em; color: #888; padding-left: 0; padding-right: 24px; } -#content.insearch #search { background-position: center right; } -#search input { width: 110px; } - -#full_list.insearch ul { display: block; } -#full_list.insearch .item { display: none; } -#full_list.insearch .found { display: block; padding-left: 11px !important; } -#full_list.insearch li a.toggle { display: none; } -#full_list.insearch li small.search_info { display: block; } diff --git a/doc/css/style.css b/doc/css/style.css deleted file mode 100644 index 0bf7e2c7..00000000 --- a/doc/css/style.css +++ /dev/null @@ -1,496 +0,0 @@ -html { - width: 100%; - height: 100%; -} -body { - font-family: "Lucida Sans", "Lucida Grande", Verdana, Arial, sans-serif; - font-size: 13px; - width: 100%; - margin: 0; - padding: 0; - display: flex; - display: -webkit-flex; - display: -ms-flexbox; -} - -#nav { - position: relative; - width: 100%; - height: 100%; - border: 0; - border-right: 1px dotted #eee; - overflow: auto; -} -.nav_wrap { - margin: 0; - padding: 0; - width: 20%; - height: 100%; - position: relative; - display: flex; - display: -webkit-flex; - display: -ms-flexbox; - flex-shrink: 0; - -webkit-flex-shrink: 0; - -ms-flex: 1 0; -} -#resizer { - position: absolute; - right: -5px; - top: 0; - width: 10px; - height: 100%; - cursor: col-resize; - z-index: 9999; -} -#main { - flex: 5 1; - -webkit-flex: 5 1; - -ms-flex: 5 1; - outline: none; - position: relative; - background: #fff; - padding: 1.2em; - padding-top: 0.2em; -} - -@media (max-width: 920px) { - .nav_wrap { width: 100%; top: 0; right: 0; overflow: visible; position: absolute; } - #resizer { display: none; } - #nav { - z-index: 9999; - background: #fff; - display: none; - position: absolute; - top: 40px; - right: 12px; - width: 500px; - max-width: 80%; - height: 80%; - overflow-y: scroll; - border: 1px solid #999; - border-collapse: collapse; - box-shadow: -7px 5px 25px #aaa; - border-radius: 2px; - } -} - -@media (min-width: 920px) { - body { height: 100%; overflow: hidden; } - #main { height: 100%; overflow: auto; } - #search { display: none; } -} - -#main img { max-width: 100%; } -h1 { font-size: 25px; margin: 1em 0 0.5em; padding-top: 4px; border-top: 1px dotted #d5d5d5; } -h1.noborder { border-top: 0px; margin-top: 0; padding-top: 4px; } -h1.title { margin-bottom: 10px; } -h1.alphaindex { margin-top: 0; font-size: 22px; } -h2 { - padding: 0; - padding-bottom: 3px; - border-bottom: 1px #aaa solid; - font-size: 1.4em; - margin: 1.8em 0 0.5em; - position: relative; -} -h2 small { font-weight: normal; font-size: 0.7em; display: inline; position: absolute; right: 0; } -h2 small a { - display: block; - height: 20px; - border: 1px solid #aaa; - border-bottom: 0; - border-top-left-radius: 5px; - background: #f8f8f8; - position: relative; - padding: 2px 7px; -} -.clear { clear: both; } -.inline { display: inline; } -.inline p:first-child { display: inline; } -.docstring, .tags, #filecontents { font-size: 15px; line-height: 1.5145em; } -.docstring p > code, .docstring p > tt, .tags p > code, .tags p > tt { - color: #c7254e; background: #f9f2f4; padding: 2px 4px; font-size: 1em; - border-radius: 4px; -} -.docstring h1, .docstring h2, .docstring h3, .docstring h4 { padding: 0; border: 0; border-bottom: 1px dotted #bbb; } -.docstring h1 { font-size: 1.2em; } -.docstring h2 { font-size: 1.1em; } -.docstring h3, .docstring h4 { font-size: 1em; border-bottom: 0; padding-top: 10px; } -.summary_desc .object_link a, .docstring .object_link a { - font-family: monospace; font-size: 1.05em; - color: #05a; background: #EDF4FA; padding: 2px 4px; font-size: 1em; - border-radius: 4px; -} -.rdoc-term { padding-right: 25px; font-weight: bold; } -.rdoc-list p { margin: 0; padding: 0; margin-bottom: 4px; } -.summary_desc pre.code .object_link a, .docstring pre.code .object_link a { - padding: 0px; background: inherit; color: inherit; border-radius: inherit; -} - -/* style for */ -#filecontents table, .docstring table { border-collapse: collapse; } -#filecontents table th, #filecontents table td, -.docstring table th, .docstring table td { border: 1px solid #ccc; padding: 8px; padding-right: 17px; } -#filecontents table tr:nth-child(odd), -.docstring table tr:nth-child(odd) { background: #eee; } -#filecontents table tr:nth-child(even), -.docstring table tr:nth-child(even) { background: #fff; } -#filecontents table th, .docstring table th { background: #fff; } - -/* style for
a",d=q.getElementsByTagName("*"),e=q.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=q.getElementsByTagName("input")[0],b={leadingWhitespace:q.firstChild.nodeType===3,tbody:!q.getElementsByTagName("tbody").length,htmlSerialize:!!q.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:q.className!=="t",enctype:!!c.createElement("form").enctype,html5Clone:c.createElement("nav").cloneNode(!0).outerHTML!=="<:nav>",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0},i.checked=!0,b.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,b.optDisabled=!h.disabled;try{delete q.test}catch(s){b.deleteExpando=!1}!q.addEventListener&&q.attachEvent&&q.fireEvent&&(q.attachEvent("onclick",function(){b.noCloneEvent=!1}),q.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),b.radioValue=i.value==="t",i.setAttribute("checked","checked"),q.appendChild(i),k=c.createDocumentFragment(),k.appendChild(q.lastChild),b.checkClone=k.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=i.checked,k.removeChild(i),k.appendChild(q),q.innerHTML="",a.getComputedStyle&&(j=c.createElement("div"),j.style.width="0",j.style.marginRight="0",q.style.width="2px",q.appendChild(j),b.reliableMarginRight=(parseInt((a.getComputedStyle(j,null)||{marginRight:0}).marginRight,10)||0)===0);if(q.attachEvent)for(o in{submit:1,change:1,focusin:1})n="on"+o,p=n in q,p||(q.setAttribute(n,"return;"),p=typeof q[n]=="function"),b[o+"Bubbles"]=p;k.removeChild(q),k=g=h=j=q=i=null,f(function(){var a,d,e,g,h,i,j,k,m,n,o,r=c.getElementsByTagName("body")[0];!r||(j=1,k="position:absolute;top:0;left:0;width:1px;height:1px;margin:0;",m="visibility:hidden;border:0;",n="style='"+k+"border:5px solid #000;padding:0;'",o="
"+""+"
",a=c.createElement("div"),a.style.cssText=m+"width:0;height:0;position:static;top:0;margin-top:"+j+"px",r.insertBefore(a,r.firstChild),q=c.createElement("div"),a.appendChild(q),q.innerHTML="
t
",l=q.getElementsByTagName("td"),p=l[0].offsetHeight===0,l[0].style.display="",l[1].style.display="none",b.reliableHiddenOffsets=p&&l[0].offsetHeight===0,q.innerHTML="",q.style.width=q.style.paddingLeft="1px",f.boxModel=b.boxModel=q.offsetWidth===2,typeof q.style.zoom!="undefined"&&(q.style.display="inline",q.style.zoom=1,b.inlineBlockNeedsLayout=q.offsetWidth===2,q.style.display="",q.innerHTML="
",b.shrinkWrapBlocks=q.offsetWidth!==2),q.style.cssText=k+m,q.innerHTML=o,d=q.firstChild,e=d.firstChild,h=d.nextSibling.firstChild.firstChild,i={doesNotAddBorder:e.offsetTop!==5,doesAddBorderForTableAndCells:h.offsetTop===5},e.style.position="fixed",e.style.top="20px",i.fixedPosition=e.offsetTop===20||e.offsetTop===15,e.style.position=e.style.top="",d.style.overflow="hidden",d.style.position="relative",i.subtractsBorderForOverflowNotVisible=e.offsetTop===-5,i.doesNotIncludeMarginInBodyOffset=r.offsetTop!==j,r.removeChild(a),q=a=null,f.extend(b,i))});return b}();var j=/^(?:\{.*\}|\[.*\])$/,k=/([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!m(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g,h,i,j=f.expando,k=typeof c=="string",l=a.nodeType,m=l?f.cache:a,n=l?a[j]:a[j]&&j,o=c==="events";if((!n||!m[n]||!o&&!e&&!m[n].data)&&k&&d===b)return;n||(l?a[j]=n=++f.uuid:n=j),m[n]||(m[n]={},l||(m[n].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?m[n]=f.extend(m[n],c):m[n].data=f.extend(m[n].data,c);g=h=m[n],e||(h.data||(h.data={}),h=h.data),d!==b&&(h[f.camelCase(c)]=d);if(o&&!h[c])return g.events;k?(i=h[c],i==null&&(i=h[f.camelCase(c)])):i=h;return i}},removeData:function(a,b,c){if(!!f.acceptData(a)){var d,e,g,h=f.expando,i=a.nodeType,j=i?f.cache:a,k=i?a[h]:h;if(!j[k])return;if(b){d=c?j[k]:j[k].data;if(d){f.isArray(b)||(b in d?b=[b]:(b=f.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,g=b.length;e-1)return!0;return!1},val:function(a){var c,d,e,g=this[0];{if(!!arguments.length){e=f.isFunction(a);return this.each(function(d){var g=f(this),h;if(this.nodeType===1){e?h=a.call(this,d,g.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.nodeName.toLowerCase()]||f.valHooks[this.type];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}if(g){c=f.valHooks[g.nodeName.toLowerCase()]||f.valHooks[g.type];if(c&&"get"in c&&(d=c.get(g,"value"))!==b)return d;d=g.value;return typeof d=="string"?d.replace(q,""):d==null?"":d}}}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,g=a.selectedIndex,h=[],i=a.options,j=a.type==="select-one";if(g<0)return null;c=j?g:0,d=j?g+1:i.length;for(;c=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attr:function(a,c,d,e){var g,h,i,j=a.nodeType;if(!!a&&j!==3&&j!==8&&j!==2){if(e&&c in f.attrFn)return f(a)[c](d);if(typeof a.getAttribute=="undefined")return f.prop(a,c,d);i=j!==1||!f.isXMLDoc(a),i&&(c=c.toLowerCase(),h=f.attrHooks[c]||(u.test(c)?x:w));if(d!==b){if(d===null){f.removeAttr(a,c);return}if(h&&"set"in h&&i&&(g=h.set(a,d,c))!==b)return g;a.setAttribute(c,""+d);return d}if(h&&"get"in h&&i&&(g=h.get(a,c))!==null)return g;g=a.getAttribute(c);return g===null?b:g}},removeAttr:function(a,b){var c,d,e,g,h=0;if(b&&a.nodeType===1){d=b.toLowerCase().split(p),g=d.length;for(;h=0}})});var z=/^(?:textarea|input|select)$/i,A=/^([^\.]*)?(?:\.(.+))?$/,B=/\bhover(\.\S+)?\b/,C=/^key/,D=/^(?:mouse|contextmenu)|click/,E=/^(?:focusinfocus|focusoutblur)$/,F=/^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/,G=function(a){var b=F.exec(a);b&&(b[1]=(b[1]||"").toLowerCase(),b[3]=b[3]&&new RegExp("(?:^|\\s)"+b[3]+"(?:\\s|$)"));return b},H=function(a,b){var c=a.attributes||{};return(!b[1]||a.nodeName.toLowerCase()===b[1])&&(!b[2]||(c.id||{}).value===b[2])&&(!b[3]||b[3].test((c["class"]||{}).value))},I=function(a){return f.event.special.hover?a:a.replace(B,"mouseenter$1 mouseleave$1")}; -f.event={add:function(a,c,d,e,g){var h,i,j,k,l,m,n,o,p,q,r,s;if(!(a.nodeType===3||a.nodeType===8||!c||!d||!(h=f._data(a)))){d.handler&&(p=d,d=p.handler),d.guid||(d.guid=f.guid++),j=h.events,j||(h.events=j={}),i=h.handle,i||(h.handle=i=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.dispatch.apply(i.elem,arguments):b},i.elem=a),c=f.trim(I(c)).split(" ");for(k=0;k=0&&(h=h.slice(0,-1),k=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if((!e||f.event.customEvent[h])&&!f.event.global[h])return;c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.isTrigger=!0,c.exclusive=k,c.namespace=i.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)"):null,o=h.indexOf(":")<0?"on"+h:"";if(!e){j=f.cache;for(l in j)j[l].events&&j[l].events[h]&&f.event.trigger(c,d,j[l].handle.elem,!0);return}c.result=b,c.target||(c.target=e),d=d!=null?f.makeArray(d):[],d.unshift(c),p=f.event.special[h]||{};if(p.trigger&&p.trigger.apply(e,d)===!1)return;r=[[e,p.bindType||h]];if(!g&&!p.noBubble&&!f.isWindow(e)){s=p.delegateType||h,m=E.test(s+h)?e:e.parentNode,n=null;for(;m;m=m.parentNode)r.push([m,s]),n=m;n&&n===e.ownerDocument&&r.push([n.defaultView||n.parentWindow||a,s])}for(l=0;le&&i.push({elem:this,matches:d.slice(e)});for(j=0;j0?this.on(b,null,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0),C.test(b)&&(f.event.fixHooks[b]=f.event.keyHooks),D.test(b)&&(f.event.fixHooks[b]=f.event.mouseHooks)}),function(){function x(a,b,c,e,f,g){for(var h=0,i=e.length;h0){k=j;break}}j=j[a]}e[h]=k}}}function w(a,b,c,e,f,g){for(var h=0,i=e.length;h+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d="sizcache"+(Math.random()+"").replace(".",""),e=0,g=Object.prototype.toString,h=!1,i=!0,j=/\\/g,k=/\r\n/g,l=/\W/;[0,0].sort(function(){i=!1;return 0});var m=function(b,d,e,f){e=e||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return e;var i,j,k,l,n,q,r,t,u=!0,v=m.isXML(d),w=[],x=b;do{a.exec(""),i=a.exec(x);if(i){x=i[3],w.push(i[1]);if(i[2]){l=i[3];break}}}while(i);if(w.length>1&&p.exec(b))if(w.length===2&&o.relative[w[0]])j=y(w[0]+w[1],d,f);else{j=o.relative[w[0]]?[d]:m(w.shift(),d);while(w.length)b=w.shift(),o.relative[b]&&(b+=w.shift()),j=y(b,j,f)}else{!f&&w.length>1&&d.nodeType===9&&!v&&o.match.ID.test(w[0])&&!o.match.ID.test(w[w.length-1])&&(n=m.find(w.shift(),d,v),d=n.expr?m.filter(n.expr,n.set)[0]:n.set[0]);if(d){n=f?{expr:w.pop(),set:s(f)}:m.find(w.pop(),w.length===1&&(w[0]==="~"||w[0]==="+")&&d.parentNode?d.parentNode:d,v),j=n.expr?m.filter(n.expr,n.set):n.set,w.length>0?k=s(j):u=!1;while(w.length)q=w.pop(),r=q,o.relative[q]?r=w.pop():q="",r==null&&(r=d),o.relative[q](k,r,v)}else k=w=[]}k||(k=j),k||m.error(q||b);if(g.call(k)==="[object Array]")if(!u)e.push.apply(e,k);else if(d&&d.nodeType===1)for(t=0;k[t]!=null;t++)k[t]&&(k[t]===!0||k[t].nodeType===1&&m.contains(d,k[t]))&&e.push(j[t]);else for(t=0;k[t]!=null;t++)k[t]&&k[t].nodeType===1&&e.push(j[t]);else s(k,e);l&&(m(l,h,e,f),m.uniqueSort(e));return e};m.uniqueSort=function(a){if(u){h=i,a.sort(u);if(h)for(var b=1;b0},m.find=function(a,b,c){var d,e,f,g,h,i;if(!a)return[];for(e=0,f=o.order.length;e":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!l.test(b)){b=b.toLowerCase();for(;e=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(j,"")},TAG:function(a,b){return a[1].replace(j,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||m.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&m.error(a[0]);a[0]=e++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(j,"");!f&&o.attrMap[g]&&(a[1]=o.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(j,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=m(b[3],null,null,c);else{var g=m.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(o.match.POS.test(b[0])||o.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!m(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return bc[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=o.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||n([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||!!a.nodeName&&a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=m.attr?m.attr(a,c):o.attrHandle[c]?o.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":!f&&m.attr?d!=null:f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=o.setFilters[e];if(f)return f(a,c,b,d)}}},p=o.match.POS,q=function(a,b){return"\\"+(b-0+1)};for(var r in o.match)o.match[r]=new RegExp(o.match[r].source+/(?![^\[]*\])(?![^\(]*\))/.source),o.leftMatch[r]=new RegExp(/(^(?:.|\r|\n)*?)/.source+o.match[r].source.replace(/\\(\d+)/g,q));var s=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(t){s=function(a,b){var c=0,d=b||[];if(g.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var e=a.length;c",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(o.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},o.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(o.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(o.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=m,b=c.createElement("div"),d="__sizzle__";b.innerHTML="

";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){m=function(b,e,f,g){e=e||c;if(!g&&!m.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return s(e.getElementsByTagName(b),f);if(h[2]&&o.find.CLASS&&e.getElementsByClassName)return s(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return s([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return s([],f);if(i.id===h[3])return s([i],f)}try{return s(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var k=e,l=e.getAttribute("id"),n=l||d,p=e.parentNode,q=/^\s*[+~]/.test(b);l?n=n.replace(/'/g,"\\$&"):e.setAttribute("id",n),q&&p&&(e=e.parentNode);try{if(!q||p)return s(e.querySelectorAll("[id='"+n+"'] "+b),f)}catch(r){}finally{l||k.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)m[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}m.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!m.isXML(a))try{if(e||!o.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return m(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="
";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;o.order.splice(1,0,"CLASS"),o.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?m.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?m.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:m.contains=function(){return!1},m.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var y=function(a,b,c){var d,e=[],f="",g=b.nodeType?[b]:b;while(d=o.match.PSEUDO.exec(a))f+=d[0],a=a.replace(o.match.PSEUDO,"");a=o.relative[a]?a+"*":a;for(var h=0,i=g.length;h0)for(h=g;h=0:f.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h=1;while(g&&g.ownerDocument&&g!==b){for(d=0;d-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a)return this[0]&&this[0].parentNode?this.prevAll().length:-1;if(typeof a=="string")return f.inArray(this[0],f(a));return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(S(c[0])||S(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling(a.parentNode.firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c);L.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!R[a]?f.unique(e):e,(this.length>1||N.test(d))&&M.test(a)&&(e=e.reverse());return this.pushStack(e,a,P.call(arguments).join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var V="abbr|article|aside|audio|canvas|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",W=/ jQuery\d+="(?:\d+|null)"/g,X=/^\s+/,Y=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,Z=/<([\w:]+)/,$=/",""],legend:[1,"
","
"],thead:[1,"","
"],tr:[2,"","
"],td:[3,"","
"],col:[2,"","
"],area:[1,"",""],_default:[0,"",""]},bh=U(c);bg.optgroup=bg.option,bg.tbody=bg.tfoot=bg.colgroup=bg.caption=bg.thead,bg.th=bg.td,f.support.htmlSerialize||(bg._default=[1,"div
","
"]),f.fn.extend({text:function(a){if(f.isFunction(a))return this.each(function(b){var c=f(this);c.text(a.call(this,b,c.text()))});if(typeof a!="object"&&a!==b)return this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a));return f.text(this)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=f.isFunction(a);return this.each(function(c){f(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f.clean(arguments);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f.clean(arguments));return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function() -{for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){if(a===b)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(W,""):null;if(typeof a=="string"&&!ba.test(a)&&(f.support.leadingWhitespace||!X.test(a))&&!bg[(Z.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Y,"<$1>");try{for(var c=0,d=this.length;c1&&l0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d,e,g,h=f.support.html5Clone||!bc.test("<"+a.nodeName)?a.cloneNode(!0):bo(a);if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bk(a,h),d=bl(a),e=bl(h);for(g=0;d[g];++g)e[g]&&bk(d[g],e[g])}if(b){bj(a,h);if(c){d=bl(a),e=bl(h);for(g=0;d[g];++g)bj(d[g],e[g])}}d=e=null;return h},clean:function(a,b,d,e){var g;b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);var h=[],i;for(var j=0,k;(k=a[j])!=null;j++){typeof k=="number"&&(k+="");if(!k)continue;if(typeof k=="string")if(!_.test(k))k=b.createTextNode(k);else{k=k.replace(Y,"<$1>");var l=(Z.exec(k)||["",""])[1].toLowerCase(),m=bg[l]||bg._default,n=m[0],o=b.createElement("div");b===c?bh.appendChild(o):U(b).appendChild(o),o.innerHTML=m[1]+k+m[2];while(n--)o=o.lastChild;if(!f.support.tbody){var p=$.test(k),q=l==="table"&&!p?o.firstChild&&o.firstChild.childNodes:m[1]===""&&!p?o.childNodes:[];for(i=q.length-1;i>=0;--i)f.nodeName(q[i],"tbody")&&!q[i].childNodes.length&&q[i].parentNode.removeChild(q[i])}!f.support.leadingWhitespace&&X.test(k)&&o.insertBefore(b.createTextNode(X.exec(k)[0]),o.firstChild),k=o.childNodes}var r;if(!f.support.appendChecked)if(k[0]&&typeof (r=k.length)=="number")for(i=0;i=0)return b+"px"}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return br.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=f.isNumeric(b)?"alpha(opacity="+b*100+")":"",g=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&f.trim(g.replace(bq,""))===""){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bq.test(g)?g.replace(bq,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){var c;f.swap(a,{display:"inline-block"},function(){b?c=bz(a,"margin-right","marginRight"):c=a.style.marginRight});return c}})}),c.defaultView&&c.defaultView.getComputedStyle&&(bA=function(a,b){var c,d,e;b=b.replace(bs,"-$1").toLowerCase(),(d=a.ownerDocument.defaultView)&&(e=d.getComputedStyle(a,null))&&(c=e.getPropertyValue(b),c===""&&!f.contains(a.ownerDocument.documentElement,a)&&(c=f.style(a,b)));return c}),c.documentElement.currentStyle&&(bB=function(a,b){var c,d,e,f=a.currentStyle&&a.currentStyle[b],g=a.style;f===null&&g&&(e=g[b])&&(f=e),!bt.test(f)&&bu.test(f)&&(c=g.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),g.left=b==="fontSize"?"1em":f||0,f=g.pixelLeft+"px",g.left=c,d&&(a.runtimeStyle.left=d));return f===""?"auto":f}),bz=bA||bB,f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style&&a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)});var bD=/%20/g,bE=/\[\]$/,bF=/\r?\n/g,bG=/#.*$/,bH=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bI=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bJ=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,bK=/^(?:GET|HEAD)$/,bL=/^\/\//,bM=/\?/,bN=/)<[^<]*)*<\/script>/gi,bO=/^(?:select|textarea)/i,bP=/\s+/,bQ=/([?&])_=[^&]*/,bR=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bS=f.fn.load,bT={},bU={},bV,bW,bX=["*/"]+["*"];try{bV=e.href}catch(bY){bV=c.createElement("a"),bV.href="",bV=bV.href}bW=bR.exec(bV.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bS)return bS.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("
").append(c.replace(bN,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bO.test(this.nodeName)||bI.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bF,"\r\n")}}):{name:b.name,value:c.replace(bF,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.on(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?b_(a,f.ajaxSettings):(b=a,a=f.ajaxSettings),b_(a,b);return a},ajaxSettings:{url:bV,isLocal:bJ.test(bW[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":bX},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:bZ(bT),ajaxTransport:bZ(bU),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a>0?4:0;var o,r,u,w=c,x=l?cb(d,v,l):b,y,z;if(a>=200&&a<300||a===304){if(d.ifModified){if(y=v.getResponseHeader("Last-Modified"))f.lastModified[k]=y;if(z=v.getResponseHeader("Etag"))f.etag[k]=z}if(a===304)w="notmodified",o=!0;else try{r=cc(d,x),w="success",o=!0}catch(A){w="parsererror",u=A}}else{u=w;if(!w||a)w="error",a<0&&(a=0)}v.status=a,v.statusText=""+(c||w),o?h.resolveWith(e,[r,w,v]):h.rejectWith(e,[v,w,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.fireWith(e,[v,w]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f.Callbacks("once memory"),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bH.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.add,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bG,"").replace(bL,bW[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bP),d.crossDomain==null&&(r=bR.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bW[1]&&r[2]==bW[2]&&(r[3]||(r[1]==="http:"?80:443))==(bW[3]||(bW[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),b$(bT,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bK.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bM.test(d.url)?"&":"?")+d.data,delete d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bQ,"$1_="+x);d.url=y+(y===d.url?(bM.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", "+bX+"; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=b$(bU,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){if(s<2)w(-1,z);else throw z}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)ca(g,a[g],c,e);return d.join("&").replace(bD,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var cd=f.now(),ce=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+cd++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=b.contentType==="application/x-www-form-urlencoded"&&typeof b.data=="string";if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(ce.test(b.url)||e&&ce.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(ce,l),b.url===j&&(e&&(k=k.replace(ce,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var cf=a.ActiveXObject?function(){for(var a in ch)ch[a](0,1)}:!1,cg=0,ch;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ci()||cj()}:ci,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,cf&&delete ch[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n),m.text=h.responseText;try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cg,cf&&(ch||(ch={},f(a).unload(cf)),ch[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var ck={},cl,cm,cn=/^(?:toggle|show|hide)$/,co=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,cp,cq=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],cr;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(cu("show",3),a,b,c);for(var g=0,h=this.length;g=i.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),i.animatedProperties[this.prop]=!0;for(b in i.animatedProperties)i.animatedProperties[b]!==!0&&(g=!1);if(g){i.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){h.style["overflow"+b]=i.overflow[a]}),i.hide&&f(h).hide();if(i.hide||i.show)for(b in i.animatedProperties)f.style(h,b,i.orig[b]),f.removeData(h,"fxshow"+b,!0),f.removeData(h,"toggle"+b,!0);d=i.complete,d&&(i.complete=!1,d.call(h))}return!1}i.duration==Infinity?this.now=e:(c=e-this.startTime,this.state=c/i.duration,this.pos=f.easing[i.animatedProperties[this.prop]](this.state,c,0,1,i.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){var a,b=f.timers,c=0;for(;c-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cx.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cx.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each(["Left","Top"],function(a,c){var d="scroll"+c;f.fn[d]=function(c){var e,g;if(c===b){e=this[0];if(!e)return null;g=cy(e);return g?"pageXOffset"in g?g[a?"pageYOffset":"pageXOffset"]:f.support.boxModel&&g.document.documentElement[d]||g.document.body[d]:e[d]}return this.each(function(){g=cy(this),g?g.scrollTo(a?f(g).scrollLeft():c,a?c:f(g).scrollTop()):this[d]=c})}}),f.each(["Height","Width"],function(a,c){var d=c.toLowerCase();f.fn["inner"+c]=function(){var a=this[0];return a?a.style?parseFloat(f.css(a,d,"padding")):this[d]():null},f.fn["outer"+c]=function(a){var b=this[0];return b?b.style?parseFloat(f.css(b,d,a?"margin":"border")):this[d]():null},f.fn[d]=function(a){var e=this[0];if(!e)return a==null?null:this;if(f.isFunction(a))return this.each(function(b){var c=f(this);c[d](a.call(this,b,c[d]()))});if(f.isWindow(e)){var g=e.document.documentElement["client"+c],h=e.document.body;return e.document.compatMode==="CSS1Compat"&&g||h&&h["client"+c]||g}if(e.nodeType===9)return Math.max(e.documentElement["client"+c],e.body["scroll"+c],e.documentElement["scroll"+c],e.body["offset"+c],e.documentElement["offset"+c]);if(a===b){var i=f.css(e,d),j=parseFloat(i);return f.isNumeric(j)?j:i}return this.css(d,typeof a=="string"?a:a+"px")}}),a.jQuery=a.$=f,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return f})})(window); \ No newline at end of file diff --git a/doc/method_list.html b/doc/method_list.html deleted file mode 100644 index 46778bff..00000000 --- a/doc/method_list.html +++ /dev/null @@ -1,1451 +0,0 @@ - - - - - - - - - - - - - - - - - - Method List - - - -
-
-

Method List

- - - -
- - -
- - diff --git a/doc/models_brief.svg b/doc/models_brief.svg deleted file mode 100644 index 048b0502..00000000 --- a/doc/models_brief.svg +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - -models_diagram - - -_diagram_info -Models diagram -Date: Sep 22 2018 - 22:15 -Migration version: 20180913185640 -Generated by RailRoady 1.5.3 -http://railroady.prestonlee.com - - -ApplicationRecord - -ApplicationRecord - - - diff --git a/doc/models_complete.svg b/doc/models_complete.svg deleted file mode 100644 index 048b0502..00000000 --- a/doc/models_complete.svg +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - -models_diagram - - -_diagram_info -Models diagram -Date: Sep 22 2018 - 22:15 -Migration version: 20180913185640 -Generated by RailRoady 1.5.3 -http://railroady.prestonlee.com - - -ApplicationRecord - -ApplicationRecord - - - diff --git a/doc/top-level-namespace.html b/doc/top-level-namespace.html deleted file mode 100644 index b2b6a362..00000000 --- a/doc/top-level-namespace.html +++ /dev/null @@ -1,112 +0,0 @@ - - - - - - - Top Level Namespace - - — Documentation by YARD 0.9.16 - - - - - - - - - - - - - - - - - - - -
- - -

Top Level Namespace - - - -

-
- - - - - - - - - - - -
- -

Defined Under Namespace

-

- - - Modules: Gera - - - - Classes: Money, Numeric - - -

- - - - - - - - - -
- - - -
- - \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..7a067356 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + mysql: + image: mysql:8.0 + ports: + - "3306:3306" + environment: + MYSQL_ROOT_PASSWORD: 1111 + MYSQL_DATABASE: kassa_admin_test + volumes: + - mysql_data:/var/lib/mysql + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p1111"] + interval: 5s + timeout: 5s + retries: 10 + start_period: 30s + command: --default-authentication-plugin=mysql_native_password + +volumes: + mysql_data: diff --git a/factories/external_rate_snapshots.rb b/factories/external_rate_snapshots.rb index c024a0f9..4b64f549 100644 --- a/factories/external_rate_snapshots.rb +++ b/factories/external_rate_snapshots.rb @@ -1,4 +1,5 @@ FactoryBot.define do factory :external_rate_snapshot, class: Gera::ExternalRateSnapshot do + actual_for { Date.yesterday } end end diff --git a/factories/external_rates.rb b/factories/external_rates.rb index c532b581..66700a6d 100644 --- a/factories/external_rates.rb +++ b/factories/external_rates.rb @@ -1,7 +1,12 @@ FactoryBot.define do factory :external_rate, class: Gera::ExternalRate do - cur_from { "USD" } - cur_to { "RUB" } + cur_from { "ETH" } + cur_to { "BTC" } + rate_value { 1.5 } + end + factory :inverse_external_rate, class: Gera::ExternalRate do + cur_from { "BTC" } + cur_to { "ETH" } rate_value { 1.5 } end end diff --git a/factories/rate_sources.rb b/factories/rate_sources.rb index c74b9e21..9f92e01e 100644 --- a/factories/rate_sources.rb +++ b/factories/rate_sources.rb @@ -13,8 +13,9 @@ title { generate :title } end factory :rate_source_manual, parent: :rate_source, class: Gera::RateSourceManual - factory :rate_source_cbr, parent: :rate_source, class: Gera::RateSourceCBR - factory :rate_source_cbr_avg, parent: :rate_source, class: Gera::RateSourceCBRAvg - factory :rate_source_exmo, parent: :rate_source, class: Gera::RateSourceEXMO + factory :rate_source_cbr, parent: :rate_source, class: Gera::RateSourceCbr + factory :rate_source_cbr_avg, parent: :rate_source, class: Gera::RateSourceCbrAvg + factory :rate_source_exmo, parent: :rate_source, class: Gera::RateSourceExmo factory :rate_source_bitfinex, parent: :rate_source, class: Gera::RateSourceBitfinex + factory :rate_source_binance, parent: :rate_source, class: Gera::RateSourceBinance end diff --git a/gera.gemspec b/gera.gemspec index 55252f94..acaf9535 100644 --- a/gera.gemspec +++ b/gera.gemspec @@ -17,7 +17,7 @@ Gem::Specification.new do |s| s.files = Dir["{app,config,db,lib}/**/*", "LICENSE", "Rakefile", "README.md"] s.add_dependency 'simple_form' - s.add_dependency "rails", "~> 5.2.1" + s.add_dependency "rails", "~> 6.0.6" s.add_dependency 'best_in_place' s.add_dependency 'virtus' s.add_dependency 'kaminari' @@ -29,11 +29,11 @@ Gem::Specification.new do |s| s.add_dependency 'business_time' s.add_dependency 'dapi-archivable' s.add_dependency 'authority' - s.add_dependency 'psych' + s.add_dependency 'psych', '~> 3.1.0' s.add_dependency 'money' s.add_dependency 'money-rails' s.add_dependency 'percentable' - s.add_dependency 'draper', '~> 3.0.1' + s.add_dependency 'draper', '~> 3.1.0' s.add_dependency 'active_link_to' s.add_dependency 'breadcrumbs_on_rails' s.add_dependency 'noty_flash' diff --git a/lib/builders/currency_rate_auto_builder.rb b/lib/builders/currency_rate_auto_builder.rb index 8bfc621f..e722b6e6 100644 --- a/lib/builders/currency_rate_auto_builder.rb +++ b/lib/builders/currency_rate_auto_builder.rb @@ -13,7 +13,7 @@ def build def build_from_sources RateSource.enabled.ordered.each do |rate_source| - result = build_from_source rate_source + result = build_from_source(rate_source) return result if result.present? end @@ -26,13 +26,12 @@ def build_cross result.currency_rate end - def build_from_source source + def build_from_source(source) CurrencyRateDirectBuilder.new(currency_pair: currency_pair, source: source).build_currency_rate.currency_rate end def build_same - return unless currency_pair.same? - CurrencyRate.new currency_pair: currency_pair, rate_value: 1, mode: :same + CurrencyRate.new(currency_pair: currency_pair, rate_value: 1, mode: :same) if currency_pair.same? end end end diff --git a/lib/builders/currency_rate_builder.rb b/lib/builders/currency_rate_builder.rb index 2964c8e9..e86ce826 100644 --- a/lib/builders/currency_rate_builder.rb +++ b/lib/builders/currency_rate_builder.rb @@ -37,10 +37,11 @@ def error? attribute :currency_pair, CurrencyPair def build_currency_rate - success build - rescue => err - Rails.logger.error err unless err.is_a? Error - failure err + currency_rate = build + SuccessResult.new(currency_rate: currency_rate).freeze + rescue => error + Rails.logger.error(error) unless error.is_a?(Error) + ErrorResult.new(error: error).freeze end private @@ -48,13 +49,5 @@ def build_currency_rate def build raise 'not implemented' end - - def success(currency_rate) - SuccessResult.new(currency_rate: currency_rate).freeze - end - - def failure(error) - ErrorResult.new(error: error).freeze - end end end diff --git a/lib/gera.rb b/lib/gera.rb index ecbb7210..fd3fe304 100644 --- a/lib/gera.rb +++ b/lib/gera.rb @@ -11,6 +11,13 @@ require "gera/configuration" require "gera/mathematic" require 'gera/bitfinex_fetcher' +require 'gera/binance_fetcher' +require 'gera/exmo_fetcher' +require 'gera/garantexio_fetcher' +require 'gera/bybit_fetcher' +require 'gera/cryptomus_fetcher' +require 'gera/ff_fixed_fetcher' +require 'gera/ff_float_fetcher' require 'gera/currency_pair' require 'gera/rate' require 'gera/money_support' diff --git a/lib/gera/binance_fetcher.rb b/lib/gera/binance_fetcher.rb new file mode 100644 index 00000000..bd2befa8 --- /dev/null +++ b/lib/gera/binance_fetcher.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require 'uri' +require 'net/http' +require 'rest-client' +require 'virtus' + +module Gera + class BinanceFetcher + API_URL = 'https://api.binance.com/api/v3/ticker/bookTicker' + + def perform + rates.each_with_object({}) do |rate, memo| + symbol = rate['symbol'] + + cur_from = find_cur_from(symbol) + next unless cur_from + + cur_to = find_cur_to(symbol, cur_from) + next unless cur_to + + next if price_is_missed?(rate: rate) + + pair = CurrencyPair.new(cur_from: cur_from, cur_to: cur_to) + memo[pair] = rate + end + end + + private + + # NOTE: for some pairs price is "0.00000000" + def price_is_missed?(rate:) + rate['askPrice'].to_f.zero? || rate['bidPrice'].to_f.zero? + end + + def rates + response = RestClient::Request.execute url: API_URL, method: :get, verify_ssl: false + + raise response.code unless response.code == 200 + JSON.parse response.body + end + + def find_cur_from(symbol) + supported_currencies.find do |currency| + symbol.start_with?(currency_name(currency)) + end + end + + def find_cur_to(symbol, cur_from) + Money::Currency.find(symbol.split(currency_name(cur_from)).last) + end + + def currency_name(currency) + name = currency.to_s + name = 'DASH' if name == 'DSH' + name + end + + def supported_currencies + @supported_currencies ||= RateSourceBinance.supported_currencies + end + + def http + Net::HTTP.new(uri.host, uri.port).tap do |http| + http.use_ssl = true + http.verify_mode = OpenSSL::SSL::VERIFY_NONE + http + end + end + end +end diff --git a/lib/gera/bitfinex_fetcher.rb b/lib/gera/bitfinex_fetcher.rb index 6de04b29..51e0b76b 100644 --- a/lib/gera/bitfinex_fetcher.rb +++ b/lib/gera/bitfinex_fetcher.rb @@ -1,36 +1,53 @@ -require 'uri' -require 'net/http' +# frozen_string_literal: true + require 'rest-client' -require 'virtus' module Gera class BitfinexFetcher - API_URL = 'https://api.bitfinex.com/v1/pubticker/' + API_URL = 'https://api-pub.bitfinex.com/v2/tickers?symbols=ALL' - include Virtus.model strict: true + def perform + rates.each_with_object({}) do |rate, memo| + symbol = rate[0] - # Например btcusd - attribute :ticker, String + cur_from = find_cur_from(symbol) + next unless cur_from - def perform - response = RestClient::Request.execute url: url, method: :get, verify_ssl: false + cur_to = find_cur_to(symbol, cur_from) + next unless cur_to - raise response.code unless response.code == 200 - JSON.parse response.body + next if price_is_missed?(rate: rate) + + pair = CurrencyPair.new(cur_from: cur_from, cur_to: cur_to) + memo[pair] = rate + end end private - def url - API_URL + ticker + def rates + response = RestClient::Request.execute url: API_URL, method: :get, verify_ssl: true + + raise response.code unless response.code == 200 + JSON.parse response.body + end + + def supported_currencies + @supported_currencies ||= RateSourceBitfinex.supported_currencies end - def http - Net::HTTP.new(uri.host, uri.port).tap do |http| - http.use_ssl = true - http.verify_mode = OpenSSL::SSL::VERIFY_NONE - http + def find_cur_from(symbol) + supported_currencies.find do |currency| + symbol.start_with?("t#{currency}") end end + + def find_cur_to(symbol, cur_from) + Money::Currency.find(symbol.split("t#{cur_from}").last) + end + + def price_is_missed?(rate:) + rate[7].to_f.zero? + end end end diff --git a/lib/gera/bybit_fetcher.rb b/lib/gera/bybit_fetcher.rb new file mode 100644 index 00000000..5d6aa777 --- /dev/null +++ b/lib/gera/bybit_fetcher.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require 'rest-client' + +module Gera + class BybitFetcher < PaymentServices::Base::Client + API_URL = 'https://api2.bytick.com/fiat/otc/item/online' + Error = Class.new StandardError + + def perform + rates.each_with_object({}) do |rate, memo| + cur_from, cur_to = rate['tokenId'], rate['currencyId'] + next unless supported_currencies.include?(cur_from) && supported_currencies.include?(cur_to) + + pair = CurrencyPair.new(cur_from: cur_from, cur_to: cur_to) + memo[pair] = rate + end + end + + private + + def rates + items = safely_parse(http_request( + url: API_URL, + method: :POST, + body: params.to_json, + headers: build_headers + )).dig('result', 'items') + + rate = items[2] || items[1] || raise(Error, 'No rates') + + [rate] + end + + def params + { + userId: '', + tokenId: 'USDT', + currencyId: 'RUB', + payment: ['75', '377', '582', '581'], + side: '1', + size: '3', + page: '1', + amount: '', + authMaker: false, + canTrade: false + } + end + + def supported_currencies + @supported_currencies ||= RateSourceBybit.supported_currencies + end + + def build_headers + { + 'Content-Type' => 'application/json', + 'Host' => 'api2.bytick.com', + 'Content-Length' => '182' + } + end + end +end diff --git a/lib/gera/configuration.rb b/lib/gera/configuration.rb index 2ef7c39f..e98e9f15 100644 --- a/lib/gera/configuration.rb +++ b/lib/gera/configuration.rb @@ -40,6 +40,14 @@ def cross_pairs end h end + + # @param [Integer] ID нашего обменника в BestChange (для исключения из расчёта позиции) + mattr_accessor :our_exchanger_id + @@our_exchanger_id = nil + + # @param [Boolean] Включить debug-логирование для автокурса (по умолчанию выключено) + mattr_accessor :autorate_debug_enabled + @@autorate_debug_enabled = false end end diff --git a/lib/gera/cryptomus_fetcher.rb b/lib/gera/cryptomus_fetcher.rb new file mode 100644 index 00000000..28c9b02e --- /dev/null +++ b/lib/gera/cryptomus_fetcher.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module Gera + class CryptomusFetcher < PaymentServices::Base::Client + API_URL = 'https://api.cryptomus.com/v1/exchange-rate' + Error = Class.new StandardError + + def perform + rates.each_with_object({}) do |rate, memo| + cur_from, cur_to = rate['from'], rate['to'] + cur_from = 'DSH' if cur_from == 'DASH' + cur_to = 'DSH' if cur_to == 'DASH' + next unless supported_currencies.include?(cur_to) + + pair = CurrencyPair.new(cur_from: cur_from, cur_to: cur_to) + memo[pair] = rate + end + end + + private + + def rates + data = supported_currencies.map(&:iso_code).map { |code| rate(currency: code) }.flatten.filter { |rate| rate['from'] != rate['to'] } + unique_pairs = Set.new + filtered_data = data.reverse.select do |hash| + pair = [hash['from'], hash['to']].sort + unique_pairs.add?(pair) ? true : false + end + filtered_data + end + + def rate(currency:) + currency = 'DASH' if currency == 'DSH' + + safely_parse(http_request( + url: "#{API_URL}/#{currency}/list", + method: :GET, + headers: build_headers + )).dig('result') + end + + def supported_currencies + @supported_currencies ||= RateSourceCryptomus.supported_currencies + end + + def build_headers + { + 'Content-Type' => 'application/json' + } + end + end +end diff --git a/lib/gera/exmo_fetcher.rb b/lib/gera/exmo_fetcher.rb new file mode 100644 index 00000000..ca598371 --- /dev/null +++ b/lib/gera/exmo_fetcher.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'uri' +require 'net/http' + +module Gera + class ExmoFetcher + URL = 'https://api.exmo.me/v1/ticker/' # https://api.exmo.com/v1/ticker/ + Error = Class.new StandardError + + def perform + raw_rates = load_rates.to_a + rates = {} + raw_rates.each do |currency_pair_keys, rate| + currency_from, currency_to = find_currencies(currency_pair_keys) + next if currency_from.nil? || currency_to.nil? + + currency_pair = Gera::CurrencyPair.new(cur_from: currency_from, cur_to: currency_to) + rates[currency_pair] = rate + end + + rates + end + + private + + def find_currencies(currency_pair_keys) + currency_key_from, currency_key_to = split_currency_pair_keys(currency_pair_keys) + [find_currency(currency_key_from), find_currency(currency_key_to)] + end + + def split_currency_pair_keys(currency_pair_keys) + currency_pair_keys.split('_') .map { |c| c == 'DASH' ? 'DSH' : c } + end + + def find_currency(key) + Money::Currency.find(key) + end + + def load_rates + url = URI.parse(URL) + result = JSON.parse(open(url).read) + raise Error, 'Result is not a hash' unless result.is_a?(Hash) + raise Error, result['error'] if result['error'].present? + + result + end + end +end diff --git a/lib/gera/ff_fixed_fetcher.rb b/lib/gera/ff_fixed_fetcher.rb new file mode 100644 index 00000000..a194338f --- /dev/null +++ b/lib/gera/ff_fixed_fetcher.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module Gera + class FfFixedFetcher + API_URL = 'https://ff.io/rates/fixed.xml' + Error = Class.new(StandardError) + + def perform + result = {} + raw_rates = rates + + raw_rates.each do |raw_rate| + rate = raw_rate.transform_keys(&:to_s) + + cur_from = rate['from'] + cur_to = rate['to'] + + cur_from = 'BNB' if cur_from == 'BSC' + cur_to = 'BNB' if cur_to == 'BSC' + + next unless supported_currencies.include?(cur_from) + next unless supported_currencies.include?(cur_to) + + pair = Gera::CurrencyPair.new(cur_from: cur_from, cur_to: cur_to) + reverse_pair = Gera::CurrencyPair.new(cur_from: cur_to, cur_to: cur_from) + + result[pair] = rate unless result.key?(reverse_pair) + end + + result + end + + private + + def rates + xml_data = URI.open(API_URL).read + doc = Nokogiri::XML(xml_data) + + doc.xpath('//item').map do |item| + { + from: item.at('from')&.text, + to: item.at('to')&.text, + in: item.at('in')&.text.to_f, + out: item.at('out')&.text.to_f, + amount: item.at('amount')&.text.to_f, + tofee: item.at('tofee')&.text, + minamount: item.at('minamount')&.text, + maxamount: item.at('maxamount')&.text + } + end + end + + def supported_currencies + @supported_currencies ||= RateSourceFfFixed.supported_currencies + end + end +end diff --git a/lib/gera/ff_float_fetcher.rb b/lib/gera/ff_float_fetcher.rb new file mode 100644 index 00000000..ed0f618d --- /dev/null +++ b/lib/gera/ff_float_fetcher.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module Gera + class FfFloatFetcher + API_URL = 'https://ff.io/rates/float.xml' + Error = Class.new(StandardError) + + def perform + result = {} + raw_rates = rates + + raw_rates.each do |raw_rate| + rate = raw_rate.transform_keys(&:to_s) + + cur_from = rate['from'] + cur_to = rate['to'] + + cur_from = 'BNB' if cur_from == 'BSC' + cur_to = 'BNB' if cur_to == 'BSC' + + next unless supported_currencies.include?(cur_from) + next unless supported_currencies.include?(cur_to) + + pair = Gera::CurrencyPair.new(cur_from: cur_from, cur_to: cur_to) + reverse_pair = Gera::CurrencyPair.new(cur_from: cur_to, cur_to: cur_from) + + result[pair] = rate unless result.key?(reverse_pair) + end + + result + end + + private + + def rates + xml_data = URI.open(API_URL).read + doc = Nokogiri::XML(xml_data) + + doc.xpath('//item').map do |item| + { + from: item.at('from')&.text, + to: item.at('to')&.text, + in: item.at('in')&.text.to_f, + out: item.at('out')&.text.to_f, + amount: item.at('amount')&.text.to_f, + tofee: item.at('tofee')&.text, + minamount: item.at('minamount')&.text, + maxamount: item.at('maxamount')&.text + } + end + end + + def supported_currencies + @supported_currencies ||= RateSourceFfFloat.supported_currencies + end + end +end diff --git a/lib/gera/garantexio_fetcher.rb b/lib/gera/garantexio_fetcher.rb new file mode 100644 index 00000000..d730be12 --- /dev/null +++ b/lib/gera/garantexio_fetcher.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'rest-client' + +module Gera + class GarantexioFetcher + API_URL = 'https://stage.garantex.biz/api/v2/coinmarketcap/ticker' + + def perform + rates.each_with_object({}) do |rate, memo| + symbol, rate_info = rate.keys[0], rate.values[0] + cur_from, cur_to = symbol.split('_') + next unless supported_currencies.include?(cur_from) && supported_currencies.include?(cur_to) + + pair = CurrencyPair.new(cur_from: cur_from, cur_to: cur_to) + memo[pair] = rate_info + end + end + + private + + def rates + response = RestClient::Request.execute url: API_URL, method: :get, verify_ssl: false + + raise response.code unless response.code == 200 + JSON.parse response.body + end + + def supported_currencies + @supported_currencies ||= RateSourceGarantexio.supported_currencies + end + end +end diff --git a/lib/gera/money_support.rb b/lib/gera/money_support.rb index 59ce6ff9..5f990e69 100644 --- a/lib/gera/money_support.rb +++ b/lib/gera/money_support.rb @@ -77,6 +77,8 @@ def zero_money end class ::Money + DEFAULT_MONEY_PRECISION = 2 + CRYPTO_MONEY_PRECISION = 8 # TODO Отказаться # Это сумма, до которой разрешено безопасное округление # при приеме суммы от клиента @@ -84,6 +86,24 @@ def authorized_round return self unless currency.authorized_round.is_a? Numeric Money.from_amount to_f.round(currency.authorized_round), currency end + + def kassa_round + Money.from_amount to_f.round(money_precision), currency + end + + def alikassa_round + Money.from_amount to_f.round(1), currency + end + + private + + def money_precision + if currency.is_crypto? + CRYPTO_MONEY_PRECISION + else + DEFAULT_MONEY_PRECISION + end + end end end end diff --git a/lib/gera/repositories/exchange_rates_repository.rb b/lib/gera/repositories/exchange_rates_repository.rb index 8baab52f..b04a18ec 100644 --- a/lib/gera/repositories/exchange_rates_repository.rb +++ b/lib/gera/repositories/exchange_rates_repository.rb @@ -1,7 +1,7 @@ module Gera class ExchangeRatesRepository def find_by_direction direction - get_matrix[direction.ps_from_id][direction.ps_to_id] + get_matrix.dig(direction.ps_from_id, direction.ps_to_id) end def get_matrix diff --git a/lib/gera/version.rb b/lib/gera/version.rb index b32e0cf8..a1746130 100644 --- a/lib/gera/version.rb +++ b/lib/gera/version.rb @@ -1,3 +1,3 @@ module Gera - VERSION = '0.3.3' + VERSION = '0.5.0' end diff --git a/mise.toml b/mise.toml new file mode 100644 index 00000000..b4a0d839 --- /dev/null +++ b/mise.toml @@ -0,0 +1,3 @@ +[tools] +node = "latest" +ruby = "2.7.8" diff --git a/spec/dummy/config/database.yml b/spec/dummy/config/database.yml index 2410d35a..fec07317 100644 --- a/spec/dummy/config/database.yml +++ b/spec/dummy/config/database.yml @@ -1,35 +1,8 @@ -# SQLite version 3.x -# gem install sqlite3 -# -# Ensure the SQLite 3 gem is defined in your Gemfile -# gem 'sqlite3' -# -mysql: &mysql +test: adapter: mysql2 - username: <%= ENV['MYSQL_USERNAME'] %> - database: gera_<%= Rails.env %> - # The password associated with the postgres role (username). - password: <%= ENV['MYSQL_PASSWORD'] %> - host: localhost - -postgresq: &postgresql - adapter: postgresql - database: gera_<%= Rails.env %> - min_messages: ERROR - -defaults: &defaults pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> timeout: 5000 - <<: *<%= ENV['DB'] || "postgresql" %> - -# Warning: The database defined as "test" will be erased and -# re-generated from your development database when you run "rake". -# Do not set this db to the same as development or production. -test: - <<: *defaults - -development: - <<: *defaults - -production: - <<: *defaults + host: localhost + username: root + password: 1111 + database: kassa_admin_test diff --git a/spec/dummy/db/schema.rb b/spec/dummy/db/schema.rb index b2144d32..b9c328cd 100644 --- a/spec/dummy/db/schema.rb +++ b/spec/dummy/db/schema.rb @@ -2,77 +2,73 @@ # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. # -# Note that this schema.rb definition is the authoritative source for your -# database schema. If you need to create the application database on another -# system, you should be using db:schema:load, not running all the migrations -# from scratch. The latter is a flawed and unsustainable approach (the more migrations -# you'll amass, the slower it'll run and the greater likelihood for issues). +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_03_15_113046) do - - # These are extensions that must be enabled in order to support this database - enable_extension "plpgsql" - +ActiveRecord::Schema[8.1].define(version: 2025_12_25_000001) do create_table "gera_cbr_external_rates", force: :cascade do |t| - t.date "date", null: false + t.datetime "created_at", precision: nil, null: false t.string "cur_from", null: false t.string "cur_to", null: false - t.float "rate", null: false - t.float "original_rate", null: false + t.date "date", null: false t.integer "nominal", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.float "original_rate", null: false + t.float "rate", null: false + t.datetime "updated_at", precision: nil, null: false t.index ["cur_from", "cur_to", "date"], name: "index_cbr_external_rates_on_cur_from_and_cur_to_and_date", unique: true end create_table "gera_cross_rate_modes", force: :cascade do |t| - t.bigint "currency_rate_mode_id", null: false + t.datetime "created_at", precision: nil, null: false t.string "cur_from", null: false t.string "cur_to", null: false + t.bigint "currency_rate_mode_id", null: false t.bigint "rate_source_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "updated_at", precision: nil, null: false t.index ["currency_rate_mode_id"], name: "index_cross_rate_modes_on_currency_rate_mode_id" t.index ["rate_source_id"], name: "index_cross_rate_modes_on_rate_source_id" end create_table "gera_currency_rate_history_intervals", force: :cascade do |t| - t.integer "cur_from_id", limit: 2, null: false - t.integer "cur_to_id", limit: 2, null: false - t.float "min_rate", null: false t.float "avg_rate", null: false + t.integer "cur_from_id", limit: 1, null: false + t.integer "cur_to_id", limit: 1, null: false + t.datetime "interval_from", precision: nil, default: -> { "CURRENT_TIMESTAMP" }, null: false + t.datetime "interval_to", precision: nil, null: false t.float "max_rate", null: false - t.datetime "interval_from", default: -> { "now()" }, null: false - t.datetime "interval_to", null: false + t.float "min_rate", null: false t.index ["cur_from_id", "cur_to_id", "interval_from"], name: "crhi_unique_index", unique: true t.index ["interval_from"], name: "index_currency_rate_history_intervals_on_interval_from" end create_table "gera_currency_rate_mode_snapshots", force: :cascade do |t| - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.text "details" t.integer "status", default: 0, null: false t.string "title" - t.text "details" + t.datetime "updated_at", precision: nil, null: false t.index ["status"], name: "index_currency_rate_mode_snapshots_on_status" t.index ["title"], name: "index_currency_rate_mode_snapshots_on_title", unique: true end create_table "gera_currency_rate_modes", force: :cascade do |t| - t.string "cur_from", null: false - t.string "cur_to", null: false - t.integer "mode", default: 0, null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.bigint "currency_rate_mode_snapshot_id", null: false + t.datetime "created_at", precision: nil, null: false t.string "cross_currency1" - t.bigint "cross_rate_source1_id" t.string "cross_currency2" t.string "cross_currency3" + t.bigint "cross_rate_source1_id" t.bigint "cross_rate_source2_id" t.bigint "cross_rate_source3_id" + t.string "cur_from", null: false + t.string "cur_to", null: false + t.bigint "currency_rate_mode_snapshot_id", null: false + t.integer "mode", default: 0, null: false + t.datetime "updated_at", precision: nil, null: false t.index ["cross_rate_source1_id"], name: "index_currency_rate_modes_on_cross_rate_source1_id" t.index ["cross_rate_source2_id"], name: "index_currency_rate_modes_on_cross_rate_source2_id" t.index ["cross_rate_source3_id"], name: "index_currency_rate_modes_on_cross_rate_source3_id" @@ -80,24 +76,24 @@ end create_table "gera_currency_rate_snapshots", force: :cascade do |t| - t.datetime "created_at", default: -> { "now()" }, null: false + t.datetime "created_at", precision: nil, default: -> { "CURRENT_TIMESTAMP" }, null: false t.bigint "currency_rate_mode_snapshot_id", null: false t.index ["currency_rate_mode_snapshot_id"], name: "fk_rails_456167e2a9" end create_table "gera_currency_rates", force: :cascade do |t| + t.datetime "created_at", precision: nil t.string "cur_from", null: false t.string "cur_to", null: false - t.float "rate_value", null: false - t.bigint "snapshot_id", null: false - t.json "metadata", null: false - t.datetime "created_at" - t.bigint "external_rate_id" - t.integer "mode", null: false - t.bigint "rate_source_id" t.bigint "external_rate1_id" t.bigint "external_rate2_id" t.bigint "external_rate3_id" + t.bigint "external_rate_id" + t.json "metadata", null: false + t.integer "mode", null: false + t.bigint "rate_source_id" + t.float "rate_value", limit: 53, null: false + t.bigint "snapshot_id", null: false t.index ["created_at", "cur_from", "cur_to"], name: "currency_rates_created_at" t.index ["external_rate1_id"], name: "index_currency_rates_on_external_rate1_id" t.index ["external_rate2_id"], name: "index_currency_rates_on_external_rate2_id" @@ -108,34 +104,34 @@ end create_table "gera_direction_rate_history_intervals", force: :cascade do |t| - t.float "min_rate", null: false + t.float "avg_rate", null: false + t.datetime "interval_from", precision: nil, default: -> { "CURRENT_TIMESTAMP" }, null: false + t.datetime "interval_to", precision: nil, null: false + t.float "max_comission", null: false t.float "max_rate", null: false t.float "min_comission", null: false - t.float "max_comission", null: false - t.datetime "interval_from", default: -> { "now()" }, null: false - t.datetime "interval_to", null: false - t.bigint "payment_system_to_id", null: false + t.float "min_rate", null: false t.bigint "payment_system_from_id", null: false - t.float "avg_rate", null: false + t.bigint "payment_system_to_id", null: false t.index ["interval_from", "payment_system_from_id", "payment_system_to_id"], name: "drhi_uniq", unique: true t.index ["payment_system_from_id"], name: "fk_rails_70f35124fc" t.index ["payment_system_to_id"], name: "fk_rails_5c92dd1b7f" end create_table "gera_direction_rate_snapshots", force: :cascade do |t| - t.datetime "created_at", default: -> { "now()" }, null: false + t.datetime "created_at", precision: nil, default: -> { "CURRENT_TIMESTAMP" }, null: false end create_table "gera_direction_rates", force: :cascade do |t| - t.bigint "ps_from_id", null: false - t.bigint "ps_to_id", null: false + t.float "base_rate_value", limit: 53, null: false + t.datetime "created_at", precision: nil, default: -> { "CURRENT_TIMESTAMP" }, null: false t.bigint "currency_rate_id", null: false - t.float "rate_value", null: false - t.float "base_rate_value", null: false - t.float "rate_percent", null: false - t.datetime "created_at", default: -> { "now()" }, null: false t.bigint "exchange_rate_id", null: false t.boolean "is_used", default: false, null: false + t.bigint "ps_from_id", null: false + t.bigint "ps_to_id", null: false + t.float "rate_percent", null: false + t.float "rate_value", limit: 53, null: false t.bigint "snapshot_id" t.index ["created_at", "ps_from_id", "ps_to_id"], name: "direction_rates_created_at" t.index ["currency_rate_id"], name: "fk_rails_d6f1847478" @@ -146,89 +142,109 @@ end create_table "gera_exchange_rates", force: :cascade do |t| - t.bigint "income_payment_system_id", null: false + t.boolean "auto_rate", default: false + t.string "calculator_type", default: "legacy", null: false + t.datetime "created_at", precision: nil, default: -> { "CURRENT_TIMESTAMP" }, null: false t.string "in_cur", limit: 4, null: false + t.bigint "income_payment_system_id", null: false + t.boolean "is_enabled", default: false, null: false + t.integer "maxamount_cents", default: 0 + t.integer "minamount_cents", default: 0 t.string "out_cur", limit: 4, null: false t.bigint "outcome_payment_system_id", null: false + t.datetime "updated_at", precision: nil, default: -> { "CURRENT_TIMESTAMP" }, null: false t.float "value", null: false - t.boolean "is_enabled", default: false, null: false - t.datetime "updated_at", default: -> { "now()" }, null: false - t.datetime "created_at", default: -> { "now()" }, null: false + t.index ["calculator_type"], name: "index_gera_exchange_rates_on_calculator_type" t.index ["income_payment_system_id", "outcome_payment_system_id"], name: "exchange_rate_unique_index", unique: true t.index ["is_enabled"], name: "index_exchange_rates_on_is_enabled" t.index ["outcome_payment_system_id"], name: "fk_rails_ef77ea3609" end create_table "gera_external_rate_snapshots", force: :cascade do |t| + t.datetime "actual_for", precision: nil, default: -> { "CURRENT_TIMESTAMP" }, null: false + t.datetime "created_at", precision: nil, null: false t.bigint "rate_source_id", null: false - t.datetime "actual_for", default: -> { "now()" }, null: false - t.datetime "created_at", null: false t.index ["rate_source_id", "actual_for"], name: "index_external_rate_snapshots_on_rate_source_id_and_actual_for", unique: true t.index ["rate_source_id"], name: "index_external_rate_snapshots_on_rate_source_id" end create_table "gera_external_rates", force: :cascade do |t| - t.bigint "source_id", null: false + t.datetime "created_at", precision: nil t.string "cur_from", null: false t.string "cur_to", null: false - t.float "rate_value" + t.float "rate_value", limit: 53 t.bigint "snapshot_id", null: false - t.datetime "created_at" + t.bigint "source_id", null: false t.index ["snapshot_id", "cur_from", "cur_to"], name: "index_external_rates_on_snapshot_id_and_cur_from_and_cur_to", unique: true t.index ["source_id"], name: "index_external_rates_on_source_id" end create_table "gera_payment_systems", force: :cascade do |t| - t.string "name", limit: 60 - t.integer "priority", limit: 2 + t.float "commission", default: 0.0, null: false + t.datetime "created_at", precision: nil, default: -> { "CURRENT_TIMESTAMP" }, null: false + t.datetime "deleted_at", precision: nil + t.string "icon_url" t.string "img" - t.integer "type_cy", null: false t.boolean "income_enabled", default: false, null: false - t.boolean "outcome_enabled", default: false, null: false - t.datetime "deleted_at" - t.datetime "updated_at", default: -> { "now()" }, null: false - t.datetime "created_at", default: -> { "now()" }, null: false t.boolean "is_available", default: true, null: false - t.string "icon_url" + t.string "name", limit: 60 + t.boolean "outcome_enabled", default: false, null: false + t.integer "priority", limit: 1 + t.integer "total_computation_method", default: 0 + t.integer "transfer_comission_payer", default: 0 + t.integer "type_cy", null: false + t.datetime "updated_at", precision: nil, default: -> { "CURRENT_TIMESTAMP" }, null: false t.index ["income_enabled"], name: "index_payment_systems_on_income_enabled" t.index ["outcome_enabled"], name: "index_payment_systems_on_outcome_enabled" end create_table "gera_rate_sources", force: :cascade do |t| - t.string "title", null: false - t.string "type", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "key", null: false t.bigint "actual_snapshot_id" - t.integer "priority", default: 0, null: false + t.datetime "created_at", precision: nil, null: false t.boolean "is_enabled", default: true, null: false + t.string "key", null: false + t.integer "priority", default: 0, null: false + t.string "title", null: false + t.string "type", null: false + t.datetime "updated_at", precision: nil, null: false t.index ["actual_snapshot_id"], name: "fk_rails_0b6cf3ddaa" t.index ["key"], name: "index_rate_sources_on_key", unique: true t.index ["title"], name: "index_rate_sources_on_title", unique: true end - add_foreign_key "gera_cross_rate_modes", "gera_currency_rate_modes", column: "currency_rate_mode_id" - add_foreign_key "gera_cross_rate_modes", "gera_rate_sources", column: "rate_source_id" - add_foreign_key "gera_currency_rate_modes", "gera_currency_rate_mode_snapshots", column: "currency_rate_mode_snapshot_id" - add_foreign_key "gera_currency_rate_modes", "gera_rate_sources", column: "cross_rate_source1_id" - add_foreign_key "gera_currency_rate_modes", "gera_rate_sources", column: "cross_rate_source2_id" - add_foreign_key "gera_currency_rate_modes", "gera_rate_sources", column: "cross_rate_source3_id" - add_foreign_key "gera_currency_rate_snapshots", "gera_currency_rate_mode_snapshots", column: "currency_rate_mode_snapshot_id" + create_table "gera_target_autorate_settings", force: :cascade do |t| + t.decimal "autorate_from", precision: 10, scale: 4 + t.decimal "autorate_to", precision: 10, scale: 4 + t.datetime "created_at", null: false + t.integer "exchange_rate_id", null: false + t.integer "position_from" + t.integer "position_to" + t.datetime "updated_at", null: false + t.index ["exchange_rate_id"], name: "index_gera_target_autorate_settings_on_exchange_rate_id", unique: true + end + + add_foreign_key "gera_cross_rate_modes", "gera_currency_rate_modes", column: "currency_rate_mode_id", on_delete: :cascade + add_foreign_key "gera_cross_rate_modes", "gera_rate_sources", column: "rate_source_id", on_delete: :cascade + add_foreign_key "gera_currency_rate_modes", "gera_currency_rate_mode_snapshots", column: "currency_rate_mode_snapshot_id", on_delete: :cascade + add_foreign_key "gera_currency_rate_modes", "gera_rate_sources", column: "cross_rate_source1_id", on_delete: :cascade + add_foreign_key "gera_currency_rate_modes", "gera_rate_sources", column: "cross_rate_source2_id", on_delete: :cascade + add_foreign_key "gera_currency_rate_modes", "gera_rate_sources", column: "cross_rate_source3_id", on_delete: :cascade + add_foreign_key "gera_currency_rate_snapshots", "gera_currency_rate_mode_snapshots", column: "currency_rate_mode_snapshot_id", on_delete: :cascade add_foreign_key "gera_currency_rates", "gera_currency_rate_snapshots", column: "snapshot_id", on_delete: :cascade - add_foreign_key "gera_currency_rates", "gera_external_rates", column: "external_rate1_id" - add_foreign_key "gera_currency_rates", "gera_external_rates", column: "external_rate2_id" - add_foreign_key "gera_currency_rates", "gera_external_rates", column: "external_rate3_id" + add_foreign_key "gera_currency_rates", "gera_external_rates", column: "external_rate1_id", on_delete: :cascade + add_foreign_key "gera_currency_rates", "gera_external_rates", column: "external_rate2_id", on_delete: :cascade + add_foreign_key "gera_currency_rates", "gera_external_rates", column: "external_rate3_id", on_delete: :cascade add_foreign_key "gera_currency_rates", "gera_external_rates", column: "external_rate_id", on_delete: :nullify - add_foreign_key "gera_currency_rates", "gera_rate_sources", column: "rate_source_id" - add_foreign_key "gera_direction_rate_history_intervals", "gera_payment_systems", column: "payment_system_from_id" - add_foreign_key "gera_direction_rate_history_intervals", "gera_payment_systems", column: "payment_system_to_id" + add_foreign_key "gera_currency_rates", "gera_rate_sources", column: "rate_source_id", on_delete: :cascade + add_foreign_key "gera_direction_rate_history_intervals", "gera_payment_systems", column: "payment_system_from_id", on_delete: :cascade + add_foreign_key "gera_direction_rate_history_intervals", "gera_payment_systems", column: "payment_system_to_id", on_delete: :cascade add_foreign_key "gera_direction_rates", "gera_currency_rates", column: "currency_rate_id", on_delete: :cascade - add_foreign_key "gera_direction_rates", "gera_exchange_rates", column: "exchange_rate_id" - add_foreign_key "gera_direction_rates", "gera_payment_systems", column: "ps_from_id" - add_foreign_key "gera_direction_rates", "gera_payment_systems", column: "ps_to_id" - add_foreign_key "gera_exchange_rates", "gera_payment_systems", column: "income_payment_system_id" - add_foreign_key "gera_exchange_rates", "gera_payment_systems", column: "outcome_payment_system_id" + add_foreign_key "gera_direction_rates", "gera_exchange_rates", column: "exchange_rate_id", on_delete: :cascade + add_foreign_key "gera_direction_rates", "gera_payment_systems", column: "ps_from_id", on_delete: :cascade + add_foreign_key "gera_direction_rates", "gera_payment_systems", column: "ps_to_id", on_delete: :cascade + add_foreign_key "gera_exchange_rates", "gera_payment_systems", column: "income_payment_system_id", on_delete: :cascade + add_foreign_key "gera_exchange_rates", "gera_payment_systems", column: "outcome_payment_system_id", on_delete: :cascade add_foreign_key "gera_external_rates", "gera_external_rate_snapshots", column: "snapshot_id", on_delete: :cascade - add_foreign_key "gera_external_rates", "gera_rate_sources", column: "source_id" + add_foreign_key "gera_external_rates", "gera_rate_sources", column: "source_id", on_delete: :cascade + add_foreign_key "gera_target_autorate_settings", "gera_exchange_rates", column: "exchange_rate_id", on_delete: :cascade end diff --git a/spec/dummy/db/test.sqlite3 b/spec/dummy/db/test.sqlite3 index 01b224e2..177b431d 100644 Binary files a/spec/dummy/db/test.sqlite3 and b/spec/dummy/db/test.sqlite3 differ diff --git a/spec/lib/money_support_spec.rb b/spec/lib/money_support_spec.rb index 6370fd4c..6b77377c 100644 --- a/spec/lib/money_support_spec.rb +++ b/spec/lib/money_support_spec.rb @@ -3,6 +3,6 @@ require 'spec_helper' RSpec.describe 'Gera define money' do - it { expect(Money::Currency.all.count).to eq 14 } + it { expect(Money::Currency.all.count).to eq 40 } it { expect(USD).to be_a Money::Currency } end diff --git a/spec/models/gera/exchange_rate_spec.rb b/spec/models/gera/exchange_rate_spec.rb index 3a48e4a5..4a4fb0b6 100644 --- a/spec/models/gera/exchange_rate_spec.rb +++ b/spec/models/gera/exchange_rate_spec.rb @@ -9,5 +9,47 @@ module Gera end subject { create :gera_exchange_rate } it { expect(subject).to be_persisted } + + describe '.available scope' do + it 'includes same-to-same direction (PIR-076)' do + ps = create(:gera_payment_system, income_enabled: true, outcome_enabled: true) + er = create(:gera_exchange_rate, payment_system_from: ps, payment_system_to: ps) + + expect(Gera::ExchangeRate.available).to include(er) + end + end + + describe '#autorate_calculator_class' do + context 'when calculator_type is legacy' do + before { subject.calculator_type = 'legacy' } + + it 'returns AutorateCalculators::Legacy' do + expect(subject.autorate_calculator_class).to eq(AutorateCalculators::Legacy) + end + end + + context 'when calculator_type is position_aware' do + before { subject.calculator_type = 'position_aware' } + + it 'returns AutorateCalculators::PositionAware' do + expect(subject.autorate_calculator_class).to eq(AutorateCalculators::PositionAware) + end + end + + context 'when calculator_type is nil' do + before { subject.calculator_type = nil } + + it 'defaults to AutorateCalculators::Legacy' do + expect(subject.autorate_calculator_class).to eq(AutorateCalculators::Legacy) + end + end + + context 'when calculator_type is unknown' do + it 'raises ArgumentError' do + subject.calculator_type = 'unknown' + expect { subject.autorate_calculator_class }.to raise_error(ArgumentError, /Unknown calculator_type/) + end + end + end end end diff --git a/spec/services/gera/autorate_calculators/isolated_spec.rb b/spec/services/gera/autorate_calculators/isolated_spec.rb new file mode 100644 index 00000000..9ae9b753 --- /dev/null +++ b/spec/services/gera/autorate_calculators/isolated_spec.rb @@ -0,0 +1,742 @@ +# frozen_string_literal: true + +# Полностью изолированные тесты - не загружают Rails и spec_helper + +require 'rspec' +require 'virtus' + +# Загружаем только необходимые файлы +$LOAD_PATH.unshift File.expand_path('../../../../app/services', __dir__) +$LOAD_PATH.unshift File.expand_path('../../../../lib', __dir__) + +require 'gera/autorate_calculators/base' +require 'gera/autorate_calculators/legacy' +require 'gera/autorate_calculators/position_aware' + +# Stub для Gera модуля - настройки конфигурации +module Gera + class << self + attr_accessor :our_exchanger_id, :autorate_debug_enabled + end +end + +RSpec.describe 'AutorateCalculators (isolated)' do + let(:exchange_rate) { double('ExchangeRate') } + let(:target_autorate_setting) { double('TargetAutorateSetting') } + + before do + allow(exchange_rate).to receive(:target_autorate_setting).and_return(target_autorate_setting) + allow(exchange_rate).to receive(:autorate_from).and_return(1.0) + allow(exchange_rate).to receive(:autorate_to).and_return(3.0) + allow(exchange_rate).to receive(:id).and_return(1) + allow(target_autorate_setting).to receive(:could_be_calculated?).and_return(true) + # Сбрасываем конфигурацию Gera + Gera.our_exchanger_id = nil + end + + describe Gera::AutorateCalculators::Legacy do + let(:calculator) do + described_class.new( + exchange_rate: exchange_rate, + external_rates: external_rates + ) + end + + context 'с валидными rates' do + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 2.5), + double('ExternalRate', target_rate_percent: 2.8), + double('ExternalRate', target_rate_percent: 3.0) + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(1) + allow(exchange_rate).to receive(:position_to).and_return(3) + end + + it 'вычитает GAP из первого matching rate' do + expect(calculator.call).to eq(2.5 - 0.0001) + end + end + + context 'когда rates пустые' do + let(:external_rates) { [] } + + before do + allow(exchange_rate).to receive(:position_from).and_return(1) + allow(exchange_rate).to receive(:position_to).and_return(3) + end + + it 'возвращает autorate_from' do + expect(calculator.call).to eq(1.0) + end + end + + # UC-5: Диапазон позиций не совпадает с диапазоном курсов + # Реализован вариант A: возвращаем autorate_from + describe 'UC-5: диапазон позиций не совпадает с диапазоном курсов (Вариант A)' do + context 'курсы конкурентов выше допустимого диапазона' do + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 4.0), + double('ExternalRate', target_rate_percent: 4.5), + double('ExternalRate', target_rate_percent: 5.0) + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(1) + allow(exchange_rate).to receive(:position_to).and_return(3) + end + + it 'возвращает autorate_from (вариант A)' do + expect(calculator.call).to eq(1.0) + end + end + + context 'курсы конкурентов ниже допустимого диапазона' do + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: -0.5), + double('ExternalRate', target_rate_percent: -0.3), + double('ExternalRate', target_rate_percent: -0.1) + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(1) + allow(exchange_rate).to receive(:position_to).and_return(3) + end + + it 'возвращает autorate_from (вариант A)' do + expect(calculator.call).to eq(1.0) + end + end + end + end + + describe Gera::AutorateCalculators::PositionAware do + let(:calculator) do + described_class.new( + exchange_rate: exchange_rate, + external_rates: external_rates + ) + end + + context 'UC-1: все позиции имеют одинаковый курс' do + let(:external_rates) do + 10.times.map { double('ExternalRate', target_rate_percent: 2.5) } + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(5) + allow(exchange_rate).to receive(:position_to).and_return(10) + end + + it 'не перепрыгивает позицию выше, возвращает ту же комиссию' do + expect(calculator.call).to eq(2.5) + end + end + + context 'UC-2: есть разрыв между позициями' do + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 1.0), + double('ExternalRate', target_rate_percent: 1.2), + double('ExternalRate', target_rate_percent: 1.4), + double('ExternalRate', target_rate_percent: 1.6), + double('ExternalRate', target_rate_percent: 2.5), + double('ExternalRate', target_rate_percent: 2.6), + double('ExternalRate', target_rate_percent: 2.7), + double('ExternalRate', target_rate_percent: 2.8), + double('ExternalRate', target_rate_percent: 2.9), + double('ExternalRate', target_rate_percent: 3.0) + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(5) + allow(exchange_rate).to receive(:position_to).and_return(10) + end + + it 'безопасно вычитает GAP когда есть разрыв' do + expect(calculator.call).to eq(2.5 - 0.0001) + end + end + + context 'UC-3: целевая позиция 1' do + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 2.5), + double('ExternalRate', target_rate_percent: 2.8), + double('ExternalRate', target_rate_percent: 3.0) + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(1) + allow(exchange_rate).to receive(:position_to).and_return(3) + end + + it 'вычитает GAP когда нет позиции выше' do + expect(calculator.call).to eq(2.5 - 0.0001) + end + end + + context 'UC-4: позиция выше с очень близкой комиссией' do + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 1.0), + double('ExternalRate', target_rate_percent: 1.5), + double('ExternalRate', target_rate_percent: 2.0), + double('ExternalRate', target_rate_percent: 2.4999), + double('ExternalRate', target_rate_percent: 2.5), + double('ExternalRate', target_rate_percent: 2.8) + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(5) + allow(exchange_rate).to receive(:position_to).and_return(6) + end + + it 'не перепрыгивает позицию 4' do + expect(calculator.call).to eq(2.4999) + end + end + + # UC-5: Диапазон позиций не совпадает с диапазоном курсов + # Реализован вариант A: возвращаем autorate_from (минимально допустимую комиссию) + # и занимаем позицию ниже целевого диапазона + describe 'UC-5: диапазон позиций не совпадает с диапазоном курсов (Вариант A)' do + context 'курсы конкурентов выше допустимого диапазона' do + # autorate_from..autorate_to = 1.0..3.0 + # Курсы на позициях 2-4: 4.0, 4.5, 5.0 (все выше 3.0) + # Ожидаемый результат: autorate_from = 1.0 + + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 3.5), # pos 1 + double('ExternalRate', target_rate_percent: 4.0), # pos 2 + double('ExternalRate', target_rate_percent: 4.5), # pos 3 + double('ExternalRate', target_rate_percent: 5.0), # pos 4 + double('ExternalRate', target_rate_percent: 5.5) # pos 5 + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(2) + allow(exchange_rate).to receive(:position_to).and_return(4) + end + + it 'возвращает autorate_from (вариант A)' do + expect(calculator.call).to eq(1.0) + end + end + + context 'курсы конкурентов ниже допустимого диапазона' do + # autorate_from..autorate_to = 1.0..3.0 + # Курсы на позициях 2-4: -0.5, -0.3, -0.1 (все ниже 1.0) + # Мы не можем им соответствовать, возвращаем autorate_from + + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: -0.8), # pos 1 + double('ExternalRate', target_rate_percent: -0.5), # pos 2 + double('ExternalRate', target_rate_percent: -0.3), # pos 3 + double('ExternalRate', target_rate_percent: -0.1), # pos 4 + double('ExternalRate', target_rate_percent: 0.2) # pos 5 + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(2) + allow(exchange_rate).to receive(:position_to).and_return(4) + end + + it 'возвращает autorate_from (вариант A)' do + expect(calculator.call).to eq(1.0) + end + end + + context 'нет курсов на целевых позициях (список короче)' do + # position_from..position_to = 5..10 + # Но в списке только 3 позиции + + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 2.0), # pos 1 + double('ExternalRate', target_rate_percent: 2.5), # pos 2 + double('ExternalRate', target_rate_percent: 3.0) # pos 3 + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(5) + allow(exchange_rate).to receive(:position_to).and_return(10) + end + + it 'возвращает autorate_from (вариант A)' do + expect(calculator.call).to eq(1.0) + end + end + + context 'частичное совпадение: только некоторые позиции вне диапазона' do + # autorate_from..autorate_to = 1.0..3.0 + # Позиция 1: 0.5 (лучший курс, вне диапазона - ниже 1.0) + # Позиция 2: 4.0 (вне диапазона - выше 3.0) + # Позиция 3: 2.5 (в диапазоне) + # Позиция 4: 2.8 (в диапазоне) + # Должен использовать курс с позиции 3 + + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 0.5), # pos 1 - лучший, но вне диапазона + double('ExternalRate', target_rate_percent: 4.0), # pos 2 - вне диапазона + double('ExternalRate', target_rate_percent: 2.5), # pos 3 - в диапазоне + double('ExternalRate', target_rate_percent: 2.8), # pos 4 - в диапазоне + double('ExternalRate', target_rate_percent: 5.0) # pos 5 + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(2) + allow(exchange_rate).to receive(:position_to).and_return(4) + end + + it 'использует первый подходящий курс в диапазоне' do + # valid_rates = [2.5, 2.8] + # target = 2.5 - GAP = 2.499 + # rate_above (pos 1) = 0.5, 2.499 > 0.5 - не перепрыгиваем + expect(calculator.call).to eq(2.5 - 0.0001) + end + end + end + + # UC-12: Не вычитать GAP при одинаковых курсах (для любого position_from) + describe 'UC-12: пропуск GAP при одинаковых курсах' do + context 'position_from=1, одинаковые курсы на позициях 1 и 2' do + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 2.5, exchanger_id: 1), + double('ExternalRate', target_rate_percent: 2.5, exchanger_id: 2), + double('ExternalRate', target_rate_percent: 2.8, exchanger_id: 3) + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(1) + allow(exchange_rate).to receive(:position_to).and_return(3) + end + + it 'возвращает курс без GAP' do + expect(calculator.call).to eq(2.5) + end + end + + context 'position_from=2, одинаковые курсы на позициях 1 и 2' do + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 2.5, exchanger_id: 1), + double('ExternalRate', target_rate_percent: 2.5, exchanger_id: 2), + double('ExternalRate', target_rate_percent: 2.8, exchanger_id: 3) + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(2) + allow(exchange_rate).to receive(:position_to).and_return(3) + end + + it 'возвращает курс без GAP' do + expect(calculator.call).to eq(2.5) + end + end + + context 'position_from=3, одинаковые курсы на позициях 2 и 3' do + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 2.0, exchanger_id: 1), + double('ExternalRate', target_rate_percent: 2.5, exchanger_id: 2), + double('ExternalRate', target_rate_percent: 2.5, exchanger_id: 3), + double('ExternalRate', target_rate_percent: 2.8, exchanger_id: 4) + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(3) + allow(exchange_rate).to receive(:position_to).and_return(4) + end + + it 'возвращает курс без GAP' do + expect(calculator.call).to eq(2.5) + end + end + + context 'position_from=5, одинаковые курсы на позициях 4 и 5' do + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 1.0, exchanger_id: 1), + double('ExternalRate', target_rate_percent: 1.5, exchanger_id: 2), + double('ExternalRate', target_rate_percent: 2.0, exchanger_id: 3), + double('ExternalRate', target_rate_percent: 2.5, exchanger_id: 4), + double('ExternalRate', target_rate_percent: 2.5, exchanger_id: 5), + double('ExternalRate', target_rate_percent: 2.8, exchanger_id: 6) + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(5) + allow(exchange_rate).to receive(:position_to).and_return(6) + end + + it 'возвращает курс без GAP' do + expect(calculator.call).to eq(2.5) + end + end + + context 'position_from=2, РАЗНЫЕ курсы на позициях 1 и 2' do + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 2.0, exchanger_id: 1), + double('ExternalRate', target_rate_percent: 2.5, exchanger_id: 2), + double('ExternalRate', target_rate_percent: 2.8, exchanger_id: 3) + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(2) + allow(exchange_rate).to receive(:position_to).and_return(3) + end + + it 'вычитает GAP так как курсы разные' do + # target_rate = 2.5, rate_above = 2.0 + # diff = 0.5 > AUTO_COMISSION_GAP, используем стандартный GAP + # target_comission = 2.5 - 0.0001 = 2.4999 + # 2.4999 > 2.0, не перепрыгиваем + expect(calculator.call).to eq(2.5 - 0.0001) + end + end + end + + # UC-14: Fallback на первую целевую позицию при отсутствии rate_above (issue #83) + # При position_from > 1 и rate_above = nil — ВСЕГДА занимаем первую целевую позицию + describe 'UC-14: fallback при отсутствии rate_above' do + context 'когда rate_above = nil (разреженный список)' do + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 1.0), # pos 1 + double('ExternalRate', target_rate_percent: 1.5), # pos 2 + double('ExternalRate', target_rate_percent: 2.0), # pos 3 + nil, # pos 4 - отсутствует + double('ExternalRate', target_rate_percent: 2.5), # pos 5 + double('ExternalRate', target_rate_percent: 2.6), # pos 6 + double('ExternalRate', target_rate_percent: 2.7) # pos 7 + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(5) + allow(exchange_rate).to receive(:position_to).and_return(7) + end + + it 'всегда использует курс первой целевой позиции' do + # rate_above = rates[3] = nil + # UC-14: ВСЕГДА используем first_target_rate = rates[4] = 2.5 + expect(calculator.call).to eq(2.5) + end + end + + context 'когда rate_above = nil (другой пример)' do + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 1.0), # pos 1 + double('ExternalRate', target_rate_percent: 1.5), # pos 2 + double('ExternalRate', target_rate_percent: 2.0), # pos 3 + nil, # pos 4 - отсутствует + double('ExternalRate', target_rate_percent: 2.4), # pos 5 + double('ExternalRate', target_rate_percent: 2.6) # pos 6 + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(5) + allow(exchange_rate).to receive(:position_to).and_return(6) + end + + it 'всегда использует курс первой целевой позиции' do + # rate_above = rates[3] = nil + # UC-14: ВСЕГДА используем first_target_rate = rates[4] = 2.4 + expect(calculator.call).to eq(2.4) + end + end + + context 'когда first_target_rate вне допустимого диапазона' do + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 1.0), # pos 1 + double('ExternalRate', target_rate_percent: 1.5), # pos 2 + nil, # pos 3 - отсутствует + double('ExternalRate', target_rate_percent: 5.0), # pos 4 - вне диапазона + double('ExternalRate', target_rate_percent: 5.5) # pos 5 + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(4) + allow(exchange_rate).to receive(:position_to).and_return(5) + end + + it 'возвращает autorate_from' do + # valid_rates пуст (5.0 и 5.5 > 3.0) + expect(calculator.call).to eq(1.0) + end + end + + context 'когда rate_above = nil И first_target вне диапазона, но valid_rates не пуст' do + # Edge case: first_target_rate (для fallback) вне диапазона, + # но valid_rates содержит другие позиции в диапазоне + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 1.0), # pos 1 + double('ExternalRate', target_rate_percent: 1.5), # pos 2 + double('ExternalRate', target_rate_percent: 2.0), # pos 3 + nil, # pos 4 - отсутствует (rate_above) + double('ExternalRate', target_rate_percent: 5.0), # pos 5 - first_target, но вне диапазона! + double('ExternalRate', target_rate_percent: 2.5), # pos 6 - в диапазоне + double('ExternalRate', target_rate_percent: 2.8) # pos 7 - в диапазоне + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(5) + allow(exchange_rate).to receive(:position_to).and_return(7) + end + + it 'возвращает autorate_from так как first_target_rate вне диапазона' do + # valid_rates = [2.5, 2.8] (после фильтрации по диапазону) + # target_rate = 2.5 + # rate_above = rates[3] = nil → fallback + # UC-14: first_target_rate = rates[4] = 5.0 (ВНЕ диапазона 1.0..3.0!) + # Должен вернуть autorate_from = 1.0 + expect(calculator.call).to eq(1.0) + end + end + + context 'когда rate_above = nil И first_target_rate = nil' do + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 1.0), # pos 1 + double('ExternalRate', target_rate_percent: 1.5), # pos 2 + double('ExternalRate', target_rate_percent: 2.0), # pos 3 + nil, # pos 4 - отсутствует (rate_above) + nil, # pos 5 - целевая, но nil + double('ExternalRate', target_rate_percent: 2.6) # pos 6 + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(5) + allow(exchange_rate).to receive(:position_to).and_return(6) + end + + it 'возвращает autorate_from (first_target_rate = nil)' do + # rates_in_target_position = [nil, 2.6] + # valid_rates = [2.6] (после compact) + # target_rate = 2.6, rate_above = rates[3] = nil + # UC-14: first_target_rate = rates[4] = nil → возвращаем autorate_from + expect(calculator.call).to eq(1.0) + end + end + end + + # UC-6: Адаптивный GAP для плотных рейтингов + describe 'UC-6: адаптивный GAP для плотных рейтингов' do + before do + allow(exchange_rate).to receive(:position_from).and_return(3) + allow(exchange_rate).to receive(:position_to).and_return(5) + allow(exchange_rate).to receive(:autorate_from).and_return(1.0) + allow(exchange_rate).to receive(:autorate_to).and_return(3.0) + end + + context 'когда разница между позициями меньше стандартного GAP' do + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 2.0, exchanger_id: 100), + double('ExternalRate', target_rate_percent: 2.00003, exchanger_id: 101), # rate_above + double('ExternalRate', target_rate_percent: 2.00005, exchanger_id: 102), # target_rate (позиция 3) + double('ExternalRate', target_rate_percent: 2.5, exchanger_id: 103), + double('ExternalRate', target_rate_percent: 2.8, exchanger_id: 104) + ] + end + + it 'использует MIN_GAP когда diff/2 <= MIN_GAP' do + # diff = 2.00005 - 2.00003 = 0.00002 + # adaptive_gap = max(0.00002 / 2, MIN_GAP) = max(0.00001, 0.00001) = 0.00001 (MIN_GAP) + # target_comission = 2.00005 - 0.00001 = 1.99994, округляется до 1.9999 + # adjust_for_position_above: 1.9999 < 2.00003? Да → корректируем до 2.00003 + # round(2.00003, 4) = 2.0 + expect(calculator.call).to eq(2.0) + end + end + + context 'когда разница достаточна для адаптивного GAP' do + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 2.0, exchanger_id: 100), + double('ExternalRate', target_rate_percent: 2.0005, exchanger_id: 101), # rate_above + double('ExternalRate', target_rate_percent: 2.001, exchanger_id: 102), # target_rate + double('ExternalRate', target_rate_percent: 2.5, exchanger_id: 103), + double('ExternalRate', target_rate_percent: 2.8, exchanger_id: 104) + ] + end + + it 'использует стандартный GAP когда diff >= AUTO_COMISSION_GAP' do + # diff = 2.001 - 2.0005 = 0.0005 >= AUTO_COMISSION_GAP (0.0001) + # Адаптивный режим НЕ используется, gap = AUTO_COMISSION_GAP = 0.0001 + # Результат = 2.001 - 0.0001 = 2.0009 + expect(calculator.call).to eq(2.0009) + end + end + + context 'когда position_from = 1 (нет позиции выше)' do + before do + allow(exchange_rate).to receive(:position_from).and_return(1) + allow(exchange_rate).to receive(:position_to).and_return(3) + end + + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 2.5, exchanger_id: 100), + double('ExternalRate', target_rate_percent: 2.6, exchanger_id: 101), + double('ExternalRate', target_rate_percent: 2.8, exchanger_id: 102) + ] + end + + it 'использует стандартный GAP' do + # position_from = 1, используем стандартный AUTO_COMISSION_GAP = 0.0001 + expect(calculator.call).to eq(2.5 - 0.0001) + end + end + end + + # UC-8: Исключение своего обменника из расчёта + describe 'UC-8: исключение своего обменника из расчёта' do + before do + allow(exchange_rate).to receive(:position_from).and_return(1) + allow(exchange_rate).to receive(:position_to).and_return(3) + allow(exchange_rate).to receive(:autorate_from).and_return(1.0) + allow(exchange_rate).to receive(:autorate_to).and_return(3.0) + end + + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 2.0, exchanger_id: 999), # наш обменник + double('ExternalRate', target_rate_percent: 2.5, exchanger_id: 100), + double('ExternalRate', target_rate_percent: 2.8, exchanger_id: 101) + ] + end + + context 'когда our_exchanger_id задан' do + before do + Gera.our_exchanger_id = 999 + end + + after do + Gera.our_exchanger_id = nil + end + + it 'исключает свой обменник и берёт следующий' do + # Наш обменник (id=999) исключается + # Следующий - 2.5, минус GAP = 2.4999 + expect(calculator.call).to eq(2.5 - 0.0001) + end + end + + context 'когда our_exchanger_id не задан' do + before do + Gera.our_exchanger_id = nil + end + + it 'не исключает обменники' do + # Берём первый - 2.0, минус GAP = 1.9999 + expect(calculator.call).to eq(2.0 - 0.0001) + end + end + + context 'когда rate содержит nil exchanger_id' do + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 2.0, exchanger_id: nil), + double('ExternalRate', target_rate_percent: 2.5, exchanger_id: 100), + double('ExternalRate', target_rate_percent: 2.8, exchanger_id: 101) + ] + end + + before do + Gera.our_exchanger_id = 999 + end + + after do + Gera.our_exchanger_id = nil + end + + it 'не падает на nil exchanger_id' do + # rate с nil exchanger_id не равен 999, поэтому не исключается + expect(calculator.call).to eq(2.0 - 0.0001) + end + end + end + + # UC-13: Защита от перепрыгивания позиции position_from - 1 + describe 'UC-13: защита от перепрыгивания позиции выше' do + before do + allow(exchange_rate).to receive(:position_from).and_return(3) + allow(exchange_rate).to receive(:position_to).and_return(5) + allow(exchange_rate).to receive(:autorate_from).and_return(1.0) + allow(exchange_rate).to receive(:autorate_to).and_return(3.0) + end + + context 'когда после вычитания GAP курс станет лучше позиции выше' do + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 1.5, exchanger_id: 100), + double('ExternalRate', target_rate_percent: 2.0001, exchanger_id: 101), # rate_above + double('ExternalRate', target_rate_percent: 2.0002, exchanger_id: 102), # target_rate + double('ExternalRate', target_rate_percent: 2.5, exchanger_id: 103), + double('ExternalRate', target_rate_percent: 2.8, exchanger_id: 104) + ] + end + + it 'использует адаптивный GAP и не перепрыгивает' do + # target_rate = 2.0002, rate_above = 2.0001 + # target_comission = 2.0002 - GAP (адаптивный) + # diff = 2.0002 - 2.0001 = 0.0001 + # adaptive_gap = max(0.0001/2, MIN_GAP) = max(0.00005, 0.00001) = 0.00005 + # target_comission = 2.0002 - 0.00005 = 2.00015, round(4) = 2.0002 + # 2.0002 > 2.0001 → не перепрыгиваем + expect(calculator.call).to eq(2.0002) + end + end + + context 'когда курс после GAP перепрыгнет позицию выше' do + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 1.5, exchanger_id: 100), + double('ExternalRate', target_rate_percent: 2.5, exchanger_id: 101), # rate_above + double('ExternalRate', target_rate_percent: 2.5, exchanger_id: 102), # target_rate (одинаковый) + double('ExternalRate', target_rate_percent: 2.6, exchanger_id: 103), + double('ExternalRate', target_rate_percent: 2.8, exchanger_id: 104) + ] + end + + it 'не вычитает GAP при одинаковых курсах (UC-12)' do + # UC-12: курсы одинаковые, GAP не вычитаем + expect(calculator.call).to eq(2.5) + end + end + end + end +end diff --git a/spec/services/gera/autorate_calculators/legacy_spec.rb b/spec/services/gera/autorate_calculators/legacy_spec.rb new file mode 100644 index 00000000..959d466c --- /dev/null +++ b/spec/services/gera/autorate_calculators/legacy_spec.rb @@ -0,0 +1,160 @@ +# frozen_string_literal: true + +require 'spec_helper' + +module Gera + module AutorateCalculators + RSpec.describe Legacy do + let(:exchange_rate) { double('ExchangeRate') } + let(:target_autorate_setting) { double('TargetAutorateSetting') } + let(:external_rate_1) { double('ExternalRate', target_rate_percent: 2.5) } + let(:external_rate_2) { double('ExternalRate', target_rate_percent: 2.8) } + let(:external_rate_3) { double('ExternalRate', target_rate_percent: 3.1) } + let(:external_rates) { [external_rate_1, external_rate_2, external_rate_3] } + + let(:calculator) do + described_class.new( + exchange_rate: exchange_rate, + external_rates: external_rates + ) + end + + before do + allow(exchange_rate).to receive(:target_autorate_setting).and_return(target_autorate_setting) + allow(exchange_rate).to receive(:position_from).and_return(1) + allow(exchange_rate).to receive(:position_to).and_return(3) + allow(exchange_rate).to receive(:autorate_from).and_return(1.0) + allow(exchange_rate).to receive(:autorate_to).and_return(3.0) + end + + describe '#call' do + context 'when could_be_calculated? is false' do + before do + allow(target_autorate_setting).to receive(:could_be_calculated?).and_return(false) + end + + it 'returns 0' do + expect(calculator.call).to eq(0) + end + end + + context 'when external_rates is nil' do + let(:external_rates) { nil } + + before do + allow(target_autorate_setting).to receive(:could_be_calculated?).and_return(true) + end + + it 'returns 0' do + expect(calculator.call).to eq(0) + end + end + + context 'when external_rates_in_target_position is empty' do + let(:external_rates) { [] } + + before do + allow(target_autorate_setting).to receive(:could_be_calculated?).and_return(true) + end + + it 'returns autorate_from' do + expect(calculator.call).to eq(1.0) + end + end + + context 'when no rates match target comission range' do + let(:external_rate_1) { double('ExternalRate', target_rate_percent: 5.0) } + let(:external_rate_2) { double('ExternalRate', target_rate_percent: 6.0) } + let(:external_rate_3) { double('ExternalRate', target_rate_percent: 7.0) } + + before do + allow(target_autorate_setting).to receive(:could_be_calculated?).and_return(true) + end + + it 'returns autorate_from' do + expect(calculator.call).to eq(1.0) + end + end + + context 'when rates match target comission range' do + before do + allow(target_autorate_setting).to receive(:could_be_calculated?).and_return(true) + end + + it 'returns first matching rate minus GAP' do + # first matching rate is 2.5, GAP is 0.0001 + expect(calculator.call).to eq(2.5 - 0.0001) + end + end + + # UC-5: Диапазон позиций не совпадает с диапазоном курсов + # Реализован вариант A: возвращаем autorate_from (минимально допустимую комиссию) + # и занимаем позицию ниже целевого диапазона + describe 'UC-5: диапазон позиций не совпадает с диапазоном курсов (Вариант A)' do + before do + allow(target_autorate_setting).to receive(:could_be_calculated?).and_return(true) + end + + context 'курсы конкурентов выше допустимого диапазона' do + # autorate_from..autorate_to = 1.0..3.0 + # Курсы на позициях 1-3: 4.0, 4.5, 5.0 (все выше 3.0) + # Ожидаемый результат: autorate_from = 1.0 + + let(:external_rate_1) { double('ExternalRate', target_rate_percent: 4.0) } + let(:external_rate_2) { double('ExternalRate', target_rate_percent: 4.5) } + let(:external_rate_3) { double('ExternalRate', target_rate_percent: 5.0) } + + it 'возвращает autorate_from (вариант A)' do + expect(calculator.call).to eq(1.0) + end + end + + context 'курсы конкурентов ниже допустимого диапазона' do + # autorate_from..autorate_to = 1.0..3.0 + # Курсы на позициях 1-3: -0.5, -0.3, -0.1 (все ниже 1.0) + # Мы не можем им соответствовать, возвращаем autorate_from + + let(:external_rate_1) { double('ExternalRate', target_rate_percent: -0.5) } + let(:external_rate_2) { double('ExternalRate', target_rate_percent: -0.3) } + let(:external_rate_3) { double('ExternalRate', target_rate_percent: -0.1) } + + it 'возвращает autorate_from (вариант A)' do + expect(calculator.call).to eq(1.0) + end + end + + context 'нет курсов на целевых позициях (список короче)' do + # position_from..position_to = 5..10 + # Но в списке только 3 позиции + + before do + allow(exchange_rate).to receive(:position_from).and_return(5) + allow(exchange_rate).to receive(:position_to).and_return(10) + end + + it 'возвращает autorate_from (вариант A)' do + expect(calculator.call).to eq(1.0) + end + end + + context 'частичное совпадение: только некоторые позиции вне диапазона' do + # autorate_from..autorate_to = 1.0..3.0 + # Позиция 1: 4.0 (вне диапазона) + # Позиция 2: 2.5 (в диапазоне) + # Должен использовать курс с позиции 2 + + let(:external_rate_1) { double('ExternalRate', target_rate_percent: 4.0) } + let(:external_rate_2) { double('ExternalRate', target_rate_percent: 2.5) } + let(:external_rate_3) { double('ExternalRate', target_rate_percent: 2.8) } + + it 'использует первый подходящий курс в диапазоне' do + # valid_rates = [2.5, 2.8] + # target = 2.5 - GAP = 2.4999 + expect(calculator.call).to eq(2.5 - 0.0001) + end + end + end + end + end + end +end diff --git a/spec/services/gera/autorate_calculators/position_aware_spec.rb b/spec/services/gera/autorate_calculators/position_aware_spec.rb new file mode 100644 index 00000000..681b0db9 --- /dev/null +++ b/spec/services/gera/autorate_calculators/position_aware_spec.rb @@ -0,0 +1,657 @@ +# frozen_string_literal: true + +require 'spec_helper' + +module Gera + module AutorateCalculators + RSpec.describe PositionAware do + let(:exchange_rate) { double('ExchangeRate') } + let(:target_autorate_setting) { double('TargetAutorateSetting') } + + let(:calculator) do + described_class.new( + exchange_rate: exchange_rate, + external_rates: external_rates + ) + end + + before do + allow(exchange_rate).to receive(:target_autorate_setting).and_return(target_autorate_setting) + allow(exchange_rate).to receive(:autorate_from).and_return(1.0) + allow(exchange_rate).to receive(:autorate_to).and_return(3.0) + allow(exchange_rate).to receive(:id).and_return(1) + allow(target_autorate_setting).to receive(:could_be_calculated?).and_return(true) + # Сбрасываем конфигурацию перед каждым тестом + Gera.our_exchanger_id = nil + end + + describe '#call' do + context 'UC-1: все позиции имеют одинаковый курс' do + # Позиции 1-10 все имеют комиссию 2.5 + # position_from: 5, position_to: 10 + # Legacy вычтет GAP и займёт позицию 1 + # PositionAware должен оставить 2.5 и занять позицию 5-10 + + let(:external_rates) do + 10.times.map { double('ExternalRate', target_rate_percent: 2.5) } + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(5) + allow(exchange_rate).to receive(:position_to).and_return(10) + end + + it 'не перепрыгивает позицию выше' do + # Позиция 4 (index 3) имеет комиссию 2.5 + # Если мы вычтем GAP (2.5 - 0.0001 = 2.4999), мы станем выше позиции 4 + # PositionAware должен вернуть 2.5 (равную позиции выше) + expect(calculator.call).to eq(2.5) + end + end + + context 'UC-2: есть разрыв между позициями' do + # Позиции 1-4 имеют комиссии 1.0, 1.2, 1.4, 1.6 + # Позиции 5-10 имеют комиссии 2.5, 2.6, 2.7, 2.8, 2.9, 3.0 + # position_from: 5, position_to: 10 + # После вычитания GAP (2.5 - 0.0001 = 2.4999) мы всё ещё хуже чем позиция 4 (1.6) + # Поэтому безопасно занимать позицию 5 + + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 1.0), # pos 1 + double('ExternalRate', target_rate_percent: 1.2), # pos 2 + double('ExternalRate', target_rate_percent: 1.4), # pos 3 + double('ExternalRate', target_rate_percent: 1.6), # pos 4 + double('ExternalRate', target_rate_percent: 2.5), # pos 5 + double('ExternalRate', target_rate_percent: 2.6), # pos 6 + double('ExternalRate', target_rate_percent: 2.7), # pos 7 + double('ExternalRate', target_rate_percent: 2.8), # pos 8 + double('ExternalRate', target_rate_percent: 2.9), # pos 9 + double('ExternalRate', target_rate_percent: 3.0) # pos 10 + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(5) + allow(exchange_rate).to receive(:position_to).and_return(10) + end + + it 'безопасно вычитает GAP' do + # 2.5 - 0.0001 = 2.4999 > 1.6 (позиция 4) + # Не перепрыгиваем, возвращаем target - GAP + expect(calculator.call).to eq(2.5 - 0.0001) + end + end + + context 'UC-3: целевая позиция 1' do + # Когда position_from = 1, нет позиции выше + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 2.5), # pos 1 + double('ExternalRate', target_rate_percent: 2.8), # pos 2 + double('ExternalRate', target_rate_percent: 3.0) # pos 3 + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(1) + allow(exchange_rate).to receive(:position_to).and_return(3) + end + + it 'безопасно вычитает GAP' do + expect(calculator.call).to eq(2.5 - 0.0001) + end + end + + context 'UC-4: позиция выше с очень близкой комиссией' do + # Позиция 4 имеет комиссию 2.4999 + # Позиция 5 имеет комиссию 2.5 + # 2.5 - 0.0001 = 2.4999 >= 2.4999 - равны, но мы не перепрыгнем + # PositionAware вернёт 2.4999 (равную позиции выше) + + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 1.0), # pos 1 + double('ExternalRate', target_rate_percent: 1.5), # pos 2 + double('ExternalRate', target_rate_percent: 2.0), # pos 3 + double('ExternalRate', target_rate_percent: 2.4999), # pos 4 + double('ExternalRate', target_rate_percent: 2.5), # pos 5 + double('ExternalRate', target_rate_percent: 2.8) # pos 6 + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(5) + allow(exchange_rate).to receive(:position_to).and_return(6) + end + + it 'не перепрыгивает позицию 4' do + # 2.5 - 0.0001 = 2.4999 = 2.4999, курсы равны + # Возвращаем 2.4999 (равный позиции выше) + expect(calculator.call).to eq(2.4999) + end + end + + context 'UC-6: адаптивный GAP для плотного рейтинга' do + # Разница между позициями 4 и 5 = 0.0005 (БОЛЬШЕ стандартного GAP 0.0001) + # Так что используется стандартный GAP = 0.0001 + + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 1.0), # pos 1 + double('ExternalRate', target_rate_percent: 1.5), # pos 2 + double('ExternalRate', target_rate_percent: 2.0), # pos 3 + double('ExternalRate', target_rate_percent: 2.4995), # pos 4 + double('ExternalRate', target_rate_percent: 2.5), # pos 5 + double('ExternalRate', target_rate_percent: 2.8) # pos 6 + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(5) + allow(exchange_rate).to receive(:position_to).and_return(6) + end + + it 'использует адаптивный GAP' do + # diff = 2.5 - 2.4995 = 0.0005 > 0.0001 (AUTO_COMISSION_GAP) + # так что адаптивный GAP не применяется, используем стандартный 0.0001 + # target = 2.5 - 0.0001 = 2.4999 + # 2.4999 > 2.4995 - не перепрыгиваем + expect(calculator.call).to eq(2.4999) + end + end + + context 'UC-6: минимальный GAP (diff < AUTO_COMISSION_GAP)' do + # Разница между позициями очень маленькая: 0.00005 < 0.0001 (AUTO_COMISSION_GAP) + # Используется адаптивный расчёт: diff/2 = 0.000025, но это меньше MIN_GAP + # Поэтому используется MIN_GAP = 0.0001 + + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 1.0), # pos 1 + double('ExternalRate', target_rate_percent: 1.5), # pos 2 + double('ExternalRate', target_rate_percent: 2.0), # pos 3 + double('ExternalRate', target_rate_percent: 2.49995), # pos 4 + double('ExternalRate', target_rate_percent: 2.5), # pos 5 + double('ExternalRate', target_rate_percent: 2.8) # pos 6 + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(5) + allow(exchange_rate).to receive(:position_to).and_return(6) + end + + it 'использует минимальный GAP и корректирует результат' do + # diff = 2.5 - 2.49995 = 0.00005 < AUTO_COMISSION_GAP (0.0001) + # adaptive_gap = max(0.00005 / 2, MIN_GAP) = max(0.000025, 0.0001) = 0.0001 + # target = 2.5 - 0.0001 = 2.4999 + # 2.4999 < 2.49995 - перепрыгиваем! Корректируем до 2.49995 + expect(calculator.call).to eq(2.49995) + end + end + + context 'UC-8: наш обменник в рейтинге' do + # Наш обменник на позиции 3, мы должны его игнорировать + + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 1.0, exchanger_id: 101), # pos 1 + double('ExternalRate', target_rate_percent: 1.5, exchanger_id: 102), # pos 2 + double('ExternalRate', target_rate_percent: 2.0, exchanger_id: 999), # pos 3 - наш + double('ExternalRate', target_rate_percent: 2.3, exchanger_id: 103), # pos 4 + double('ExternalRate', target_rate_percent: 2.5, exchanger_id: 104), # pos 5 + double('ExternalRate', target_rate_percent: 2.8, exchanger_id: 105) # pos 6 + ] + end + + before do + Gera.our_exchanger_id = 999 + allow(exchange_rate).to receive(:position_from).and_return(4) + allow(exchange_rate).to receive(:position_to).and_return(5) + end + + it 'исключает наш обменник из расчёта' do + # После фильтрации: позиции пересчитываются без нашего обменника (id=999) + # Новые позиции: 1.0, 1.5, 2.3, 2.5, 2.8 + # position_from=4 -> 2.5 (index 3) + # position_to=5 -> 2.8 (index 4) + # target = 2.5 - GAP = 2.4999 + # rate_above (pos 3) = 2.3, 2.4999 > 2.3 - не перепрыгиваем + expect(calculator.call).to eq(2.5 - 0.0001) + end + end + + # UC-9 ОТМЕНЁН: Защита от аномалий по медиане не работает с отрицательными курсами + # Тест удалён, так как функциональность не реализована + + context 'when external_rates is empty' do + let(:external_rates) { [] } + + before do + allow(exchange_rate).to receive(:position_from).and_return(1) + allow(exchange_rate).to receive(:position_to).and_return(3) + end + + it 'returns autorate_from' do + expect(calculator.call).to eq(1.0) + end + end + + context 'when no rates match target comission range' do + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 5.0), + double('ExternalRate', target_rate_percent: 6.0), + double('ExternalRate', target_rate_percent: 7.0) + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(1) + allow(exchange_rate).to receive(:position_to).and_return(3) + end + + it 'returns autorate_from' do + expect(calculator.call).to eq(1.0) + end + end + + # UC-5: Диапазон позиций не совпадает с диапазоном курсов + # Реализован вариант A: возвращаем autorate_from (минимально допустимую комиссию) + # и занимаем позицию ниже целевого диапазона + describe 'UC-5: диапазон позиций не совпадает с диапазоном курсов (Вариант A)' do + context 'курсы конкурентов выше допустимого диапазона' do + # autorate_from..autorate_to = 1.0..3.0 + # Курсы на позициях 2-4: 4.0, 4.5, 5.0 (все выше 3.0) + # Ожидаемый результат: autorate_from = 1.0 + + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 3.5), # pos 1 + double('ExternalRate', target_rate_percent: 4.0), # pos 2 + double('ExternalRate', target_rate_percent: 4.5), # pos 3 + double('ExternalRate', target_rate_percent: 5.0), # pos 4 + double('ExternalRate', target_rate_percent: 5.5) # pos 5 + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(2) + allow(exchange_rate).to receive(:position_to).and_return(4) + end + + it 'возвращает autorate_from (вариант A)' do + expect(calculator.call).to eq(1.0) + end + end + + context 'курсы конкурентов ниже допустимого диапазона' do + # autorate_from..autorate_to = 1.0..3.0 + # Курсы на позициях 2-4: -0.5, -0.3, -0.1 (все ниже 1.0) + # Мы не можем им соответствовать, возвращаем autorate_from + + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: -0.8), # pos 1 + double('ExternalRate', target_rate_percent: -0.5), # pos 2 + double('ExternalRate', target_rate_percent: -0.3), # pos 3 + double('ExternalRate', target_rate_percent: -0.1), # pos 4 + double('ExternalRate', target_rate_percent: 0.2) # pos 5 + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(2) + allow(exchange_rate).to receive(:position_to).and_return(4) + end + + it 'возвращает autorate_from (вариант A)' do + expect(calculator.call).to eq(1.0) + end + end + + context 'нет курсов на целевых позициях (список короче)' do + # position_from..position_to = 5..10 + # Но в списке только 3 позиции + + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 2.0), # pos 1 + double('ExternalRate', target_rate_percent: 2.5), # pos 2 + double('ExternalRate', target_rate_percent: 3.0) # pos 3 + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(5) + allow(exchange_rate).to receive(:position_to).and_return(10) + end + + it 'возвращает autorate_from (вариант A)' do + expect(calculator.call).to eq(1.0) + end + end + + context 'частичное совпадение: только некоторые позиции вне диапазона' do + # autorate_from..autorate_to = 1.0..3.0 + # Позиция 1: 0.5 (лучший курс, вне диапазона - ниже 1.0) + # Позиция 2: 4.0 (вне диапазона - выше 3.0) + # Позиция 3: 2.5 (в диапазоне) + # Позиция 4: 2.8 (в диапазоне) + # Должен использовать курс с позиции 3 + + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 0.5), # pos 1 - лучший, но вне диапазона + double('ExternalRate', target_rate_percent: 4.0), # pos 2 - вне диапазона + double('ExternalRate', target_rate_percent: 2.5), # pos 3 - в диапазоне + double('ExternalRate', target_rate_percent: 2.8), # pos 4 - в диапазоне + double('ExternalRate', target_rate_percent: 5.0) # pos 5 + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(2) + allow(exchange_rate).to receive(:position_to).and_return(4) + end + + it 'использует первый подходящий курс в диапазоне' do + # valid_rates = [2.5, 2.8] + # target = 2.5 - GAP = 2.4999 + # rate_above (pos 1) = 0.5, 2.4999 > 0.5 - не перепрыгиваем + expect(calculator.call).to eq(2.5 - 0.0001) + end + end + end + + context 'UC-12: position_from=1 и одинаковые курсы' do + # При position_from=1 и одинаковых курсах не вычитаем GAP, + # позволяем BestChange определить позицию по резервам + + context 'все позиции имеют одинаковый курс' do + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 78.7752), # pos 1 + double('ExternalRate', target_rate_percent: 78.7752), # pos 2 + double('ExternalRate', target_rate_percent: 78.7752), # pos 3 + double('ExternalRate', target_rate_percent: 78.7752), # pos 4 + double('ExternalRate', target_rate_percent: 78.7852) # pos 5 + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(1) + allow(exchange_rate).to receive(:position_to).and_return(20) + allow(exchange_rate).to receive(:autorate_from).and_return(70.0) + allow(exchange_rate).to receive(:autorate_to).and_return(80.0) + end + + it 'не вычитает GAP и оставляет курс как есть' do + # Без UC-12: 78.7752 - 0.0001 = 78.7751 (перепрыгнули бы всех) + # С UC-12: 78.7752 (остаёмся на одном уровне) + expect(calculator.call).to eq(78.7752) + end + end + + context 'первая позиция имеет уникальный курс' do + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 78.7752), # pos 1 + double('ExternalRate', target_rate_percent: 78.7852), # pos 2 - другой курс + double('ExternalRate', target_rate_percent: 78.7952) # pos 3 + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(1) + allow(exchange_rate).to receive(:position_to).and_return(3) + allow(exchange_rate).to receive(:autorate_from).and_return(70.0) + allow(exchange_rate).to receive(:autorate_to).and_return(80.0) + end + + it 'вычитает GAP как обычно' do + # Курс позиции 1 отличается от позиции 2 - вычитаем GAP + expect(calculator.call).to eq(78.7752 - 0.0001) + end + end + + context 'position_from > 1 с одинаковыми курсами' do + # UC-11: Для position_from > 1 логика adjust_for_position_above уже работает + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 78.7752), # pos 1 + double('ExternalRate', target_rate_percent: 78.7752), # pos 2 + double('ExternalRate', target_rate_percent: 78.7752), # pos 3 + double('ExternalRate', target_rate_percent: 78.7752) # pos 4 + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(2) + allow(exchange_rate).to receive(:position_to).and_return(4) + allow(exchange_rate).to receive(:autorate_from).and_return(70.0) + allow(exchange_rate).to receive(:autorate_to).and_return(80.0) + end + + it 'корректируется через adjust_for_position_above' do + # UC-11: adjust_for_position_above возвращает safe_comission = 78.7752 + expect(calculator.call).to eq(78.7752) + end + end + end + + context 'медиана равна нулю (защита от деления на ноль)' do + # Если все курсы равны 0, медиана будет 0 + # Алгоритм должен обработать это без ошибки деления на ноль + + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 0.0), # pos 1 + double('ExternalRate', target_rate_percent: 0.0), # pos 2 + double('ExternalRate', target_rate_percent: 0.0), # pos 3 + double('ExternalRate', target_rate_percent: 0.0), # pos 4 + double('ExternalRate', target_rate_percent: 0.0) # pos 5 + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(3) + allow(exchange_rate).to receive(:position_to).and_return(5) + allow(exchange_rate).to receive(:autorate_from).and_return(-1.0) + allow(exchange_rate).to receive(:autorate_to).and_return(1.0) + end + + it 'не вызывает деление на ноль и возвращает корректный результат' do + # При median=0 возвращаем ближайшую позицию выше + # Это позиция 2 (index 1) с курсом 0.0 + # adjust_for_position_above скорректирует до 0.0 + expect { calculator.call }.not_to raise_error + expect(calculator.call).to eq(0.0) + end + end + + context 'UC-14: fallback на первую целевую позицию при отсутствии позиций выше' do + # Когда rate_above = nil (нет данных для позиции position_from - 1), + # используем курс первой целевой позиции если он в допустимом диапазоне + + context 'когда позиция выше не существует в списке' do + # position_from=5, но в списке только 4 позиции + # rate_above = rates[3] = nil + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 2.0), # pos 1 + double('ExternalRate', target_rate_percent: 2.2), # pos 2 + double('ExternalRate', target_rate_percent: 2.4), # pos 3 + double('ExternalRate', target_rate_percent: 2.6) # pos 4 + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(5) + allow(exchange_rate).to receive(:position_to).and_return(10) + allow(exchange_rate).to receive(:autorate_from).and_return(1.0) + allow(exchange_rate).to receive(:autorate_to).and_return(3.0) + end + + it 'возвращает autorate_from так как нет целевых позиций' do + # rates[4..9] = nil - нет данных для позиций 5-10 + expect(calculator.call).to eq(1.0) + end + end + + context 'когда целевая позиция существует но rate_above = nil' do + # position_from=3, position_to=5 + # rates имеет 5 элементов, rate_above = rates[1] существует + # Но если массив короче - rate_above будет nil + + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 2.0), # pos 1 + double('ExternalRate', target_rate_percent: 2.5), # pos 2 + double('ExternalRate', target_rate_percent: 2.8) # pos 3 + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(5) + allow(exchange_rate).to receive(:position_to).and_return(10) + allow(exchange_rate).to receive(:autorate_from).and_return(1.0) + allow(exchange_rate).to receive(:autorate_to).and_return(3.0) + end + + it 'возвращает autorate_from так как целевые позиции не существуют' do + expect(calculator.call).to eq(1.0) + end + end + + context 'когда rate_above существует но nil из-за разреженного списка' do + # Симулируем случай из issue: position_from=5, rates[3] = nil + # При этом позиции 5+ существуют + # Это edge case когда массив имеет nil элементы + + let(:external_rates) do + # 10 позиций, но позиция 4 (index 3) = nil + rates = [ + double('ExternalRate', target_rate_percent: 1.0), # pos 1 + double('ExternalRate', target_rate_percent: 1.5), # pos 2 + double('ExternalRate', target_rate_percent: 2.0), # pos 3 + nil, # pos 4 - отсутствует + double('ExternalRate', target_rate_percent: 2.5), # pos 5 + double('ExternalRate', target_rate_percent: 2.6), # pos 6 + double('ExternalRate', target_rate_percent: 2.7), # pos 7 + double('ExternalRate', target_rate_percent: 2.8), # pos 8 + double('ExternalRate', target_rate_percent: 2.9), # pos 9 + double('ExternalRate', target_rate_percent: 3.0) # pos 10 + ] + rates + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(5) + allow(exchange_rate).to receive(:position_to).and_return(10) + allow(exchange_rate).to receive(:autorate_from).and_return(1.0) + allow(exchange_rate).to receive(:autorate_to).and_return(3.0) + end + + it 'использует первую целевую позицию через UC-14 fallback' do + # rate_above = rates[3] = nil + # UC-14: first_target_rate = rates[4] = 2.5 + # 2.5 в диапазоне 1.0..3.0 + # target_comission = 2.5 - GAP = 2.4999 + # 2.4999 < 2.5 → корректируем до 2.5 + expect(calculator.call).to eq(2.5) + end + end + + context 'когда первая целевая позиция вне допустимого диапазона' do + let(:external_rates) do + rates = [ + double('ExternalRate', target_rate_percent: 1.0), # pos 1 + double('ExternalRate', target_rate_percent: 1.5), # pos 2 + double('ExternalRate', target_rate_percent: 2.0), # pos 3 + nil, # pos 4 - отсутствует + double('ExternalRate', target_rate_percent: 5.0), # pos 5 - вне диапазона + double('ExternalRate', target_rate_percent: 5.5) # pos 6 + ] + rates + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(5) + allow(exchange_rate).to receive(:position_to).and_return(6) + allow(exchange_rate).to receive(:autorate_from).and_return(1.0) + allow(exchange_rate).to receive(:autorate_to).and_return(3.0) + end + + it 'возвращает autorate_from так как нет подходящих курсов' do + # valid_rates пуст (5.0 и 5.5 > 3.0) + expect(calculator.call).to eq(1.0) + end + end + + context 'когда target_comission уже хуже первой целевой позиции' do + let(:external_rates) do + rates = [ + double('ExternalRate', target_rate_percent: 1.0), # pos 1 + double('ExternalRate', target_rate_percent: 1.5), # pos 2 + double('ExternalRate', target_rate_percent: 2.0), # pos 3 + nil, # pos 4 - отсутствует + double('ExternalRate', target_rate_percent: 2.5), # pos 5 + double('ExternalRate', target_rate_percent: 2.8) # pos 6 + ] + rates + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(5) + allow(exchange_rate).to receive(:position_to).and_return(6) + allow(exchange_rate).to receive(:autorate_from).and_return(1.0) + allow(exchange_rate).to receive(:autorate_to).and_return(3.0) + end + + it 'не корректирует если target_comission >= first_target' do + # target = 2.5, GAP = 0.0001 + # target_comission = 2.5 - 0.0001 = 2.4999 + # 2.4999 < 2.5 → UC-14 корректирует до 2.5 + expect(calculator.call).to eq(2.5) + end + end + end + + context 'округление комиссии до 4 знаков' do + # Проверяем, что результат округляется до COMMISSION_PRECISION (4) знаков + # Это исправляет проблему с float точностью: -2.8346999999999998 → -2.8347 + + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 1.0), # pos 1 + double('ExternalRate', target_rate_percent: 1.5), # pos 2 + double('ExternalRate', target_rate_percent: 2.83469999) # pos 3 - число с избыточной точностью + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(3) + allow(exchange_rate).to receive(:position_to).and_return(3) + allow(exchange_rate).to receive(:autorate_from).and_return(0.0) + allow(exchange_rate).to receive(:autorate_to).and_return(5.0) + end + + it 'округляет результат до 4 знаков после запятой' do + result = calculator.call + # Проверяем что результат имеет максимум 4 знака после запятой + decimal_places = result.to_s.split('.').last&.length || 0 + expect(decimal_places).to be <= 4 + end + + it 'корректно округляет вычисления с GAP' do + result = calculator.call + # 2.83469999 - 0.0001 = 2.83459999 → округляется до 2.8346 + expect(result).to eq(2.8346) + end + end + end + end + end +end diff --git a/spec/services/gera/autorate_calculators/standalone_spec.rb b/spec/services/gera/autorate_calculators/standalone_spec.rb new file mode 100644 index 00000000..5736b7f0 --- /dev/null +++ b/spec/services/gera/autorate_calculators/standalone_spec.rb @@ -0,0 +1,268 @@ +# frozen_string_literal: true + +# Standalone тесты для калькуляторов автокурса +# Не требуют полной загрузки Rails + +require 'virtus' + +# Загружаем только необходимые файлы +require_relative '../../../../app/services/gera/autorate_calculators/base' +require_relative '../../../../app/services/gera/autorate_calculators/legacy' +require_relative '../../../../app/services/gera/autorate_calculators/position_aware' + +RSpec.describe 'AutorateCalculators' do + let(:exchange_rate) { double('ExchangeRate') } + let(:target_autorate_setting) { double('TargetAutorateSetting') } + + before do + allow(exchange_rate).to receive(:target_autorate_setting).and_return(target_autorate_setting) + allow(exchange_rate).to receive(:autorate_from).and_return(1.0) + allow(exchange_rate).to receive(:autorate_to).and_return(3.0) + allow(target_autorate_setting).to receive(:could_be_calculated?).and_return(true) + end + + describe Gera::AutorateCalculators::Legacy do + let(:calculator) do + described_class.new( + exchange_rate: exchange_rate, + external_rates: external_rates + ) + end + + context 'с валидными rates' do + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 2.5), + double('ExternalRate', target_rate_percent: 2.8), + double('ExternalRate', target_rate_percent: 3.0) + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(1) + allow(exchange_rate).to receive(:position_to).and_return(3) + end + + it 'вычитает GAP из первого matching rate' do + expect(calculator.call).to eq(2.5 - 0.0001) + end + end + + context 'когда rates пустые' do + let(:external_rates) { [] } + + before do + allow(exchange_rate).to receive(:position_from).and_return(1) + allow(exchange_rate).to receive(:position_to).and_return(3) + end + + it 'возвращает autorate_from' do + expect(calculator.call).to eq(1.0) + end + end + end + + describe Gera::AutorateCalculators::PositionAware do + let(:calculator) do + described_class.new( + exchange_rate: exchange_rate, + external_rates: external_rates + ) + end + + context 'UC-1: все позиции имеют одинаковый курс' do + let(:external_rates) do + 10.times.map { double('ExternalRate', target_rate_percent: 2.5) } + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(5) + allow(exchange_rate).to receive(:position_to).and_return(10) + end + + it 'не перепрыгивает позицию выше, возвращает ту же комиссию' do + # Позиция 4 имеет 2.5, после GAP было бы 2.499 < 2.5 - перепрыгнем! + # PositionAware должен вернуть 2.5 + expect(calculator.call).to eq(2.5) + end + end + + context 'UC-2: есть разрыв между позициями' do + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 1.0), + double('ExternalRate', target_rate_percent: 1.2), + double('ExternalRate', target_rate_percent: 1.4), + double('ExternalRate', target_rate_percent: 1.6), + double('ExternalRate', target_rate_percent: 2.5), + double('ExternalRate', target_rate_percent: 2.6), + double('ExternalRate', target_rate_percent: 2.7), + double('ExternalRate', target_rate_percent: 2.8), + double('ExternalRate', target_rate_percent: 2.9), + double('ExternalRate', target_rate_percent: 3.0) + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(5) + allow(exchange_rate).to receive(:position_to).and_return(10) + end + + it 'безопасно вычитает GAP когда есть разрыв' do + # 2.5 - 0.0001 = 2.4999 > 1.6 - не перепрыгиваем + expect(calculator.call).to eq(2.5 - 0.0001) + end + end + + context 'UC-3: целевая позиция 1' do + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 2.5), + double('ExternalRate', target_rate_percent: 2.8), + double('ExternalRate', target_rate_percent: 3.0) + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(1) + allow(exchange_rate).to receive(:position_to).and_return(3) + end + + it 'вычитает GAP когда нет позиции выше' do + expect(calculator.call).to eq(2.5 - 0.0001) + end + end + + context 'UC-4: позиция выше с очень близкой комиссией' do + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 1.0), + double('ExternalRate', target_rate_percent: 1.5), + double('ExternalRate', target_rate_percent: 2.0), + double('ExternalRate', target_rate_percent: 2.4999), + double('ExternalRate', target_rate_percent: 2.5), + double('ExternalRate', target_rate_percent: 2.8) + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(5) + allow(exchange_rate).to receive(:position_to).and_return(6) + end + + it 'не перепрыгивает позицию 4' do + # 2.5 - 0.0001 = 2.4999 = 2.4999, курсы равны + # Возвращаем 2.4999 (равный позиции выше) + expect(calculator.call).to eq(2.4999) + end + end + + # UC-5: Диапазон позиций не совпадает с диапазоном курсов + # Реализован вариант A: возвращаем autorate_from (минимально допустимую комиссию) + # и занимаем позицию ниже целевого диапазона + describe 'UC-5: диапазон позиций не совпадает с диапазоном курсов (Вариант A)' do + context 'курсы конкурентов выше допустимого диапазона' do + # autorate_from..autorate_to = 1.0..3.0 + # Курсы на позициях 2-4: 4.0, 4.5, 5.0 (все выше 3.0) + # Ожидаемый результат: autorate_from = 1.0 + + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 3.5), # pos 1 + double('ExternalRate', target_rate_percent: 4.0), # pos 2 + double('ExternalRate', target_rate_percent: 4.5), # pos 3 + double('ExternalRate', target_rate_percent: 5.0), # pos 4 + double('ExternalRate', target_rate_percent: 5.5) # pos 5 + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(2) + allow(exchange_rate).to receive(:position_to).and_return(4) + end + + it 'возвращает autorate_from (вариант A)' do + expect(calculator.call).to eq(1.0) + end + end + + context 'курсы конкурентов ниже допустимого диапазона' do + # autorate_from..autorate_to = 1.0..3.0 + # Курсы на позициях 2-4: -0.5, -0.3, -0.1 (все ниже 1.0) + # Мы не можем им соответствовать, возвращаем autorate_from + + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: -0.8), # pos 1 + double('ExternalRate', target_rate_percent: -0.5), # pos 2 + double('ExternalRate', target_rate_percent: -0.3), # pos 3 + double('ExternalRate', target_rate_percent: -0.1), # pos 4 + double('ExternalRate', target_rate_percent: 0.2) # pos 5 + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(2) + allow(exchange_rate).to receive(:position_to).and_return(4) + end + + it 'возвращает autorate_from (вариант A)' do + expect(calculator.call).to eq(1.0) + end + end + + context 'нет курсов на целевых позициях (список короче)' do + # position_from..position_to = 5..10 + # Но в списке только 3 позиции + + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 2.0), # pos 1 + double('ExternalRate', target_rate_percent: 2.5), # pos 2 + double('ExternalRate', target_rate_percent: 3.0) # pos 3 + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(5) + allow(exchange_rate).to receive(:position_to).and_return(10) + end + + it 'возвращает autorate_from (вариант A)' do + expect(calculator.call).to eq(1.0) + end + end + + context 'частичное совпадение: только некоторые позиции вне диапазона' do + # autorate_from..autorate_to = 1.0..3.0 + # Позиция 1: 0.5 (лучший курс, вне диапазона - ниже 1.0) + # Позиция 2: 4.0 (вне диапазона - выше 3.0) + # Позиция 3: 2.5 (в диапазоне) + # Позиция 4: 2.8 (в диапазоне) + # Должен использовать курс с позиции 3 + + let(:external_rates) do + [ + double('ExternalRate', target_rate_percent: 0.5), # pos 1 - лучший, но вне диапазона + double('ExternalRate', target_rate_percent: 4.0), # pos 2 - вне диапазона + double('ExternalRate', target_rate_percent: 2.5), # pos 3 - в диапазоне + double('ExternalRate', target_rate_percent: 2.8), # pos 4 - в диапазоне + double('ExternalRate', target_rate_percent: 5.0) # pos 5 + ] + end + + before do + allow(exchange_rate).to receive(:position_from).and_return(2) + allow(exchange_rate).to receive(:position_to).and_return(4) + end + + it 'использует первый подходящий курс в диапазоне' do + # valid_rates = [2.5, 2.8] + # target = 2.5 - GAP = 2.4999 + # rate_above (pos 1) = 0.5, 2.4999 > 0.5 - не перепрыгиваем + expect(calculator.call).to eq(2.5 - 0.0001) + end + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 542b4eec..06d3f4af 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -2,6 +2,9 @@ ENV['RAILS_ENV'] ||= 'test' +# Stub for missing PaymentServices::Base::Client (issue #78) +require_relative 'support/payment_services_stub' + require File.expand_path('dummy/config/environment.rb', __dir__) require 'rspec/rails' require 'factory_bot' diff --git a/spec/support/payment_services_stub.rb b/spec/support/payment_services_stub.rb new file mode 100644 index 00000000..618bed4f --- /dev/null +++ b/spec/support/payment_services_stub.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# Stub for PaymentServices::Base::Client which is missing in this project +# See: https://github.com/alfagen/gera/issues/78 +module PaymentServices + module Base + class Client + end + end +end diff --git a/spec/vcr_cassettes/binance_with_two_external_rates.yml b/spec/vcr_cassettes/binance_with_two_external_rates.yml new file mode 100644 index 00000000..341d2f32 --- /dev/null +++ b/spec/vcr_cassettes/binance_with_two_external_rates.yml @@ -0,0 +1,75 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.binance.com/api/v3/ticker/bookTicker + body: + encoding: US-ASCII + string: '' + headers: + Accept: + - "*/*" + User-Agent: + - rest-client/2.1.0 (darwin19.6.0 x86_64) ruby/2.7.2p137 + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Host: + - api.binance.com + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - application/json;charset=UTF-8 + Content-Length: + - '34021' + Connection: + - keep-alive + Date: + - Wed, 17 Aug 2022 12:34:34 GMT + Server: + - nginx + X-Mbx-Uuid: + - 70fbf9c1-a620-476f-94bd-2c826741bf42 + X-Mbx-Used-Weight: + - '2' + X-Mbx-Used-Weight-1m: + - '2' + Strict-Transport-Security: + - max-age=31536000; includeSubdomains + X-Frame-Options: + - SAMEORIGIN + X-Xss-Protection: + - 1; mode=block + X-Content-Type-Options: + - nosniff + Content-Security-Policy: + - default-src 'self' + X-Content-Security-Policy: + - default-src 'self' + X-Webkit-Csp: + - default-src 'self' + Cache-Control: + - no-cache, no-store, must-revalidate + Pragma: + - no-cache + Expires: + - '0' + Access-Control-Allow-Origin: + - "*" + Access-Control-Allow-Methods: + - GET, HEAD, OPTIONS + X-Cache: + - Miss from cloudfront + Via: + - 1.1 ade2b5e2170ccd4f394b741b27bb0eec.cloudfront.net (CloudFront) + X-Amz-Cf-Pop: + - FRA56-P4 + X-Amz-Cf-Id: + - 9LIfhkm3n34aqOfa73PX9RTGI-HJii_EFAxpge_cJEm46JCL0_5VPQ== + body: + encoding: ASCII-8BIT + string: '[{"symbol":"ETHBTC","bidPrice":"0.07893200","bidQty":"18.88840000","askPrice":"0.07893300","askQty":"11.29040000"}]' + recorded_at: Wed, 17 Aug 2022 12:34:34 GMT +recorded_with: VCR 6.1.0 diff --git a/spec/workers/gera/binance_rates_worker_spec.rb b/spec/workers/gera/binance_rates_worker_spec.rb new file mode 100644 index 00000000..bd67167f --- /dev/null +++ b/spec/workers/gera/binance_rates_worker_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'spec_helper' + +module Gera + RSpec.describe BinanceRatesWorker do + let!(:rate_source) { create(:rate_source_binance) } + + it 'should approve new snapshot if it has the same count of external rates' do + actual_snapshot = create(:external_rate_snapshot, rate_source: rate_source) + actual_snapshot.external_rates << create(:external_rate, source: rate_source, snapshot: actual_snapshot) + actual_snapshot.external_rates << create(:inverse_external_rate, source: rate_source, snapshot: actual_snapshot) + rate_source.update_column(:actual_snapshot_id, actual_snapshot.id) + + expect(rate_source.actual_snapshot_id).to eq(actual_snapshot.id) + VCR.use_cassette :binance_with_two_external_rates do + expect(BinanceRatesWorker.new.perform).to be_truthy + end + expect(rate_source.reload.actual_snapshot_id).not_to eq(actual_snapshot.id) + end + + it 'should not approve new snapshot if it has different count of external rates' do + actual_snapshot = create(:external_rate_snapshot, rate_source: rate_source) + actual_snapshot.external_rates << create(:external_rate, source: rate_source, snapshot: actual_snapshot) + rate_source.update_column(:actual_snapshot_id, actual_snapshot.id) + + expect(rate_source.actual_snapshot_id).to eq(actual_snapshot.id) + VCR.use_cassette :binance_with_two_external_rates do + expect(BinanceRatesWorker.new.perform).to be_truthy + end + expect(rate_source.reload.actual_snapshot_id).to eq(actual_snapshot.id) + end + end +end diff --git a/spec/workers/gera/cbr_rates_worker_spec.rb b/spec/workers/gera/cbr_rates_worker_spec.rb index cb1e1ae9..15473d00 100644 --- a/spec/workers/gera/cbr_rates_worker_spec.rb +++ b/spec/workers/gera/cbr_rates_worker_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' module Gera - RSpec.describe CBRRatesWorker do + RSpec.describe CbrRatesWorker do before do create :rate_source_exmo create :rate_source_cbr_avg @@ -17,7 +17,7 @@ module Gera allow(Date).to receive(:today).and_return today Timecop.freeze(today) do VCR.use_cassette :cbrf do - expect(CBRRatesWorker.new.perform).to be_truthy + expect(CbrRatesWorker.new.perform).to be_truthy end end diff --git a/spec/workers/gera/exmo_rates_worker_spec.rb b/spec/workers/gera/exmo_rates_worker_spec.rb deleted file mode 100644 index dba9521d..00000000 --- a/spec/workers/gera/exmo_rates_worker_spec.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -module Gera - RSpec.describe EXMORatesWorker do - before do - create :rate_source_exmo - create :rate_source_cbr_avg - create :rate_source_cbr - create :rate_source_manual - end - - it do - expect(CurrencyRate.count).to be_zero - VCR.use_cassette :exmo do - expect(EXMORatesWorker.new.perform).to be_truthy - end - expect(CurrencyRate.count).to eq 134 - end - end -end