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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 598c943f0debd7880311ebe7be452417b8482e7d
4
- data.tar.gz: 85c1004279edde93a2a2e56b4c1fef411d98f273
3
+ metadata.gz: b15169c2eadfa1f282eae84a1e7a3f3d39fbc594
4
+ data.tar.gz: 4ede618d723202121b19e1628ca4d7ecaece734e
5
5
  SHA512:
6
- metadata.gz: 0ce3fb2034576449270f62515454bfe48654c669f0696702358fe4fc5f0bb1c77e960c304c32d03d38267a8d3b8c3ed065691c2c11bb3b93b84ad1e1a265c7cf
7
- data.tar.gz: 0b312998d80ceb7e1f9a0e464484aa11b2c96b0977185cbc9993b9d542a47a57390158c139888adc1b8eb38ea02df0183c2ebe4fcdc14bfdaabc4ef64ad70816
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 and an Enterprise or Unlimited plan is required.
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 Enterprise or Unlimited plan** - It is needed for calling the `/time-series.json` endpoint which serves historical rates.
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
 
@@ -18,7 +18,7 @@
18
18
 
19
19
  Gem::Specification.new do |s|
20
20
  s.name = 'historical-bank'
21
- s.version = '0.1.0'
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.fetch_month_rates(date)
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 (Enterprise or Unlimited plan)
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
- def initialize(oer_app_id, base_currency, timeout)
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+'s month is the month for which we request rates. 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).
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.fetch_month_rates(Date.new(2016, 10, 5))
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 fetch_month_rates(date)
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
- end_of_month = Date.civil(date.year, date.month, -1)
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
- def request_options(start_date, end_date)
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(:fetch_month_rates).with(date).and_return(rates_provider)
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(:fetch_month_rates).and_call_original
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
- describe '#fetch_month_rates' do
25
- let(:base_currency_iso_code) { %w(GBP EUR USD).sample }
26
- let(:base_currency) { Currency.wrap(base_currency_iso_code) }
27
- let(:date) { Date.new(2010, 10, 10) }
28
- let(:app_id) { SecureRandom.hex }
29
- let(:timeout) { 15 }
30
- let(:provider) { OpenExchangeRates.new(app_id, base_currency, timeout) }
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.fetch_month_rates(date) }
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.0
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-01-16 00:00:00.000000000 Z
12
+ date: 2017-10-17 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: money