shopify-money 0.14.4 → 0.15.0

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: da457728a1f7280ea39993afbb7fedbb244a1f6e85af1bb835c0685ee5e511d5
4
- data.tar.gz: acecf21c24bd2848a399736061f1a993b009a31357b1db073741d2e0a11bf050
3
+ metadata.gz: 56953634d128da5cb62dfb11b5c60868a162c4ff7edb9bb810110d17c02469f9
4
+ data.tar.gz: c8e6f0e99b480cfcf9be4183adfc633baf14f0f9ee0d777bea01b225c01cfc09
5
5
  SHA512:
6
- metadata.gz: e1542885996288998faf5c679d79165e631a54e1f09d744e1a9828154aa4d436026eee0faaf54858a34551a697c1a2bd5d9e6ccf5c18474410e74922c6146f84
7
- data.tar.gz: 0250a200815821d6153c739a7922fbc1531ad23c6b722540dca0530d930a165477235cd603a47bb058030a880971729246aa95025c19bd42c250fe0be6c65836
6
+ metadata.gz: 4888a59e2f50348092b73c2b7255b215f994f9b47efbd379ad88d6f4624f4eca403840f584f4d5760a17c02d20f38f0fc49f286c0e19e3557e4976037d1f8049
7
+ data.tar.gz: ce77e720fae3724a0cc8303336ac12aee09a7a61862f0ba01f0dc7dce3c4e86f2fd7073ee7828d5cc313b77d9b285dfce07bd84e1d8e289e597e1cf25a349c7d
@@ -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.
@@ -43,6 +43,21 @@ ghc:
43
43
  thousands_separator: ","
44
44
  iso_numeric: '288'
45
45
  smallest_denomination: 1
46
+ mro:
47
+ priority: 100
48
+ iso_code: MRO
49
+ name: Mauritanian Ouguiya
50
+ symbol: UM
51
+ disambiguate_symbol: MRO
52
+ alternate_symbols: []
53
+ subunit: Khoums
54
+ subunit_to_unit: 5
55
+ symbol_first: false
56
+ html_entity: ''
57
+ decimal_mark: "."
58
+ thousands_separator: ","
59
+ iso_numeric: '478'
60
+ smallest_denomination: 1
46
61
  mtl:
47
62
  priority: 100
48
63
  iso_code: MTL
@@ -86,6 +101,20 @@ VEB:
86
101
  thousands_separator: "."
87
102
  iso_numeric: '862'
88
103
  smallest_denomination: 1
104
+ vef:
105
+ priority: 100
106
+ iso_code: VEF
107
+ name: Venezuelan Bolívar fuerte
108
+ symbol: Bs.F.
109
+ alternate_symbols: []
110
+ subunit: Céntimo
111
+ subunit_to_unit: 100
112
+ symbol_first: true
113
+ html_entity: ''
114
+ decimal_mark: ","
115
+ thousands_separator: "."
116
+ iso_numeric: '937'
117
+ smallest_denomination: 1
89
118
  zwd:
90
119
  priority: 100
91
120
  iso_code: ZWD
@@ -1446,19 +1446,23 @@ mop:
1446
1446
  thousands_separator: ","
1447
1447
  iso_numeric: '446'
1448
1448
  smallest_denomination: 10
1449
- mro:
1449
+ mru:
1450
1450
  priority: 100
1451
- iso_code: MRO
1452
- name: Mauritanian Ouguiya
1451
+ iso_code: MRU
1452
+ name: Mauritanian New Ouguiya
1453
1453
  symbol: UM
1454
+ disambiguate_symbol: MRU
1454
1455
  alternate_symbols: []
1455
1456
  subunit: Khoums
1456
- subunit_to_unit: 5
1457
+ # MRU actually has 5 Khoums per Ouguiya, but we treat it as if it has no subunits.
1458
+ # We do this because the Khoum's value is so small that it's not likely to be relevant in an online transaction,
1459
+ # and we suspect that, when our payment partners add support for MRU, they're likely to ignore subunits.
1460
+ subunit_to_unit: 1
1457
1461
  symbol_first: false
1458
1462
  html_entity: ''
1459
1463
  decimal_mark: "."
1460
1464
  thousands_separator: ","
1461
- iso_numeric: '478'
1465
+ iso_numeric: '929'
1462
1466
  smallest_denomination: 1
1463
1467
  mur:
1464
1468
  priority: 100
@@ -2300,11 +2304,11 @@ uzs:
2300
2304
  thousands_separator: ","
2301
2305
  iso_numeric: '860'
2302
2306
  smallest_denomination: 100
2303
- vef:
2307
+ ves:
2304
2308
  priority: 100
2305
- iso_code: VEF
2306
- name: Venezuelan Bolívar fuerte
2307
- symbol: Bs.F.
2309
+ iso_code: VES
2310
+ name: Venezuelan Bolívar soberano
2311
+ symbol: Bs.S.
2308
2312
  alternate_symbols: []
2309
2313
  subunit: Céntimo
2310
2314
  subunit_to_unit: 100
@@ -2312,8 +2316,11 @@ vef:
2312
2316
  html_entity: ''
