danconia 0.2.8 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +2 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +104 -0
  5. data/.ruby-version +1 -1
  6. data/.travis.yml +1 -1
  7. data/Gemfile.lock +63 -43
  8. data/README.md +17 -3
  9. data/bin/console +7 -0
  10. data/danconia.gemspec +5 -1
  11. data/examples/bna.rb +35 -0
  12. data/examples/currency_layer.rb +7 -3
  13. data/examples/fixed_rates.rb +1 -3
  14. data/examples/single_currency.rb +2 -3
  15. data/lib/danconia/currency.rb +2 -2
  16. data/lib/danconia/exchange.rb +47 -0
  17. data/lib/danconia/exchanges/bna.rb +61 -0
  18. data/lib/danconia/exchanges/currency_layer.rb +14 -2
  19. data/lib/danconia/exchanges/fixed_rates.rb +2 -4
  20. data/lib/danconia/integrations/active_record.rb +13 -14
  21. data/lib/danconia/money.rb +30 -24
  22. data/lib/danconia/pair.rb +15 -0
  23. data/lib/danconia/serializable.rb +17 -0
  24. data/lib/danconia/stores/active_record.rb +29 -6
  25. data/lib/danconia/stores/in_memory.rb +4 -9
  26. data/lib/danconia/version.rb +1 -1
  27. data/lib/danconia.rb +7 -4
  28. data/shell.nix +10 -0
  29. data/spec/danconia/exchanges/bna_spec.rb +52 -0
  30. data/spec/danconia/exchanges/currency_layer_spec.rb +28 -33
  31. data/spec/danconia/exchanges/exchange_spec.rb +54 -0
  32. data/spec/danconia/exchanges/fixtures/bna/home.html +124 -0
  33. data/spec/danconia/exchanges/fixtures/currency_layer/failure.json +7 -0
  34. data/spec/danconia/exchanges/fixtures/currency_layer/success.json +8 -0
  35. data/spec/danconia/integrations/active_record_spec.rb +25 -5
  36. data/spec/danconia/money_spec.rb +41 -17
  37. data/spec/danconia/serializable_spec.rb +16 -0
  38. data/spec/danconia/stores/active_record_spec.rb +81 -15
  39. data/spec/danconia/stores/in_memory_spec.rb +18 -0
  40. data/spec/spec_helper.rb +2 -1
  41. metadata +76 -14
  42. data/lib/danconia/exchanges/exchange.rb +0 -33
  43. data/spec/danconia/exchanges/fixed_rates_spec.rb +0 -30
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fc840f5913a010c41495c7fd843304d67565217acf7eb7c248be9bf4c17624f8
4
- data.tar.gz: d8906df663f918a41d91525b6ac51de9f8ac6decef179c813fc30145be876c50
3
+ metadata.gz: e919705ecb2ccf20ed2116a2330e056845882fe7aac0c51908cb7431f41b60f7
4
+ data.tar.gz: f8c791e9e14941cef253855cdb53f58a06aa0519dc733f00fdb76330aa9577cb
5
5
  SHA512:
6
- metadata.gz: dc66bbeaf202e6b288f79d4a6fb9b93cc66585771f9543c14a14980252f5a112eb4e6a2cad21d15e4132bda2400f74cdb5567eb9a47a4f7ac7b6161105c2ae9c
7
- data.tar.gz: 222d602fb10b842955d921ede2c7bae8bba4fd56dcdb1bcbcc74c12f795959c58250141f05943b607f0de80cdb3a07dc7425219e38f534c375236a0029d29ce2
6
+ metadata.gz: e264eabbbf2ce16314c6744edfee3518b1063cb87bc5681113fc6e7b7217fc5c4c0b21a84a839077580ed3dedcc7ed6c44784900313ad510a77ddd320a9ff490
7
+ data.tar.gz: 35a0b29aab773ddeefb68ddf9c754560f6590da7a4223a3f5232044cc574bb87daccd3f0e44220f25d05e8ba35a8f0d2d0186ccb8dcfdc899e87c0472fbd702e
data/.envrc ADDED
@@ -0,0 +1,2 @@
1
+ layout ruby
2
+ use nix
data/.gitignore CHANGED
@@ -15,3 +15,4 @@ test/tmp
15
15
  test/version_tmp
