historical-bank 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +7 -2
- data/historical-bank.gemspec +1 -1
- data/lib/money/bank/historical.rb +9 -2
- data/lib/money/rates_provider/open_exchange_rates.rb +67 -22
- data/spec/bank/historical_spec.rb +2 -2
- data/spec/rates_provider/open_exchange_rates_spec.rb +55 -9
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b15169c2eadfa1f282eae84a1e7a3f3d39fbc594
|
4
|
+
data.tar.gz: 4ede618d723202121b19e1628ca4d7ecaece734e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8e4a858d6f4180f9284c2d625951503e91c18263c4c09ded26cf68ea49107e9ab4eb06f622182f918c552509b4c917c25e8e14f564eb5396c1dc27d21148849c
|
7
|
+
data.tar.gz: bfdb8af5f044e55620ee125233325eda35218ee9ffadb63771be3a1198ec5ff5936551fe94d40ca42e63595dda16292524eebf4dbee4942e8e6074ad91c02f0b
|
data/CHANGELOG.md
CHANGED
@@ -2,3 +2,7 @@
|
|
2
2
|
|
3
3
|
## 0.1.0
|
4
4
|
- Added basic functionality and documentation.
|
5
|
+
|
6
|
+
## 0.1.1
|
7
|
+
- Added support for FREE and DEVELOPER OpenExchangeRates accounts. Downside when using these accounts is that OER rates are fetched for a single day at a time (as opposed to a whole month for more advanced plans).
|
8
|
+
- Developers have to specify `oer_account_type` during configuration.
|
data/README.md
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
# historical-bank-ruby
|
2
2
|
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/historical-bank.svg)](https://badge.fury.io/rb/historical-bank)
|
3
4
|
[![Build Status](https://travis-ci.org/Skyscanner/historical-bank-ruby.svg?branch=master)](https://travis-ci.org/Skyscanner/historical-bank-ruby)
|
4
5
|
|
5
6
|
## Description
|
6
7
|
|
7
8
|
This gem provides a bank that can serve historical rates,
|
8
9
|
contrary to most bank implementations that provide only current market rates.
|
9
|
-
[Open Exchange Rates](https://openexchangerates.org/) (OER) is used as the provider of the rates
|
10
|
+
[Open Exchange Rates](https://openexchangerates.org/) (OER) is used as the provider of the rates.
|
10
11
|
As the HTTP requests to OER can add latency to your calls, a `RatesStore` (cache) based on Redis was added, making it super-fast.
|
11
12
|
|
12
13
|
You can use it as your default bank and keep calling the standard `money` gem methods (`Money#exchange_to`, `Bank#exchange_with`). On top of that, we've added a few more methods that allow accessing historical rates (`Money#exchange_to_historical`, `Bank#exchange_with_historical`).
|
@@ -84,7 +85,7 @@ Don't worry, the bank is thread-safe!
|
|
84
85
|
|
85
86
|
## Requirements
|
86
87
|
|
87
|
-
- **OpenExchangeRates
|
88
|
+
- **OpenExchangeRates account** - If on the Free or Developer plan, the `/historical/*.json` endpoint is used for fetching rates for single days. If on the Enterprise or Unlimited plan, the `/time-series.json` endpoint is used for fetching rates for whole months, increasing efficiency.
|
88
89
|
- **Redis >= 2.8** (versions of Redis earlier than 2.8 may also work fine, but this gem has only been tested with 2.8 and above.)
|
89
90
|
- **Ruby >= 2.0**
|
90
91
|
|
@@ -115,6 +116,10 @@ Money::Bank::Historical.configure do |config|
|
|
115
116
|
# (required) your OpenExchangeRates App ID
|
116
117
|
config.oer_app_id = 'XXXXXXXXXXXXXXX'
|
117
118
|
|
119
|
+
# (optional) account type on openexchangerate.org (default: 'Enterprise')
|
120
|
+
# replace 'FREE' with 'DEVELOPER', 'ENTERPRISE', or 'UNLIMITED', according to your account type.
|
121
|
+
config.oer_account_type = Money::RatesProvider::OpenExchangeRates::AccountType::FREE
|
122
|
+
|
118
123
|
# (optional) currency relative to which all the rates are stored (default: EUR)
|
119
124
|
config.base_currency = Money::Currency.new('USD')
|
120
125
|
|
data/historical-bank.gemspec
CHANGED
@@ -18,7 +18,7 @@
|
|
18
18
|
|
19
19
|
Gem::Specification.new do |s|
|
20
20
|
s.name = 'historical-bank'
|
21
|
-
s.version = '0.1.
|
21
|
+
s.version = '0.1.1'
|
22
22
|
s.summary = 'Historical Bank'
|
23
23
|
s.description = 'A `Money::Bank::Base` with historical exchange rates'
|
24
24
|
s.authors = ['Kostis Dadamis', 'Emili Parreno']
|
@@ -27,6 +27,7 @@ class Money
|
|
27
27
|
class Historical < Bank::Base
|
28
28
|
# Configuration class for +Money::Bank::Historical+
|
29
29
|
class Configuration
|
30
|
+
|
30
31
|
# +Money::Currency+ relative to which all exchange rates will be cached
|
31
32
|
attr_accessor :base_currency
|
32
33
|
# URL of the Redis server
|
@@ -37,6 +38,8 @@ class Money
|
|
37
38
|
attr_accessor :oer_app_id
|
38
39
|
# timeout to set in the OpenExchangeRates requests
|
39
40
|
attr_accessor :timeout
|
41
|
+
# type of account on OpenExchangeRates, to know which API endpoints are useable
|
42
|
+
attr_accessor :oer_account_type
|
40
43
|
|
41
44
|
def initialize
|
42
45
|
@base_currency = Currency.new('EUR')
|
@@ -44,6 +47,7 @@ class Money
|
|
44
47
|
@redis_namespace = 'currency'
|
45
48
|
@oer_app_id = nil
|
46
49
|
@timeout = 15
|
50
|
+
@oer_account_type = RatesProvider::OpenExchangeRates::AccountType::ENTERPRISE
|
47
51
|
end
|
48
52
|
end
|
49
53
|
|
@@ -54,6 +58,7 @@ class Money
|
|
54
58
|
|
55
59
|
# Configures the bank. Parameters that can be configured:
|
56
60
|
# - +oer_app_id+ - (required) your OpenExchangeRates App ID
|
61
|
+
# - +oer_account_type+ - (optional) your OpenExchangeRates account type. Choose one of the values in the +Money::RatesProvider::OpenExchangeRates::AccountType+ module (default: +Money::RatesProvider::OpenExchangeRates::AccountType::ENTERPRISE+)
|
57
62
|
# - +base_currency+ - (optional) +Money::Currency+ relative to which all the rates are stored (default: EUR)
|
58
63
|
# - +redis_url+ - (optional) the URL of the Redis server (default: +redis://localhost:6379+)
|
59
64
|
# - +redis_namespace+ - (optional) Redis namespace to prefix all keys (default: +currency+)
|
@@ -63,6 +68,7 @@ class Money
|
|
63
68
|
#
|
64
69
|
# Money::Bank::Historical.configure do |config|
|
65
70
|
# config.oer_app_id = 'XXXXXXXXXXXXXXXXXXXXX'
|
71
|
+
# config.oer_account_type = Money::RatesProvider::OpenExchangeRates::AccountType::FREE
|
66
72
|
# config.base_currency = Money::Currency.new('USD')
|
67
73
|
# config.redis_url = 'redis://localhost:6379'
|
68
74
|
# config.redis_namespace = 'currency'
|
@@ -84,7 +90,8 @@ class Money
|
|
84
90
|
Historical.configuration.redis_namespace)
|
85
91
|
@provider = RatesProvider::OpenExchangeRates.new(Historical.configuration.oer_app_id,
|
86
92
|
@base_currency,
|
87
|
-
Historical.configuration.timeout
|
93
|
+
Historical.configuration.timeout,
|
94
|
+
Historical.configuration.oer_account_type)
|
88
95
|
# for controlling access to @rates
|
89
96
|
@mutex = Mutex.new
|
90
97
|
end
|
@@ -284,7 +291,7 @@ class Money
|
|
284
291
|
end
|
285
292
|
|
286
293
|
def fetch_provider_base_rate(currency, date)
|
287
|
-
currency_date_rate_hash = @provider.
|
294
|
+
currency_date_rate_hash = @provider.fetch_rates(date)
|
288
295
|
|
289
296
|
date_rate_hash = currency_date_rate_hash[currency.iso_code]
|
290
297
|
rate = date_rate_hash && date_rate_hash[date.iso8601]
|
@@ -36,26 +36,39 @@ class Money
|
|
36
36
|
# (https://docs.openexchangerates.org/docs/historical-json)
|
37
37
|
MIN_DATE = Date.new(1999, 1, 1).freeze
|
38
38
|
|
39
|
+
module AccountType
|
40
|
+
FREE = 'Free'.freeze
|
41
|
+
DEVELOPER = 'Developer'.freeze
|
42
|
+
ENTERPRISE = 'Enterprise'.freeze
|
43
|
+
UNLIMITED = 'Unlimited'.freeze
|
44
|
+
end
|
45
|
+
|
39
46
|
# ==== Parameters
|
40
|
-
# - +oer_app_id+ - App ID for the OpenExchangeRates API access
|
47
|
+
# - +oer_app_id+ - App ID for the OpenExchangeRates API access
|
41
48
|
# - +base_currency+ - The base currency that will be used for the OER requests. It should be a +Money::Currency+ object.
|
42
49
|
# - +timeout+ - The timeout in seconds to set on the requests
|
43
|
-
|
50
|
+
# - +oer_account_type+ - The OpenExchangeRates account type. Should be one of +AccountType::FREE+, +DEVELOPER+, +ENTERPRISE+, or +UNLIMITED+
|
51
|
+
def initialize(oer_app_id, base_currency, timeout, oer_account_type)
|
44
52
|
@oer_app_id = oer_app_id
|
45
53
|
@base_currency_code = base_currency.iso_code
|
46
54
|
@timeout = timeout
|
55
|
+
@fetch_rates_method_name = if oer_account_type == AccountType::FREE || oer_account_type == AccountType::DEVELOPER
|
56
|
+
:fetch_historical_rates
|
57
|
+
else
|
58
|
+
:fetch_time_series_rates
|
59
|
+
end
|
47
60
|
end
|
48
61
|
|
49
|
-
# Fetches the rates for all available quote currencies for a whole month.
|
62
|
+
# Fetches the rates for all available quote currencies (for given date or for a whole month, depending on openexchangerates.org account type).
|
50
63
|
# Fetching for all currencies or just one has the same API charge.
|
51
|
-
# In addition, the API doesn't allow fetching more than a month's data.
|
52
64
|
#
|
53
65
|
# It returns a +Hash+ with the rates for each quote currency and date
|
54
66
|
# as shown in the example. Rates are +BigDecimal+.
|
55
67
|
#
|
56
68
|
# ==== Parameters
|
57
69
|
#
|
58
|
-
# - +date+ - +date+
|
70
|
+
# - +date+ - +date+ for which the rates are requested. Minimum +date+ is January 1st 1999, as defined by the OER API (https://docs.openexchangerates.org/docs/api-introduction). Maximum +date+ is yesterday (UTC), as today's rates are not final (https://openexchangerates.org/faq/#timezone).
|
71
|
+
# If Enterprise or Unlimited account in openexchangerates.org, the +date+'s month is the month for which we request rates
|
59
72
|
#
|
60
73
|
# ==== Errors
|
61
74
|
#
|
@@ -64,26 +77,19 @@ class Money
|
|
64
77
|
#
|
65
78
|
# ==== Examples
|
66
79
|
#
|
67
|
-
# oer.
|
80
|
+
# oer.fetch_rates(Date.new(2016, 10, 5))
|
81
|
+
# If Free or Developer account in openexchangerates.org, it will return only for the given date
|
82
|
+
# # => {"AED"=>{"2016-10-05"=>#<BigDecimal:7fa19a188e98,'0.3672682E1',18(36)>}, {"AFN"=>{"2016-10-05"=>#<BigDecimal:7fa19a188e98,'0.3672682E1',18(36)>}, ...
|
83
|
+
# If Enterprise or Unlimited account, it will return for the entire month for the given date
|
68
84
|
# # => {"AED"=>{"2016-10-01"=>#<BigDecimal:7fa19a188e98,'0.3672682E1',18(36)>, "2016-10-02"=>#<BigDecimal:7fa19b11a5c8,'0.367296E1',18(36)>, ...
|
69
|
-
def
|
85
|
+
def fetch_rates(date)
|
70
86
|
if date < MIN_DATE || date > max_date
|
71
87
|
raise ArgumentError, "Provided date #{date} for OER query should be "\
|
72
88
|
"between #{MIN_DATE} and #{max_date}"
|
73
89
|
end
|
74
90
|
|
75
|
-
|
76
|
-
|
77
|
-
start_date = Date.civil(date.year, date.month, 1)
|
78
|
-
end_date = [end_of_month, max_date].min
|
79
|
-
|
80
|
-
options = request_options(start_date, end_date)
|
81
|
-
response = self.class.get('/time-series.json', options)
|
91
|
+
response = send(@fetch_rates_method_name, date)
|
82
92
|
|
83
|
-
unless response.success?
|
84
|
-
raise RequestFailed, "Month rates request failed for #{date} - "\
|
85
|
-
"Code: #{response.code} - Body: #{response.body}"
|
86
|
-
end
|
87
93
|
|
88
94
|
result = Hash.new { |hash, key| hash[key] = {} }
|
89
95
|
|
@@ -99,14 +105,53 @@ class Money
|
|
99
105
|
result
|
100
106
|
end
|
101
107
|
|
108
|
+
|
102
109
|
private
|
103
110
|
|
104
|
-
|
105
|
-
|
111
|
+
# the API doesn't allow fetching more than a month's data.
|
112
|
+
def fetch_time_series_rates(date)
|
113
|
+
options = request_options
|
114
|
+
|
115
|
+
end_of_month = Date.civil(date.year, date.month, -1)
|
116
|
+
start_date = Date.civil(date.year, date.month, 1)
|
117
|
+
end_date = [end_of_month, max_date].min
|
118
|
+
options[:query][:start] = start_date
|
119
|
+
options[:query][:end] = end_date
|
120
|
+
|
121
|
+
response = self.class.get('/time-series.json', options)
|
122
|
+
|
123
|
+
unless response.success?
|
124
|
+
raise RequestFailed, "Month rates request failed for #{date} - "\
|
125
|
+
"Code: #{response.code} - Body: #{response.body}"
|
126
|
+
end
|
127
|
+
response
|
128
|
+
end
|
129
|
+
|
130
|
+
def fetch_historical_rates(date)
|
131
|
+
date_string = date.iso8601
|
132
|
+
options = request_options
|
133
|
+
response = self.class.get("/historical/#{date_string}.json", options)
|
134
|
+
|
135
|
+
unless response.success?
|
136
|
+
raise RequestFailed, "Historical rates request failed for #{date} - "\
|
137
|
+
"Code: #{response.code} - Body: #{response.body}"
|
138
|
+
end
|
139
|
+
|
140
|
+
# Making the return value comply to the same structure returned from the #fetch_month_rates method (/time-series.json API)
|
141
|
+
{
|
142
|
+
'start_date' => date_string,
|
143
|
+
'end_date' => date_string,
|
144
|
+
'base' => response['base'],
|
145
|
+
'rates' => {
|
146
|
+
date_string => response['rates']
|
147
|
+
}
|
148
|
+
}
|
149
|
+
end
|
150
|
+
|
151
|
+
def request_options
|
152
|
+
options = {
|
106
153
|
query: {
|
107
154
|
app_id: @oer_app_id,
|
108
|
-
start: start_date,
|
109
|
-
end: end_date,
|
110
155
|
base: @base_currency_code
|
111
156
|
},
|
112
157
|
timeout: @timeout
|
@@ -228,7 +228,7 @@ class Money
|
|
228
228
|
allow_any_instance_of(RatesStore::HistoricalRedis).to receive(:get_rates)
|
229
229
|
.with(to_currency).and_return(to_currency_base_rates_store)
|
230
230
|
allow_any_instance_of(RatesProvider::OpenExchangeRates)
|
231
|
-
.to receive(:
|
231
|
+
.to receive(:fetch_rates).with(date).and_return(rates_provider)
|
232
232
|
end
|
233
233
|
|
234
234
|
subject { bank.exchange_with_historical(from_money, to_currency, datetime) }
|
@@ -421,7 +421,7 @@ class Money
|
|
421
421
|
before do
|
422
422
|
# unstub and let it blow up
|
423
423
|
allow_any_instance_of(RatesProvider::OpenExchangeRates)
|
424
|
-
.to receive(:
|
424
|
+
.to receive(:fetch_rates).and_call_original
|
425
425
|
end
|
426
426
|
|
427
427
|
it 'fails' do
|
@@ -21,13 +21,58 @@ require 'spec_helper'
|
|
21
21
|
class Money
|
22
22
|
module RatesProvider
|
23
23
|
describe OpenExchangeRates do
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
24
|
+
let(:base_currency_iso_code) { %w(GBP EUR USD).sample }
|
25
|
+
let(:base_currency) { Currency.wrap(base_currency_iso_code) }
|
26
|
+
let(:date) { Date.new(2010, 10, 10) }
|
27
|
+
let(:app_id) { SecureRandom.hex }
|
28
|
+
let(:timeout) { 15 }
|
29
|
+
let(:response_headers) { { 'Content-Type' => 'application/json; charset=utf-8' } }
|
30
|
+
|
31
|
+
describe '#fetch_rates with FREE account' do
|
32
|
+
let(:provider) { OpenExchangeRates.new(app_id, base_currency, timeout, OpenExchangeRates::AccountType::FREE) }
|
33
|
+
let(:url) { 'https://openexchangerates.org/api/historical/2010-10-01.json' }
|
34
|
+
let(:date) { Date.new(2010, 10, 01) }
|
35
|
+
let(:query) do
|
36
|
+
{
|
37
|
+
app_id: app_id,
|
38
|
+
base: base_currency_iso_code,
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
before do
|
43
|
+
stub_request(:get, url).with(query: query)
|
44
|
+
.to_return(status: status, body: response_body, headers: response_headers)
|
45
|
+
end
|
46
|
+
|
47
|
+
subject { provider.fetch_rates(date) }
|
48
|
+
|
49
|
+
context 'when request succeeds' do
|
50
|
+
let(:status) { 200 }
|
51
|
+
let(:base_currency_iso_code) { 'USD' }
|
52
|
+
let(:response_body) { File.read('./spec/fixtures/historical-2010-10-01.json') }
|
53
|
+
|
54
|
+
it 'format response similar to the full-month/time-series response' do
|
55
|
+
expect(subject.keys =~ ['base', 'rates', 'start_date', 'end_date'])
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'return rates only for given date' do
|
59
|
+
dates = subject.map { |country, dates_hash| dates_hash.keys }.flatten.uniq
|
60
|
+
expect(dates.size == 1)
|
61
|
+
expect(dates.first == '2010-10-01')
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'returns correct rates' do
|
65
|
+
expect(subject['VND']['2010-10-01']).to eq 19474.963646.to_d
|
66
|
+
expect(subject['EUR']['2010-10-01']).to eq 0.726556.to_d
|
67
|
+
expect(subject['CAD']['2010-10-01']).to eq 1.022502.to_d
|
68
|
+
expect(subject['CNY']['2010-10-01']).to eq 6.691335.to_d
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
describe '#fetch_rates with UNLIMITED account' do
|
75
|
+
let(:provider) { OpenExchangeRates.new(app_id, base_currency, timeout, OpenExchangeRates::AccountType::UNLIMITED) }
|
31
76
|
let(:url) { 'https://openexchangerates.org/api/time-series.json' }
|
32
77
|
let(:query) do
|
33
78
|
{
|
@@ -37,14 +82,13 @@ class Money
|
|
37
82
|
end: '2010-10-31'
|
38
83
|
}
|
39
84
|
end
|
40
|
-
let(:response_headers) { { 'Content-Type' => 'application/json; charset=utf-8' } }
|
41
85
|
|
42
86
|
before do
|
43
87
|
stub_request(:get, url).with(query: query)
|
44
88
|
.to_return(status: status, body: response_body, headers: response_headers)
|
45
89
|
end
|
46
90
|
|
47
|
-
subject { provider.
|
91
|
+
subject { provider.fetch_rates(date) }
|
48
92
|
|
49
93
|
context 'when date is before 1999' do
|
50
94
|
let(:date) { Faker::Date.between(Date.new(1900, 1, 1), Date.new(1998, 12, 31)) }
|
@@ -195,6 +239,8 @@ class Money
|
|
195
239
|
end
|
196
240
|
end
|
197
241
|
end
|
242
|
+
|
243
|
+
|
198
244
|
end
|
199
245
|
end
|
200
246
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: historical-bank
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kostis Dadamis
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2017-
|
12
|
+
date: 2017-10-17 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: money
|