2313
2317
  decimal_mark: ","
2314
2318
  thousands_separator: "."
2315
- iso_numeric: '937'
2316
- smallest_denomination: 1
2319
+ iso_numeric: '928'
2320
+ # "On 20 August 2018, [...] New coins in denominations of 50 céntimos and 1 bolívar soberano, and new banknotes
2321
+ # in denominations of 2, 5, 10, 20, 50, 100, 200 and 500 bolívares soberanos were introduced."
2322
+ # Source: https://en.wikipedia.org/wiki/Venezuelan_bol%C3%ADvar
2323
+ smallest_denomination: 50
2317
2324
  vnd:
2318
2325
  priority: 100
2319
2326
  iso_code: VND
@@ -14,6 +14,25 @@ jep:
14
14
  thousands_separator: ","
15
15
  iso_numeric: ''
16
16
  smallest_denomination: 1
17
+ kid:
18
+ # The Kiribati Dollar is pegged 1:1 to the AUD, but banknotes and coins in both currencies circulate interchangeably.
19
+ # "Although Kiribati retained 1 and 2 cents coins well after Australia demoted theirs,
20
+ # redundancy and devaluation has slowly removed these coins from general circulation."
21
+ # Source: https://en.wikipedia.org/wiki/Kiribati_dollar
22
+ priority: 100
23
+ iso_code: KID
24
+ name: Kiribati Dollar
25
+ symbol: "$"
26
+ disambiguate_symbol: KID
27
+ alternate_symbols: []
28
+ subunit: Cent
29
+ subunit_to_unit: 100
30
+ symbol_first: true
31
+ html_entity: "$"
32
+ decimal_mark: "."
33
+ thousands_separator: ","
34
+ iso_numeric: ''
35
+ smallest_denomination: 5
17
36
  ggp:
18
37
  priority: 100
19
38
  iso_code: GGP
@@ -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
@@ -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
@@ -26,22 +26,22 @@ class Money
26
26
  end
27
27
  alias_method :from_amount, :new
28
28
 
29
- def zero(currency = NULL_CURRENCY)
30
- new(0, currency)
31
- end
32
- alias_method :empty, :zero
33
-
34
29
  def parse(*args, **kwargs)
35
30
  parser.parse(*args, **kwargs)
36
31
  end
37
32
 
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)
33
+ def from_subunits(subunits, currency_iso, format: :iso4217)
43
34
  currency = Helpers.value_to_currency(currency_iso)
44
- value = Helpers.value_to_decimal(subunits) / currency.subunit_to_unit
35
+
36
+ subunit_to_unit_value = if format == :iso4217
37
+ currency.subunit_to_unit
38
+ elsif format == :stripe
39
+ Helpers::STRIPE_SUBUNIT_OVERRIDE.fetch(currency.iso_code, currency.subunit_to_unit)
40
+ else
41
+ raise ArgumentError, "unknown format #{format}"
42
+ end
43
+
44
+ value = Helpers.value_to_decimal(subunits) / subunit_to_unit_value
45
45
  new(value, currency)
46
46
  end
47
47
 
@@ -84,7 +84,7 @@ class Money
84
84
  def initialize(value, currency)
85
85
  raise ArgumentError if value.nan?
86
86
  @currency = Helpers.value_to_currency(currency)
87
- @value = value.round(@currency.minor_units)
87
+ @value = BigDecimal(value.round(@currency.minor_units))
88
88
  freeze
89
89
  end
90
90
 
@@ -97,13 +97,16 @@ class Money
97
97
  coder['currency'] = @currency.iso_code
98
98
  end
99
99
 
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
100
+ def subunits(format: :iso4217)
101
+ subunit_to_unit_value = if format == :iso4217
102
+ @currency.subunit_to_unit
103
+ elsif format == :stripe
104
+ Helpers::STRIPE_SUBUNIT_OVERRIDE.fetch(@currency.iso_code, @currency.subunit_to_unit)
105
+ else
106
+ raise ArgumentError, "unknown format #{format}"
107
+ end
104
108
 
105
- def subunits
106
- (@value * @currency.subunit_to_unit).to_i
109
+ (@value * subunit_to_unit_value).to_i
107
110
  end
108
111
 
109
112
  def no_currency?
@@ -207,16 +210,23 @@ class Money
207
210
  end
208
211
 
209
212
  def to_s(style = nil)
210
- case style
213
+ units = case style
211
214
  when :legacy_dollars
212
- sprintf("%.2f", value)
215
+ 2
213
216
  when :amount, nil
214
- sprintf("%.#{currency.minor_units}f", value)
217
+ currency.minor_units
218
+ else
219
+ raise ArgumentError, "Unexpected style: #{style}"
215
220
  end
216
- end
217
221
 
218
- def to_liquid
219
- cents
222
+ rounded_value = value.round(units)
223
+ if units == 0
224
+ sprintf("%d", rounded_value)
225
+ else
226
+ sign = rounded_value < 0 ? '-' : ''
227
+ rounded_value = rounded_value.abs
228
+ sprintf("%s%d.%0#{units}d", sign, rounded_value.truncate, rounded_value.frac * (10 ** units))
229
+ end
220
230
  end
