danconia 0.2.9 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,10 @@
1
- require 'spec_helper'
2
-
3
1
  module Danconia
4
2
  describe Integrations::ActiveRecord, active_record: true do
5
3
  context 'single currency' do
6
4
  it 'setter' do
7
- expect(Product.new(price: 1.536).read_attribute :price).to eq 1.54
8
- expect(Product.new(price: nil).read_attribute :price).to eq nil
9
- expect(Product.new(price: Money(3)).read_attribute :price).to eq 3
5
+ expect(Product.new(price: 1.536).read_attribute(:price)).to eq 1.54
6
+ expect(Product.new(price: nil).read_attribute(:price)).to eq nil
7
+ expect(Product.new(price: Money(3)).read_attribute(:price)).to eq 3
10
8
  end
11
9
 
12
10
  it 'getter' do
@@ -34,6 +32,28 @@ module Danconia
34
32
  end
35
33
  end
36
34
 
35
+ context 'exchange options support' do
36
+ let(:exchange) do
37
+ Class.new(Exchange) do
38
+ def rates rate_type:
39
+ case rate_type
40
+ when 'divisa' then {'USDARS' => 7}
41
+ when 'billete' then {'USDARS' => 8}
42
+ end
43
+ end
44
+ end.new
45
+ end
46
+
47
+ it 'allows to specify options that will be pass to the exchange when exchanging to other currencies' do
48
+ klass = Class.new(ActiveRecord::Base) do
49
+ self.table_name = 'products'
50
+ money :price, rate_type: 'divisa'
51
+ end
52
+
53
+ expect(klass.new(price: Money(10, 'USD')).price.exchange_to('ARS', exchange: exchange)).to eq Money(70, 'ARS')
54
+ end
55
+ end
56
+
37
57
  class Product < ActiveRecord::Base
38
58
  money :price, :tax, :discount, :cost
39
59
  end
@@ -1,5 +1,3 @@
1
- require 'spec_helper'
2
-
3
1
  module Danconia
4
2
  describe Money do
5
3
  context 'instantiation' do
@@ -39,16 +37,15 @@ module Danconia
39
37
  end
40
38
 
41
39
  it 'should exchange the other currency if it is different' do
42
- expect(Money(1, 'ARS', exchange: fake_exchange(rate: 4)) + Money(1, 'USD')).to eq Money(5, 'ARS')
40
+ m1 = Money(1, 'ARS', exchange_opts: {exchange: fake_exchange(rate: 4)})
41
+ expect(m1 + Money(1, 'USD')).to eq Money(5, 'ARS')
43
42
  end
44
43
 
45
44
  it 'should return a new object with the same options' do
46
- e = fake_exchange
47
- m1 = Money(4, decimals: 3, exchange: e)
45
+ m1 = Money(4, decimals: 3)
48
46
  m2 = m1 * 2
49
47
  expect(m2).to_not eql m1
50
48
  expect(m2.decimals).to eq 3
51
- expect(m2.exchange).to eq e
52
49
  end
53
50
 
54
51
  it 'round should return a money object with the same currency' do
@@ -72,7 +69,7 @@ module Danconia
72
69
  end
73
70
 
74
71
  it 'should exchange to the source currency if they differ' do
75
- TestHelpers.with_rates 'USDARS' => 4 do |config|
72
+ TestHelpers.with_rates 'USDARS' => 4 do |_config|
76
73
  expect(Money(3, 'ARS') < Money(1, 'USD')).to be true
77
74
  expect(Money(4, 'ARS') < Money(1, 'USD')).to be false
78
75
  end
@@ -129,7 +126,8 @@ module Danconia
129
126
  end
130
127
 
131
128
  it 'should allow to pass the exchange to the instance' do
132
- expect(Money(2, 'USD', exchange: fake_exchange(rate: 3)).exchange_to('ARS')).to eq Money(6, 'ARS')
129
+ m = Money(2, 'USD', exchange_opts: {exchange: fake_exchange(rate: 3)})
130
+ expect(m.exchange_to('ARS')).to eq Money(6, 'ARS')
133
131
  end
134
132
 
135
133
  it 'should allow to pass the exchange when converting' do
@@ -153,8 +151,8 @@ module Danconia
153
151
  end
154
152
 
155
153
  it 'should return a new object with the same opts' do
156
- m1 = Money(1, 'USD', decimals: 0, exchange: fake_exchange(rate: 3))
157
- m2 = m1.exchange_to('ARS')
154
+ m1 = Money(1, 'USD', decimals: 0)
155
+ m2 = m1.exchange_to('ARS', exchange: fake_exchange(rate: 3))
158
156
  expect(m2).to_not eql m1
