shopify-money 0.10.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.
- 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
|