221
231
 
222
232
  def to_json(options = {})
@@ -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
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  class Money
3
- VERSION = "0.14.4"
3
+ VERSION = "0.15.0"
4
4
  end
@@ -1,3 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'rubocop/cop/money/missing_currency'
4
+ require 'rubocop/cop/money/zero_money'
@@ -20,11 +20,11 @@ module RuboCop
20
20
  #
21
21
 
22
22
  def_node_matcher :money_new, <<~PATTERN
23
- (send (const nil? :Money) {:new :from_amount :from_cents} $...)
23
+ (send (const {nil? cbase} :Money) {:new :from_amount :from_cents} $...)
24
24
  PATTERN
25
25
 
26
26
  def_node_matcher :to_money_without_currency?, <<~PATTERN
27
- (send _ :to_money)
27
+ ({send csend} _ :to_money)
28
28
  PATTERN
29
29
 
30
30
  def_node_matcher :to_money_block?, <<~PATTERN
@@ -42,11 +42,9 @@ module RuboCop
42
42
  add_offense(node, message: 'to_money is missing currency argument')
43
43
  end
44
44
  end
45
+ alias on_csend on_send
45
46
 
46
47
  def autocorrect(node)
47
- currency = cop_config['ReplacementCurrency']
48
- return unless currency
49
-
50
48
  receiver, method, _ = *node
51
49
 
52
50
  lambda do |corrector|
@@ -55,20 +53,30 @@ module RuboCop
55
53
 
56
54
  corrector.replace(
57
55
  node.loc.expression,
58
- "#{receiver.source}.#{method}(#{amount&.source || 0}, '#{currency}')"
56
+ "#{receiver.source}.#{method}(#{amount&.source || 0}, #{replacement_currency})"
59
57
  )
60
58
  end
61
59
 
62
60
  if to_money_without_currency?(node)
63
- corrector.insert_after(node.loc.expression, "('#{currency}')")
61
+ corrector.insert_after(node.loc.expression, "(#{replacement_currency})")
64
62
  elsif to_money_block?(node)
65
63
  corrector.replace(
66
64
  node.loc.expression,
67
- "#{receiver.source}.#{method} { |x| x.to_money('#{currency}') }"
65
+ "#{receiver.source}.#{method} { |x| x.to_money(#{replacement_currency}) }"
68
66
  )
69
67
  end
70
68
  end
71
69
  end
70
+
71
+ private
72
+
73
+ def replacement_currency
74
+ if cop_config['ReplacementCurrency']
75
+ "'#{cop_config['ReplacementCurrency']}'"
76
+ else
77
+ 'Money::NULL_CURRENCY'
78
+ end
79
+ end
72
80
  end
73
81
  end
74
82
  end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Money
6
+ class ZeroMoney < Cop
7
+ # `Money.zero` and it's alias `empty`, with or without currency
8
+ # argument is removed in favour of the more explicit Money.new
9
+ # syntax. Supplying it with a real currency is preferred for
10
+ # additional currency safety checks.
11
+ #
12
+ # If no currency was supplied, it defaults to
13
+ # Money::NULL_CURRENCY which was the default setting of
14
+ # Money.default_currency and should effectively be the same. The cop
15
+ # can be configured with a ReplacementCurrency in case that is more
16
+ # appropriate for your application.
17
+ #
18
+ # @example
19
+ #
20
+ # # bad
21
+ # Money.zero
22
+ #
23
+ # # good when configured with `ReplacementCurrency: CAD`
24
+ # Money.new(0, 'CAD')
25
+ #
26
+
27
+ MSG = 'Money.zero is removed, use `Money.new(0, %<currency>s)`.'
28
+
29
+ def_node_matcher :money_zero, <<~PATTERN
30
+ (send (const {nil? cbase} :Money) {:zero :empty} $...)
31
+ PATTERN
32
+
33
+ def on_send(node)
34
+ money_zero(node) do |currency_arg|
35
+ add_offense(node, message: format(MSG, currency: replacement_currency(currency_arg)))
36
+ end
37
+ end
38
+
39
+ def autocorrect(node)
40
+ receiver, _ = *node
41
+
42
+ lambda do |corrector|
43
+ money_zero(node) do |currency_arg|
44
+ replacement_currency = replacement_currency(currency_arg)
45
+
46
+ corrector.replace(
47
+ node.loc.expression,
48
+ "#{receiver.source}.new(0, #{replacement_currency})"
49
+ )
50
+ end
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def replacement_currency(currency_arg)
57
+ return currency_arg.first.source unless currency_arg.empty?
58
+ return "'#{cop_config['ReplacementCurrency']}'" if cop_config['ReplacementCurrency']
59
+
60
+ 'Money::NULL_CURRENCY'
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -15,7 +15,7 @@ Gem::Specification.new do |s|
15
15
 
16
16
  s.metadata['allowed_push_host'] = "https://rubygems.org"
17
17
 
18
- s.add_development_dependency("bundler", ">= 1.5")
18
+ s.add_development_dependency("bundler")
19
19
  s.add_development_dependency("simplecov", ">= 0")
