danconia 0.2.5 → 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.
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