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 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