20
20
  s.add_development_dependency("rails", "~> 6.0")
21
21
  s.add_development_dependency("rspec", "~> 3.2")
@@ -43,7 +43,7 @@ RSpec.describe "Allocator" do
43
43
 
44
44
  specify "#allocate will convert rationals with high precision" do
45
45
  ratios = [Rational(1, 1), Rational(0)]
46
- expect(new_allocator("858993456.12").allocate(ratios)).to eq([Money.new("858993456.12"), Money.empty])
46
+ expect(new_allocator("858993456.12").allocate(ratios)).to eq([Money.new("858993456.12"), Money.new(0, Money::NULL_CURRENCY)])
47
47
  ratios = [Rational(1, 6), Rational(5, 6)]
48
48
  expect(new_allocator("3.00").allocate(ratios)).to eq([Money.new("0.50"), Money.new("2.50")])
49
49
  end
@@ -131,8 +131,8 @@ RSpec.describe "Allocator" do
131
131
 
132
132
  specify "#allocate_max_amounts supports all-zero maxima" do
133
133
  expect(
134
- new_allocator(3).allocate_max_amounts([Money.empty, Money.empty, Money.empty]),
135
- ).to eq([Money.empty, Money.empty, Money.empty])
134
+ new_allocator(3).allocate_max_amounts([Money.new(0, Money::NULL_CURRENCY), Money.new(0, Money::NULL_CURRENCY), Money.new(0, Money::NULL_CURRENCY)]),
135
+ ).to eq([Money.new(0, Money::NULL_CURRENCY), Money.new(0, Money::NULL_CURRENCY), Money.new(0, Money::NULL_CURRENCY)])
136
136
  end
137
137
 
138
138
  specify "#allocate_max_amounts allocates the right amount without rounding error" do
@@ -17,7 +17,7 @@ RSpec.describe Integer do
17
17
  it_should_behave_like "an object supporting to_money"
18
18
 
19
19
  it "parses 0 to Money.zero" do
20
- expect(0.to_money).to eq(Money.zero)
20
+ expect(0.to_money).to eq(Money.new(0, Money::NULL_CURRENCY))
21
21
  end
22
22
  end
23
23
 
@@ -30,7 +30,7 @@ RSpec.describe Float do
30
30
  it_should_behave_like "an object supporting to_money"
31
31
 
32
32
  it "parses 0.0 to Money.zero" do
33
- expect(0.0.to_money).to eq(Money.zero)
33
+ expect(0.0.to_money).to eq(Money.new(0, Money::NULL_CURRENCY))
34
34
  end
35
35
  end
36
36
 
@@ -43,8 +43,8 @@ RSpec.describe String do
43
43
  it_should_behave_like "an object supporting to_money"
44
44
 
45
45
  it "parses an empty string to Money.zero" do
46
- expect(''.to_money).to eq(Money.zero)
47
- expect(' '.to_money).to eq(Money.zero)
46
+ expect(''.to_money).to eq(Money.new(0, Money::NULL_CURRENCY))
47
+ expect(' '.to_money).to eq(Money.new(0, Money::NULL_CURRENCY))
48
48
  end
49
49
  end
50
50
 
@@ -57,6 +57,6 @@ RSpec.describe BigDecimal do
57
57
  it_should_behave_like "an object supporting to_money"
58
58
 
59
59
  it "parses a zero BigDecimal to Money.zero" do
60
- expect(BigDecimal("-0.000").to_money).to eq(Money.zero)
60
+ expect(BigDecimal("-0.000").to_money).to eq(Money.new(0, Money::NULL_CURRENCY))
61
61
  end
62
62
  end
@@ -77,6 +77,16 @@ RSpec.describe "Currency" do
77
77
  end
78
78
  end
79
79
 
80
+ describe "#hash" do
81
+ specify "equal currencies from different loaders have the same hash" do
82
+ currency_1 = Money::Currency.find('USD')
83
+ currency_2 = YAML.load(Money::Currency.find('USD').to_yaml)
84
+
85
+ expect(currency_1.eql?(currency_2)).to eq(true)
86
+ expect(currency_1.hash).to eq(currency_2.hash)
87
+ end
88
+ end
89
+
80
90
  describe "==" do
81
91
  it "returns true when both objects have the same currency" do
82
92
  expect(currency == Money.new(1, 'USD').currency).to eq(true)
@@ -136,7 +136,7 @@ RSpec.describe 'MoneyColumn' do
136
136
 
137
137
  it 'does not overwrite a currency column with a default currency when saving zero' do
138
138
  expect(record.currency.to_s).to eq('EUR')
139
- record.update(price: Money.zero)
139
+ record.update(price: Money.new(0, Money::NULL_CURRENCY))
140
140
  expect(record.currency.to_s).to eq('EUR')
141
141
  end
142
142
 
@@ -8,12 +8,12 @@ RSpec.describe MoneyParser do
8
8
 
9
9
  describe "parsing of amounts with period decimal separator" do
10
10
  it "parses an empty string to $0" do