159
157
  expect(m2.decimals).to eq 0
160
158
  expect(m1).to eq Money(1, 'USD')
@@ -164,6 +162,38 @@ module Danconia
164
162
  expect(Money(1, 'USD').exchange_to('')).to eq Money(1, 'USD')
165
163
  expect(Money(1, 'ARS').exchange_to('')).to eq Money(1, 'ARS')
166
164
  end
165
+
166
+ context 'opts' do
167
+ let(:exchange) do
168
+ Class.new(Exchange) do
169
+ def rates opts
170
+ case opts[:type]
171
+ when 'divisa' then {'USDARS' => 7}
172
+ when 'billete' then {'USDARS' => 8}
173
+ else {}
174
+ end
175
+ end
176
+ end.new
177
+ end
178
+
179
+ it 'allows to specify opts to pass to the exchange (filters for example)' do
180
+ expect(Money(1, 'USD').exchange_to('ARS', type: 'divisa', exchange: exchange)).to eq Money(7, 'ARS')
181
+ expect { Money(1, 'USD').exchange_to('ARS', exchange: exchange) }.to raise_error Errors::ExchangeRateNotFound
182
+ end
183
+
184
+ it 'should be preserved after operations' do
185
+ m1 = Money(1, 'USD').exchange_to('ARS', type: 'divisa', exchange: exchange)
186
+ m2 = m1 + Money(2, 'USD')
187
+
188
+ expect(m2).to eq Money(21, 'ARS')
189
+ expect(m1 < Money(2, 'USD')).to eq true
190
+ end
191
+
192
+ it 'should use the instance exchange_opts by default' do
193
+ m = Money(1, 'USD', exchange_opts: {exchange: exchange, type: 'billete'})
194
+ expect(m.exchange_to('ARS')).to eq Money(8, 'ARS')
195
+ end
196
+ end
167
197
  end
168
198
 
169
199
  context 'default_currency?' do
@@ -190,7 +220,7 @@ module Danconia
190
220
  end
191
221
 
192
222
  def fake_exchange args = {}
193
- double 'Danconia::Exchanges::Exchange', args.reverse_merge(rate: nil)
223
+ double 'Danconia::Exchange', args.reverse_merge(rate: nil)
194
224
  end
195
225
  end
196
226
  end
@@ -1,31 +1,97 @@
1
- require 'spec_helper'
2
-
3
1
  module Danconia
4
2
  module Stores
5
3
  describe ActiveRecord, active_record: true do
4
+ before do
5
+ ::ActiveRecord::Schema.define do
6
+ create_table :exchange_rates do |t|
7
+ t.string :pair, limit: 6
8
+ t.decimal :rate, precision: 12, scale: 6
9
+ t.string :rate_type
10
+ t.date :date
11
+ end
12
+ end
13
+ end
14
+
6
15
  context 'save_rates' do
7
16
  it 'should create or update the rates' do
8
17
  ExchangeRate.create! pair: 'USDEUR', rate: 2
9
- expect { subject.save_rates 'USDEUR' => 3, 'USDARS' => 4 }.to change { ExchangeRate.count }.by 1
10
- expect(subject.rates).to eq('USDEUR' => 3, 'USDARS' => 4)
18
+
19
+ expect do
20
+ subject.save_rates [{pair: 'USDEUR', rate: 3}, {pair: 'USDARS', rate: 4}]
21
+ end.to change { ExchangeRate.count }.by 1
22
+
23
+ expect(subject.rates).to match [
24
+ include(pair: 'USDEUR', rate: 3),
25
+ include(pair: 'USDARS', rate: 4)
26
+ ]
27
+ end
28
+
29
+ it 'allows to specify other keys to use as unique' do
30
+ store = ActiveRecord.new(unique_keys: %i[pair rate_type])
31
+ store.save_rates [
32
+ {pair: 'USDARS', rate: 3, rate_type: 'billetes'},
33
+ {pair: 'USDARS', rate: 4, rate_type: 'divisas'}
34
+ ]
35
+ store.save_rates [
36
+ {pair: 'USDARS', rate: 33, rate_type: 'billetes'}
37
+ ]
38
+ expect(subject.rates).to match [
39
+ include(pair: 'USDARS', rate: 33, rate_type: 'billetes'),
40
+ include(pair: 'USDARS', rate: 4, rate_type: 'divisas')
41
+ ]
42
+ end
43
+
44
+ it 'ignores fields not present in the database table' do
45
+ subject.save_rates [{pair: 'USDEUR', rate: 3, non_existant: 'ignoreme'}]
11
46
  end
12
47
  end
13
48
 
14
49
  context 'rates' do