16
16
  tmp
17
17
  .DS_Store
18
+ .direnv
data/.rubocop.yml ADDED
@@ -0,0 +1,104 @@
1
+ Style/Documentation:
2
+ Enabled: false
3
+
4
+ Style/MethodDefParentheses:
5
+ Enabled: false
6
+
7
+ Style/ParallelAssignment:
8
+ Enabled: false
9
+
10
+ Style/FrozenStringLiteralComment:
11
+ Enabled: false
12
+
13
+ Style/FormatStringToken:
14
+ Enabled: false
15
+
16
+ Style/EmptyMethod:
17
+ Enabled: false
18
+
19
+ Style/GuardClause:
20
+ Enabled: false
21
+
22
+ Style/CommentAnnotation:
23
+ Enabled: false
24
+
25
+ Style/AsciiComments:
26
+ Enabled: false
27
+
28
+ Style/StabbyLambdaParentheses:
29
+ EnforcedStyle: require_no_parentheses
30
+
31
+ Style/MultilineIfModifier:
32
+ Enabled: false
33
+
34
+ Style/SafeNavigation:
35
+ Enabled: false
36
+
37
+ Style/FormatString:
38
+ Enabled: false
39
+
40
+ Style/AndOr:
41
+ Enabled: false
42
+
43
+ Style/Next:
44
+ Enabled: false
45
+
46
+ Style/NumericPredicate:
47
+ Enabled: false
48
+
49
+ Style/SymbolArray:
50
+ Enabled: false
51
+
52
+ Style/MutableConstant:
53
+ Enabled: false
54
+
55
+ Style/RescueModifier:
56
+ Enabled: false
57
+
58
+ Layout/LineLength:
59
+ Max: 120
60
+
61
+ Layout/SpaceInsideHashLiteralBraces:
62
+ Enabled: false
63
+
64
+ Layout/MultilineMethodCallIndentation:
65
+ Enabled: false
66
+
67
+ Layout/TrailingEmptyLines:
68
+ Enabled: false
69
+
70
+ Layout/FirstArrayElementIndentation:
71
+ EnforcedStyle: consistent
72
+
73
+ Layout/ParameterAlignment:
74
+ EnforcedStyle: with_fixed_indentation
75
+
76
+ Layout/SpaceInLambdaLiteral:
77
+ Enabled: false
78
+
79
+ Layout/EmptyLineAfterGuardClause:
80
+ Enabled: false
81
+
82
+ Metrics:
83
+ Enabled: false
84
+
85
+ Bundler:
86
+ Enabled: false
87
+
88
+ Lint/AssignmentInCondition:
89
+ Enabled: false
90
+
91
+ Lint/ShadowingOuterLocalVariable:
92
+ Enabled: false
93
+
94
+ Lint/AmbiguousRegexpLiteral:
95
+ Enabled: false
96
+
97
+ Lint/AmbiguousOperator:
98
+ Enabled: false
99
+
100
+ Lint/AmbiguousBlockAssociation:
101
+ Enabled: false
102
+
103
+ Naming/AccessorMethodName:
104
+ Enabled: false
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 2.5.0
1
+ 2.7.5
data/.travis.yml CHANGED
@@ -2,4 +2,4 @@
2
2
  sudo: false
3
3
  language: ruby
4
4
  cache: bundler
5
- before_install: gem install bundler -v 1.16.1
5
+ before_install: gem install bundler -v 1.17.3
data/Gemfile.lock CHANGED
@@ -1,39 +1,39 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- danconia (0.2.8)
5
- activerecord (>= 3.0.0)
4
+ danconia (0.4.0)
5
+ activesupport (>= 4.0)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
- activemodel (6.0.3.1)
11
- activesupport (= 6.0.3.1)
12
- activerecord (6.0.3.1)
13
- activemodel (= 6.0.3.1)
14
- activesupport (= 6.0.3.1)
15
- activesupport (6.0.3.1)
10
+ activemodel (7.0.2.2)
11
+ activesupport (= 7.0.2.2)
12
+ activerecord (7.0.2.2)
13
+ activemodel (= 7.0.2.2)
14
+ activesupport (= 7.0.2.2)
15
+ activesupport (7.0.2.2)
16
16
  concurrent-ruby (~> 1.0, >= 1.0.2)