11
- expect(@parser.parse("")).to eq(Money.zero)
11
+ expect(@parser.parse("")).to eq(Money.new(0, Money::NULL_CURRENCY))
12
12
  end
13
13
 
14
14
  it "parses an invalid string when not strict" do
15
15
  expect(Money).to receive(:deprecate).twice
16
- expect(@parser.parse("no money")).to eq(Money.zero)
16
+ expect(@parser.parse("no money")).to eq(Money.new(0, Money::NULL_CURRENCY))
17
17
  expect(@parser.parse("1..")).to eq(Money.new(1))
18
18
  end
19
19
 
@@ -9,10 +9,6 @@ RSpec.describe "Money" do
9
9
  let (:non_fractional_money) { Money.new(1, 'JPY') }
10
10
  let (:zero_money) { Money.new(0) }
11
11
 
12
- it "is contructable with empty class method" do
13
- expect(Money.empty).to eq(Money.new)
14
- end
15
-
16
12
  context "default currency not set" do
17
13
  before(:each) do
18
14
  @default_currency = Money.default_currency
@@ -28,15 +24,11 @@ RSpec.describe "Money" do
28
24
  end
29
25
 
30
26
  it ".zero has no currency" do
31
- expect(Money.zero.currency).to be_a(Money::NullCurrency)
27
+ expect(Money.new(0, Money::NULL_CURRENCY).currency).to be_a(Money::NullCurrency)
32
28
  end
33
29
 
34
30
  it ".zero is a 0$ value" do
35
- expect(Money.zero).to eq(Money.new(0))
36
- end
37
-
38
- it ".zero accepts an optional currency" do
39
- expect(Money.zero('USD')).to eq(Money.new(0, 'USD'))
31
+ expect(Money.new(0, Money::NULL_CURRENCY)).to eq(Money.new(0))
40
32
  end
41
33
 
42
34
  it "returns itself with to_money" do
@@ -80,6 +72,33 @@ RSpec.describe "Money" do
80
72
  expect(non_fractional_money.to_s(:amount)).to eq("1")
81
73
  end
82
74
 
75
+ it "to_s correctly displays negative numbers" do
76
+ expect((-money).to_s).to eq("-1.00")
77
+ expect((-amount_money).to_s).to eq("-1.23")
78
+ expect((-non_fractional_money).to_s).to eq("-1")
79
+ expect((-Money.new("0.05")).to_s).to eq("-0.05")
80
+ end
81
+
82
+ it "to_s rounds when more fractions than currency allows" do
83
+ expect(Money.new("9.999", "USD").to_s).to eq("10.00")
84
+ expect(Money.new("9.889", "USD").to_s).to eq("9.89")
85
+ end
86
+
87
+ it "to_s does not round when fractions same as currency allows" do
88
+ expect(Money.new("1.25", "USD").to_s).to eq("1.25")
89
+ expect(Money.new("9.99", "USD").to_s).to eq("9.99")
90
+ expect(Money.new("9.999", "BHD").to_s).to eq("9.999")
91
+ end
92
+
93
+ it "to_s does not round if amount is larger than float allows" do
94
+ expect(Money.new("99999999999999.99", "USD").to_s).to eq("99999999999999.99")
95
+ expect(Money.new("999999999999999999.99", "USD").to_s).to eq("999999999999999999.99")
96
+ end
97
+
98
+ it "to_s raises ArgumentError on unsupported style" do
99
+ expect{ money.to_s(:some_weird_style) }.to raise_error(ArgumentError)
100
+ end
101
+
83
102
  it "as_json as a float with 2 decimal places" do
84
103
  expect(money.as_json).to eq("1.00")
85
104
  end
@@ -220,11 +239,6 @@ RSpec.describe "Money" do
220
239
  expect((1.50 * Money.new(1.00))).to eq(Money.new(1.50))
221
240
  end
222
241
 
223
- it "is multipliable by a cents amount" do
224
- expect((Money.new(1.00) * 0.50)).to eq(Money.new(0.50))
225
- expect((0.50 * Money.new(1.00))).to eq(Money.new(0.50))
226
- end
227
-
228
242
  it "is multipliable by a rational" do
229
243
  expect((Money.new(3.3) * Rational(1, 12))).to eq(Money.new(0.28))
230
244
  expect((Rational(1, 12) * Money.new(3.3))).to eq(Money.new(0.28))
@@ -279,10 +293,6 @@ RSpec.describe "Money" do
279
293
  expect { Money.new(55.00) / 55 }.to raise_error(RuntimeError)
280
294
  end
281
295
 
282
- it "returns cents in to_liquid" do
283
- expect(Money.new(1.00).to_liquid).to eq(100)
284
- end
285
-
286
296
  it "returns cents in to_json" do
287
297
  expect(Money.new(1.00).to_json).to eq("1.00")
288
298
  end
@@ -303,24 +313,28 @@ RSpec.describe "Money" do
303
313
  expect(Money.new(1.50).to_f.to_s).to eq("1.5")
304
314
  end
305
315
 