15
- it 'returns a hash with rate by pair' do
50
+ it 'returns an array like the one it received' do
16
51
  ExchangeRate.create! pair: 'USDEUR', rate: 2
17
52
  ExchangeRate.create! pair: 'USDARS', rate: 40
18
- expect(subject.rates).to eq('USDEUR' => 2, 'USDARS' => 40)
53
+
54
+ expect(subject.rates).to match [
55
+ include(pair: 'USDEUR', rate: 2),
56
+ include(pair: 'USDARS', rate: 40)
57
+ ]
58
+ end
59
+
60
+ it 'allows to pass filters' do
61
+ store = ActiveRecord.new(unique_keys: %i[pair date])
62
+ store.save_rates [
63
+ {pair: 'USDEUR', rate: 10, date: Date.new(2020, 1, 1)},
64
+ {pair: 'USDEUR', rate: 20, date: Date.new(2020, 1, 2)},
65
+ {pair: 'USDEUR', rate: 30, date: Date.new(2020, 1, 3)}
66
+ ]
67
+
68
+ expect(store.rates.size).to eq 3
69
+ expect(store.rates(date: Date.new(2020, 1, 2))).to match [include(rate: 20)]
19
70
  end
20
71
  end
21
72
 
22
- before do
23
- ::ActiveRecord::Schema.define do
24
- create_table :exchange_rates do |t|
25
- t.string :pair, limit: 6
26
- t.decimal :rate, precision: 12, scale: 6
27
- t.index :pair, unique: true
28
- end
73
+ context 'special date field' do
74
+ let(:store) do
75
+ store = ActiveRecord.new(unique_keys: %i[pair date], date_field: :date)
76
+ store.save_rates [
77
+ {pair: 'USDEUR', rate: 10, date: Date.new(2000, 1, 1)},
78
+ {pair: 'USDEUR', rate: 20, date: Date.new(2000, 1, 2)},
79
+ {pair: 'USDEUR', rate: 30, date: Date.new(2000, 1, 4)}
80
+ ]
81
+ store
82
+ end
83
+
84
+ it 'calling #rates with a particular date when there are rates for that date' do
85
+ expect(store.rates(date: Date.new(2000, 1, 2))).to match [include(rate: 20)]
86
+ end
87
+
88
+ it 'calling #rates with a particular date when there are not rates for that date should return the previous' do
89
+ expect(store.rates(date: Date.new(2000, 1, 3))).to match [include(rate: 20)]
90
+ expect(store.rates(date: Date.new(1999, 12, 31))).to eq []
91
+ end
92
+
93
+ it 'calling #rates without a particular date, uses today' do
94
+ expect(store.rates).to match [include(rate: 30)]
29
95
  end
30
96
  end
31
97
  end
@@ -0,0 +1,18 @@
1
+ module Danconia
2
+ module Stores
3
+ describe InMemory do
4
+ context 'rates' do
5
+ it 'filters rates by type' do
6
+ subject.save_rates [
7
+ {pair: 'USDARS', rate: 3, rate_type: 'divisas'},
8
+ {pair: 'USDARS', rate: 4, rate_type: 'billetes'}
9
+ ]
10
+
11
+ expect(subject.rates(rate_type: 'divisas')).to match [include(rate: 3)]
12
+ expect(subject.rates(rate_type: 'billetes')).to match [include(rate: 4)]
13
+ expect(subject.rates).to match [include(rate: 3), include(rate: 4)]
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -1,8 +1,9 @@
1
1
  require 'danconia'
2
2
  require 'danconia/test_helpers'
3
+ require 'danconia/integrations/active_record'
3
4
  require 'webmock/rspec'
4
5
 
5
- Dir["#{__dir__}/support/*.rb"].each { |f| require f }
6
+ Dir["#{__dir__}/support/*.rb"].sort.each { |f| require f }
6
7
 
7
8
  RSpec.configure do |config|
8
9
  config.filter_run_when_matching :focus
metadata CHANGED
@@ -1,29 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: danconia
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.9
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emmanuel Nicolau
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-09-01 00:00:00.000000000 Z
11
+ date: 2020-09-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: activerecord
14
+ name: activesupport
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 3.0.0
19
+ version: '4.0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 3.0.0
26
+ version: '4.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activerecord
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '4.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '4.0'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: sqlite3
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -108,6 +122,34 @@ dependencies:
108
122
  - - ">="
109
123
  - !ruby/object:Gem::Version
110
124
  version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: nokogiri
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rubocop
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: 0.80.0
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: 0.80.0
111
153
  description: Multi-currency money library backed by BigDecimal
112
154
  email: emmanicolau@gmail.com
113
155
  executables:
@@ -117,6 +159,7 @@ extra_rdoc_files: []
117
159
  files:
118
160
  - ".gitignore"