17
- i18n (>= 0.7, < 2)
18
- minitest (~> 5.1)
19
- tzinfo (~> 1.1)
20
- zeitwerk (~> 2.2, >= 2.2.2)
21
- addressable (2.5.2)
22
- public_suffix (>= 2.0.2, < 4.0)
23
- coderay (1.1.2)
24
- concurrent-ruby (1.1.6)
17
+ i18n (>= 1.6, < 2)
18
+ minitest (>= 5.1)
19
+ tzinfo (~> 2.0)
20
+ addressable (2.8.0)
21
+ public_suffix (>= 2.0.2, < 5.0)
22
+ ast (2.4.1)
23
+ coderay (1.1.3)
24
+ concurrent-ruby (1.1.9)
25
25
  crack (0.4.3)
26
26
  safe_yaml (~> 1.0.0)
27
27
  diff-lcs (1.3)
28
- ffi (1.9.25)
29
- formatador (0.2.5)
30
- guard (2.14.2)
28
+ ffi (1.15.5)
29
+ formatador (1.1.0)
30
+ guard (2.18.0)
31
31
  formatador (>= 0.2.4)
32
32
  listen (>= 2.7, < 4.0)
33
33
  lumberjack (>= 1.0.12, < 2.0)
34
34
  nenv (~> 0.1)
35
35
  notiffany (~> 0.0)
36
- pry (>= 0.9.12)
36
+ pry (>= 0.13.0)
37
37
  shellany (~> 0.0)
38
38
  thor (>= 0.18.1)
39
39
  guard-compat (1.2.1)
@@ -42,27 +42,37 @@ GEM
42
42
  guard-compat (~> 1.1)
43
43
  rspec (>= 2.99.0, < 4.0)
44
44
  hashdiff (0.3.7)
45
- i18n (1.8.3)
45
+ i18n (1.10.0)
46
46
  concurrent-ruby (~> 1.0)
47
- listen (3.1.5)
48
- rb-fsevent (~> 0.9, >= 0.9.4)
49
- rb-inotify (~> 0.9, >= 0.9.7)
50
- ruby_dep (~> 1.2)
51
- lumberjack (1.0.13)
52
- method_source (0.9.0)
53
- minitest (5.14.1)
47
+ jaro_winkler (1.5.4)
48
+ listen (3.7.1)
49
+ rb-fsevent (~> 0.10, >= 0.10.3)
50
+ rb-inotify (~> 0.9, >= 0.9.10)
51
+ lumberjack (1.2.8)
52
+ method_source (1.0.0)
53
+ mini_portile2 (2.7.1)
54
+ minitest (5.15.0)
54
55
  nenv (0.3.0)
55
- notiffany (0.1.1)
56
+ nokogiri (1.13.1)
57
+ mini_portile2 (~> 2.7.0)
58
+ racc (~> 1.4)
59
+ notiffany (0.1.3)
56
60
  nenv (~> 0.1)
57
61
  shellany (~> 0.0)
58
- pry (0.11.3)
59
- coderay (~> 1.1.0)
60
- method_source (~> 0.9.0)
61
- public_suffix (3.0.2)
62
+ parallel (1.19.2)
63
+ parser (2.7.1.4)
64
+ ast (~> 2.4.1)
65
+ pry (0.14.1)
66
+ coderay (~> 1.1)
67
+ method_source (~> 1.0)
68
+ public_suffix (4.0.6)
69
+ racc (1.6.0)
70
+ rainbow (3.0.0)
62
71
  rake (13.0.1)
63
- rb-fsevent (0.10.3)
64
- rb-inotify (0.9.10)
65
- ffi (>= 0.5.0, < 2)
72
+ rb-fsevent (0.11.1)
73
+ rb-inotify (0.10.1)
74
+ ffi (~> 1.0)
75
+ rexml (3.2.5)
66
76
  rspec (3.7.0)
67
77
  rspec-core (~> 3.7.0)
68
78
  rspec-expectations (~> 3.7.0)
@@ -76,29 +86,39 @@ GEM
76
86
  diff-lcs (>= 1.2.0, < 2.0)
77
87
  rspec-support (~> 3.7.0)
78
88
  rspec-support (3.7.1)
