danconia 0.2.5 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +104 -0
  3. data/.travis.yml +4 -0
  4. data/Gemfile.lock +40 -17
  5. data/README.md +16 -2
  6. data/bin/console +7 -0
  7. data/danconia.gemspec +5 -1
  8. data/examples/bna.rb +35 -0
  9. data/examples/currency_layer.rb +8 -4
  10. data/examples/fixed_rates.rb +1 -3
  11. data/examples/single_currency.rb +2 -3
  12. data/lib/danconia.rb +6 -4
  13. data/lib/danconia/currency.rb +2 -2
  14. data/lib/danconia/exchange.rb +47 -0
  15. data/lib/danconia/exchanges/bna.rb +61 -0
  16. data/lib/danconia/exchanges/currency_layer.rb +14 -2
  17. data/lib/danconia/exchanges/fixed_rates.rb +2 -4
  18. data/lib/danconia/integrations/active_record.rb +13 -14
  19. data/lib/danconia/money.rb +25 -24
  20. data/lib/danconia/pair.rb +15 -0
  21. data/lib/danconia/stores/active_record.rb +29 -6
  22. data/lib/danconia/stores/in_memory.rb +4 -9
  23. data/lib/danconia/version.rb +1 -1
  24. data/spec/danconia/exchanges/bna_spec.rb +36 -0
  25. data/spec/danconia/exchanges/currency_layer_spec.rb +28 -33
  26. data/spec/danconia/exchanges/exchange_spec.rb +54 -0
  27. data/spec/danconia/exchanges/fixtures/bna/home.html +124 -0
  28. data/spec/danconia/exchanges/fixtures/currency_layer/failure.json +7 -0
  29. data/spec/danconia/exchanges/fixtures/currency_layer/success.json +8 -0
  30. data/spec/danconia/integrations/active_record_spec.rb +25 -5
  31. data/spec/danconia/money_spec.rb +57 -15
  32. data/spec/danconia/stores/active_record_spec.rb +81 -15
  33. data/spec/danconia/stores/in_memory_spec.rb +18 -0
  34. data/spec/spec_helper.rb +2 -1
  35. metadata +67 -9
  36. data/lib/danconia/exchanges/exchange.rb +0 -31
  37. data/spec/danconia/exchanges/fixed_rates_spec.rb +0 -30
