shopify-money 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.document +5 -0
- data/.gitignore +51 -0
- data/.rspec +1 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +20 -0
- data/README.md +156 -0
- data/Rakefile +42 -0
- data/bin/console +14 -0
- data/circle.yml +13 -0
- data/config/currency_historic.json +157 -0
- data/config/currency_iso.json +2642 -0
- data/config/currency_non_iso.json +82 -0
- data/dev.yml +9 -0
- data/lib/money.rb +10 -0
- data/lib/money/accounting_money_parser.rb +8 -0
- data/lib/money/core_extensions.rb +18 -0
- data/lib/money/currency.rb +59 -0
- data/lib/money/currency/loader.rb +26 -0
- data/lib/money/deprecations.rb +18 -0
- data/lib/money/helpers.rb +71 -0
- data/lib/money/money.rb +408 -0
- data/lib/money/money_parser.rb +152 -0
- data/lib/money/null_currency.rb +35 -0
- data/lib/money/version.rb +3 -0
- data/lib/money_accessor.rb +32 -0
- data/lib/money_column.rb +3 -0
- data/lib/money_column/active_record_hooks.rb +95 -0
- data/lib/money_column/active_record_type.rb +6 -0
- data/lib/money_column/railtie.rb +7 -0
- data/money.gemspec +27 -0
- data/spec/accounting_money_parser_spec.rb +204 -0
- data/spec/core_extensions_spec.rb +44 -0
- data/spec/currency/loader_spec.rb +21 -0
- data/spec/currency_spec.rb +113 -0
- data/spec/helpers_spec.rb +103 -0
- data/spec/money_accessor_spec.rb +86 -0
- data/spec/money_column_spec.rb +298 -0
- data/spec/money_parser_spec.rb +355 -0
- data/spec/money_spec.rb +853 -0
- data/spec/null_currency_spec.rb +46 -0
- data/spec/schema.rb +9 -0
- data/spec/spec_helper.rb +74 -0
- metadata +196 -0
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.shared_examples_for "an object supporting to_money" do
|
4
|
+
it "supports to_money" do
|
5
|
+
expect(@value.to_money).to eq(@money)
|
6
|
+
expect(@value.to_money('CAD').currency).to eq(Money::Currency.find!('CAD'))
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
RSpec.describe Integer do
|
11
|
+
before(:each) do
|
12
|
+
@value = 1
|
13
|
+
@money = Money.new("1.00")
|
14
|
+
end
|
15
|
+
|
16
|
+
it_should_behave_like "an object supporting to_money"
|
17
|
+
end
|
18
|
+
|
19
|
+
RSpec.describe Float do
|
20
|
+
before(:each) do
|
21
|
+
@value = 1.23
|
22
|
+
@money = Money.new("1.23")
|
23
|
+
end
|
24
|
+
|
25
|
+
it_should_behave_like "an object supporting to_money"
|
26
|
+
end
|
27
|
+
|
28
|
+
RSpec.describe String do
|
29
|
+
before(:each) do
|
30
|
+
@value = "1.23"
|
31
|
+
@money = Money.new(@value)
|
32
|
+
end
|
33
|
+
|
34
|
+
it_should_behave_like "an object supporting to_money"
|
35
|
+
end
|
36
|
+
|
37
|
+
RSpec.describe BigDecimal do
|
38
|
+
before(:each) do
|
39
|
+
@value = BigDecimal.new("1.23")
|
40
|
+
@money = Money.new("1.23")
|
41
|
+
end
|
42
|
+
|
43
|
+
it_should_behave_like "an object supporting to_money"
|
44
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Money::Currency::Loader do
|
4
|
+
|
5
|
+
describe 'load_currencies' do
|
6
|
+
it 'loads the iso currency file' do
|
7
|
+
expect(subject.load_currencies['usd']['iso_code']).to eq('USD')
|
8
|
+
expect(subject.load_currencies['usd']['symbol']).to eq('$')
|
9
|
+
expect(subject.load_currencies['usd']['subunit_to_unit']).to eq(100)
|
10
|
+
expect(subject.load_currencies['usd']['smallest_denomination']).to eq(1)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'loads the non iso currency file' do
|
14
|
+
expect(subject.load_currencies['jep']['iso_code']).to eq('JEP')
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'loads the historic iso currency file' do
|
18
|
+
expect(subject.load_currencies['eek']['iso_code']).to eq('EEK')
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe "Currency" do
|
4
|
+
CURRENCY_DATA = {
|
5
|
+
"iso_code": "USD",
|
6
|
+
"name": "United States Dollar",
|
7
|
+
"subunit_to_unit": 100,
|
8
|
+
"iso_numeric": "840",
|
9
|
+
"smallest_denomination": 1,
|
10
|
+
"minor_units": 2,
|
11
|
+
"symbol": '$',
|
12
|
+
"disambiguate_symbol": "US$",
|
13
|
+
"subunit_symbol": "¢",
|
14
|
+
"decimal_mark": ".",
|
15
|
+
}
|
16
|
+
|
17
|
+
let(:currency) { Money::Currency.new('usd') }
|
18
|
+
|
19
|
+
describe ".new" do
|
20
|
+
it "is constructable with a uppercase string" do
|
21
|
+
expect(Money::Currency.new('USD').iso_code).to eq('USD')
|
22
|
+
end
|
23
|
+
|
24
|
+
it "is constructable with a symbol" do
|
25
|
+
expect(Money::Currency.new(:usd).iso_code).to eq('USD')
|
26
|
+
end
|
27
|
+
|
28
|
+
it "is constructable with a lowercase string" do
|
29
|
+
expect(Money::Currency.new('usd').iso_code).to eq('USD')
|
30
|
+
end
|
31
|
+
|
32
|
+
it "raises when the currency is invalid" do
|
33
|
+
expect { Money::Currency.new('yyy') }.to raise_error(Money::Currency::UnknownCurrency)
|
34
|
+
end
|
35
|
+
|
36
|
+
it "raises when the currency is nil" do
|
37
|
+
expect { Money::Currency.new(nil) }.to raise_error(Money::Currency::UnknownCurrency)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe ".find" do
|
42
|
+
it "returns nil when the currency is invalid" do
|
43
|
+
expect(Money::Currency.find('yyy')).to eq(nil)
|
44
|
+
end
|
45
|
+
|
46
|
+
it "returns a valid currency" do
|
47
|
+
expect(Money::Currency.find('usd')).to eq(Money::Currency.new('usd'))
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe ".find!" do
|
52
|
+
it "raises when the currency is invalid" do
|
53
|
+
expect { Money::Currency.find!('yyy') }.to raise_error(Money::Currency::UnknownCurrency)
|
54
|
+
end
|
55
|
+
|
56
|
+
it "returns a valid currency" do
|
57
|
+
expect(Money::Currency.find!('CAD')).to eq(Money::Currency.new('CAD'))
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
CURRENCY_DATA.each do |attribute, value|
|
62
|
+
describe "##{attribute}" do
|
63
|
+
it 'returns the correct value' do
|
64
|
+
expect(currency.public_send(attribute)).to eq(value)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe "#eql?" do
|
70
|
+
it "returns true when both objects represent the same currency" do
|
71
|
+
expect(currency.eql?(Money.new(1, 'USD').currency)).to eq(true)
|
72
|
+
end
|
73
|
+
|
74
|
+
it "returns false when the currency iso is different" do
|
75
|
+
expect(currency.eql?(Money.new(1, 'CAD').currency)).to eq(false)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe "==" do
|
80
|
+
it "returns true when both objects have the same currency" do
|
81
|
+
expect(currency == Money.new(1, 'USD').currency).to eq(true)
|
82
|
+
end
|
83
|
+
|
84
|
+
it "returns false when the currency iso is different" do
|
85
|
+
expect(currency == Money.new(1, 'CAD').currency).to eq(false)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
describe "#to_s" do
|
90
|
+
it "to return the iso code string" do
|
91
|
+
expect(currency.to_s).to eq('USD')
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
describe "#compatible" do
|
96
|
+
|
97
|
+
it "returns true for the same currency" do
|
98
|
+
expect(currency.compatible?(Money::Currency.new('USD'))).to eq(true)
|
99
|
+
end
|
100
|
+
|
101
|
+
it "returns true for null_currency" do
|
102
|
+
expect(currency.compatible?(Money::NULL_CURRENCY)).to eq(true)
|
103
|
+
end
|
104
|
+
|
105
|
+
it "returns false for nil" do
|
106
|
+
expect(currency.compatible?(nil)).to eq(false)
|
107
|
+
end
|
108
|
+
|
109
|
+
it "returns false for a different currency" do
|
110
|
+
expect(currency.compatible?(Money::Currency.new('JPY'))).to eq(false)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Money::Helpers do
|
4
|
+
|
5
|
+
describe 'value_to_decimal' do
|
6
|
+
let (:amount) { BigDecimal.new('1.23') }
|
7
|
+
let (:money) { Money.new(amount) }
|
8
|
+
|
9
|
+
it 'returns the value of a money object' do
|
10
|
+
expect(subject.value_to_decimal(money)).to eq(amount)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'returns itself if it is already a big decimal' do
|
14
|
+
expect(subject.value_to_decimal(BigDecimal.new('1.23'))).to eq(amount)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'returns zero when nil' do
|
18
|
+
expect(subject.value_to_decimal(nil)).to eq(0)
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'returns zero when empty' do
|
22
|
+
expect(subject.value_to_decimal('')).to eq(0)
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'returns the bigdecimal version of a integer' do
|
26
|
+
expect(subject.value_to_decimal(1)).to eq(BigDecimal.new('1'))
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'returns the bigdecimal version of a float' do
|
30
|
+
expect(subject.value_to_decimal(1.23)).to eq(amount)
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'returns the bigdecimal version of a rational' do
|
34
|
+
expect(subject.value_to_decimal(amount.to_r)).to eq(amount)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'returns the bigdecimal version of a ruby number string' do
|
38
|
+
expect(subject.value_to_decimal('1.23')).to eq(amount)
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'returns the bigdecimal version of a ruby number string with whitespace padding' do
|
42
|
+
expect(subject.value_to_decimal(' 1.23 ')).to eq(amount)
|
43
|
+
expect(subject.value_to_decimal("1.23\n")).to eq(amount)
|
44
|
+
expect(subject.value_to_decimal(' -1.23 ')).to eq(-amount)
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'invalid string returns zero' do
|
48
|
+
expect(Money).to receive(:deprecate).once
|
49
|
+
expect(subject.value_to_decimal('invalid')).to eq(0)
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'returns the bigdecimal representation of numbers while they are deprecated' do
|
53
|
+
expect(Money).to receive(:deprecate).exactly(2).times
|
54
|
+
expect(subject.value_to_decimal('1.23abc')).to eq(amount)
|
55
|
+
expect(subject.value_to_decimal("1.23\n23")).to eq(amount)
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'raises on invalid object' do
|
59
|
+
expect { subject.value_to_decimal(OpenStruct.new(amount: 1)) }.to raise_error(ArgumentError)
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'returns regular zero for a negative zero value' do
|
63
|
+
expect(subject.value_to_decimal(-BigDecimal.new(0))).to eq(BigDecimal.new(0))
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe 'subject.value_to_currency' do
|
68
|
+
it 'returns itself if it is already a currency' do
|
69
|
+
expect(subject.value_to_currency(Money::Currency.new('usd'))).to eq(Money::Currency.find!('usd'))
|
70
|
+
expect(subject.value_to_currency(Money::NULL_CURRENCY)).to be_a(Money::NullCurrency)
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'returns the default currency when value is nil' do
|
74
|
+
expect(subject.value_to_currency(nil)).to eq(Money.default_currency)
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'returns the default currency when value is empty' do
|
78
|
+
expect(subject.value_to_currency('')).to eq(Money.default_currency)
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'returns the default currency when value is xxx' do
|
82
|
+
expect(subject.value_to_currency('xxx')).to eq(Money::NULL_CURRENCY)
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'returns the current currency when value is set' do
|
86
|
+
expect(Money.with_currency('USD') { subject.value_to_currency(nil) }).to eq(Money::Currency.find!('usd'))
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'returns the matching currency' do
|
90
|
+
expect(subject.value_to_currency('usd')).to eq(Money::Currency.new('USD'))
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'returns the null currency when invalid iso is passed' do
|
94
|
+
expect(Money).to receive(:deprecate).once
|
95
|
+
expect(subject.value_to_currency('invalid')).to eq(Money::NULL_CURRENCY)
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'raises on invalid object' do
|
99
|
+
expect { subject.value_to_currency(OpenStruct.new(amount: 1)) }.to raise_error(ArgumentError)
|
100
|
+
expect { subject.value_to_currency(1) }.to raise_error(ArgumentError)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class NormalObject
|
4
|
+
include MoneyAccessor
|
5
|
+
|
6
|
+
money_accessor :price
|
7
|
+
|
8
|
+
def initialize(price)
|
9
|
+
@price = price
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class StructObject < Struct.new(:price)
|
14
|
+
include MoneyAccessor
|
15
|
+
|
16
|
+
money_accessor :price
|
17
|
+
end
|
18
|
+
|
19
|
+
RSpec.shared_examples_for "an object with a money accessor" do
|
20
|
+
it "generates an attribute reader that returns a money object" do
|
21
|
+
object = described_class.new(100)
|
22
|
+
|
23
|
+
expect(object.price).to eq(Money.new(100))
|
24
|
+
end
|
25
|
+
|
26
|
+
it "generates an attribute reader that returns a nil object if the value was nil" do
|
27
|
+
object = described_class.new(nil)
|
28
|
+
|
29
|
+
expect(object.price).to eq(nil)
|
30
|
+
end
|
31
|
+
|
32
|
+
it "generates an attribute reader that returns a nil object if the value was blank" do
|
33
|
+
object = described_class.new('')
|
34
|
+
|
35
|
+
expect(object.price).to eq(nil)
|
36
|
+
end
|
37
|
+
|
38
|
+
it "generates an attribute writer that allow setting a money object" do
|
39
|
+
object = described_class.new(0)
|
40
|
+
object.price = Money.new(10)
|
41
|
+
|
42
|
+
expect(object.price).to eq(Money.new(10))
|
43
|
+
end
|
44
|
+
|
45
|
+
it "generates an attribute writer that allow setting a integer value" do
|
46
|
+
object = described_class.new(0)
|
47
|
+
object.price = 10
|
48
|
+
|
49
|
+
expect(object.price).to eq(Money.new(10))
|
50
|
+
end
|
51
|
+
|
52
|
+
it "generates an attribute writer that allow setting a float value" do
|
53
|
+
object = described_class.new(0)
|
54
|
+
object.price = 10.12
|
55
|
+
|
56
|
+
expect(object.price).to eq(Money.new(10.12))
|
57
|
+
end
|
58
|
+
|
59
|
+
it "generates an attribute writer that allow setting a nil value" do
|
60
|
+
object = described_class.new(0)
|
61
|
+
object.price = nil
|
62
|
+
|
63
|
+
expect(object.price).to eq(nil)
|
64
|
+
end
|
65
|
+
|
66
|
+
it "generates an attribute writer that allow setting a blank value" do
|
67
|
+
object = described_class.new(0)
|
68
|
+
object.price = ''
|
69
|
+
|
70
|
+
expect(object.price).to eq(nil)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
RSpec.describe NormalObject do
|
75
|
+
it_behaves_like "an object with a money accessor"
|
76
|
+
end
|
77
|
+
|
78
|
+
RSpec.describe StructObject do
|
79
|
+
it_behaves_like "an object with a money accessor"
|
80
|
+
|
81
|
+
it 'does not generate an ivar to store the price value' do
|
82
|
+
object = described_class.new(10.00)
|
83
|
+
|
84
|
+
expect(object.instance_variable_get(:@price)).to eq(nil)
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,298 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class MoneyRecord < ActiveRecord::Base
|
4
|
+
RATE = 1.17
|
5
|
+
before_validation do
|
6
|
+
self.price_usd = Money.new(self["price"] * RATE, 'USD') if self["price"]
|
7
|
+
end
|
8
|
+
money_column :price, currency_column: 'currency'
|
9
|
+
money_column :prix, currency_column: :devise
|
10
|
+
money_column :price_usd, currency: 'USD'
|
11
|
+
end
|
12
|
+
|
13
|
+
class MoneyWithValidation < ActiveRecord::Base
|
14
|
+
self.table_name = 'money_records'
|
15
|
+
validates :price, :currency, presence: true
|
16
|
+
money_column :price, currency_column: 'currency'
|
17
|
+
end
|
18
|
+
|
19
|
+
class MoneyWithReadOnlyCurrency < ActiveRecord::Base
|
20
|
+
self.table_name = 'money_records'
|
21
|
+
money_column :price, currency_column: 'currency', currency_read_only: true
|
22
|
+
end
|
23
|
+
|
24
|
+
class MoneyRecordCoerceNull < ActiveRecord::Base
|
25
|
+
self.table_name = 'money_records'
|
26
|
+
money_column :price, currency_column: 'currency', coerce_null: true
|
27
|
+
money_column :price_usd, currency: 'USD', coerce_null: true
|
28
|
+
end
|
29
|
+
|
30
|
+
class MoneyWithDelegatedCurrency < ActiveRecord::Base
|
31
|
+
self.table_name = 'money_records'
|
32
|
+
attr_accessor :delegated_record
|
33
|
+
delegate :currency, to: :delegated_record
|
34
|
+
money_column :price, currency_column: 'currency', currency_read_only: true
|
35
|
+
money_column :prix, currency_column: 'currency2', currency_read_only: true
|
36
|
+
def currency2
|
37
|
+
delegated_record.currency
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
RSpec.describe 'MoneyColumn' do
|
42
|
+
let(:amount) { 1.23 }
|
43
|
+
let(:currency) { 'EUR' }
|
44
|
+
let(:money) { Money.new(amount, currency) }
|
45
|
+
let(:toonie) { Money.new(2.00, 'CAD') }
|
46
|
+
let(:subject) { MoneyRecord.new(price: money, prix: toonie) }
|
47
|
+
let(:record) do
|
48
|
+
subject.devise = 'CAD'
|
49
|
+
subject.save
|
50
|
+
subject.reload
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'returns money with currency from the default column' do
|
54
|
+
expect(record.price).to eq(Money.new(1.23, 'EUR'))
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'writes the currency to the db' do
|
58
|
+
record.update(currency: nil)
|
59
|
+
record.update(price: Money.new(4, 'JPY'))
|
60
|
+
record.reload
|
61
|
+
expect(record.price.value).to eq(4)
|
62
|
+
expect(record.price.currency.to_s).to eq('JPY')
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'duplicating the record keeps the money values' do
|
66
|
+
expect(MoneyRecord.new(price: money).clone.price).to eq(money)
|
67
|
+
expect(MoneyRecord.new(price: money).dup.price).to eq(money)
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'returns money with currency from the specified column' do
|
71
|
+
expect(record.prix).to eq(Money.new(2.00, 'CAD'))
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'returns money with the hardcoded currency' do
|
75
|
+
expect(record.price_usd).to eq(Money.new(1.44, 'USD'))
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'returns money with null currency when the currency in the DB is invalid' do
|
79
|
+
expect(Money).to receive(:deprecate).once
|
80
|
+
record.update_columns(currency: 'invalid')
|
81
|
+
record.reload
|
82
|
+
expect(record.price.currency).to be_a(Money::NullCurrency)
|
83
|
+
expect(record.price.value).to eq(1.23)
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'handles legacy support for saving floats' do
|
87
|
+
record.update(price: 3.21, prix: 3.21)
|
88
|
+
expect(record.price.value).to eq(3.21)
|
89
|
+
expect(record.price.currency.to_s).to eq(currency)
|
90
|
+
expect(record.price_usd.value).to eq(3.76)
|
91
|
+
expect(record.price_usd.currency.to_s).to eq('USD')
|
92
|
+
expect(record.prix.value).to eq(3.21)
|
93
|
+
expect(record.prix.currency.to_s).to eq('CAD')
|
94
|
+
end
|
95
|
+
|
96
|
+
it 'handles legacy support for saving floats with correct currency rounding' do
|
97
|
+
record.update(price: 3.2112, prix: 3.2156)
|
98
|
+
expect(record.attributes['price']).to eq(3.21)
|
99
|
+
expect(record.price.value).to eq(3.21)
|
100
|
+
expect(record.price.currency.to_s).to eq(currency)
|
101
|
+
expect(record.attributes['prix']).to eq(3.22)
|
102
|
+
expect(record.prix.value).to eq(3.22)
|
103
|
+
expect(record.prix.currency.to_s).to eq('CAD')
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'handles legacy support for saving string' do
|
107
|
+
record.update(price: '3.21', prix: '3.21')
|
108
|
+
expect(record.price.value).to eq(3.21)
|
109
|
+
expect(record.price.currency.to_s).to eq(currency)
|
110
|
+
expect(record.price_usd.value).to eq(3.76)
|
111
|
+
expect(record.price_usd.currency.to_s).to eq('USD')
|
112
|
+
expect(record.prix.value).to eq(3.21)
|
113
|
+
expect(record.prix.currency.to_s).to eq('CAD')
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'does not overwrite a currency column with a default currency when saving zero' do
|
117
|
+
expect(record.currency.to_s).to eq('EUR')
|
118
|
+
record.update(price: Money.zero)
|
119
|
+
expect(record.currency.to_s).to eq('EUR')
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'does overwrite a currency if changed but will show a deprecation notice' do
|
123
|
+
expect(record.currency.to_s).to eq('EUR')
|
124
|
+
expect(Money).to receive(:deprecate).once
|
125
|
+
record.update(price: Money.new(4, 'JPY'))
|
126
|
+
expect(record.currency.to_s).to eq('JPY')
|
127
|
+
end
|
128
|
+
|
129
|
+
describe 'non-fractional-currencies' do
|
130
|
+
let(:money) { Money.new(1, 'JPY') }
|
131
|
+
|
132
|
+
it 'returns money with currency from the default column' do
|
133
|
+
expect(record.price).to eq(Money.new(1, 'JPY'))
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
describe 'three-decimal currencies' do
|
138
|
+
let(:money) { Money.new(1.234, 'JOD') }
|
139
|
+
|
140
|
+
it 'returns money with currency from the default column' do
|
141
|
+
expect(record.price).to eq(Money.new(1.234, 'JOD'))
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
describe 'garbage amount' do
|
146
|
+
let(:amount) { 'foo' }
|
147
|
+
|
148
|
+
it 'raises a deprecation warning' do
|
149
|
+
expect { subject }.to raise_error(ActiveSupport::DeprecationException)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
describe 'garbage currency' do
|
154
|
+
let(:currency) { 'foo' }
|
155
|
+
|
156
|
+
it 'raises an UnknownCurrency error' do
|
157
|
+
expect { subject }.to raise_error(ActiveSupport::DeprecationException)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
describe 'wrong money_column currency arguments' do
|
162
|
+
let(:subject) do
|
163
|
+
class MoneyWithWrongCurrencyArguments < ActiveRecord::Base
|
164
|
+
self.table_name = 'money_records'
|
165
|
+
money_column :price, currency_column: :currency, currency: 'USD'
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
it 'raises an ArgumentError' do
|
170
|
+
expect { subject }.to raise_error(ArgumentError, 'cannot set both currency_column and a fixed currency')
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
describe 'missing money_column currency arguments' do
|
175
|
+
let(:subject) do
|
176
|
+
class MoneyWithWrongCurrencyArguments < ActiveRecord::Base
|
177
|
+
self.table_name = 'money_records'
|
178
|
+
money_column :price
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
it 'raises an ArgumentError' do
|
183
|
+
expect { subject }.to raise_error(ArgumentError, 'must set one of :currency_column or :currency options')
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
describe 'null currency and validations' do
|
188
|
+
let(:currency) { Money::NULL_CURRENCY }
|
189
|
+
let(:subject) { MoneyWithValidation.new(price: money) }
|
190
|
+
|
191
|
+
it 'is not allowed to be saved because `to_s` returns a blank string' do
|
192
|
+
subject.valid?
|
193
|
+
expect(subject.errors[:currency]).to include("can't be blank")
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
describe 'read_only_currency true' do
|
198
|
+
it 'does not write the currency to the db' do
|
199
|
+
record = MoneyWithReadOnlyCurrency.create
|
200
|
+
record.update_columns(price: 1, currency: 'USD')
|
201
|
+
expect(Money).to receive(:deprecate).once
|
202
|
+
record.update(price: Money.new(4, 'CAD'))
|
203
|
+
expect(record.price.value).to eq(4)
|
204
|
+
expect(record.price.currency.to_s).to eq('USD')
|
205
|
+
end
|
206
|
+
|
207
|
+
it 'reads the currency that is already in the db' do
|
208
|
+
record = MoneyWithReadOnlyCurrency.create
|
209
|
+
record.update_columns(currency: 'USD', price: 1)
|
210
|
+
record.reload
|
211
|
+
expect(record.price.value).to eq(1)
|
212
|
+
expect(record.price.currency.to_s).to eq('USD')
|
213
|
+
end
|
214
|
+
|
215
|
+
it 'reads an invalid currency from the db and generates a no currency object' do
|
216
|
+
expect(Money).to receive(:deprecate).once
|
217
|
+
record = MoneyWithReadOnlyCurrency.create
|
218
|
+
record.update_columns(currency: 'invalid', price: 1)
|
219
|
+
record.reload
|
220
|
+
expect(record.price.value).to eq(1)
|
221
|
+
expect(record.price.currency.to_s).to eq('')
|
222
|
+
end
|
223
|
+
|
224
|
+
it 'sets the currency correctly when the currency is changed' do
|
225
|
+
record = MoneyWithReadOnlyCurrency.create(currency: 'CAD', price: 1)
|
226
|
+
record.currency = 'USD'
|
227
|
+
expect(record.price.currency.to_s).to eq('USD')
|
228
|
+
end
|
229
|
+
|
230
|
+
it 'handle cases where the delegate allow_nil is false' do
|
231
|
+
record = MoneyWithDelegatedCurrency.new(price: Money.new(10, 'USD'), delegated_record: MoneyRecord.new(currency: 'USD'))
|
232
|
+
expect(record.price.currency.to_s).to eq('USD')
|
233
|
+
end
|
234
|
+
|
235
|
+
it 'handle cases where a manual delegate does not allow nil' do
|
236
|
+
record = MoneyWithDelegatedCurrency.new(prix: Money.new(10, 'USD'), delegated_record: MoneyRecord.new(currency: 'USD'))
|
237
|
+
expect(record.price.currency.to_s).to eq('USD')
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
describe 'coerce_null' do
|
242
|
+
it 'returns nil when money value have not been set and coerce_null is false' do
|
243
|
+
record = MoneyRecord.new(price: nil)
|
244
|
+
expect(record.price).to eq(nil)
|
245
|
+
expect(record.price_usd).to eq(nil)
|
246
|
+
end
|
247
|
+
|
248
|
+
it 'returns 0$ when money value have not been set and coerce_null is true' do
|
249
|
+
record = MoneyRecordCoerceNull.new(price: nil)
|
250
|
+
expect(record.price.value).to eq(0)
|
251
|
+
expect(record.price_usd.value).to eq(0)
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
describe 'memoization' do
|
256
|
+
it 'correctly memoizes the read value' do
|
257
|
+
expect(record.instance_variable_get(:@money_column_cache)["price"]).to eq(nil)
|
258
|
+
price = Money.new(1, 'USD')
|
259
|
+
record = MoneyRecord.new(price: price)
|
260
|
+
expect(record.price).to eq(price)
|
261
|
+
expect(record.instance_variable_get(:@money_column_cache)["price"]).to eq(price)
|
262
|
+
end
|
263
|
+
|
264
|
+
it 'memoizes values get reset when writing a new value' do
|
265
|
+
price = Money.new(1, 'USD')
|
266
|
+
record = MoneyRecord.new(price: price)
|
267
|
+
expect(record.price).to eq(price)
|
268
|
+
price = Money.new(2, 'USD')
|
269
|
+
record.update!(price: price)
|
270
|
+
expect(record.price).to eq(price)
|
271
|
+
expect(record.instance_variable_get(:@money_column_cache)["price"]).to eq(price)
|
272
|
+
end
|
273
|
+
|
274
|
+
it 'reload will clear memoizes money values' do
|
275
|
+
price = Money.new(1, 'USD')
|
276
|
+
record = MoneyRecord.create(price: price)
|
277
|
+
expect(record.price).to eq(price)
|
278
|
+
expect(record.instance_variable_get(:@money_column_cache)["price"]).to eq(price)
|
279
|
+
record.reload
|
280
|
+
expect(record.instance_variable_get(:@money_column_cache)["price"]).to eq(nil)
|
281
|
+
record.price
|
282
|
+
expect(record.instance_variable_get(:@money_column_cache)["price"]).to eq(price)
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
describe 'ActiveRecord querying' do
|
287
|
+
it 'can be serialized for querying on the value' do
|
288
|
+
price = Money.new(1, 'USD')
|
289
|
+
record = MoneyRecord.create!(price: price)
|
290
|
+
expect(MoneyRecord.find_by(price: price)).to eq(record)
|
291
|
+
end
|
292
|
+
|
293
|
+
it 'nil value persist in the DB' do
|
294
|
+
record = MoneyRecord.create!(price: nil)
|
295
|
+
expect(MoneyRecord.find_by(price: nil)).to eq(record)
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|