79
- ruby_dep (1.5.0)
89
+ rubocop (0.80.1)
90
+ jaro_winkler (~> 1.5.1)
91
+ parallel (~> 1.10)
92
+ parser (>= 2.7.0.1)
93
+ rainbow (>= 2.2.2, < 4.0)
94
+ rexml
95
+ ruby-progressbar (~> 1.7)
96
+ unicode-display_width (>= 1.4.0, < 1.7)
97
+ ruby-progressbar (1.10.1)
80
98
  safe_yaml (1.0.4)
81
99
  shellany (0.0.1)
82
- sqlite3 (1.4.1)
83
- thor (0.20.0)
84
- thread_safe (0.3.6)
85
- tzinfo (1.2.7)
86
- thread_safe (~> 0.1)
100
+ sqlite3 (1.4.2)
101
+ thor (1.2.1)
102
+ tzinfo (2.0.4)
103
+ concurrent-ruby (~> 1.0)
104
+ unicode-display_width (1.6.1)
87
105
  webmock (3.4.2)
88
106
  addressable (>= 2.3.6)
89
107
  crack (>= 0.3.2)
90
108
  hashdiff
91
- zeitwerk (2.3.0)
92
109
 
93
110
  PLATFORMS
94
111
  ruby
95
112
 
96
113
  DEPENDENCIES
114
+ activerecord (>= 4.0)
97
115
  danconia!
98
116
  guard
99
117
  guard-rspec
118
+ nokogiri
100
119
  rake
101
120
  rspec
121
+ rubocop (~> 0.80.0)
102
122
  sqlite3
103
123
  webmock
104
124
 
data/README.md CHANGED
@@ -1,8 +1,18 @@
1
1
  # Danconia
2
2
 
3
- A very simple money library for Ruby, backed by BigDecimal (no conversion to cents, i.e. "infinite precision") with support for external exchange rates services.
3
+ A very simple money library for Ruby, backed by BigDecimal, with multi-currency support.
4
4
 
