danconia 0.2.1

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f379e57876333600210d3f3669e1a45654b8016238abd82df38eaf6674084c5c
4
+ data.tar.gz: b85beca8d071d199c13771c14916efc26aac999df2d0dc7bb167597fb5822f29
5
+ SHA512:
6
+ metadata.gz: 5712b367efd1638949888f2e1fe254a174cd63c23d7e4d14ee8f9601380f4b0cf376b572642fad40d93b4d1d7b8333de2788eff5042fe29e79c36bb9d444315a
7
+ data.tar.gz: b55eab153bed5d5572789b7ed4b7a3cbb6ef8907269a5babfdebe12c1947152baac4c5bde5fc0b4bada904323c6524ceafe0d7991aa64262123362ee22209764
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ InstalledFiles
7
+ _yardoc
8
+ coverage
9
+ doc/
10
+ lib/bundler/man
11
+ pkg
12
+ rdoc
13
+ spec/reports
14
+ test/tmp
15
+ test/version_tmp
16
+ tmp
17
+ .DS_Store
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
@@ -0,0 +1 @@
1
+ 2.5.0
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in money.gemspec
4
+ gemspec
@@ -0,0 +1,106 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ danconia (0.2.1)
5
+ activerecord (>= 3.0.0)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ activemodel (5.2.0)
11
+ activesupport (= 5.2.0)
12
+ activerecord (5.2.0)
13
+ activemodel (= 5.2.0)
14
+ activesupport (= 5.2.0)
15
+ arel (>= 9.0)
16
+ activesupport (5.2.0)
17
+ concurrent-ruby (~> 1.0, >= 1.0.2)
18
+ i18n (>= 0.7, < 2)
19
+ minitest (~> 5.1)
20
+ tzinfo (~> 1.1)
21
+ addressable (2.5.2)
22
+ public_suffix (>= 2.0.2, < 4.0)
23
+ arel (9.0.0)
24
+ coderay (1.1.2)
25
+ concurrent-ruby (1.0.5)
26
+ crack (0.4.3)
27
+ safe_yaml (~> 1.0.0)
28
+ diff-lcs (1.3)
29
+ ffi (1.9.25)
30
+ formatador (0.2.5)
31
+ guard (2.14.2)
32
+ formatador (>= 0.2.4)
33
+ listen (>= 2.7, < 4.0)
34
+ lumberjack (>= 1.0.12, < 2.0)
35
+ nenv (~> 0.1)
36
+ notiffany (~> 0.0)
37
+ pry (>= 0.9.12)
38
+ shellany (~> 0.0)
39
+ thor (>= 0.18.1)
40
+ guard-compat (1.2.1)
41
+ guard-rspec (4.7.3)
42
+ guard (~> 2.1)
43
+ guard-compat (~> 1.1)
44
+ rspec (>= 2.99.0, < 4.0)
45
+ hashdiff (0.3.7)
46
+ i18n (1.0.1)
47
+ concurrent-ruby (~> 1.0)
48
+ listen (3.1.5)
49
+ rb-fsevent (~> 0.9, >= 0.9.4)
50
+ rb-inotify (~> 0.9, >= 0.9.7)
51
+ ruby_dep (~> 1.2)
52
+ lumberjack (1.0.13)
53
+ method_source (0.9.0)
54
+ minitest (5.11.3)
55
+ nenv (0.3.0)
56
+ notiffany (0.1.1)
57
+ nenv (~> 0.1)
58
+ shellany (~> 0.0)
59
+ pry (0.11.3)
60
+ coderay (~> 1.1.0)
61
+ method_source (~> 0.9.0)
62
+ public_suffix (3.0.2)
63
+ rake (10.3.2)
64
+ rb-fsevent (0.10.3)
65
+ rb-inotify (0.9.10)
66
+ ffi (>= 0.5.0, < 2)
67
+ rspec (3.7.0)
68
+ rspec-core (~> 3.7.0)
69
+ rspec-expectations (~> 3.7.0)
70
+ rspec-mocks (~> 3.7.0)
71
+ rspec-core (3.7.1)
72
+ rspec-support (~> 3.7.0)
73
+ rspec-expectations (3.7.0)
74
+ diff-lcs (>= 1.2.0, < 2.0)
75
+ rspec-support (~> 3.7.0)
76
+ rspec-mocks (3.7.0)
77
+ diff-lcs (>= 1.2.0, < 2.0)
78
+ rspec-support (~> 3.7.0)
79
+ rspec-support (3.7.1)
80
+ ruby_dep (1.5.0)
81
+ safe_yaml (1.0.4)
82
+ shellany (0.0.1)
83
+ sqlite3 (1.3.13)
84
+ thor (0.20.0)
85
+ thread_safe (0.3.6)
86
+ tzinfo (1.2.5)
87
+ thread_safe (~> 0.1)
88
+ webmock (3.4.2)
89
+ addressable (>= 2.3.6)
90
+ crack (>= 0.3.2)
91
+ hashdiff
92
+
93
+ PLATFORMS
94
+ ruby
95
+
96
+ DEPENDENCIES
97
+ danconia!
98
+ guard
99
+ guard-rspec
100
+ rake
101
+ rspec
102
+ sqlite3
103
+ webmock
104
+
105
+ BUNDLED WITH
106
+ 1.16.1
@@ -0,0 +1,5 @@
1
+ guard :rspec, cmd: 'bundle exec rspec' do
2
+ watch(%r{^spec/.+_spec\.rb$})
3
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
4
+ watch('spec/spec_helper.rb') { "spec" }
5
+ end
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Emmanuel Nicolau
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,29 @@
1
+ # Danconia
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'danconia'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install danconia
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
@@ -0,0 +1,11 @@
1
+ require 'bundler'
2
+ Bundler.setup
3
+ Bundler::GemHelper.install_tasks
4
+
5
+ begin
6
+ require 'rspec/core/rake_task'
7
+ RSpec::Core::RakeTask.new(:spec)
8
+ rescue LoadError
9
+ end
10
+
11
+ task default: :spec
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'danconia/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "danconia"
8
+ gem.version = Danconia::VERSION
9
+ gem.authors = ["Emmanuel Nicolau"]
10
+ gem.email = ["emmanicolau@gmail.com"]
11
+ gem.description = %q{Multi-currency money library backed by BigDecimal}
12
+ gem.summary = %q{Multi-currency money library backed by BigDecimal}
13
+ gem.homepage = ""
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+ gem.add_dependency "activerecord", '>= 3.0.0'
20
+ gem.add_development_dependency "sqlite3"
21
+ gem.add_development_dependency "rake"
22
+ gem.add_development_dependency "guard"
23
+ gem.add_development_dependency "guard-rspec"
24
+ gem.add_development_dependency "rspec"
25
+ gem.add_development_dependency "webmock"
26
+ end
@@ -0,0 +1,8 @@
1
+ require 'danconia'
2
+
3
+ Danconia.configure do |config|
4
+ config.default_currency = 'ARS'
5
+ config.default_exchange = Danconia::Exchanges::FixedRates.new(rates: {'USDARS' => 27.5, 'USDEUR' => 0.86})
6
+ end
7
+
8
+ puts Money(10, 'ARS').exchange_to('EUR').inspect
@@ -0,0 +1,7 @@
1
+ require 'danconia/version'
2
+ require 'danconia/config'
3
+ require 'danconia/currency'
4
+ require 'danconia/money'
5
+ require 'danconia/kernel'
6
+ require 'danconia/integrations/active_record'
7
+ require 'danconia/exchanges/fixed_rates'
@@ -0,0 +1,26 @@
1
+ require 'danconia/exchange'
2
+
3
+ module Danconia
4
+ class << self
5
+ def config
6
+ @config ||= Config.new
7
+ end
8
+
9
+ def config= c
10
+ @config = c
11
+ end
12
+
13
+ def configure
14
+ yield config
15
+ end
16
+ end
17
+
18
+ class Config
19
+ attr_accessor :default_currency, :default_exchange
20
+
21
+ def initialize
22
+ @default_currency = 'USD'
23
+ @default_exchange = Exchanges::FixedRates.new
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,8 @@
1
+ module Danconia
2
+ class Currency < Struct.new(:code, :symbol, :description, keyword_init: true)
3
+ def self.find code, exchange
4
+ return code if code.is_a? Currency
5
+ new exchange.available_currencies.find { |c| c[:code] == code } || {code: code, symbol: '$'}
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,6 @@
1
+ module Danconia
2
+ module Errors
3
+ class APIError < StandardError
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,9 @@
1
+ module Danconia
2
+ module Errors
3
+ class ExchangeRateNotFound < StandardError
4
+ def initialize src, dst
5
+ super "No exchange rate found from #{src} to #{dst}"
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,32 @@
1
+ require 'danconia/stores/in_memory'
2
+
3
+ module Danconia
4
+ class Exchange
5
+ attr_reader :store
6
+
7
+ def initialize store: Stores::InMemory.new
8
+ @store = store
9
+ end
10
+
11
+ def rate from, to
12
+ if from == 'USD' and direct_rate = @store.direct_rate(from, to)
13
+ direct_rate
14
+ elsif to == 'USD' and inverse_rate = @store.direct_rate(to, from)
15
+ (1.0 / inverse_rate).round 6
16
+ elsif from != 'USD' and to != 'USD' and from_in_usd = rate(from, 'USD') and to_per_usd = rate('USD', to)
17
+ (from_in_usd * to_per_usd).round 6
18
+ else
19
+ raise Errors::ExchangeRateNotFound.new(from, to)
20
+ end
21
+ end
22
+
23
+ def rates
24
+ @store.rates
25
+ end
26
+
27
+ # Currently is only needed for money formatting
28
+ def available_currencies
29
+ []
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,27 @@
1
+ require 'danconia/errors/api_error'
2
+ require 'net/http'
3
+ require 'json'
4
+
5
+ module Danconia
6
+ module Exchanges
7
+ class CurrencyLayer < Exchange
8
+ def initialize access_key:
9
+ super()
10
+ @access_key = access_key
11
+ end
12
+
13
+ def update_rates!
14
+ @store.save_rates fetch_rates
15
+ end
16
+
17
+ def fetch_rates
18
+ response = JSON.parse Net::HTTP.get URI "http://www.apilayer.net/api/live?access_key=#{@access_key}"
19
+ if response['success']
20
+ response['quotes']
21
+ else
22
+ raise Errors::APIError, response
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,10 @@
1
+ module Danconia
2
+ module Exchanges
3
+ class FixedRates < Exchange
4
+ def initialize rates: {}
5
+ super()
6
+ @store.save_rates rates
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,36 @@
1
+ require 'active_record'
2
+
3
+ module Danconia
4
+ module Integrations
5
+ module ActiveRecord
6
+ def self.included(base)
7
+ base.extend(ClassMethods)
8
+ end
9
+
10
+ module ClassMethods
11
+ def money(*attr_names)
12
+ attr_names.each do |attr_name|
13
+ amount_column = attr_name
14
+ currency_column = "#{attr_name}_currency"
15
+
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
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
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ ActiveRecord::Base.send :include, Danconia::Integrations::ActiveRecord
@@ -0,0 +1,5 @@
1
+ module Kernel
2
+ def Money *args
3
+ Danconia::Money.new *args
4
+ end
5
+ end
@@ -0,0 +1,95 @@
1
+ require 'bigdecimal'
2
+ require 'danconia/errors/exchange_rate_not_found'
3
+
4
+ module Danconia
5
+ class Money
6
+ include Comparable
7
+ attr_reader :amount, :currency, :decimals, :exchange
8
+
9
+ def initialize amount, currency_code = nil, decimals: 2, exchange: Danconia.config.default_exchange
10
+ @decimals = decimals
11
+ @amount = parse(amount)
12
+ @currency = Currency.find(currency_code || Danconia.config.default_currency, exchange)
13
+ @exchange = exchange
14
+ end
15
+
16
+ def to_s
17
+ ActiveSupport::NumberHelper.number_to_currency amount, precision: decimals, unit: currency.symbol
18
+ end
19
+
20
+ def inspect
21
+ "#<#{self.class.name} #{amount} #{currency.code}>"
22
+ end
23
+
24
+ def == other
25
+ if other.is_a?(Money)
26
+ amount == other.amount && currency == other.currency
27
+ else
28
+ amount == other && currency.code == Danconia.config.default_currency
29
+ end
30
+ end
31
+
32
+ def eql? other
33
+ self == other
34
+ end
35
+
36
+ def hash
37
+ [amount, currency].hash
38
+ end
39
+
40
+ def <=> other
41
+ other = other.exchange_to(currency).amount if other.is_a? Money
42
+ amount <=> other
43
+ end
44
+
45
+ def exchange_to other_currency
46
+ other_currency = Currency.find(other_currency, exchange)
47
+ rate = exchange_rate(currency, other_currency)
48
+ clone_with amount * rate, other_currency
49
+ end
50
+
51
+ %w(+ - * /).each do |op|
52
+ class_eval <<-EOR, __FILE__, __LINE__ + 1
53
+ def #{op} other
54
+ other = other.exchange_to(currency).amount if other.is_a? Money
55
+ clone_with amount #{op} other
56
+ end
57
+ EOR
58
+ end
59
+
60
+ def round *args
61
+ clone_with amount.round(*args)
62
+ end
63
+
64
+ def in_cents
65
+ (self * 100).round
66
+ end
67
+
68
+ def method_missing method, *args
69
+ if @amount.respond_to? method
70
+ @amount.send method, *args
71
+ else
72
+ super
73
+ end
74
+ end
75
+
76
+ def respond_to? method, *args
77
+ super or @amount.respond_to?(method, *args)
78
+ end
79
+
80
+ private
81
+
82
+ def parse object
83
+ BigDecimal(object.to_s) rescue BigDecimal('0')
84
+ end
85
+
86
+ def clone_with amount, currency = @currency
87
+ Money.new amount, currency, decimals: decimals, exchange: exchange
88
+ end
89
+
90
+ def exchange_rate from, to
91
+ return 1 if from == to
92
+ exchange.rate from.code, to.code
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,20 @@
1
+ module Danconia
2
+ module Stores
3
+ class InMemory
4
+ attr_reader :rates
5
+
6
+ def initialize rates: {}
7
+ save_rates rates
8
+ end
9
+
10
+ # @rates should be of a map of pair->rate like {'USDEUR' => 1.25}
11
+ def save_rates rates
12
+ @rates = rates
13
+ end
14
+
15
+ def direct_rate from, to
16
+ @rates[[from, to].join]
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,3 @@
1
+ module Danconia
2
+ VERSION = '0.2.1'
3
+ end
@@ -0,0 +1,56 @@
1
+ require 'spec_helper'
2
+ require 'danconia/exchanges/currency_layer'
3
+
4
+ module Danconia
5
+ module Exchanges
6
+ describe CurrencyLayer do
7
+ subject { CurrencyLayer.new access_key: '[KEY]' }
8
+
9
+ context 'fetch_rates' do
10
+ it 'uses the API to retrive the rates' do
11
+ stub_request(:get, 'http://www.apilayer.net/api/live?access_key=[KEY]').to_return body: <<~END
12
+ {
13
+ "success": true,
14
+ "source": "USD",
15
+ "quotes": {
16
+ "USDARS": 27.110001,
17
+ "USDAUD": 1.346196
18
+ }
19
+ }
20
+ END
21
+ expect(subject.fetch_rates).to eq 'USDARS' => 27.110001, 'USDAUD' => 1.346196
22
+ end
23
+
24
+ it 'when the API returns an error' do
25
+ stub_request(:get, 'http://www.apilayer.net/api/live?access_key=[KEY]').to_return body: <<~END
26
+ {
27
+ "success": false,
28
+ "error": {
29
+ "code": 104,
30
+ "info": "Your monthly usage limit has been reached. Please upgrade your subscription plan."
31
+ }
32
+ }
33
+ END
34
+ expect { subject.fetch_rates }.to raise_error Errors::APIError
35
+ end
36
+ end
37
+
38
+ context 'update_rates!' do
39
+ it 'fetches the rates and stores them' do
40
+ expect(subject).to receive(:fetch_rates) { {'USDARS' => 3, 'USDAUD' => 4} }
41
+ subject.update_rates!
42
+ expect(subject.rates.size).to eq 2
43
+ expect(subject.rate('USD', 'ARS')).to eq 3
44
+ expect(subject.rate('USD', 'AUD')).to eq 4
45
+ end
46
+
47
+ it 'if a rate already exists should update it' do
48
+ subject.store.save_rates 'USDARS' => 3
49
+ expect(subject).to receive(:fetch_rates) { {'USDARS' => 3.1} }
50
+ subject.update_rates!
51
+ expect(subject.rate('USD', 'ARS')).to eq 3.1
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,30 @@
1
+ require 'spec_helper'
2
+
3
+ module Danconia
4
+ module Exchanges
5
+ describe FixedRates do
6
+ context 'rate' do
7
+ it 'returns the exchange rate value for the supplied currencies' do
8
+ exchange = FixedRates.new rates: {'USDEUR' => 3, 'USDARS' => 4}
9
+ expect(exchange.rate 'USD', 'EUR').to eq 3
10
+ expect(exchange.rate 'USD', 'ARS').to eq 4
11
+ end
12
+
13
+ it 'returns nil if not found' do
14
+ expect { subject.rate 'USD', 'EUR' }.to raise_error Errors::ExchangeRateNotFound
15
+ end
16
+
17
+ it 'if the direct conversion is not found, tries to find the inverse' do
18
+ exchange = FixedRates.new rates: {'USDEUR' => 3}
19
+ expect(exchange.rate 'EUR', 'USD').to eq (1.0 / 3).round 6
20
+ end
21
+
22
+ it 'if not direct nor inverse conversion is found and both are different than USD, tries to convert through USD' do
23
+ exchange = FixedRates.new rates: {'USDEUR' => 3, 'USDARS' => 6}
24
+ expect(exchange.rate 'EUR', 'ARS').to be_within(0.00001).of 2
25
+ expect(exchange.rate 'ARS', 'EUR').to be_within(0.00001).of 0.5
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,53 @@
1
+ require 'spec_helper'
2
+
3
+ module Danconia
4
+ describe Integrations::ActiveRecord, active_record: true do
5
+ context 'single currency' do
6
+ 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
10
+ end
11
+
12
+ it 'getter' do
13
+ expect(Product.new(price: 1).price).to eq Money(1)
14
+ expect(Product.new(price: nil).price).to eq nil
15
+ end
16
+
17
+ it 'should use the scale from the columns as decimals' do
18
+ expect(Product.new(discount: 2).discount.decimals).to eq 3
19
+ end
20
+ end
21
+
22
+ context 'multicurrency support' do
23
+ it 'setter' do
24
+ expect(Product.new(cost: Money(1, 'ARS')).attributes.values_at('cost', 'cost_currency')).to eq [1, 'ARS']
25
+ expect(Product.new(cost: 2).attributes.values_at('cost', 'cost_currency')).to eq [2, nil]
26
+ expect(Product.new(cost: nil).attributes.values_at('cost', 'cost_currency')).to eq [nil, nil]
27
+ end
28
+
29
+ it 'getter' do
30
+ expect(Product.new(cost: 1, cost_currency: 'ARS').cost).to eq Money(1, 'ARS')
31
+ expect(Product.new(cost_currency: 'ARS', cost: 1).cost).to eq Money(1, 'ARS')
32
+ expect(Product.new(cost: 1, cost_currency: nil).cost).to eq Money(1, 'USD')
33
+ expect(Product.new(cost: nil, cost_currency: nil).cost).to eq nil
34
+ end
35
+ end
36
+
37
+ class Product < ActiveRecord::Base
38
+ money :price, :tax, :discount, :cost
39
+ end
40
+
41
+ before do
42
+ ActiveRecord::Schema.define version: 1 do
43
+ create_table :products do |t|
44
+ t.column :price, :decimal, precision: 12, scale: 2
45
+ t.column :tax, :decimal, precision: 12, scale: 2
46
+ t.column :discount, :decimal, precision: 12, scale: 3
47
+ t.column :cost, :decimal, precision: 6, scale: 2
48
+ t.column :cost_currency, :string, limit: 3
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,159 @@
1
+ require 'spec_helper'
2
+
3
+ module Danconia
4
+ describe Money do
5
+ context 'instantiation' do
6
+ it 'should accept integers and strings' do
7
+ expect(Money(10).amount).to eq BigDecimal('10')
8
+ expect(Money('10.235').amount).to eq BigDecimal('10.235')
9
+ end
10
+
11
+ it 'non numeric values are treated as zero' do
12
+ expect(Money('a')).to eq Money(0)
13
+ end
14
+
15
+ it 'should use the default currency if not specified' do
16
+ with_config do |config|
17
+ config.default_currency = 'ARS'
18
+ expect(Money(0).currency.code).to eq 'ARS'
19
+
20
+ config.default_currency = 'EUR'
21
+ expect(Money(0).currency.code).to eq 'EUR'
22
+ end
23
+ end
24
+ end
25
+
26
+ context 'arithmetic' do
27
+ it 'does the operation on the amount' do
28
+ expect(Money(3) + Money(2)).to eq Money(5)
29
+ expect(Money(3.5) + 0.5).to eq Money(4)
30
+ expect(Money(78.55) * 0.25).to eq Money(19.6375)
31
+ expect(Money(3) / 2).to eq Money(1.5)
32
+ expect { Money(3.5) + 'a' }.to raise_error TypeError
33
+ end
34
+
35
+ it 'should preserve the currency' do
36
+ expect(Money(1, 'ARS') + 2).to eq Money(3, 'ARS')
37
+ expect(Money(1, 'ARS') + Money(2, 'ARS')).to eq Money(3, 'ARS')
38
+ end
39
+
40
+ it 'should exchange the other currency if it is different' do
41
+ expect(Money(1, 'ARS') + Money(1, 'USD', exchange: fake_exchange(rate: 4))).to eq Money(5, 'ARS')
42
+ end
43
+
44
+ it 'should return a new object with the same options' do
45
+ e = fake_exchange
46
+ m1 = Money(4, decimals: 3, exchange: e)
47
+ m2 = m1 * 2
48
+ expect(m2).to_not eql m1
49
+ expect(m2.decimals).to eq 3
50
+ expect(m2.exchange).to eq e
51
+ end
52
+
53
+ it 'round should return a money object with the same currency' do
54
+ expect(Money(1.9, 'ARS').round).to eq Money(2, 'ARS')
55
+ end
56
+ end
57
+
58
+ context 'comparisson' do
59
+ it 'two money objects are equal when the amount and currency are the same' do
60
+ expect(Money(1.01)).to eq Money(1.01)
61
+ expect(Money(1.01)).not_to eq Money(1.02)
62
+ expect(Money(1, 'ARS')).to eq Money(1, 'ARS')
63
+ expect(Money(1, 'USD')).not_to eq Money(1, 'ARS')
64
+ end
65
+
66
+ it 'allows to compare against numeric values when using the default currency' do
67
+ expect(Money(1)).to eq 1
68
+ expect(Money(1.35)).to eq 1.35
69
+ expect(Money(1)).not_to eq 2
70
+ expect(Money(1, 'ARS')).not_to eq 1
71
+ end
72
+
73
+ it 'should exchange to the source currency if they differ' do
74
+ with_config do |config|
75
+ config.default_exchange = fake_exchange(rate: 4)
76
+
77
+ expect(Money(3, 'ARS') < Money(1, 'USD')).to be true
78
+ expect(Money(4, 'ARS') < Money(1, 'USD')).to be false
79
+ end
80
+ end
81
+
82
+ it 'should work with uniq' do
83
+ expect([Money(1), Money(1)].uniq.size).to eq 1
84
+ expect([Money(1), Money(1.1)].uniq.size).to eq 2
85
+ expect([Money(1, 'ARS'), Money(1, 'USD')].uniq.size).to eq 2
86
+ end
87
+ end
88
+
89
+ context 'to_s' do
90
+ it 'should add the currency symbol' do
91
+ expect(Money(3.25).to_s).to eq '$3.25'
92
+
93
+ with_config do |config|
94
+ config.default_exchange = double 'exchange', available_currencies: [{code: 'EUR', symbol: '€'}, {code: 'JPY', symbol: '¥'}]
95
+
96
+ expect(Money(1, 'EUR').to_s).to eq '€1.00'
97
+ expect(Money(1, 'JPY').to_s).to eq '¥1.00'
98
+ expect(Money(1, 'OTHER').to_s).to eq '$1.00'
99
+ end
100
+ end
101
+
102
+ it 'should round according to decimals' do
103
+ expect(Money(3.256, decimals: 2).to_s).to eq '$3.26'
104
+ expect(Money(3.2517, decimals: 3).to_s).to eq '$3.252'
105
+ end
106
+
107
+ it 'nil should be like zero' do
108
+ expect(Money(nil).to_s).to eq '$0.00'
109
+ end
110
+ end
111
+
112
+ context '.inspect' do
113
+ it 'should display the object internals' do
114
+ expect(Money(10.25, 'ARS', decimals: 3).inspect).to eq '#<Danconia::Money 10.25 ARS>'
115
+ end
116
+ end
117
+
118
+ context 'exchange_to' do
119
+ it 'should use the exchange passed to the instance to get the rate' do
120
+ expect(Money(2, 'USD', exchange: fake_exchange(rate: 3)).exchange_to('ARS')).to eq Money(6, 'ARS')
121
+ end
122
+
123
+ it 'should use the default exchange if not set' do
124
+ with_config do |config|
125
+ config.default_exchange = Exchanges::FixedRates.new(rates: {'USDEUR' => 3, 'USDARS' => 4})
126
+ expect(Money(2, 'USD').exchange_to('EUR')).to eq Money(6, 'EUR')
127
+ expect(Money(2, 'USD').exchange_to('ARS')).to eq Money(8, 'ARS')
128
+ end
129
+ end
130
+
131
+ it 'if no rate if found should raise error' do
132
+ expect { Money(2, 'USD').exchange_to('ARS') }.to raise_error Errors::ExchangeRateNotFound
133
+ end
134
+
135
+ it 'exchange between the same currency is always 1' do
136
+ expect(Money(5, 'EUR').exchange_to('EUR')).to eq Money(5, 'EUR')
137
+ end
138
+
139
+ it 'should return a new object with the same opts' do
140
+ m1 = Money(1, 'USD', decimals: 0, exchange: fake_exchange(rate: 3))
141
+ m2 = m1.exchange_to('ARS')
142
+ expect(m2).to_not eql m1
143
+ expect(m2.decimals).to eq 0
144
+ expect(m1).to eq Money(1, 'USD')
145
+ end
146
+ end
147
+
148
+ context 'delegation' do
149
+ it 'should delegate missing methods to the amount' do
150
+ expect(Money(10).positive?).to be true
151
+ expect(Money(10).respond_to?(:positive?)).to be true
152
+ end
153
+ end
154
+
155
+ def fake_exchange args = {}
156
+ double 'exchange', args.reverse_merge(rate: nil, available_currencies: [])
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,10 @@
1
+ require 'danconia'
2
+ require 'webmock/rspec'
3
+
4
+ Dir["#{__dir__}/support/*.rb"].each { |f| require f }
5
+
6
+ RSpec.configure do |config|
7
+ config.filter_run_when_matching :focus
8
+ config.include Helpers
9
+ config.include DatabaseCleaner
10
+ end
@@ -0,0 +1,12 @@
1
+ ActiveRecord::Migration.verbose = false
2
+ ActiveRecord::Base.establish_connection adapter: 'sqlite3', database: ':memory:'
3
+
4
+ module DatabaseCleaner
5
+ def self.included config
6
+ config.after :each, :active_record do
7
+ ActiveRecord::Base.connection.tables.each do |table|
8
+ ActiveRecord::Base.connection.drop_table(table)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,7 @@
1
+ module Helpers
2
+ def with_config &block
3
+ old_config = Danconia.config.dup
4
+ Danconia.configure &block
5
+ Danconia.config = old_config
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,179 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: danconia
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.1
5
+ platform: ruby
6
+ authors:
7
+ - Emmanuel Nicolau
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-06-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 3.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 3.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: sqlite3
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: guard
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: guard-rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: webmock
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description: Multi-currency money library backed by BigDecimal
112
+ email:
113
+ - emmanicolau@gmail.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - ".gitignore"
119
+ - ".rspec"
120
+ - ".ruby-version"
121
+ - Gemfile
122
+ - Gemfile.lock
123
+ - Guardfile
124
+ - LICENSE.txt
125
+ - README.md
126
+ - Rakefile
127
+ - danconia.gemspec
128
+ - examples/basic.rb
129
+ - lib/danconia.rb
130
+ - lib/danconia/config.rb
131
+ - lib/danconia/currency.rb
132
+ - lib/danconia/errors/api_error.rb
133
+ - lib/danconia/errors/exchange_rate_not_found.rb
134
+ - lib/danconia/exchange.rb
135
+ - lib/danconia/exchanges/currency_layer.rb
136
+ - lib/danconia/exchanges/fixed_rates.rb
137
+ - lib/danconia/integrations/active_record.rb
138
+ - lib/danconia/kernel.rb
139
+ - lib/danconia/money.rb
140
+ - lib/danconia/stores/in_memory.rb
141
+ - lib/danconia/version.rb
142
+ - spec/danconia/exchanges/currency_layer_spec.rb
143
+ - spec/danconia/exchanges/fixed_rates_spec.rb
144
+ - spec/danconia/integrations/active_record_spec.rb
145
+ - spec/danconia/money_spec.rb
146
+ - spec/spec_helper.rb
147
+ - spec/support/database.rb
148
+ - spec/support/helpers.rb
149
+ homepage: ''
150
+ licenses: []
151
+ metadata: {}
152
+ post_install_message:
153
+ rdoc_options: []
154
+ require_paths:
155
+ - lib
156
+ required_ruby_version: !ruby/object:Gem::Requirement
157
+ requirements:
158
+ - - ">="
159
+ - !ruby/object:Gem::Version
160
+ version: '0'
161
+ required_rubygems_version: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - ">="
164
+ - !ruby/object:Gem::Version
165
+ version: '0'
166
+ requirements: []
167
+ rubyforge_project:
168
+ rubygems_version: 2.7.3
169
+ signing_key:
170
+ specification_version: 4
171
+ summary: Multi-currency money library backed by BigDecimal
172
+ test_files:
173
+ - spec/danconia/exchanges/currency_layer_spec.rb
174
+ - spec/danconia/exchanges/fixed_rates_spec.rb
175
+ - spec/danconia/integrations/active_record_spec.rb
176
+ - spec/danconia/money_spec.rb
177
+ - spec/spec_helper.rb
178
+ - spec/support/database.rb
179
+ - spec/support/helpers.rb