306
- it "is creatable from an integer value in cents" do
307
- expect(Money.from_cents(1950)).to eq(Money.new(19.50))
308
- end
316
+ describe '#from_subunits' do
317
+ it "creates Money object from an integer value in cents and currency" do
318
+ expect(Money.from_subunits(1950, 'CAD')).to eq(Money.new(19.50))
319
+ end
309
320
 
310
- it "is creatable from an integer value of 0 in cents" do
311
- expect(Money.from_cents(0)).to eq(Money.new)
312
- end
321
+ it "creates Money object from an integer value in dollars and currency with no cents" do
322
+ expect(Money.from_subunits(1950, 'JPY')).to eq(Money.new(1950, 'JPY'))
323
+ end
313
324
 
314
- it "is creatable from a float cents amount" do
315
- expect(Money.from_cents(1950.5)).to eq(Money.new(19.51))
316
- end
325
+ describe 'with format specified' do
326
+ it 'overrides the subunit_to_unit amount' do
327
+ expect(Money.from_subunits(100, 'ISK', format: :stripe)).to eq(Money.new(1, 'ISK'))
328
+ end
317
329
 
318
- it "is creatable from an integer value in cents and currency" do
319
- expect(Money.from_subunits(1950, 'CAD')).to eq(Money.new(19.50))
320
- end
330
+ it 'fallbacks to the default subunit_to_unit amount if no override is specified' do
331
+ expect(Money.from_subunits(100, 'USD', format: :stripe)).to eq(Money.new(1, 'USD'))
332
+ end
321
333
 
322
- it "is creatable from an integer value in dollars and currency with no cents" do
323
- expect(Money.from_subunits(1950, 'JPY')).to eq(Money.new(1950, 'JPY'))
334
+ it 'raises if the format is not found' do
335
+ expect { Money.from_subunits(100, 'ISK', format: :unknown) }.to(raise_error(ArgumentError))
336
+ end
337
+ end
324
338
  end
325
339
 
326
340
  it "raises when constructed with a NaN value" do
@@ -517,6 +531,20 @@ RSpec.describe "Money" do
517
531
  expect(Money.new(1, 'IQD').subunits).to eq(1000)
518
532
  expect(Money.new(1).subunits).to eq(100)
519
533
  end
534
+
535
+ describe 'with format specified' do
536
+ it 'overrides the subunit_to_unit amount' do
537
+ expect(Money.new(1, 'ISK').subunits(format: :stripe)).to eq(100)
538
+ end
539
+
540
+ it 'fallbacks to the default subunit_to_unit amount if no override is specified' do
541
+ expect(Money.new(1, 'USD').subunits(format: :stripe)).to eq(100)
542
+ end
543
+
544
+ it 'raises if the format is not found' do
545
+ expect { Money.new(1, 'ISK').subunits(format: :unknown) }.to(raise_error(ArgumentError))
546
+ end
547
+ end
520
548
  end
521
549
 
522
550
  describe "value" do
@@ -562,10 +590,6 @@ RSpec.describe "Money" do
562
590
  expect(money.value).to eq(BigDecimal("1.00"))
563
591
  end
564
592
 
565
- it "returns cents as 100 cents" do
566
- expect(money.cents).to eq(100)
567
- end
568
-
569
593
  it "returns cents as 100 cents" do
570
594
  expect(money.subunits).to eq(100)
571
595
  end
@@ -726,20 +750,6 @@ RSpec.describe "Money" do
726
750
  end
727
751
 
728
752
  describe "from_amount quacks like RubyMoney" do
729
- it "accepts numeric values" do
730
- expect(Money.from_amount(1)).to eq Money.from_cents(1_00)
731
- expect(Money.from_amount(1.0)).to eq Money.from_cents(1_00)
732
- expect(Money.from_amount(BigDecimal("1"))).to eq Money.from_cents(1_00)
733
- end
734
-
735
- it "accepts string values" do
736
- expect(Money.from_amount("1")).to eq Money.from_cents(1_00)
737
- end
738
-
739
- it "accepts nil values" do
740
- expect(Money.from_amount(nil)).to eq Money.from_cents(0)
741
- end
742
-
743
753
  it "accepts an optional currency parameter" do
744
754
  expect { Money.from_amount(1, "CAD") }.to_not raise_error
745
755
  end
@@ -759,10 +769,14 @@ RSpec.describe "Money" do
759
769
  money = YAML.dump(Money.new(750, 'usd'))
760
770
  expect(money).to eq("--- !ruby/object:Money\nvalue: '750.0'\ncurrency: USD\n")
761
771
  end
772
+
773
+ it "does not change BigDecimal value to Integer while rounding for currencies without subunits" do
774
+ money = Money.new(100, 'JPY').to_yaml
775
+ expect(money).to eq("--- !ruby/object:Money\nvalue: '100.0'\ncurrency: JPY\n")
776
+ end
762
777
  end
763
778
 
764
779
  describe "YAML deserialization" do
765
-
766
780
  it "accepts values with currencies" do
767
781
  money = YAML.load("--- !ruby/object:Money\nvalue: '750.0'\ncurrency: USD\n")
768
782
  expect(money).to eq(Money.new(750, 'usd'))
