shopify-money 0.14.6 → 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/.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
|
-
[](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
|