@@ -0,0 +1,54 @@
1
+ module Danconia
2
+ module Exchanges
3
+ describe Exchange do
4
+ context 'rate' do
5
+ it 'returns the exchange rate value for the supplied currencies' do
6
+ exchange = fake_exchange('USDEUR' => 3, 'USDARS' => 4)
7
+ expect(exchange.rate('USD', 'EUR')).to eq 3
8
+ expect(exchange.rate('USD', 'ARS')).to eq 4
9
+ end
10
+
11
+ it 'if the direct conversion is not found, tries to find the inverse' do
12
+ exchange = fake_exchange('USDEUR' => 3)
13
+ expect(exchange.rate('EUR', 'USD')).to be_within(0.00001).of(1.0 / 3)
14
+ end
15
+
16
+ it 'if not direct nor inverse conversion is found, tries to convert through USD' do
17
+ exchange = fake_exchange('USDEUR' => 3, 'USDARS' => 6)
18
+ expect(exchange.rate('EUR', 'ARS')).to be_within(0.00001).of 2
19
+ expect(exchange.rate('ARS', 'EUR')).to be_within(0.00001).of 0.5
20
+ end
21
+
22
+ it 'pairs can have a different common currency' do
23
+ exchange = fake_exchange('EURARS' => 3, 'BRLARS' => 1.5)
24
+ expect(exchange.rate('EUR', 'ARS')).to eq 3
25
+ expect(exchange.rate('ARS', 'EUR')).to be_within(0.00001).of(1.0 / 3)
26
+ expect(exchange.rate('BRL', 'ARS')).to eq 1.5
27
+ expect(exchange.rate('EUR', 'BRL')).to eq 3 / 1.5
28
+ end
29
+
30
+ it 'raises an error if the conversion cannot be made' do
31
+ expect { fake_exchange({}).rate('USD', 'EUR') }.to raise_error Errors::ExchangeRateNotFound
32
+ end
33
+
34
+ it 'should allow to pass options to filter the rates' do
35
+ exchange = Class.new(Exchange) do
36
+ def rates type:
37
+ case type
38
+ when 'divisa' then {'USDARS' => 7}
39
+ when 'billete' then {'USDARS' => 8}
40
+ end
41
+ end
42
+ end.new
43
+
44
+ expect(exchange.rate('USD', 'ARS', type: 'divisa')).to eq 7
45
+ expect(exchange.rate('USD', 'ARS', type: 'billete')).to eq 8
46
+ end
47
+
48
+ def fake_exchange(rates)
49
+ FixedRates.new(rates: rates)
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,124 @@
1
+ <!DOCTYPE html>
2
+ <html lang="es" class="pc">
3
+
4
+ <body>
5
+ <div id="rightHome">
6
+ <div class="col-md-3">
7
+ <div class="tabSmall">
8
+ <ul class="nav nav-tabs">
9
+
10
+ <li class="active"><a href="#billetes" data-toggle="tab">Cotización Billetes</a>
11
+ <div class="arrow"></div>
12
+ </li>
13
+ <li><a href="#divisas" data-toggle="tab">Cotización Divisas</a>
14
+ <div class="arrow"></div>
15
+ </li>
16
+ </ul>
17
+
18
+ <div class="tab-content">
19
+
20
+ <div class="tab-pane fade in active" id="billetes">
21
+
22
+ <table class="table cotizacion">
23
+ <thead>
24
+ <tr>
25
+ <th class="fechaCot">1/9/2020</th>
26
+ <th>Compra</th>
27
+ <th>Venta</th>
28
+ </tr>
29
+ </thead>
30
+ <tbody>
31
+
32
+ <tr>
33
+ <td class="tit">Dolar U.S.A</td>
34
+ <td>73,2500</td>
35
+ <td>78,2500</td>
36
+ </tr>
37
+ <tr>
38
+ <td class="tit">Euro</td>
39
+ <td>84,0000</td>
40
+ <td>89,0000</td>
41
+ </tr>
42
+ <tr>
43
+ <td class="tit">Real *</td>
44
+ <td>1250,0000</td>
45
+ <td>1450,0000</td>
46
+ </tr>
47
+
48
+ </tbody>
49
+ </table>
50
+ <a href="#" class="link-cotizacion" data-toggle="modal" data-target="#modalHistorico" id="buttonHistoricoBilletes">Ver histórico</a>
51
+ <div class="legal">Hora Actualización: 10:40</div>
52
+ <div class="legal">(*) cotización cada 100 unidades.</div>
53
+
54
+ </div>
55
+
56
+ <div class="tab-pane fade" id="divisas">
57
+ <table class="table cotizacion">
58
+ <thead>
59
+ <tr>
60
+ <th class="fechaCot">31/8/2020</th>
61
+ <th>Compra</th>
62
+ <th>Venta</th>
63
+ </tr>
64
+ </thead>
65
+ <tbody>
66
+
67
+ <tr>
68
+ <td class="tit">Dolar U.S.A</td>
69
+ <td>73.9800</td>
70
+ <td>74.1800</td>
71
+ </tr>
72
+ <tr>
73
+ <td class="tit">Libra Esterlina</td>
74
+ <td>98.8743</td>
75
+ <td>99.3641</td>
76
+ </tr>
77
+ <tr>
78
+ <td class="tit">Euro</td>
79
+ <td>88.2581</td>
80
+ <td>88.6822</td>
81
+ </tr>
82
+ <tr>
83
+ <td class="tit">Franco Suizos *</td>
84
+ <td>8188.4905</td>
85
+ <td>8221.8519</td>
86
+ </tr>
87
+ <tr>
88
+ <td class="tit">YENES *</td>
89
+ <td>69.8238</td>
90
+ <td>70.1124</td>
91
+ </tr>
92
+ <tr>
93
+ <td class="tit">Dolares Canadienses *</td>
94
+ <td>5675.3160</td>
95
+ <td>5698.6457</td>
96
+ </tr>
97
+ <tr>
98
+ <td class="tit">Coronas Danesas *</td>
99
+ <td>1184.4394</td>
100
+ <td>1195.0806</td>
101
+ </tr>
102
+ <tr>
103
+ <td class="tit">Coronas Noruegas *</td>
104
+ <td>845.1963</td>
105
+ <td>854.8381</td>
106
+ </tr>
107
+ <tr>
108
+ <td class="tit">Coronas Suecas *</td>
109
+ <td>853.8596</td>
110
+ <td>863.8369</td>
111
+ </tr>
112
+
113
+ </tbody>
114
+ </table>
115
+ <a href="#" class="link-cotizacion" data-toggle="modal" data-target="#modalHistorico" id="buttonHistoricoMonedas">Ver histórico</a>
116
+ <div class="legal">(*) cotización cada 100 unidades.</div>
117
+ <div class="legal leyenda">El tipo de cambio de cierre de divisa es suministrado al público a fines informativos, como referencia de la cotización de la divisa en el mercado mayorista al final de cada día.</div>
118
+ </div>
119
+ </div>
120
+ </div>
121
+ </div>
122
+ </body>
123
+
124
+ </html>
@@ -0,0 +1,7 @@
1
+ {
2
+ "success": false,
3
+ "error": {
4
+ "code": 104,
5
+ "info": "Your monthly usage limit has been reached. Please upgrade your subscription plan."
6
+ }
7
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "success": true,
3
+ "source": "USD",
4
+ "quotes": {
5
+ "USDARS": 27.110001,
6
+ "USDAUD": 1.346196
7
+ }
8
+ }
@@ -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') + Money(1, 'USD', exchange: fake_exchange(rate: 4))).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
@@ -121,17 +118,30 @@ module Danconia
121
118
  end
