Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update 1.1.0 #16

Merged
merged 14 commits into from
Mar 29, 2019
44 changes: 44 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
version: 2
jobs:
build:
docker:
- image: circleci/ruby:2.4.2
environment:
BUNDLE_JOBS: 3
BUNDLE_RETRY: 3
BUNDLE_PATH: vendor/bundle
steps:
- checkout

# Which version of bundler?
- run:
name: Which bundler?
command: bundle -v

# Restore bundle cache
- restore_cache:
keys:
- russian_central_bank-{{ checksum "russian_central_bank.gemspec" }}
- russian_central_bank

- run:
name: Bundle Install
command: bundle check || bundle install

# Store bundle cache
- save_cache:
key: russian_central_bank-{{ checksum "russian_central_bank.gemspec" }}
paths:
- vendor/bundle

# Run rspec
- run:
name: Run rspec
command: |
gem install rspec && \
bundle exec rspec --format progress \
$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)

# Save test results for timing analysis
- store_test_results:
path: test_results
5 changes: 0 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,8 @@
.config
.yardoc
Gemfile.lock
InstalledFiles
_yardoc
coverage
doc/
lib/bundler/man
pkg
rdoc
spec/reports
test/tmp
test/version_tmp
Expand Down
8 changes: 8 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Metrics/BlockLength:
Enabled: true
Exclude:
- spec/**/*

Metrics/LineLength:
Enabled: true
Max: 120
28 changes: 16 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,42 +19,46 @@ Or install it yourself as:

$ gem install russian_central_bank

NOTE: use 0.x version of `russian_central_bank` for > 6.0 `money` versions
NOTE: use 0.x version of `russian_central_bank` for `money` versions < 6.0

##Dependencies
## Dependencies