@@ -8,12 +8,16 @@ RSpec.describe RuboCop::Cop::Money::MissingCurrency do
8
8
 
9
9
  let(:config) { RuboCop::Config.new }
10
10
 
11
- describe '#on_send' do
12
- it 'registers an offense for Money.new without a currency argument' do
11
+ context 'with default configuration' do
12
+ it 'registers an offense and corrects for Money.new without a currency argument' do
13
13
  expect_offense(<<~RUBY)
14
14
  Money.new(1)
15
15
  ^^^^^^^^^^^^ Money is missing currency argument
16
16
  RUBY
17
+
18
+ expect_correction(<<~RUBY)
19
+ Money.new(1, Money::NULL_CURRENCY)
20
+ RUBY
17
21
  end
18
22
 
19
23
  it 'does not register an offense for Money.new with currency argument' do
@@ -22,18 +26,26 @@ RSpec.describe RuboCop::Cop::Money::MissingCurrency do
22
26
  RUBY
23
27
  end
24
28
 
25
- it 'registers an offense for Money.new without a currency argument' do
29
+ it 'registers an offense and corrects for Money.new without a currency argument' do
26
30
  expect_offense(<<~RUBY)
27
31
  Money.new
28
32
  ^^^^^^^^^ Money is missing currency argument
29
33
  RUBY
34
+
35
+ expect_correction(<<~RUBY)
36
+ Money.new(0, Money::NULL_CURRENCY)
37
+ RUBY
30
38
  end
31
39
 
32
- it 'registers an offense for Money.from_amount without a currency argument' do
40
+ it 'registers an offense and corrects for Money.from_amount without a currency argument' do
33
41
  expect_offense(<<~RUBY)
34
42
  Money.from_amount(1)
35
43
  ^^^^^^^^^^^^^^^^^^^^ Money is missing currency argument
36
44
  RUBY
45
+
46
+ expect_correction(<<~RUBY)
47
+ Money.from_amount(1, Money::NULL_CURRENCY)
48
+ RUBY
37
49
  end
38
50
 
39
51
  it 'does not register an offense for Money.from_amount with currency argument' do
@@ -42,11 +54,15 @@ RSpec.describe RuboCop::Cop::Money::MissingCurrency do
42
54
  RUBY
43
55
  end
44
56
 
45
- it 'registers an offense for Money.from_cents without a currency argument' do
57
+ it 'registers an offense and corrects for Money.from_cents without a currency argument' do
46
58
  expect_offense(<<~RUBY)
47
59
  Money.from_cents(1)
48
60
  ^^^^^^^^^^^^^^^^^^^ Money is missing currency argument
49
61
  RUBY
62
+
63
+ expect_correction(<<~RUBY)
64
+ Money.from_cents(1, Money::NULL_CURRENCY)
65
+ RUBY
50
66
  end
51
67
 
52
68
  it 'does not register an offense for Money.from_cents with currency argument' do
@@ -55,11 +71,26 @@ RSpec.describe RuboCop::Cop::Money::MissingCurrency do
55
71
  RUBY
56
72
  end
57
73
 
58
- it 'registers an offense for to_money without a currency argument' do
74
+ it 'registers an offense and corrects for to_money without a currency argument' do
59
75
  expect_offense(<<~RUBY)
60
76
  '1'.to_money
61
77
  ^^^^^^^^^^^^ to_money is missing currency argument
62
78
  RUBY
79
+
80
+ expect_correction(<<~RUBY)
81
+ '1'.to_money(Money::NULL_CURRENCY)
82
+ RUBY
83
+ end
84
+
85
+ it 'registers an offense and corrects for safe navigation to_money without a currency argument' do
86
+ expect_offense(<<~RUBY)
87
+ item&.to_money
88
+ ^^^^^^^^^^^^^^ to_money is missing currency argument
89
+ RUBY
90
+
91
+ expect_correction(<<~RUBY)
92
+ item&.to_money(Money::NULL_CURRENCY)
93
+ RUBY
63
94
  end
64
95
 
65
96
  it 'does not register an offense for to_money with currency argument' do
@@ -68,15 +99,19 @@ RSpec.describe RuboCop::Cop::Money::MissingCurrency do
68
99
  RUBY
69
100
  end
70
101
 
71
- it 'registers an offense for to_money block pass form' do
102
+ it 'registers an offense and corrects for to_money block pass form' do
72
103
  expect_offense(<<~RUBY)
73
104
  ['1'].map(&:to_money)
74
105
  ^^^^^^^^^^^^^^^^^^^^^ to_money is missing currency argument
75
106
  RUBY
107
+
108
+ expect_correction(<<~RUBY)
109
+ ['1'].map { |x| x.to_money(Money::NULL_CURRENCY) }
110
+ RUBY
76
111
  end
77
112
  end
78
113
 
79
- describe '#autocorrect' do
114
+ context 'with ReplacementCurrency configuration' do
80
115
  let(:config) do
