danconia 0.2.8 → 0.4.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 (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