* [savon](http://savonrb.com/)
* [httparty](https://github.com/jnunemaker/httparty)
* [money](https://github.com/RubyMoney/money)

## Usage

Regular usage
### Regular usage

require 'russian_central_bank'

Money.locale_backend = :currency
bank = Money::Bank::RussianCentralBank.new

Money.default_bank = bank

# Load today's rates
bank.update_rates

# Exchange 100 USD to RUB
Money.new(1000, "USD").exchange_to('RUB')
# Exchange 1000 USD to RUB
Money.new(1000_00, "USD").exchange_to('RUB').format # => 64.592,50 ₽

Specific date rates
# Use indirect exchange rates, USD -> RUB -> EUR
Money.new(1000_00, "USD").exchange_to('EUR').format # => €888,26

### Specific date rates

# Specify rates date
bank.update_rates(Date.today - 3000)
Money.new(1000, "USD").exchange_to('RUB') # makes you feel better
bank.update_rates(Date.new(2010, 12, 31))
Money.new(1000_00, "USD").exchange_to('RUB').format # => 30.476,90 ₽

# Check last rates update
bank.rates_updated_at

# Check on which date rates were updated
bank.rates_updated_on

Autoupdate
### Autoupdate

# Use ttl attribute to enable rates autoupdate
bank.ttl = 1.day
Expand All @@ -64,14 +68,14 @@ Autoupdate

### Safe rates fetch

There are some cases, when the `cbr.ru` returns HTTP 302.
There are some cases, when the `cbr.ru` doesn't return HTTP 200.
To avoid issues in production, you use fallback:

```ruby
bank = Money::Bank::RussianCentralBank.new
begin
bank.update_rates
rescue Money::Bank::RussianCentralBank::FetchError => e
rescue Money::Bank::RussianCentralBankFetcher::FetchError => e
Rails.logger.info "CBR failed: #{e.response}"

## fallback
Expand Down
85 changes: 85 additions & 0 deletions lib/money/bank/russian_central_bank.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
require 'money'

class Money
module Bank
class RussianCentralBank < Money::Bank::VariableExchange
attr_reader :rates_updated_at, :rates_updated_on, :rates_expired_at, :ttl

def flush_rates
@store = Money::RatesStore::Memory.new
end

def update_rates(date = Date.today)
store.transaction do
update_parsed_rates(exchange_rates(date))
@rates_updated_at = Time.now
@rates_updated_on = date
update_expired_at
store.send(:index)
end
end

def add_rate(from, to, rate)
super(from, to, rate)
super(to, from, 1.0 / rate)
end

def get_rate(from, to)
update_rates if rates_expired?
super || indirect_rate(from, to)
end

def ttl=(value)
@ttl = value
update_expired_at
@ttl
end

def rates_expired?
rates_expired_at && rates_expired_at <= Time.now
end

private

def fetcher
@fetcher ||= RussianCentralBankFetcher.new
end

def exchange_rates(date)
fetcher.perform(date)
end

def update_expired_at
@rates_expired_at = if ttl
@rates_updated_at ? @rates_updated_at + ttl : Time.now
else
nil
end
end

def indirect_rate(from, to)
get_rate('RUB', to) / get_rate('RUB', from)
end

def local_currencies
@local_currencies ||= Money::Currency.table.map { |currency| currency.last[:iso_code] }
end

def update_parsed_rates(rates)
add_rate('RUB', 'RUB', 1)
rates.each do |rate|
begin
if local_currencies.include?(rate[:code])
add_rate(
'RUB',
rate[:code],
1 / (rate[:value] / rate[:nominal])
)
end
rescue Money::Currency::UnknownCurrency
end
end
end
end
end
end
49 changes: 49 additions & 0 deletions lib/money/bank/russian_central_bank_fetcher.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
require 'httparty'

class Money
module Bank
class RussianCentralBankFetcher
DAILY_RATES_URL = 'http://www.cbr.ru/scripts/XML_daily.asp'.freeze

class FetchError < StandardError
attr_reader :response

def initialize(message, response = nil)
super(message)
@response = response
end
end

def perform(date = Date.today)
response = HTTParty.get(rates_url(date))
unless response.success?
raise_fetch_error("cbr.ru respond with #{response.code}", response)
end
extract_rates(response.parsed_response)
rescue HTTParty::Error => e
raise_fetch_error(e.message)
end

private

def raise_fetch_error(message, response = nil)
raise FetchError.new(message, response)
end

def rates_url(date)
"#{DAILY_RATES_URL}?date_req=#{date.strftime('%d/%m/%Y')}"
end

def extract_rates(parsed_response)
rates_arr = parsed_response['ValCurs']['Valute']
rates_arr.map do |rate|
{
code: rate['CharCode'],
nominal: rate['Nominal'].to_i,
value: rate['Value'].tr(',', '.').to_f
}
end
end
end
end
end
95 changes: 2 additions & 93 deletions lib/russian_central_bank.rb
Original file line number Diff line number Diff line change
@@ -1,93 +1,2 @@
require 'money'
require 'savon'

class Money
module Bank
class RussianCentralBank < Money::Bank::VariableExchange
class FetchError < StandardError
attr_reader :response

def initialize(message, response=nil)
super(message)
@response = response
end
end

CBR_SERVICE_URL = 'http://www.cbr.ru/DailyInfoWebServ/DailyInfo.asmx?WSDL'

attr_reader :rates_updated_at, :rates_updated_on, :ttl, :rates_expired_at

def flush_rates
@store = Money::RatesStore::Memory.new
end

def update_rates(date = Date.today)
store.transaction do
update_parsed_rates(exchange_rates(date))
@rates_updated_at = Time.now
@rates_updated_on = date
update_expired_at
store.send(:index)
end
end

def add_rate(from, to, rate)
super(from, to, rate)
super(to, from, 1.0 / rate)
end

def get_rate(from, to)
update_rates if rates_expired?
super || indirect_rate(from, to)
end

def ttl=(value)
@ttl = value
update_expired_at
@ttl
end

def rates_expired?
rates_expired_at && rates_expired_at <= Time.now
end

private

def update_expired_at
@rates_expired_at = if ttl
@rates_updated_at ? @rates_updated_at + ttl : Time.now
else
nil
end
end

def indirect_rate(from, to)
from_base_rate = get_rate('RUB', from)
to_base_rate = get_rate('RUB', to)
to_base_rate / from_base_rate
end

def exchange_rates(date = Date.today)
client = Savon::Client.new wsdl: CBR_SERVICE_URL, log: false, log_level: :error,
follow_redirects: true
response = client.call(:get_curs_on_date, message: { 'On_date' => date.strftime('%Y-%m-%dT%H:%M:%S') })
response.body[:get_curs_on_date_response][:get_curs_on_date_result][:diffgram][:valute_data][:valute_curs_on_date]
rescue Wasabi::Resolver::HTTPError => e
raise FetchError.new(e.message, e.response)
end

def update_parsed_rates(rates)
local_currencies = Money::Currency.table.map { |currency| currency.last[:iso_code] }
add_rate('RUB', 'RUB', 1)
rates.each do |rate|
begin
if local_currencies.include? rate[:vch_code]
add_rate('RUB', rate[:vch_code], 1/ (rate[:vcurs].to_f / rate[:vnom].to_i))
end
rescue Money::Currency::UnknownCurrency
end
end
end
end
end
end
require 'money/bank/russian_central_bank_fetcher'
require 'money/bank/russian_central_bank'
11 changes: 7 additions & 4 deletions russian_central_bank.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)

Gem::Specification.new do |spec|
spec.name = 'russian_central_bank'
spec.version = '1.0.1'
spec.version = '1.1.0'
spec.authors = ['Ramil Mustafin']
spec.email = ['[email protected]']
spec.description = 'RussianCentralBank extends Money::Bank::VariableExchange and gives you access to the Central Bank of Russia currency exchange rates.'
Expand All @@ -17,10 +17,13 @@ Gem::Specification.new do |spec|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
spec.require_paths = ['lib']

spec.add_development_dependency 'bundler', '~>1.3'
spec.add_development_dependency 'bundler'
spec.add_development_dependency 'pry'
spec.add_development_dependency 'rake'
spec.add_development_dependency 'rspec', '~>3'
spec.add_development_dependency 'rubocop'
spec.add_development_dependency 'webmock', '~>3'

spec.add_dependency 'money', '~> 6.7.0'
spec.add_dependency 'savon', '~>2.0'
spec.add_dependency 'httparty', '~>0'
spec.add_dependency 'money', '~> 6'
end
Loading