5
- [![Build Status](https://travis-ci.org/eeng/danconia.svg?branch=master)](https://travis-ci.org/eeng/danconia)
5
+ [![Build Status](https://app.travis-ci.com/eeng/danconia.svg?branch=master)](https://app.travis-ci.com/eeng/danconia)
6
+
7
+ ## Features
8
+
9
+ * Backed by BigDecimal (no conversion to cents is done, i.e. "infinite precision")
10
+ * Multi-currency support
11
+ * Pluggable exchange rates services:
12
+ - [CurrencyLayer API](https://currencylayer.com/)
13
+ - [BNA](https://www.bna.com.ar/)
14
+ - FixedRates (for testing use)
15
+ * Pluggable stores for persisting the exchange rates
6
16
 
7
17
  ## Installation
8
18
 
@@ -29,10 +39,12 @@ Please refer to `examples/single_currency.rb` for some configuration options.
29
39
 
30
40
  ## Multi-Currency Support
31
41
 
32
- To handle multiple currencies you need to configure an `Exchange` in order to fetch the rates. For example, with [CurrencyLayer](https://currencylayer.com/):
42
+ To handle multiple currencies you need to configure an `Exchange` in order to fetch the rates. For example, with CurrencyLayer:
33
43
 
34
44
  ```ruby
35
45
  # This can be placed in a Rails initializer
46
+ require 'danconia/exchanges/currency_layer'
47
+
36
48
  Danconia.configure do |config|
37
49
  config.default_exchange = Danconia::Exchanges::CurrencyLayer.new(access_key: '...')
38
50
  end
@@ -56,6 +68,8 @@ By default, rates are stored in memory, but you can supply a store in the exchan
56
68
  Given a `products` table with a decimal column `price` and a string column `price_currency` (optional), then you can use the `money` class method to automatically convert it to Money:
57
69
 
58
70
  ```ruby
71
+ require 'danconia/integrations/active_record'
72
+
59
73
  class Product < ActiveRecord::Base
60
74
  money :price
61
75
  end
data/bin/console ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'danconia'
5
+
6
+ require 'pry'
7
+ Pry.start
data/danconia.gemspec CHANGED
@@ -17,11 +17,15 @@ Gem::Specification.new do |gem|
17
17
  gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
18
18
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
19
  gem.require_paths = ["lib"]
20
- gem.add_dependency "activerecord", '>= 3.0.0'
20
+
21
+ gem.add_dependency 'activesupport', '>= 4.0'
22
+ gem.add_development_dependency "activerecord", '>= 4.0'
21
23
  gem.add_development_dependency "sqlite3"
22
24
  gem.add_development_dependency "rake"
23
25
  gem.add_development_dependency "guard"
24
26
  gem.add_development_dependency "guard-rspec"
25
27
  gem.add_development_dependency "rspec"
26
28
  gem.add_development_dependency "webmock"
29
+ gem.add_development_dependency "nokogiri"
30
+ gem.add_development_dependency 'rubocop', '~> 0.80.0'
27
31
  end
data/examples/bna.rb ADDED
@@ -0,0 +1,35 @@
1
+ # Example using BNA (Banco Nación de Argentina) and storing rates daily in ActiveRecord.
2
+
3
+ require 'danconia/integrations/active_record'
4
+ require 'danconia/exchanges/bna'
5
+
6
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
7
+ ActiveRecord::Base.establish_connection adapter: 'sqlite3', database: ':memory:'
8
+
9
+ ActiveRecord::Schema.define do
10
+ # You can use this in a Rails migration
11
+ create_table :exchange_rates do |t|
12
+ t.date :date
13
+ t.string :pair, limit: 6
14
+ t.string :rate_type
15
+ t.decimal :rate, precision: 12, scale: 6
16
+ t.index [:date, :pair, :rate_type], unique: true
17
+ end
18
+ end
19
+
20
+ Danconia.configure do |config|
21
+ config.default_exchange = Danconia::Exchanges::BNA.new(
22
+ store: Danconia::Stores::ActiveRecord.new(unique_keys: %i[date pair rate_type], date_field: :date)
23
+ )
24
+ end
25
+
26
+ # Periodically call this method to keep the rates up to date
27
+ puts 'Updating rates...'
28
+ Danconia.config.default_exchange.update_rates!
29
+
30
+ # Uses the latest rate
31
+ puts Money(1, 'USD').exchange_to('ARS', rate_type: 'billetes').inspect
32
+ puts Money(1, 'USD').exchange_to('ARS', rate_type: 'divisas').inspect
33
+
34
+ # Raises Danconia::Errors::ExchangeRateNotFound as there is no rate for that date
35
+ # Money(1, 'USD').exchange_to('ARS', rate_type: 'billetes', date: Date.new(2000))
@@ -1,6 +1,10 @@
1
+ # Example using CurrencyLayer API and storing only the latests rates in ActiveRecord.
1
2
  # Remember to supply your CurrencyLayer key in the ACCESS_KEY environment variable to run this example
2
- require 'danconia'
3
3
 
4
+ require 'danconia/integrations/active_record'
5
+ require 'danconia/exchanges/currency_layer'
6
+
7
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
4
8
  ActiveRecord::Base.establish_connection adapter: 'sqlite3', database: ':memory:'
5
9
 
6
10
  ActiveRecord::Schema.define do
@@ -19,8 +23,8 @@ Danconia.configure do |config|
19
23
  )
20
24
  end
21
25
 
22
- # Periodically call this method to keep rates up to date
23
- puts 'Updating dates with CurrencyLayer API...'
26
+ # Periodically call this method to keep the rates up to date
27
+ puts 'Updating rates...'
24
28
  Danconia.config.default_exchange.update_rates!
25
29
 
26
30
  puts Money(1, 'USD').exchange_to('EUR').inspect # => 0.854896 EUR
@@ -1,8 +1,6 @@
1
- require 'danconia'
2
-
3
1
  Danconia.configure do |config|
4
2
  config.default_currency = 'ARS'
5
3
  config.default_exchange = Danconia::Exchanges::FixedRates.new(rates: {'USDARS' => 27.5, 'USDEUR' => 0.86})
6
4
  end
7
5
 
8
- puts Money(10, 'ARS').exchange_to('EUR').inspect # => 0.31273 EUR
6
+ puts Money(10, 'ARS').exchange_to('EUR').inspect # => 0.31273 EUR
@@ -1,8 +1,7 @@
1
- require 'danconia'
1
+ # Example showing how to use a single currency.
2
2
 
3
3
  # USD is the default currency if no configuration is provided
4
- puts (Money(10.25) / 2).inspect # => 5.125 USD
5
- puts (Money(10.25) / 2).to_s # => $5.13
4
+ puts Money(10.25).inspect # => 5.125 USD
6
5
 
7
6
  # Lets switch to other currency
8
7
  Danconia.configure do |config|
@@ -1,6 +1,6 @@
1
1
  module Danconia
2
- class Currency < Struct.new(:code, :symbol, :description, keyword_init: true)
3
- def self.find code, exchange
2
+ Currency = Struct.new(:code, :symbol, :description, keyword_init: true) do
3
+ def self.find code
4
4
  return code if code.is_a? Currency
5
5
  new Danconia.config.available_currencies.find { |c| c[:code] == code } || {code: code, symbol: '$'}
6
6
  end
@@ -0,0 +1,47 @@
1
+ require 'danconia/pair'
2
+
3
+ module Danconia
4
+ class Exchange
5
+ def rate from, to, **opts
6
+ return 1.0 if from == to
7
+
8
+ pair = Pair.new(from, to)
9
+ rates = direct_and_inverted_rates(opts)
10
+ rates[pair] or indirect_rate(pair, rates) or raise Errors::ExchangeRateNotFound.new(from, to)
11
+ end
12
+
13
+ # Override this method in subclasses. Should return a map of pairs to rates.
14
+ # See `FixedRates` for an example implementation.
15
+ def rates **_opts
16
+ raise NotImplementedError
17
+ end
18
+
19
+ private
20
+
21
+ # Returns the original rates plus the inverted ones, to simplify rate finding logic.
22
+ # Also wraps the pair strings into Pair objects.
23
+ def direct_and_inverted_rates opts
24
+ rates(opts).each_with_object({}) do |(pair_str, rate), rs|
25
+ pair = Pair.parse(pair_str)
26
+ rs[pair] = rate
27
+ rs[pair.invert] ||= 1.0 / rate
28
+ end
29
+ end
30
+
31
+ def indirect_rate ind_pair, rates
32
+ if (from_pair = rates.keys.detect { |pair| pair.from == ind_pair.from }) &&
33
+ (to_pair = rates.keys.detect { |pair| pair.to == ind_pair.to })
34
+ rates[from_pair] * rates[to_pair]
35
+ end
36
+ end
37
+
38
+ def array_of_rates_to_hash array
39
+ pairs = array.map { |er| er[:pair] }
40
+ if pairs.size != pairs.uniq.size
41
+ raise ArgumentError, "Exchange returned duplicate pairs. Maybe you forgot a filter?\n#{array}"
42
+ end
43
+
44
+ Hash[array.map { |er| er.values_at(:pair, :rate) }]
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,61 @@
1
+ require 'danconia/errors/api_error'
2
+ require 'net/http'
3
+ require 'nokogiri'
4
+
5
+ module Danconia
6
+ module Exchanges
7
+ # The BNA does not provide an API to pull the rates, so this implementation scrapes the home HTML directly.
8
+ # Returns rates of both types, "Billetes" and "Divisas", and only the "tipo de cambio vendedor" ones.
9
+ # See `examples/bna.rb` for a complete usage example.
10
+ class BNA < Exchange
11
+ attr_reader :store
12
+
13
+ def initialize store: Stores::InMemory.new
14
+ @store = store
15
+ end
16
+
17
+ def fetch_rates
18
+ response = Net::HTTP.get URI 'https://www.bna.com.ar/Personas'
19
+ scrape_rates(response, 'billetes') + scrape_rates(response, 'divisas')
20
+ end
21
+
22
+ def update_rates!
23
+ @store.save_rates fetch_rates
24
+ end
25
+
26
+ def rates rate_type:, date: nil
27
+ array_of_rates_to_hash @store.rates(rate_type: rate_type, date: date)
28
+ end
29
+
30
+ private
31
+
32
+ def scrape_rates response, type
33
+ doc = Nokogiri::XML(response).css("##{type}")
34
+
35
+ if doc.css('thead th:last-child').text != 'Venta'
36
+ raise Errors::APIError, "Could not scrape '#{type}' rates. Maybe the format changed?"
37
+ end
38
+
39
+ doc.css('tbody tr').map do |row|
40
+ pair = parse_pair(row.css('td:first-child').text) or next
41
+ rate = parse_rate(row.css('td:last-child').text, pair)
42
+ date = Date.parse(doc.css('.fechaCot').text)
43
+ {pair: pair, rate: rate, date: date, rate_type: type}
44
+ end.compact.presence or raise Errors::APIError, "Could not scrape '#{type}' rates. Maybe the format changed?"
45
+ end
46
+
47
+ def parse_pair cur_name
48
+ case cur_name
49
+ when 'Dolar U.S.A' then 'USDARS'
50
+ when 'Euro' then 'EURARS'
51
+ when 'Real *' then 'BRLARS'
52
+ end
53
+ end
54
+
55
+ def parse_rate str, pair
56
+ val = Float(str.tr(',', '.'))
57
+ pair == 'BRLARS' ? val / 100.0 : val
58
+ end
59
+ end
60
+ end
61
+ end
@@ -4,10 +4,14 @@ require 'json'
4
4
 
5
5
  module Danconia
6
6
  module Exchanges
7
+ # Fetches the rates from https://currencylayer.com/. The `access_key` must be provided.
8
+ # See `examples/currency_layer.rb` for a complete usage example.
7
9
  class CurrencyLayer < Exchange
8
- def initialize access_key:, **args
9
- super args
10
+ attr_reader :store
11
+
12
+ def initialize access_key:, store: Stores::InMemory.new
10
13
  @access_key = access_key
14
+ @store = store
11
15
  end
12
16
 
13
17
  def fetch_rates
@@ -18,6 +22,14 @@ module Danconia
18
22
  raise Errors::APIError, response
19
23
  end
20
24
  end
25
+
26
+ def update_rates!
27
+ @store.save_rates fetch_rates.map { |pair, rate| {pair: pair, rate: rate} }
28
+ end
29
+
30
+ def rates **_opts
31
+ array_of_rates_to_hash @store.rates
32
+ end
21
33
  end
22
34
  end
23
35
  end
@@ -1,13 +1,11 @@
1
1
  module Danconia
2
2
  module Exchanges
3
3
  class FixedRates < Exchange
4
- def initialize rates: {}, **args
5
- super args
4
+ def initialize rates: {}
6
5
  @rates = rates
7
- update_rates!
8
6
  end
9
7
 
10
- def fetch_rates
8
+ def rates **_opts
11
9
  @rates
12
10
  end
13
11
  end
@@ -1,4 +1,5 @@
1
1
  require 'active_record'
2
+ require 'danconia/stores/active_record'
2
3
 
3
4
  module Danconia
4
5
  module Integrations
@@ -8,24 +9,22 @@ module Danconia
8
9
  end
9
10
 
10
11
  module ClassMethods
11
- def money(*attr_names)
12
+ def money(*attr_names, **exchange_opts)
12
13
  attr_names.each do |attr_name|
13
14
  amount_column = attr_name
14
- currency_column = "#{attr_name}_currency"
15
+ currency_column = :"#{attr_name}_currency"
15
16
 
16
- class_eval <<-EOR, __FILE__, __LINE__ + 1
17
- def #{attr_name}= value
18
- write_attribute :#{amount_column}, value.is_a?(Money) ? value.amount : value
19
- write_attribute :#{currency_column}, value.currency.code if respond_to?(:#{currency_column}) && value.is_a?(Money)
20
- end
17
+ define_method "#{attr_name}=" do |value|
18
+ write_attribute amount_column, value.is_a?(Money) ? value.amount : value
19
+ write_attribute currency_column, value.currency.code if respond_to?(currency_column) && value.is_a?(Money)
20
+ end
21
21
 
22
- def #{attr_name}
23
- amount = read_attribute :#{amount_column}
24
- currency = read_attribute :#{currency_column}
25
- decimals = self.class.columns.detect { |c| c.name == '#{amount_column}' }.scale
26
- Money.new amount, currency, decimals: decimals if amount
27
- end
28
- EOR
22
+ define_method attr_name do
23
+ amount = read_attribute amount_column
24
+ currency = read_attribute currency_column
25
+ decimals = self.class.columns.detect { |c| c.name == amount_column.to_s }.scale
26
+ Money.new amount, currency, decimals: decimals, exchange_opts: exchange_opts if amount
27
+ end
29
28
  end
30
29
  end
31
30
  end