122
119
 
123
120
  context 'exchange_to' do
124
- it 'should use the exchange passed to the instance to get the rate' do
125
- expect(Money(2, 'USD', exchange: fake_exchange(rate: 3)).exchange_to('ARS')).to eq Money(6, 'ARS')
126
- end
127
-
128
- it 'should use the default exchange if not set' do
121
+ it 'should use a default exchange if not overriden' do
129
122
  TestHelpers.with_rates 'USDEUR' => 3, 'USDARS' => 4 do
130
123
  expect(Money(2, 'USD').exchange_to('EUR')).to eq Money(6, 'EUR')
131
124
  expect(Money(2, 'USD').exchange_to('ARS')).to eq Money(8, 'ARS')
132
125
  end
133
126
  end
134
127
 
128
+ it 'should allow to pass the exchange to the instance' do
129
+ m = Money(2, 'USD', exchange_opts: {exchange: fake_exchange(rate: 3)})
130
+ expect(m.exchange_to('ARS')).to eq Money(6, 'ARS')
131
+ end
132
+
133
+ it 'should allow to pass the exchange when converting' do
134
+ expect(Money(2, 'USD').exchange_to('ARS', exchange: fake_exchange(rate: 4))).to eq Money(8, 'ARS')
135
+ end
136
+
137
+ it 'when overriding the exchange, should preserve it in the new instances' do
138
+ m1 = Money(1, 'USD').exchange_to('ARS', exchange: fake_exchange(rate: 2))
139
+ m2 = m1 + Money(3, 'USD')
140
+ m3 = m2 * Money(1, 'USD')
141
+ expect(m2).to eq Money(8, 'ARS')
142
+ expect(m3).to eq Money(16, 'ARS')
143
+ end
144
+
135
145
  it 'if no rate if found should raise error' do
136
146
  expect { Money(2, 'USD').exchange_to('ARS') }.to raise_error Errors::ExchangeRateNotFound
137
147
  end
@@ -141,8 +151,8 @@ module Danconia
141
151
  end
142
152
 
143
153
  it 'should return a new object with the same opts' do
144
- m1 = Money(1, 'USD', decimals: 0, exchange: fake_exchange(rate: 3))
145
- m2 = m1.exchange_to('ARS')
154
+ m1 = Money(1, 'USD', decimals: 0)
155
+ m2 = m1.exchange_to('ARS', exchange: fake_exchange(rate: 3))
146
156
  expect(m2).to_not eql m1
147
157
  expect(m2.decimals).to eq 0
148
158
  expect(m1).to eq Money(1, 'USD')
@@ -152,6 +162,38 @@ module Danconia
152
162
  expect(Money(1, 'USD').exchange_to('')).to eq Money(1, 'USD')
153
163
  expect(Money(1, 'ARS').exchange_to('')).to eq Money(1, 'ARS')
154
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
155
197
  end
156
198
 
157
199
  context 'default_currency?' do
@@ -178,7 +220,7 @@ module Danconia
178
220
  end
179
221
 
180
222
  def fake_exchange args = {}
181
- double 'exchange', args.reverse_merge(rate: nil)
223
+ double 'Danconia::Exchange', args.reverse_merge(rate: nil)
182
224
  end
183
225
  end
184
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.map { |e| [e.pair, e.rate] }).to eq({'USDEUR' => 3, 'USDARS' => 4}.to_a)
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
- context '#direct_rate' do
15
- it 'should find the rate for the pair' do
49
+ context 'rates' do
50
+ it 'returns an array like the one it received' do
16
51
  ExchangeRate.create! pair: 'USDEUR', rate: 2
17
- expect(subject.direct_rate('USD', 'EUR')).to eq 2
18
- expect(subject.direct_rate('USD', 'ARS')).to eq nil
52
+ ExchangeRate.create! pair: 'USDARS', rate: 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