81
116
  RuboCop::Config.new(
82
117
  'Money/MissingCurrency' => {
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../../rubocop_helper'
4
+ require 'rubocop/cop/money/zero_money'
5
+
6
+ RSpec.describe RuboCop::Cop::Money::ZeroMoney do
7
+ subject(:cop) { described_class.new(config) }
8
+
9
+ let(:config) { RuboCop::Config.new }
10
+
11
+ context 'with default configuration' do
12
+ it 'registers an offense and corrects Money.zero without currency' do
13
+ expect_offense(<<~RUBY)
14
+ Money.zero
15
+ ^^^^^^^^^^ Money.zero is removed, use `Money.new(0, Money::NULL_CURRENCY)`.
16
+ RUBY
17
+
18
+ expect_correction(<<~RUBY)
19
+ Money.new(0, Money::NULL_CURRENCY)
20
+ RUBY
21
+ end
22
+
23
+ it 'registers an offense and corrects Money.zero with currency' do
24
+ expect_offense(<<~RUBY)
25
+ Money.zero('CAD')
26
+ ^^^^^^^^^^^^^^^^^ Money.zero is removed, use `Money.new(0, 'CAD')`.
27
+ RUBY
28
+
29
+ expect_correction(<<~RUBY)
30
+ Money.new(0, 'CAD')
31
+ RUBY
32
+ end
33
+
34
+ it 'does not register an offense when using Money.new with a currency' do
35
+ expect_no_offenses(<<~RUBY)
36
+ Money.new(0, 'CAD')
37
+ RUBY
38
+ end
39
+ end
40
+
41
+ context 'with ReplacementCurrency configuration' do
42
+ let(:config) do
43
+ RuboCop::Config.new(
44
+ 'Money/ZeroMoney' => {
45
+ 'ReplacementCurrency' => 'CAD'
46
+ }
47
+ )
48
+ end
49
+
50
+ it 'registers an offense and corrects Money.zero without currency' do
51
+ expect_offense(<<~RUBY)
52
+ Money.zero
53
+ ^^^^^^^^^^ Money.zero is removed, use `Money.new(0, 'CAD')`.
54
+ RUBY
55
+
56
+ expect_correction(<<~RUBY)
57
+ Money.new(0, 'CAD')
58
+ RUBY
59
+ end
60
+
61
+ it 'registers an offense and corrects Money.zero with currency' do
62
+ expect_offense(<<~RUBY)
63
+ Money.zero('EUR')
64
+ ^^^^^^^^^^^^^^^^^ Money.zero is removed, use `Money.new(0, 'EUR')`.
65
+ RUBY
66
+
67
+ expect_correction(<<~RUBY)
68
+ Money.new(0, 'EUR')
69
+ RUBY
70
+ end
71
+
72
+ it 'does not register an offense when using Money.new with a currency' do
73
+ expect_no_offenses(<<~RUBY)
74
+ Money.new(0, 'EUR')
75
+ RUBY
76
+ end
77
+ end
78
+ 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.14.4
4
+ version: 0.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify Inc
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-07-21 00:00:00.000000000 Z
11
+ date: 2021-01-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '1.5'
19
+ version: '0'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '1.5'
26
+ version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: simplecov
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -103,9 +103,9 @@ extra_rdoc_files: []
103
103
  files:
104
104
  - ".document"
105
105
  - ".github/probots.yml"
106
+ - ".github/workflows/tests.yml"
106
107
  - ".gitignore"
107
108
  - ".rspec"
108
- - ".travis.yml"
109
109
  - Gemfile
110
110
  - LICENSE.txt
111
111
  - README.md
@@ -135,6 +135,7 @@ files:
135
135
  - lib/money_column/railtie.rb
136
136
  - lib/rubocop/cop/money.rb
137
137
  - lib/rubocop/cop/money/missing_currency.rb
138
+ - lib/rubocop/cop/money/zero_money.rb
138
139
  - lib/shopify-money.rb
139
140
  - money.gemspec
140
141
  - spec/accounting_money_parser_spec.rb
@@ -149,6 +150,7 @@ files:
149
150
  - spec/money_spec.rb
150
151
  - spec/null_currency_spec.rb
151
152
  - spec/rubocop/cop/money/missing_currency_spec.rb
153
+ - spec/rubocop/cop/money/zero_money_spec.rb
152
154
  - spec/rubocop_helper.rb
153
155
  - spec/schema.rb
154
156
  - spec/spec_helper.rb
@@ -189,6 +191,7 @@ test_files:
189
191
  - spec/money_spec.rb
190
192
  - spec/null_currency_spec.rb
191
193
  - spec/rubocop/cop/money/missing_currency_spec.rb
194
+ - spec/rubocop/cop/money/zero_money_spec.rb
192
195
  - spec/rubocop_helper.rb
193
196
  - spec/schema.rb
194
197
  - spec/spec_helper.rb
@@ -1,13 +0,0 @@
1
- sudo: false
2
- language: ruby
3
- cache: bundler
4
- branches:
5
- only:
6
- - master
7
- rvm:
8
- - 2.7
9
- - 2.6
10
- before_install:
11
- # https://github.com/travis-ci/travis-ci/issues/8978#issuecomment-354036443
12
- - gem update --system
13
- - gem install bundler