119
161
  - ".rspec"
162
+ - ".rubocop.yml"
120
163
  - ".ruby-version"
121
164
  - ".travis.yml"
122
165
  - Gemfile
@@ -127,6 +170,7 @@ files:
127
170
  - Rakefile
128
171
  - bin/console
129
172
  - danconia.gemspec
173
+ - examples/bna.rb
130
174
  - examples/currency_layer.rb
131
175
  - examples/fixed_rates.rb
132
176
  - examples/single_currency.rb
@@ -135,8 +179,9 @@ files:
135
179
  - lib/danconia/currency.rb
136
180
  - lib/danconia/errors/api_error.rb
137
181
  - lib/danconia/errors/exchange_rate_not_found.rb
182
+ - lib/danconia/exchange.rb
183
+ - lib/danconia/exchanges/bna.rb
138
184
  - lib/danconia/exchanges/currency_layer.rb
139
- - lib/danconia/exchanges/exchange.rb
140
185
  - lib/danconia/exchanges/fixed_rates.rb
141
186
  - lib/danconia/integrations/active_record.rb
142
187
  - lib/danconia/kernel.rb
@@ -146,13 +191,16 @@ files:
146
191
  - lib/danconia/stores/in_memory.rb
147
192
  - lib/danconia/test_helpers.rb
148
193
  - lib/danconia/version.rb
194
+ - spec/danconia/exchanges/bna_spec.rb
149
195
  - spec/danconia/exchanges/currency_layer_spec.rb
150
196
  - spec/danconia/exchanges/exchange_spec.rb
197
+ - spec/danconia/exchanges/fixtures/bna/home.html
151
198
  - spec/danconia/exchanges/fixtures/currency_layer/failure.json
152
199
  - spec/danconia/exchanges/fixtures/currency_layer/success.json
153
200
  - spec/danconia/integrations/active_record_spec.rb
154
201
  - spec/danconia/money_spec.rb
155
202
  - spec/danconia/stores/active_record_spec.rb
203
+ - spec/danconia/stores/in_memory_spec.rb
156
204
  - spec/spec_helper.rb
157
205
  - spec/support/database.rb
158
206
  homepage: https://github.com/eeng/danconia
@@ -180,12 +228,15 @@ signing_key:
180
228
  specification_version: 4
181
229
  summary: Multi-currency money library backed by BigDecimal
182
230
  test_files:
231
+ - spec/danconia/exchanges/bna_spec.rb
183
232
  - spec/danconia/exchanges/currency_layer_spec.rb
184
233
  - spec/danconia/exchanges/exchange_spec.rb
234
+ - spec/danconia/exchanges/fixtures/bna/home.html
185
235
  - spec/danconia/exchanges/fixtures/currency_layer/failure.json
186
236
  - spec/danconia/exchanges/fixtures/currency_layer/success.json
187
237
  - spec/danconia/integrations/active_record_spec.rb
188
238
  - spec/danconia/money_spec.rb
189
239
  - spec/danconia/stores/active_record_spec.rb
240
+ - spec/danconia/stores/in_memory_spec.rb
190
241
  - spec/spec_helper.rb
191
242
  - spec/support/database.rb
@@ -1,47 +0,0 @@
1
- require 'danconia/pair'
2
-
3
- module Danconia
4
- module Exchanges
5
- class Exchange
6
- attr_reader :store
7
-
8
- def initialize store: Stores::InMemory.new
9
- @store = store
10
- end
11
-
12
- def rate from, to
13
- return 1.0 if from == to
14
-
15
- pair = Pair.new(from, to)
16
- rates = direct_and_inverted_rates()
17
- rates[pair] or indirect_rate(pair, rates) or raise Errors::ExchangeRateNotFound.new(from, to)
18
- end
19
-
20
- def rates
21
- @store.rates
22
- end
23
-
24
- def update_rates!
25
- @store.save_rates fetch_rates
26
- end
27
-
28
- private
29
-
30
- # Returns the original rates plus the inverted ones, to simplify rate finding logic.
31
- def direct_and_inverted_rates
32
- rates.each_with_object({}) do |(pair_str, rate), rs|
33
- pair = Pair.parse(pair_str)
34
- rs[pair] = rate
35
- rs[pair.invert] ||= 1.0 / rate
36
- end
37
- end
38
-
39
- def indirect_rate ind_pair, rates
40
- if (from_pair = rates.keys.detect { |(pair, rate)| pair.from == ind_pair.from }) &&
41
- (to_pair = rates.keys.detect { |(pair, rate)| pair.to == ind_pair.to })
42
- rates[from_pair] * rates[to_pair]
43
- end
44
- end
45
- end
46
- end
47
- end