shopify-money 0.14.6 → 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/.github/workflows/tests.yml +34 -0
- data/README.md +8 -2
- data/UPGRADING.md +57 -0
- data/config/currency_historic.yml +1 -1
- data/lib/money.rb +1 -1
- data/lib/money/allocator.rb +3 -1
- data/lib/money/config.rb +26 -0
- data/lib/money/currency.rb +4 -0
- data/lib/money/helpers.rb +12 -4
- data/lib/money/money.rb +52 -35
- data/lib/money/money_parser.rb +11 -5
- data/lib/money/null_currency.rb +32 -1
- data/lib/money/version.rb +1 -1
- data/lib/money_column/active_record_hooks.rb +12 -7
- data/lib/rubocop/cop/money.rb +1 -0
- data/lib/rubocop/cop/money/missing_currency.rb +16 -8
- data/lib/rubocop/cop/money/zero_money.rb +65 -0
- data/money.gemspec +1 -1
- data/spec/accounting_money_parser_spec.rb +4 -2
- data/spec/allocator_spec.rb +3 -3
- data/spec/config_spec.rb +60 -0
- data/spec/core_extensions_spec.rb +5 -5
- data/spec/currency_spec.rb +10 -0
- data/spec/helpers_spec.rb +11 -5
- data/spec/money_column_spec.rb +55 -34
- data/spec/money_parser_spec.rb +20 -8
- data/spec/money_spec.rb +123 -90
- data/spec/rubocop/cop/money/missing_currency_spec.rb +43 -8
- data/spec/rubocop/cop/money/zero_money_spec.rb +78 -0
- data/spec/spec_helper.rb +15 -0
- metadata +14 -10
- data/.travis.yml +0 -13
- data/lib/money_accessor.rb +0 -33
- data/spec/money_accessor_spec.rb +0 -87
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
|
@@ -0,0 +1,34 @@
|
|
1
|
+
name: tests
|
2
|
+
|
3
|
+
on: [push, pull_request]
|
4
|
+
|
5
|
+
jobs:
|
6
|
+
build:
|
7
|
+
|
8
|
+
runs-on: ubuntu-latest
|
9
|
+
|
10
|
+
strategy:
|
11
|
+
matrix:
|
12
|
+
ruby: ['2.6', '2.7', '3.0']
|
13
|
+
|
14
|
+
name: Ruby ${{ matrix.ruby }}
|
15
|
+
steps:
|
16
|
+
- uses: actions/checkout@v2
|
17
|
+
- name: Set up Ruby ${{ matrix.ruby }}
|
18
|
+
uses: ruby/setup-ruby@v1
|
19
|
+
with:
|
20
|
+
ruby-version: ${{ matrix.ruby }}
|
21
|
+
- uses: actions/cache@v1
|
22
|
+
with:
|
23
|
+
path: vendor/bundle
|
24
|
+
key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
|
25
|
+
restore-keys: |
|
26
|
+
${{ runner.os }}-gems-
|
27
|
+
- name: Install dependencies
|
28
|
+
run: |
|
29
|
+
gem install bundler
|
30
|
+
bundle config path vendor/bundle
|
31
|
+
bundle install --jobs 4 --retry 3
|
32
|
+
- name: Run tests
|
33
|
+
run: |
|
34
|
+
bundle exec rake
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# money
|
2
2
|
|
3
|
-
[![
|
3
|
+
[![tests](https://github.com/Shopify/money/workflows/tests/badge.svg)](https://github.com/Shopify/money/actions?query=workflow%3Atests+branch%3Amaster)
|
4
4
|
|
5
5
|
|
6
6
|
money_column expects a DECIMAL(21,3) database field.
|
@@ -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,12 +4,12 @@ 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'
|
10
11
|
require_relative 'money/accounting_money_parser'
|
11
12
|
require_relative 'money/core_extensions'
|
12
|
-
require_relative 'money_accessor'
|
13
13
|
require_relative 'money_column' if defined?(ActiveRecord)
|
14
14
|
|
15
15
|
require_relative 'rubocop/cop/money' if defined?(RuboCop)
|
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/currency.rb
CHANGED
data/lib/money/helpers.rb
CHANGED
@@ -8,6 +8,10 @@ class Money
|
|
8
8
|
DECIMAL_ZERO = BigDecimal(0).freeze
|
9
9
|
MAX_DECIMAL = 21
|
10
10
|
|
11
|
+
STRIPE_SUBUNIT_OVERRIDE = {
|
12
|
+
'ISK' => 100,
|
13
|
+
}.freeze
|
14
|
+
|
11
15
|
def value_to_decimal(num)
|
12
16
|
value =
|
13
17
|
case num
|
@@ -24,7 +28,7 @@ class Money
|
|
24
28
|
when Rational
|
25
29
|
BigDecimal(num, MAX_DECIMAL)
|
26
30
|
when String
|
27
|
-
decimal = BigDecimal(num, exception:
|
31
|
+
decimal = BigDecimal(num, exception: !Money.config.legacy_deprecations)
|
28
32
|
return decimal if decimal
|
29
33
|
|
30
34
|
Money.deprecate("using Money.new('#{num}') is deprecated and will raise an ArgumentError in the next major release")
|
@@ -42,7 +46,7 @@ class Money
|
|
42
46
|
currency
|
43
47
|
when nil, ''
|
44
48
|
default = Money.current_currency || Money.default_currency
|
45
|
-
raise(
|
49
|
+
raise(Money::Currency::UnknownCurrency, 'missing currency') if default.nil? || default == ''
|
46
50
|
value_to_currency(default)
|
47
51
|
when 'xxx', 'XXX'
|
48
52
|
Money::NULL_CURRENCY
|
@@ -50,8 +54,12 @@ class Money
|
|
50
54
|
begin
|
51
55
|
Currency.find!(currency)
|
52
56
|
rescue Money::Currency::UnknownCurrency => error
|
53
|
-
Money.
|
54
|
-
|
57
|
+
if Money.config.legacy_deprecations
|
58
|
+
Money.deprecate(error.message)
|
59
|
+
Money::NULL_CURRENCY
|
60
|
+
else
|
61
|
+
raise error
|
62
|
+
end
|
55
63
|
end
|
56
64
|
else
|
57
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)
|
@@ -26,22 +33,22 @@ class Money
|
|
26
33
|
end
|
27
34
|
alias_method :from_amount, :new
|
28
35
|
|
29
|
-
def zero(currency = NULL_CURRENCY)
|
30
|
-
new(0, currency)
|
31
|
-
end
|
32
|
-
alias_method :empty, :zero
|
33
|
-
|
34
36
|
def parse(*args, **kwargs)
|
35
37
|
parser.parse(*args, **kwargs)
|
36
38
|
end
|
37
39
|
|
38
|
-
def
|
39
|
-
new(cents.round.to_f / 100, currency)
|
40
|
-
end
|
41
|
-
|
42
|
-
def from_subunits(subunits, currency_iso)
|
40
|
+
def from_subunits(subunits, currency_iso, format: :iso4217)
|
43
41
|
currency = Helpers.value_to_currency(currency_iso)
|
44
|
-
|
42
|
+
|
43
|
+
subunit_to_unit_value = if format == :iso4217
|
44
|
+
currency.subunit_to_unit
|
45
|
+
elsif format == :stripe
|
46
|
+
Helpers::STRIPE_SUBUNIT_OVERRIDE.fetch(currency.iso_code, currency.subunit_to_unit)
|
47
|
+
else
|
48
|
+
raise ArgumentError, "unknown format #{format}"
|
49
|
+
end
|
50
|
+
|
51
|
+
value = Helpers.value_to_decimal(subunits) / subunit_to_unit_value
|
45
52
|
new(value, currency)
|
46
53
|
end
|
47
54
|
|
@@ -73,18 +80,13 @@ 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?
|
86
88
|
@currency = Helpers.value_to_currency(currency)
|
87
|
-
@value = value.round(@currency.minor_units)
|
89
|
+
@value = BigDecimal(value.round(@currency.minor_units))
|
88
90
|
freeze
|
89
91
|
end
|
90
92
|
|
@@ -97,13 +99,16 @@ class Money
|
|
97
99
|
coder['currency'] = @currency.iso_code
|
98
100
|
end
|
99
101
|
|
100
|
-
def
|
101
|
-
|
102
|
-
|
103
|
-
|
102
|
+
def subunits(format: :iso4217)
|
103
|
+
subunit_to_unit_value = if format == :iso4217
|
104
|
+
@currency.subunit_to_unit
|
105
|
+
elsif format == :stripe
|
106
|
+
Helpers::STRIPE_SUBUNIT_OVERRIDE.fetch(@currency.iso_code, @currency.subunit_to_unit)
|
107
|
+
else
|
108
|
+
raise ArgumentError, "unknown format #{format}"
|
109
|
+
end
|
104
110
|
|
105
|
-
|
106
|
-
(@value * @currency.subunit_to_unit).to_i
|
111
|
+
(@value * subunit_to_unit_value).to_i
|
107
112
|
end
|
108
113
|
|
109
114
|
def no_currency?
|
@@ -135,7 +140,11 @@ class Money
|
|
135
140
|
|
136
141
|
def *(numeric)
|
137
142
|
unless numeric.is_a?(Numeric)
|
138
|
-
|
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
|
139
148
|
end
|
140
149
|
Money.new(value.to_r * numeric, currency)
|
141
150
|
end
|
@@ -195,8 +204,12 @@ class Money
|
|
195
204
|
|
196
205
|
curr = Helpers.value_to_currency(curr)
|
197
206
|
unless currency.compatible?(curr)
|
198
|
-
|
199
|
-
|
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
|
200
213
|
end
|
201
214
|
|
202
215
|
self
|
@@ -226,16 +239,20 @@ class Money
|
|
226
239
|
end
|
227
240
|
end
|
228
241
|
|
229
|
-
def to_liquid
|
230
|
-
cents
|
231
|
-
end
|
232
|
-
|
233
242
|
def to_json(options = {})
|
234
|
-
|
243
|
+
if options.delete(:legacy_format) || Money.config.legacy_json_format
|
244
|
+
to_s
|
245
|
+
else
|
246
|
+
as_json(options).to_json
|
247
|
+
end
|
235
248
|
end
|
236
249
|
|
237
|
-
def as_json(
|
238
|
-
|
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
|
239
256
|
end
|
240
257
|
|
241
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/null_currency.rb
CHANGED
@@ -1,5 +1,36 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
class Money
|
3
|
+
# A placeholder currency for instances where no actual currency is available,
|
4
|
+
# as defined by ISO4217. You should rarely, if ever, need to use this
|
5
|
+
# directly. It's here mostly for backwards compatibility and for that reason
|
6
|
+
# behaves like a dollar, which is how this gem worked before the introduction
|
7
|
+
# of currency.
|
8
|
+
#
|
9
|
+
# Here follows a list of preferred alternatives over using Money with
|
10
|
+
# NullCurrency:
|
11
|
+
#
|
12
|
+
# For comparisons where you don't know the currency beforehand, you can use
|
13
|
+
# Numeric predicate methods like #positive?/#negative?/#zero?/#nonzero?.
|
14
|
+
# Comparison operators with Numeric (==, !=, <=, =>, <, >) work as well.
|
15
|
+
#
|
16
|
+
# @example
|
17
|
+
# Money.new(1, 'CAD').positive? #=> true
|
18
|
+
# Money.new(2, 'CAD') >= 0 #=> true
|
19
|
+
#
|
20
|
+
# Money with NullCurrency has behaviour that may surprise you, such as
|
21
|
+
# database validations or GraphQL enum not allowing the string representation
|
22
|
+
# of NullCurrency. Prefer using Money.new(0, currency) where possible, as
|
23
|
+
# this sidesteps these issues and provides additional currency check
|
24
|
+
# safeties.
|
25
|
+
#
|
26
|
+
# Unlike other currencies, it is allowed to calculate a Money object with
|
27
|
+
# NullCurrency with another currency. The resulting Money object will have
|
28
|
+
# the other currency.
|
29
|
+
#
|
30
|
+
# @example
|
31
|
+
# Money.new(0, Money::NULL_CURRENCY) + Money.new(5, 'CAD')
|
32
|
+
# #=> #<Money value:5.00 currency:CAD>
|
33
|
+
#
|
3
34
|
class NullCurrency
|
4
35
|
|
5
36
|
attr_reader :iso_code, :iso_numeric, :name, :smallest_denomination, :subunit_symbol,
|
@@ -9,7 +40,7 @@ class Money
|
|
9
40
|
@symbol = '$'
|
10
41
|
@disambiguate_symbol = nil
|
11
42
|
@subunit_symbol = nil
|
12
|
-
@iso_code = 'XXX'
|
43
|
+
@iso_code = 'XXX'
|
13
44
|
@iso_numeric = '999'
|
14
45
|
@name = 'No Currency'
|
15
46
|
@smallest_denomination = 1
|