shopify-money 0.16.0 → 1.0.0.pre
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +7 -1
- data/UPGRADING.md +57 -0
- data/lib/money.rb +1 -0
- data/lib/money/allocator.rb +3 -1
- data/lib/money/config.rb +26 -0
- data/lib/money/helpers.rb +8 -4
- data/lib/money/money.rb +31 -13
- data/lib/money/money_parser.rb +11 -5
- data/lib/money/version.rb +1 -1
- data/lib/money_column/active_record_hooks.rb +12 -7
- data/spec/accounting_money_parser_spec.rb +4 -2
- data/spec/config_spec.rb +60 -0
- data/spec/helpers_spec.rb +11 -5
- data/spec/money_column_spec.rb +54 -33
- data/spec/money_parser_spec.rb +19 -7
- data/spec/money_spec.rb +85 -39
- data/spec/spec_helper.rb +15 -0
- metadata +8 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ddeb1f02ca7fda02ed007433687bf2fdbce36e9b11d64724753cf2e2420507c0
|
4
|
+
data.tar.gz: de0de61cbe5c6ae02b2fbf333e4136edbfcd1c2388441c219ffb1a00e2d0a963
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 86ef224eb66a9d8d6249b921cdd9162594babac0e5fa7525f2e9e5125ad94ab02e395bef9af508d4f6d08622d2c3fa6628b902aac3de803ef81fd30fcae0464c
|
7
|
+
data.tar.gz: 7332ed67b6d7389123131538c7c05faae5b9e67a4b77ae66c21f0d2bbd7031f4994576fdaa14c54294ad2969c70ffe1e70958528f16116e944fa6457d5c80d6e
|
data/README.md
CHANGED
@@ -20,6 +20,10 @@ money_column expects a DECIMAL(21,3) database field.
|
|
20
20
|
|
21
21
|
gem 'shopify-money', require: 'money'
|
22
22
|
|
23
|
+
## Upgrading to v1.0
|
24
|
+
|
25
|
+
see instructions and breaking changes: https://github.com/Shopify/money/blob/master/UPGRADING.md
|
26
|
+
|
23
27
|
## Usage
|
24
28
|
|
25
29
|
``` ruby
|
@@ -77,7 +81,9 @@ By default `Money` defaults to Money::NullCurrency as its currency. This is a
|
|
77
81
|
global variable that can be changed using:
|
78
82
|
|
79
83
|
``` ruby
|
80
|
-
Money.
|
84
|
+
Money.configure do |config|
|
85
|
+
config.default_currency = Money::Currency.new("USD")
|
86
|
+
end
|
81
87
|
```
|
82
88
|
|
83
89
|
In web apps you might want to set the default currency on a per request basis.
|
data/UPGRADING.md
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
## Upgrading to v1.0
|
2
|
+
|
3
|
+
In an initializer add the following
|
4
|
+
```ruby
|
5
|
+
Money.configure do |config|
|
6
|
+
config.legacy_default_currency!
|
7
|
+
config.legacy_deprecations!
|
8
|
+
config.legacy_json_format!
|
9
|
+
#...
|
10
|
+
end
|
11
|
+
```
|
12
|
+
|
13
|
+
Remove each legacy setting making sure your app functions as expected.
|
14
|
+
|
15
|
+
### Legacy support
|
16
|
+
|
17
|
+
#### legacy_default_currency!
|
18
|
+
|
19
|
+
By enabling this setting your app will accept money object that are missing a currency
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
Money.new(1) #=> value: 1, currency: XXX
|
23
|
+
```
|
24
|
+
|
25
|
+
#### legacy_deprecations!
|
26
|
+
|
27
|
+
invalid money values return zero
|
28
|
+
```ruby
|
29
|
+
Money.new('a', 'USD') #=> Money.new(0, 'USD')
|
30
|
+
```
|
31
|
+
|
32
|
+
invalid currency is ignored
|
33
|
+
```ruby
|
34
|
+
Money.new(1, 'ABCD') #=> Money.new(1)
|
35
|
+
```
|
36
|
+
|
37
|
+
mathematical operations between objects are allowed
|
38
|
+
```ruby
|
39
|
+
Money.new(1, 'USD') + Money.new(1, 'CAD') #=> Money.new(2, 'USD')
|
40
|
+
```
|
41
|
+
|
42
|
+
parsing a string with invalid delimiters
|
43
|
+
```ruby
|
44
|
+
Money.parse('123*12') #=> Money.new(123)
|
45
|
+
```
|
46
|
+
|
47
|
+
#### legacy_json_format!
|
48
|
+
|
49
|
+
to_json will return only the value (no currency)
|
50
|
+
```ruby
|
51
|
+
# with legacy_json_format!
|
52
|
+
money.to_json #=> "1"
|
53
|
+
|
54
|
+
# without
|
55
|
+
money.to_json #=> { value: 1, currency: 'USD' }
|
56
|
+
```
|
57
|
+
|
data/lib/money.rb
CHANGED
@@ -4,6 +4,7 @@ require_relative 'money/helpers'
|
|
4
4
|
require_relative 'money/currency'
|
5
5
|
require_relative 'money/null_currency'
|
6
6
|
require_relative 'money/allocator'
|
7
|
+
require_relative 'money/config'
|
7
8
|
require_relative 'money/money'
|
8
9
|
require_relative 'money/errors'
|
9
10
|
require_relative 'money/deprecations'
|
data/lib/money/allocator.rb
CHANGED
@@ -7,6 +7,8 @@ class Money
|
|
7
7
|
super
|
8
8
|
end
|
9
9
|
|
10
|
+
ONE = BigDecimal("1")
|
11
|
+
|
10
12
|
# Allocates money between different parties without losing pennies.
|
11
13
|
# After the mathematically split has been performed, left over pennies will
|
12
14
|
# be distributed round-robin amongst the parties. This means that parties
|
@@ -43,7 +45,7 @@ class Money
|
|
43
45
|
splits.map!(&:to_r)
|
44
46
|
allocations = splits.inject(0, :+)
|
45
47
|
|
46
|
-
if (allocations -
|
48
|
+
if (allocations - ONE) > Float::EPSILON
|
47
49
|
raise ArgumentError, "splits add to more than 100%"
|
48
50
|
end
|
49
51
|
|
data/lib/money/config.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Money
|
4
|
+
class Config
|
5
|
+
attr_accessor :parser, :default_currency, :legacy_json_format, :legacy_deprecations
|
6
|
+
|
7
|
+
def legacy_default_currency!
|
8
|
+
@default_currency ||= Money::NULL_CURRENCY
|
9
|
+
end
|
10
|
+
|
11
|
+
def legacy_deprecations!
|
12
|
+
@legacy_deprecations = true
|
13
|
+
end
|
14
|
+
|
15
|
+
def legacy_json_format!
|
16
|
+
@legacy_json_format = true
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize
|
20
|
+
@parser = MoneyParser
|
21
|
+
@default_currency = nil
|
22
|
+
@legacy_json_format = false
|
23
|
+
@legacy_deprecations = false
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/money/helpers.rb
CHANGED
@@ -28,7 +28,7 @@ class Money
|
|
28
28
|
when Rational
|
29
29
|
BigDecimal(num, MAX_DECIMAL)
|
30
30
|
when String
|
31
|
-
decimal = BigDecimal(num, exception:
|
31
|
+
decimal = BigDecimal(num, exception: !Money.config.legacy_deprecations)
|
32
32
|
return decimal if decimal
|
33
33
|
|
34
34
|
Money.deprecate("using Money.new('#{num}') is deprecated and will raise an ArgumentError in the next major release")
|
@@ -46,7 +46,7 @@ class Money
|
|
46
46
|
currency
|
47
47
|
when nil, ''
|
48
48
|
default = Money.current_currency || Money.default_currency
|
49
|
-
raise(
|
49
|
+
raise(Money::Currency::UnknownCurrency, 'missing currency') if default.nil? || default == ''
|
50
50
|
value_to_currency(default)
|
51
51
|
when 'xxx', 'XXX'
|
52
52
|
Money::NULL_CURRENCY
|
@@ -54,8 +54,12 @@ class Money
|
|
54
54
|
begin
|
55
55
|
Currency.find!(currency)
|
56
56
|
rescue Money::Currency::UnknownCurrency => error
|
57
|
-
Money.
|
58
|
-
|
57
|
+
if Money.config.legacy_deprecations
|
58
|
+
Money.deprecate(error.message)
|
59
|
+
Money::NULL_CURRENCY
|
60
|
+
else
|
61
|
+
raise error
|
62
|
+
end
|
59
63
|
end
|
60
64
|
else
|
61
65
|
raise ArgumentError, "could not parse as currency #{currency.inspect}"
|
data/lib/money/money.rb
CHANGED
@@ -11,7 +11,14 @@ class Money
|
|
11
11
|
def_delegators :@value, :zero?, :nonzero?, :positive?, :negative?, :to_i, :to_f, :hash
|
12
12
|
|
13
13
|
class << self
|
14
|
-
|
14
|
+
extend Forwardable
|
15
|
+
attr_accessor :config
|
16
|
+
def_delegators :@config, :parser, :parser=, :default_currency, :default_currency=
|
17
|
+
|
18
|
+
def configure
|
19
|
+
self.config ||= Config.new
|
20
|
+
yield(config) if block_given?
|
21
|
+
end
|
15
22
|
|
16
23
|
def new(value = 0, currency = nil)
|
17
24
|
value = Helpers.value_to_decimal(value)
|
@@ -73,13 +80,8 @@ class Money
|
|
73
80
|
Money.current_currency = old_currency
|
74
81
|
end
|
75
82
|
end
|
76
|
-
|
77
|
-
def default_settings
|
78
|
-
self.parser = MoneyParser
|
79
|
-
self.default_currency = Money::NULL_CURRENCY
|
80
|
-
end
|
81
83
|
end
|
82
|
-
|
84
|
+
configure
|
83
85
|
|
84
86
|
def initialize(value, currency)
|
85
87
|
raise ArgumentError if value.nan?
|
@@ -138,7 +140,11 @@ class Money
|
|
138
140
|
|
139
141
|
def *(numeric)
|
140
142
|
unless numeric.is_a?(Numeric)
|
141
|
-
|
143
|
+
if Money.config.legacy_deprecations
|
144
|
+
Money.deprecate("Multiplying Money with #{numeric.class.name} is deprecated and will be removed in the next major release.")
|
145
|
+
else
|
146
|
+
raise ArgumentError, "Money objects can only be multiplied by a Numeric"
|
147
|
+
end
|
142
148
|
end
|
143
149
|
Money.new(value.to_r * numeric, currency)
|
144
150
|
end
|
@@ -198,8 +204,12 @@ class Money
|
|
198
204
|
|
199
205
|
curr = Helpers.value_to_currency(curr)
|
200
206
|
unless currency.compatible?(curr)
|
201
|
-
|
202
|
-
|
207
|
+
msg = "mathematical operation not permitted for Money objects with different currencies #{curr} and #{currency}"
|
208
|
+
if Money.config.legacy_deprecations
|
209
|
+
Money.deprecate("#{msg}. A Money::IncompatibleCurrencyError will raise in the next major release")
|
210
|
+
else
|
211
|
+
raise Money::IncompatibleCurrencyError, msg
|
212
|
+
end
|
203
213
|
end
|
204
214
|
|
205
215
|
self
|
@@ -230,11 +240,19 @@ class Money
|
|
230
240
|
end
|
231
241
|
|
232
242
|
def to_json(options = {})
|
233
|
-
|
243
|
+
if options.delete(:legacy_format) || Money.config.legacy_json_format
|
244
|
+
to_s
|
245
|
+
else
|
246
|
+
as_json(options).to_json
|
247
|
+
end
|
234
248
|
end
|
235
249
|
|
236
|
-
def as_json(
|
237
|
-
|
250
|
+
def as_json(options = {})
|
251
|
+
if options.delete(:legacy_format) || Money.config.legacy_json_format
|
252
|
+
to_s
|
253
|
+
else
|
254
|
+
{ value: to_s(:amount), currency: currency.to_s }
|
255
|
+
end
|
238
256
|
end
|
239
257
|
|
240
258
|
def abs
|
data/lib/money/money_parser.rb
CHANGED
@@ -82,9 +82,12 @@ class MoneyParser
|
|
82
82
|
number = number.to_s.strip
|
83
83
|
|
84
84
|
if number.empty?
|
85
|
-
|
86
|
-
|
87
|
-
|
85
|
+
if Money.config.legacy_deprecations && !strict
|
86
|
+
Money.deprecate("invalid money strings will raise in the next major release \"#{input}\"")
|
87
|
+
return '0'
|
88
|
+
else
|
89
|
+
raise MoneyFormatError, "invalid money string: #{input}"
|
90
|
+
end
|
88
91
|
end
|
89
92
|
|
90
93
|
marks = number.scan(/[#{ESCAPED_MARKS}]/).flatten
|
@@ -107,8 +110,11 @@ class MoneyParser
|
|
107
110
|
return amount.tr(ESCAPED_NON_COMMA_MARKS, '').sub(',', '.')
|
108
111
|
end
|
109
112
|
|
110
|
-
|
111
|
-
|
113
|
+
if Money.config.legacy_deprecations && !strict
|
114
|
+
Money.deprecate("invalid money strings will raise in the next major release \"#{input}\"")
|
115
|
+
else
|
116
|
+
raise MoneyFormatError, "invalid money string: #{input}"
|
117
|
+
end
|
112
118
|
|
113
119
|
normalize_number(number, marks, currency)
|
114
120
|
end
|
data/lib/money/version.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module MoneyColumn
|
3
|
+
class CurrencyReadOnlyError < StandardError; end
|
4
|
+
|
3
5
|
module ActiveRecordHooks
|
4
6
|
def self.included(base)
|
5
7
|
base.extend(ClassMethods)
|
@@ -49,16 +51,19 @@ module MoneyColumn
|
|
49
51
|
return self[column] = nil
|
50
52
|
end
|
51
53
|
|
52
|
-
|
53
|
-
|
54
|
-
if !money.is_a?(Money)
|
55
|
-
return self[column] = Money.new(money, currency_raw_source).value
|
54
|
+
unless money.is_a?(Money)
|
55
|
+
return self[column] = Money::Helpers.value_to_decimal(money)
|
56
56
|
end
|
57
57
|
|
58
58
|
if options[:currency_read_only]
|
59
|
-
|
60
|
-
if
|
61
|
-
|
59
|
+
currency = options[:currency] || try(options[:currency_column])
|
60
|
+
if currency && !money.currency.compatible?(Money::Helpers.value_to_currency(currency))
|
61
|
+
msg = "[money_column] currency mismatch between #{currency} and #{money.currency} in column #{column}."
|
62
|
+
if Money.config.legacy_deprecations
|
63
|
+
Money.deprecate(msg)
|
64
|
+
else
|
65
|
+
raise MoneyColumn::CurrencyReadOnlyError, msg
|
66
|
+
end
|
62
67
|
end
|
63
68
|
else
|
64
69
|
self[options[:currency_column]] = money.currency.to_s unless money.no_currency?
|
@@ -20,8 +20,10 @@ RSpec.describe AccountingMoneyParser do
|
|
20
20
|
end
|
21
21
|
|
22
22
|
it "parses an invalid string to $0" do
|
23
|
-
|
24
|
-
|
23
|
+
configure(legacy_deprecations: true) do
|
24
|
+
expect(Money).to receive(:deprecate).once
|
25
|
+
expect(@parser.parse("no money", 'USD')).to eq(Money.new(0, 'USD'))
|
26
|
+
end
|
25
27
|
end
|
26
28
|
|
27
29
|
it "parses a single digit integer string" do
|
data/spec/config_spec.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
RSpec.describe "Money::Config" do
|
5
|
+
describe 'legacy_deprecations' do
|
6
|
+
it "respects the default currency" do
|
7
|
+
configure(default_currency: 'USD', legacy_deprecations: true) do
|
8
|
+
expect(Money.default_currency).to eq("USD")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'defaults to not opt-in to v1' do
|
13
|
+
expect(Money::Config.new.legacy_deprecations).to eq(false)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'legacy_deprecations returns true when opting in to v1' do
|
17
|
+
configure(legacy_deprecations: true) do
|
18
|
+
expect(Money.config.legacy_deprecations).to eq(true)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'sets the deprecations to raise' do
|
23
|
+
configure(legacy_deprecations: true) do
|
24
|
+
expect { Money.deprecate("test") }.to raise_error(ActiveSupport::DeprecationException)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'legacy_deprecations defaults to NULL_CURRENCY' do
|
29
|
+
configure(legacy_default_currency: true) do
|
30
|
+
expect(Money.config.default_currency).to eq(Money::NULL_CURRENCY)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe 'parser' do
|
36
|
+
it 'defaults to MoneyParser' do
|
37
|
+
expect(Money::Config.new.parser).to eq(MoneyParser)
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'can be set to a new parser' do
|
41
|
+
configure(parser: AccountingMoneyParser) do
|
42
|
+
expect(Money.config.parser).to eq(AccountingMoneyParser)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe 'default_currency' do
|
48
|
+
it 'defaults to nil' do
|
49
|
+
configure do
|
50
|
+
expect(Money.config.default_currency).to eq(nil)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'can be set to a new currency' do
|
55
|
+
configure(default_currency: 'USD') do
|
56
|
+
expect(Money.config.default_currency).to eq('USD')
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/spec/helpers_spec.rb
CHANGED
@@ -46,8 +46,10 @@ RSpec.describe Money::Helpers do
|
|
46
46
|
end
|
47
47
|
|
48
48
|
it 'invalid string returns zero' do
|
49
|
-
|
50
|
-
|
49
|
+
configure(legacy_deprecations: true) do
|
50
|
+
expect(Money).to receive(:deprecate).once
|
51
|
+
expect(subject.value_to_decimal('invalid')).to eq(0)
|
52
|
+
end
|
51
53
|
end
|
52
54
|
|
53
55
|
it 'raises on invalid object' do
|
@@ -70,7 +72,9 @@ RSpec.describe Money::Helpers do
|
|
70
72
|
end
|
71
73
|
|
72
74
|
it 'returns the default currency when value is empty' do
|
73
|
-
|
75
|
+
configure(legacy_deprecations: true, default_currency: 'USD') do
|
76
|
+
expect(subject.value_to_currency('')).to eq(Money::Currency.new('USD'))
|
77
|
+
end
|
74
78
|
end
|
75
79
|
|
76
80
|
it 'returns the default currency when value is xxx' do
|
@@ -86,8 +90,10 @@ RSpec.describe Money::Helpers do
|
|
86
90
|
end
|
87
91
|
|
88
92
|
it 'returns the null currency when invalid iso is passed' do
|
89
|
-
|
90
|
-
|
93
|
+
configure(legacy_deprecations: true) do
|
94
|
+
expect(Money).to receive(:deprecate).once
|
95
|
+
expect(subject.value_to_currency('invalid')).to eq(Money::NULL_CURRENCY)
|
96
|
+
end
|
91
97
|
end
|
92
98
|
|
93
99
|
it 'raises on invalid object' do
|
data/spec/money_column_spec.rb
CHANGED
@@ -30,13 +30,18 @@ end
|
|
30
30
|
|
31
31
|
class MoneyWithDelegatedCurrency < ActiveRecord::Base
|
32
32
|
self.table_name = 'money_records'
|
33
|
-
attr_accessor :delegated_record
|
34
33
|
delegate :currency, to: :delegated_record
|
35
34
|
money_column :price, currency_column: 'currency', currency_read_only: true
|
36
35
|
money_column :prix, currency_column: 'currency2', currency_read_only: true
|
37
36
|
def currency2
|
38
37
|
delegated_record.currency
|
39
38
|
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def delegated_record
|
43
|
+
MoneyRecord.new(currency: 'USD')
|
44
|
+
end
|
40
45
|
end
|
41
46
|
|
42
47
|
class MoneyWithCustomAccessors < ActiveRecord::Base
|
@@ -97,11 +102,13 @@ RSpec.describe 'MoneyColumn' do
|
|
97
102
|
end
|
98
103
|
|
99
104
|
it 'returns money with null currency when the currency in the DB is invalid' do
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
+
configure(legacy_deprecations: true) do
|
106
|
+
expect(Money).to receive(:deprecate).once
|
107
|
+
record.update_columns(currency: 'invalid')
|
108
|
+
record.reload
|
109
|
+
expect(record.price.currency).to be_a(Money::NullCurrency)
|
110
|
+
expect(record.price.value).to eq(1.23)
|
111
|
+
end
|
105
112
|
end
|
106
113
|
|
107
114
|
it 'handles legacy support for saving floats' do
|
@@ -114,12 +121,12 @@ RSpec.describe 'MoneyColumn' do
|
|
114
121
|
expect(record.prix.currency.to_s).to eq('CAD')
|
115
122
|
end
|
116
123
|
|
117
|
-
it 'handles legacy support for saving floats
|
124
|
+
it 'handles legacy support for saving floats as provided' do
|
118
125
|
record.update(price: 3.2112, prix: 3.2156)
|
119
|
-
expect(record.attributes['price']).to eq(3.
|
126
|
+
expect(record.attributes['price']).to eq(3.2112)
|
120
127
|
expect(record.price.value).to eq(3.21)
|
121
128
|
expect(record.price.currency.to_s).to eq(currency)
|
122
|
-
expect(record.attributes['prix']).to eq(3.
|
129
|
+
expect(record.attributes['prix']).to eq(3.2156)
|
123
130
|
expect(record.prix.value).to eq(3.22)
|
124
131
|
expect(record.prix.currency.to_s).to eq('CAD')
|
125
132
|
end
|
@@ -165,8 +172,8 @@ RSpec.describe 'MoneyColumn' do
|
|
165
172
|
describe 'garbage amount' do
|
166
173
|
let(:amount) { 'foo' }
|
167
174
|
|
168
|
-
it 'raises
|
169
|
-
expect { subject }.to raise_error(
|
175
|
+
it 'raises an ArgumentError' do
|
176
|
+
expect { subject }.to raise_error(ArgumentError)
|
170
177
|
end
|
171
178
|
end
|
172
179
|
|
@@ -174,7 +181,7 @@ RSpec.describe 'MoneyColumn' do
|
|
174
181
|
let(:currency) { 'foo' }
|
175
182
|
|
176
183
|
it 'raises an UnknownCurrency error' do
|
177
|
-
expect { subject }.to raise_error(
|
184
|
+
expect { subject }.to raise_error(Money::Currency::UnknownCurrency)
|
178
185
|
end
|
179
186
|
end
|
180
187
|
|
@@ -217,11 +224,20 @@ RSpec.describe 'MoneyColumn' do
|
|
217
224
|
describe 'read_only_currency true' do
|
218
225
|
it 'does not write the currency to the db' do
|
219
226
|
record = MoneyWithReadOnlyCurrency.create
|
220
|
-
record.update_columns(
|
221
|
-
expect(Money).to
|
222
|
-
|
223
|
-
|
224
|
-
|
227
|
+
record.update_columns(currency: 'USD')
|
228
|
+
expect { record.update(price: Money.new(4, 'CAD')) }.to raise_error(MoneyColumn::CurrencyReadOnlyError)
|
229
|
+
end
|
230
|
+
|
231
|
+
it 'legacy_deprecations does not write the currency to the db' do
|
232
|
+
configure(legacy_deprecations: true) do
|
233
|
+
record = MoneyWithReadOnlyCurrency.create
|
234
|
+
record.update_columns(currency: 'USD')
|
235
|
+
|
236
|
+
expect(Money).to receive(:deprecate).once
|
237
|
+
record.update(price: Money.new(4, 'CAD'))
|
238
|
+
expect(record.price.value).to eq(4)
|
239
|
+
expect(record.price.currency.to_s).to eq('USD')
|
240
|
+
end
|
225
241
|
end
|
226
242
|
|
227
243
|
it 'reads the currency that is already in the db' do
|
@@ -233,12 +249,14 @@ RSpec.describe 'MoneyColumn' do
|
|
233
249
|
end
|
234
250
|
|
235
251
|
it 'reads an invalid currency from the db and generates a no currency object' do
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
252
|
+
configure(legacy_deprecations: true) do
|
253
|
+
expect(Money).to receive(:deprecate).once
|
254
|
+
record = MoneyWithReadOnlyCurrency.create
|
255
|
+
record.update_columns(currency: 'invalid', price: 1)
|
256
|
+
record.reload
|
257
|
+
expect(record.price.value).to eq(1)
|
258
|
+
expect(record.price.currency.to_s).to eq('')
|
259
|
+
end
|
242
260
|
end
|
243
261
|
|
244
262
|
it 'sets the currency correctly when the currency is changed' do
|
@@ -248,12 +266,12 @@ RSpec.describe 'MoneyColumn' do
|
|
248
266
|
end
|
249
267
|
|
250
268
|
it 'handle cases where the delegate allow_nil is false' do
|
251
|
-
record = MoneyWithDelegatedCurrency.new(price: Money.new(10, 'USD')
|
269
|
+
record = MoneyWithDelegatedCurrency.new(price: Money.new(10, 'USD'))
|
252
270
|
expect(record.price.currency.to_s).to eq('USD')
|
253
271
|
end
|
254
272
|
|
255
273
|
it 'handle cases where a manual delegate does not allow nil' do
|
256
|
-
record = MoneyWithDelegatedCurrency.new(prix: Money.new(10, 'USD')
|
274
|
+
record = MoneyWithDelegatedCurrency.new(prix: Money.new(10, 'USD'))
|
257
275
|
expect(record.price.currency.to_s).to eq('USD')
|
258
276
|
end
|
259
277
|
end
|
@@ -370,10 +388,7 @@ RSpec.describe 'MoneyColumn' do
|
|
370
388
|
|
371
389
|
describe 'default_currency = nil' do
|
372
390
|
around do |example|
|
373
|
-
default_currency
|
374
|
-
Money.default_currency = nil
|
375
|
-
example.run
|
376
|
-
Money.default_currency = default_currency
|
391
|
+
configure(default_currency: nil) { example.run }
|
377
392
|
end
|
378
393
|
|
379
394
|
it 'writes currency from input value to the db' do
|
@@ -384,11 +399,17 @@ RSpec.describe 'MoneyColumn' do
|
|
384
399
|
expect(record.price.currency.to_s).to eq('GBP')
|
385
400
|
end
|
386
401
|
|
387
|
-
it 'raises missing currency error
|
388
|
-
record.update(currency: nil)
|
402
|
+
it 'raises missing currency error reading a value that was saved using legacy non-money object' do
|
403
|
+
record.update(currency: nil, price: 3)
|
404
|
+
expect { record.price }.to raise_error(ArgumentError, 'missing currency')
|
405
|
+
end
|
389
406
|
|
390
|
-
|
391
|
-
|
407
|
+
it 'handles legacy support for saving price and currency separately' do
|
408
|
+
record.update(currency: nil)
|
409
|
+
record.update(price: 7, currency: 'GBP')
|
410
|
+
record.reload
|
411
|
+
expect(record.price.value).to eq(7)
|
412
|
+
expect(record.price.currency.to_s).to eq('GBP')
|
392
413
|
end
|
393
414
|
end
|
394
415
|
end
|
data/spec/money_parser_spec.rb
CHANGED
@@ -12,9 +12,11 @@ RSpec.describe MoneyParser do
|
|
12
12
|
end
|
13
13
|
|
14
14
|
it "parses an invalid string when not strict" do
|
15
|
-
|
16
|
-
|
17
|
-
|
15
|
+
configure(legacy_deprecations: true) do
|
16
|
+
expect(Money).to receive(:deprecate).twice
|
17
|
+
expect(@parser.parse("no money", 'USD')).to eq(Money.new(0, 'USD'))
|
18
|
+
expect(@parser.parse("1..", 'USD')).to eq(Money.new(1, 'USD'))
|
19
|
+
end
|
18
20
|
end
|
19
21
|
|
20
22
|
it "parses raise with an invalid string and strict option" do
|
@@ -22,6 +24,12 @@ RSpec.describe MoneyParser do
|
|
22
24
|
expect { @parser.parse("1..1", strict: true) }.to raise_error(MoneyParser::MoneyFormatError)
|
23
25
|
end
|
24
26
|
|
27
|
+
it "parses raise with an invalid when a currency is missing" do
|
28
|
+
configure do
|
29
|
+
expect { @parser.parse("1") }.to raise_error(Money::Currency::UnknownCurrency)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
25
33
|
it "parses a single digit integer string" do
|
26
34
|
expect(@parser.parse("1")).to eq(Money.new(1.00))
|
27
35
|
end
|
@@ -144,8 +152,10 @@ RSpec.describe MoneyParser do
|
|
144
152
|
end
|
145
153
|
|
146
154
|
it "parses amount with multiple inconsistent thousands delimiters" do
|
147
|
-
|
148
|
-
|
155
|
+
configure(legacy_deprecations: true) do
|
156
|
+
expect(Money).to receive(:deprecate).once
|
157
|
+
expect(@parser.parse("1.1.11.111", 'USD')).to eq(Money.new(1_111_111, 'USD'))
|
158
|
+
end
|
149
159
|
end
|
150
160
|
|
151
161
|
it "parses raises with multiple inconsistent thousands delimiters and strict option" do
|
@@ -216,8 +226,10 @@ RSpec.describe MoneyParser do
|
|
216
226
|
end
|
217
227
|
|
218
228
|
it "parses amount with multiple inconsistent thousands delimiters" do
|
219
|
-
|
220
|
-
|
229
|
+
configure(legacy_deprecations: true) do
|
230
|
+
expect(Money).to receive(:deprecate).once
|
231
|
+
expect(@parser.parse("1,1,11,111", 'USD')).to eq(Money.new(1_111_111, 'USD'))
|
232
|
+
end
|
221
233
|
end
|
222
234
|
|
223
235
|
it "parses raises with multiple inconsistent thousands delimiters and strict option" do
|
data/spec/money_spec.rb
CHANGED
@@ -3,23 +3,16 @@ require 'spec_helper'
|
|
3
3
|
require 'yaml'
|
4
4
|
|
5
5
|
RSpec.describe "Money" do
|
6
|
-
|
7
6
|
let (:money) { Money.new(1) }
|
8
7
|
let (:amount_money) { Money.new(1.23, 'USD') }
|
9
8
|
let (:non_fractional_money) { Money.new(1, 'JPY') }
|
10
9
|
let (:zero_money) { Money.new(0) }
|
11
10
|
|
12
11
|
context "default currency not set" do
|
13
|
-
before(:each) do
|
14
|
-
@default_currency = Money.default_currency
|
15
|
-
Money.default_currency = nil
|
16
|
-
end
|
17
|
-
after(:each) do
|
18
|
-
Money.default_currency = @default_currency
|
19
|
-
end
|
20
|
-
|
21
12
|
it "raises an error" do
|
22
|
-
|
13
|
+
configure(default_currency: nil) do
|
14
|
+
expect { money }.to raise_error(ArgumentError)
|
15
|
+
end
|
23
16
|
end
|
24
17
|
end
|
25
18
|
|
@@ -39,9 +32,15 @@ RSpec.describe "Money" do
|
|
39
32
|
expect(Money.new(1).to_money('CAD')).to eq(Money.new(1, 'CAD'))
|
40
33
|
end
|
41
34
|
|
42
|
-
it "#to_money doesn't overwrite the money object's currency" do
|
43
|
-
|
44
|
-
|
35
|
+
it "legacy_deprecations #to_money doesn't overwrite the money object's currency" do
|
36
|
+
configure(legacy_deprecations: true) do
|
37
|
+
expect(Money).to receive(:deprecate).once
|
38
|
+
expect(Money.new(1, 'USD').to_money('CAD')).to eq(Money.new(1, 'USD'))
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
it "#to_money raises when changing currency" do
|
43
|
+
expect{ Money.new(1, 'USD').to_money('CAD') }.to raise_error(Money::IncompatibleCurrencyError)
|
45
44
|
end
|
46
45
|
|
47
46
|
it "defaults to 0 when constructed with no arguments" do
|
@@ -52,9 +51,15 @@ RSpec.describe "Money" do
|
|
52
51
|
expect(Money.new('')).to eq(Money.new(0))
|
53
52
|
end
|
54
53
|
|
55
|
-
it "defaults to 0 when constructed with an invalid string" do
|
56
|
-
|
57
|
-
|
54
|
+
it "legacy_deprecations defaults to 0 when constructed with an invalid string" do
|
55
|
+
configure(legacy_deprecations: true) do
|
56
|
+
expect(Money).to receive(:deprecate).once
|
57
|
+
expect(Money.new('invalid', 'USD')).to eq(Money.new(0.00, 'USD'))
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
it "raises when constructed with an invalid string" do
|
62
|
+
expect{ Money.new('invalid') }.to raise_error(ArgumentError)
|
58
63
|
end
|
59
64
|
|
60
65
|
it "to_s correctly displays the right number of decimal places" do
|
@@ -99,8 +104,19 @@ RSpec.describe "Money" do
|
|
99
104
|
expect{ money.to_s(:some_weird_style) }.to raise_error(ArgumentError)
|
100
105
|
end
|
101
106
|
|
102
|
-
it "
|
103
|
-
|
107
|
+
it "legacy_json_format makes as_json return the legacy format" do
|
108
|
+
configure(legacy_json_format: true) do
|
109
|
+
expect(Money.new(1, 'CAD').as_json).to eq("1.00")
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
it "legacy_format correctly sets the json format" do
|
114
|
+
expect(Money.new(1, 'CAD').as_json(legacy_format: true)).to eq("1.00")
|
115
|
+
expect(Money.new(1, 'CAD').to_json(legacy_format: true)).to eq("1.00")
|
116
|
+
end
|
117
|
+
|
118
|
+
it "as_json as a json containing the value and currency" do
|
119
|
+
expect(money.as_json).to eq(value: "1.00", currency: "CAD")
|
104
120
|
end
|
105
121
|
|
106
122
|
it "is constructable with a BigDecimal" do
|
@@ -139,9 +155,15 @@ RSpec.describe "Money" do
|
|
139
155
|
expect((Money.new + Money.new)).to eq(Money.new)
|
140
156
|
end
|
141
157
|
|
142
|
-
it "adds inconsistent currencies" do
|
143
|
-
|
144
|
-
|
158
|
+
it "legacy_deprecations adds inconsistent currencies" do
|
159
|
+
configure(legacy_deprecations: true) do
|
160
|
+
expect(Money).to receive(:deprecate).once
|
161
|
+
expect(Money.new(5, 'USD') + Money.new(1, 'CAD')).to eq(Money.new(6, 'USD'))
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
it "raises when adding inconsistent currencies" do
|
166
|
+
expect{ Money.new(5, 'USD') + Money.new(1, 'CAD') }.to raise_error(Money::IncompatibleCurrencyError)
|
145
167
|
end
|
146
168
|
|
147
169
|
it "is subtractable" do
|
@@ -157,8 +179,10 @@ RSpec.describe "Money" do
|
|
157
179
|
end
|
158
180
|
|
159
181
|
it "logs a deprecation warning when adding across currencies" do
|
160
|
-
|
161
|
-
|
182
|
+
configure(legacy_deprecations: true) do
|
183
|
+
expect(Money).to receive(:deprecate)
|
184
|
+
expect(Money.new(10, 'USD') - Money.new(1, 'JPY')).to eq(Money.new(9, 'USD'))
|
185
|
+
end
|
162
186
|
end
|
163
187
|
|
164
188
|
it "keeps currency when doing a computation with a null currency" do
|
@@ -254,9 +278,15 @@ RSpec.describe "Money" do
|
|
254
278
|
expect(((1.0 / 12) * Money.new(3.3))).to eq(Money.new(0.28))
|
255
279
|
end
|
256
280
|
|
257
|
-
it "is multipliable by a money object" do
|
258
|
-
|
259
|
-
|
281
|
+
it "legacy_deprecations is multipliable by a money object" do
|
282
|
+
configure(legacy_deprecations: true) do
|
283
|
+
expect(Money).to receive(:deprecate).once
|
284
|
+
expect((Money.new(3.3, 'USD') * Money.new(1, 'USD'))).to eq(Money.new(3.3, 'USD'))
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
it "raises when multiplied by a money object" do
|
289
|
+
expect{ (Money.new(3.3) * Money.new(1)) }.to raise_error(ArgumentError)
|
260
290
|
end
|
261
291
|
|
262
292
|
it "rounds multiplication result with fractional penny of 5 or higher up" do
|
@@ -294,7 +324,13 @@ RSpec.describe "Money" do
|
|
294
324
|
end
|
295
325
|
|
296
326
|
it "returns cents in to_json" do
|
297
|
-
|
327
|
+
configure(legacy_json_format: true) do
|
328
|
+
expect(Money.new('1.23', 'USD').to_json).to eq('1.23')
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
it "returns value and currency in to_json" do
|
333
|
+
expect(Money.new(1.00).to_json).to eq('{"value":"1.00","currency":"CAD"}')
|
298
334
|
end
|
299
335
|
|
300
336
|
it "supports absolute value" do
|
@@ -352,9 +388,12 @@ RSpec.describe "Money" do
|
|
352
388
|
end
|
353
389
|
|
354
390
|
it "generates a true rational" do
|
355
|
-
expect(Money.rational(Money.new(10.0), Money.new(15.0))).to eq(Rational(2,3))
|
356
|
-
|
357
|
-
|
391
|
+
expect(Money.rational(Money.new(10.0, 'USD'), Money.new(15.0, 'USD'))).to eq(Rational(2,3))
|
392
|
+
|
393
|
+
configure(legacy_deprecations: true) do
|
394
|
+
expect(Money).to receive(:deprecate).once
|
395
|
+
expect(Money.rational(Money.new(10.0, 'USD'), Money.new(15.0, 'JPY'))).to eq(Rational(2,3))
|
396
|
+
end
|
358
397
|
end
|
359
398
|
|
360
399
|
describe "frozen with amount of $1" do
|
@@ -413,9 +452,11 @@ RSpec.describe "Money" do
|
|
413
452
|
end
|
414
453
|
|
415
454
|
it "<=> issues deprecation warning when comparing incompatible currency" do
|
416
|
-
|
417
|
-
|
418
|
-
|
455
|
+
configure(legacy_deprecations: true) do
|
456
|
+
expect(Money).to receive(:deprecate).twice
|
457
|
+
expect(Money.new(1000, 'USD') <=> Money.new(2000, 'JPY')).to eq(-1)
|
458
|
+
expect(Money.new(2000, 'JPY') <=> Money.new(1000, 'USD')).to eq(1)
|
459
|
+
end
|
419
460
|
end
|
420
461
|
|
421
462
|
describe('same values') do
|
@@ -437,15 +478,22 @@ RSpec.describe "Money" do
|
|
437
478
|
it { expect(cad_10 < nil_10).to(eq(false)) }
|
438
479
|
end
|
439
480
|
|
440
|
-
describe('different currencies') do
|
481
|
+
describe('legacy different currencies') do
|
482
|
+
around(:each) do |test|
|
483
|
+
configure(legacy_deprecations: true) { test.run }
|
484
|
+
end
|
485
|
+
|
441
486
|
it { expect(Money).to(receive(:deprecate).once); expect(cad_10 <=> usd_10).to(eq(0)) }
|
442
487
|
it { expect(Money).to(receive(:deprecate).once); expect(cad_10 > usd_10).to(eq(false)) }
|
443
488
|
it { expect(Money).to(receive(:deprecate).once); expect(cad_10 >= usd_10).to(eq(true)) }
|
444
|
-
it { expect(cad_10 == usd_10).to(eq(false)) }
|
445
489
|
it { expect(Money).to(receive(:deprecate).once); expect(cad_10 <= usd_10).to(eq(true)) }
|
446
490
|
it { expect(Money).to(receive(:deprecate).once); expect(cad_10 < usd_10).to(eq(false)) }
|
447
491
|
end
|
448
492
|
|
493
|
+
describe('different currencies') do
|
494
|
+
it { expect(cad_10 == usd_10).to(eq(false)) }
|
495
|
+
end
|
496
|
+
|
449
497
|
describe('to_money types') do
|
450
498
|
it { expect(cad_10 <=> 10.00).to(eq(0)) }
|
451
499
|
it { expect(cad_10 > 10.00).to(eq(false)) }
|
@@ -710,8 +758,7 @@ RSpec.describe "Money" do
|
|
710
758
|
end
|
711
759
|
|
712
760
|
describe "parser dependency injection" do
|
713
|
-
|
714
|
-
after(:each) { Money.parser = MoneyParser }
|
761
|
+
around(:each) { |test| configure(parser: AccountingMoneyParser, default_currency: 'CAD') { test.run }}
|
715
762
|
|
716
763
|
it "keeps AccountingMoneyParser class on new money objects" do
|
717
764
|
expect(Money.new.class.parser).to eq(AccountingMoneyParser)
|
@@ -875,8 +922,7 @@ RSpec.describe "Money" do
|
|
875
922
|
end
|
876
923
|
|
877
924
|
context "with .default_currency set" do
|
878
|
-
|
879
|
-
after(:each) { Money.default_currency = Money::NULL_CURRENCY }
|
925
|
+
around(:each) { |test| configure(default_currency: Money::Currency.new('EUR')) { test.run }}
|
880
926
|
|
881
927
|
it "can be nested and falls back to default_currency outside of the blocks" do
|
882
928
|
money2, money3 = nil
|
data/spec/spec_helper.rb
CHANGED
@@ -68,3 +68,18 @@ RSpec::Matchers.define :quack_like do
|
|
68
68
|
expected.instance_methods - actual.instance_methods
|
69
69
|
end
|
70
70
|
end
|
71
|
+
|
72
|
+
|
73
|
+
def configure(default_currency: nil, legacy_json_format: nil, legacy_deprecations: nil, legacy_default_currency: nil, parser: nil)
|
74
|
+
old_config = Money.config
|
75
|
+
Money.config = Money::Config.new.tap do |config|
|
76
|
+
config.default_currency = default_currency if default_currency
|
77
|
+
config.parser = parser if parser
|
78
|
+
config.legacy_json_format! if legacy_json_format
|
79
|
+
config.legacy_deprecations! if legacy_deprecations
|
80
|
+
config.legacy_default_currency! if legacy_default_currency
|
81
|
+
end
|
82
|
+
yield
|
83
|
+
ensure
|
84
|
+
Money.config = old_config
|
85
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: shopify-money
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0.pre
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shopify Inc
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-05-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -110,6 +110,7 @@ files:
|
|
110
110
|
- LICENSE.txt
|
111
111
|
- README.md
|
112
112
|
- Rakefile
|
113
|
+
- UPGRADING.md
|
113
114
|
- bin/console
|
114
115
|
- config/currency_historic.yml
|
115
116
|
- config/currency_iso.yml
|
@@ -118,6 +119,7 @@ files:
|
|
118
119
|
- lib/money.rb
|
119
120
|
- lib/money/accounting_money_parser.rb
|
120
121
|
- lib/money/allocator.rb
|
122
|
+
- lib/money/config.rb
|
121
123
|
- lib/money/core_extensions.rb
|
122
124
|
- lib/money/currency.rb
|
123
125
|
- lib/money/currency/loader.rb
|
@@ -139,6 +141,7 @@ files:
|
|
139
141
|
- money.gemspec
|
140
142
|
- spec/accounting_money_parser_spec.rb
|
141
143
|
- spec/allocator_spec.rb
|
144
|
+
- spec/config_spec.rb
|
142
145
|
- spec/core_extensions_spec.rb
|
143
146
|
- spec/currency/loader_spec.rb
|
144
147
|
- spec/currency_spec.rb
|
@@ -168,9 +171,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
168
171
|
version: '2.6'
|
169
172
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
170
173
|
requirements:
|
171
|
-
- - "
|
174
|
+
- - ">"
|
172
175
|
- !ruby/object:Gem::Version
|
173
|
-
version:
|
176
|
+
version: 1.3.1
|
174
177
|
requirements: []
|
175
178
|
rubygems_version: 3.0.3
|
176
179
|
signing_key:
|
@@ -179,6 +182,7 @@ summary: Shopify's money gem
|
|
179
182
|
test_files:
|
180
183
|
- spec/accounting_money_parser_spec.rb
|
181
184
|
- spec/allocator_spec.rb
|
185
|
+
- spec/config_spec.rb
|
182
186
|
- spec/core_extensions_spec.rb
|
183
187
|
- spec/currency/loader_spec.rb
|
184
188
|
- spec/currency_spec.rb
|