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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d1059640a2227392fc00f7292905bd4d4de2a71d075e8ffc1fb4d02944c5c8c2
4
- data.tar.gz: 80db4d470ef80c27c09937b7fa146827d4183c519418a8382934e9a1875fd5f7
3
+ metadata.gz: ddeb1f02ca7fda02ed007433687bf2fdbce36e9b11d64724753cf2e2420507c0
4
+ data.tar.gz: de0de61cbe5c6ae02b2fbf333e4136edbfcd1c2388441c219ffb1a00e2d0a963
5
5
  SHA512:
6
- metadata.gz: 2f8cf9b8ac4fafc4a372176ebe0e46d3b29c8fc82caf020f16732eef39d80ee3f35005faea7c9a96253791c9ce2c836776ca91dc31d55713bc577af55407eed2
7
- data.tar.gz: d484b008600973688c52d232797e9ebb1efa6e782b33b8fa408853c08d35f96d8fde50c353ea53c9d9350378fd4f0390d579b619bef45b0f28d0fbe2d7e7c8b6
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
- [![Build Status](https://travis-ci.org/Shopify/money.svg?branch=master)](https://travis-ci.org/Shopify/money)
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.default_currency = Money::Currency.new("USD")
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
+
@@ -29,7 +29,7 @@ eek:
29
29
  smallest_denomination: 5
30
30
  ghc:
31
31
  priority: 100
32
- iso_code: GHS
32
+ iso_code: GHC
33
33
  name: Ghanaian Cedi
34
34
  symbol: "₵"
35
35
  disambiguate_symbol: GH₵
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)
@@ -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 - BigDecimal("1")) > Float::EPSILON
48
+ if (allocations - ONE) > Float::EPSILON
47
49
  raise ArgumentError, "splits add to more than 100%"
48
50
  end
49
51
 
@@ -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
@@ -50,6 +50,10 @@ class Money
50
50
  self.class == other.class && iso_code == other.iso_code
51
51
  end
52
52
 
53
+ def hash
54
+ [ self.class, iso_code ].hash
55
+ end
56
+
53
57
  def compatible?(other)
54
58
  other.is_a?(NullCurrency) || eql?(other)
55
59
  end
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: false)
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(ArgumentError, 'missing currency') if default.nil? || default == ''
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.deprecate(error.message)
54
- Money::NULL_CURRENCY
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
- attr_accessor :parser, :default_currency
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 from_cents(cents, currency = nil)
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
- value = Helpers.value_to_decimal(subunits) / currency.subunit_to_unit
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
- default_settings
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 cents
101
- # Money.deprecate('`money.cents` is deprecated and will be removed in the next major release. Please use `money.subunits` instead. Keep in mind, subunits are currency aware.')
102
- (value * 100).to_i
103
- end
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
- def subunits
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
- Money.deprecate("Multiplying Money with #{numeric.class.name} is deprecated and will be removed in the next major release.")
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
- Money.deprecate("mathematical operation not permitted for Money objects with different currencies #{curr} and #{currency}. " \
199
- "A Money::IncompatibleCurrencyError will raise in the next major release")
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
- to_s
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(*args)
238
- to_s
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
@@ -82,9 +82,12 @@ class MoneyParser
82
82
  number = number.to_s.strip
83
83
 
84
84
  if number.empty?
85
- raise MoneyFormatError, "invalid money string: #{input}" if strict
86
- Money.deprecate("invalid money strings will raise in the next major release \"#{input}\"")
87
- return '0'
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
- raise MoneyFormatError, "invalid money string: #{input}" if strict
111
- Money.deprecate("invalid money strings will raise in the next major release \"#{input}\"")
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
@@ -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' # Valid ISO4217
43
+ @iso_code = 'XXX'
13
44
  @iso_numeric = '999'
14
45
  @name = 'No Currency'
15
46
  @smallest_denomination = 1