shopify-money 0.16.0 → 1.0.0.pre
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 +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
|