shopify-money 0.14.5 → 0.16.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: 683fbe81ccbaba82b0ff6aeae0cd13086453cf72eef33c8be0fb8afa453f7b6a
4
- data.tar.gz: e4fdaff4a2761fd6d37a9ff50eeda4667572b563f9bd50e6c491808ab17fe3b5
3
+ metadata.gz: f9919cf2d14833f204c05a9642efb7a48bb23822c70c48cf523ccbc68c3e7491
4
+ data.tar.gz: d73bd65e6cb49ee8f5be7add57683274bb006fbfd0ec61aec566385fd92d50f6
5
5
  SHA512:
6
- metadata.gz: 1190c9f7033aca4e0c44a60489dc2fac9b86d597afb6f3976da33b05e816719b8702af4b9c32ac0ffbf6230847c7858b18ce497c5d929336bbff8cfb89ee11fb
7
- data.tar.gz: 97bcf3f52c7985422490074cb8c920d6ccc85cfc64e401ad5fa4c7bb5f5ee9ad449a3d89b19bbf9f5afa0f79cf23a7cd61804497dfcfca02f6ce76863a7e5c16
6
+ metadata.gz: b3dc0c02cee6598d2369ce472a5df01dfb3528cfb45f517394ab7c29b059338b8d65d10c03ae397a4ecdff98a7987a61e0312b894947b728e1ed32078b726086
7
+ data.tar.gz: 1e7cec90da29c1327fdf64984f34e7b6fa01282eb7a0c2e3eda6482d688b06d148b7b7bff760555e3a7d7302ec8b20079ad468cd7c02dabb83c7e9a0aeb0a1f2
@@ -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.
@@ -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
@@ -9,7 +9,6 @@ require_relative 'money/errors'
9
9
  require_relative 'money/deprecations'
10
10
  require_relative 'money/accounting_money_parser'
11
11
  require_relative 'money/core_extensions'
12
- require_relative 'money_accessor'
13
12
  require_relative 'money_column' if defined?(ActiveRecord)
14
13
 
15
14
  require_relative 'rubocop/cop/money' if defined?(RuboCop)
@@ -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
data/lib/money/money.rb CHANGED
@@ -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
data/lib/money/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  class Money
3
- VERSION = "0.14.5"
3
+ VERSION = "0.16.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
data/money.gemspec CHANGED
@@ -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
 
data/spec/money_spec.rb CHANGED
@@ -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.5
4
+ version: 0.16.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-08-25 00:00:00.000000000 Z
11
+ date: 2021-03-03 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
@@ -128,13 +128,13 @@ files:
128
128
  - lib/money/money_parser.rb
129
129
  - lib/money/null_currency.rb
130
130
  - lib/money/version.rb
131
- - lib/money_accessor.rb
132
131
  - lib/money_column.rb
133
132
  - lib/money_column/active_record_hooks.rb
134
133
  - lib/money_column/active_record_type.rb
135
134
  - lib/money_column/railtie.rb
136
135
  - lib/rubocop/cop/money.rb
137
136
  - lib/rubocop/cop/money/missing_currency.rb
137
+ - lib/rubocop/cop/money/zero_money.rb
138
138
  - lib/shopify-money.rb
139
139
  - money.gemspec
140
140
  - spec/accounting_money_parser_spec.rb
@@ -143,12 +143,12 @@ files:
143
143
  - spec/currency/loader_spec.rb
144
144
  - spec/currency_spec.rb
145
145
  - spec/helpers_spec.rb
146
- - spec/money_accessor_spec.rb
147
146
  - spec/money_column_spec.rb
148
147
  - spec/money_parser_spec.rb
149
148
  - spec/money_spec.rb
150
149
  - spec/null_currency_spec.rb
151
150
  - spec/rubocop/cop/money/missing_currency_spec.rb
151
+ - spec/rubocop/cop/money/zero_money_spec.rb
152
152
  - spec/rubocop_helper.rb
153
153
  - spec/schema.rb
154
154
  - spec/spec_helper.rb
@@ -183,12 +183,12 @@ test_files:
183
183
  - spec/currency/loader_spec.rb
184
184
  - spec/currency_spec.rb
185
185
  - spec/helpers_spec.rb
186
- - spec/money_accessor_spec.rb
187
186
  - spec/money_column_spec.rb
188
187
  - spec/money_parser_spec.rb
189
188
  - spec/money_spec.rb
190
189
  - spec/null_currency_spec.rb
191
190
  - spec/rubocop/cop/money/missing_currency_spec.rb
191
+ - spec/rubocop/cop/money/zero_money_spec.rb
192
192
  - spec/rubocop_helper.rb
193
193
  - spec/schema.rb
194
194
  - spec/spec_helper.rb
data/.travis.yml DELETED
@@ -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
@@ -1,33 +0,0 @@
1
- # frozen_string_literal: true
2
- module MoneyAccessor
3
- def self.included(base)
4
- base.extend(ClassMethods)
5
- end
6
-
7
- module ClassMethods
8
- def money_accessor(*columns)
9
- variable_get = self <= Struct ? :[] : :instance_variable_get
10
- variable_set = self <= Struct ? :[]= : :instance_variable_set
11
-
12
- Array(columns).flatten.each do |name|
13
- variable_name = self <= Struct ? name : "@#{name}"
14
-
15
- define_method(name) do
16
- value = public_send(variable_get, variable_name)
17
- value.blank? ? nil : Money.new(value)
18
- end
19
-
20
- define_method("#{name}=") do |value|
21
- if value.blank? || !value.respond_to?(:to_money)
22
- public_send(variable_set, variable_name, nil)
23
- nil
24
- else
25
- money = value.to_money
26
- public_send(variable_set, variable_name, money.value)
27
- money
28
- end
29
- end
30
- end
31
- end
32
- end
33
- end
@@ -1,87 +0,0 @@
1
- # frozen_string_literal: true
2
- require 'spec_helper'
3
-
4
- class NormalObject
5
- include MoneyAccessor
6
-
7
- money_accessor :price
8
-
9
- def initialize(price)
10
- @price = price
11
- end
12
- end
13
-
14
- class StructObject < Struct.new(:price)
15
- include MoneyAccessor
16
-
17
- money_accessor :price
18
- end
19
-
20
- RSpec.shared_examples_for "an object with a money accessor" do
21
- it "generates an attribute reader that returns a money object" do
22
- object = described_class.new(100)
23
-
24
- expect(object.price).to eq(Money.new(100))
25
- end
26
-
27
- it "generates an attribute reader that returns a nil object if the value was nil" do
28
- object = described_class.new(nil)
29
-
30
- expect(object.price).to eq(nil)
31
- end
32
-
33
- it "generates an attribute reader that returns a nil object if the value was blank" do
34
- object = described_class.new('')
35
-
36
- expect(object.price).to eq(nil)
37
- end
38
-
39
- it "generates an attribute writer that allow setting a money object" do
40
- object = described_class.new(0)
41
- object.price = Money.new(10)
42
-
43
- expect(object.price).to eq(Money.new(10))
44
- end
45
-
46
- it "generates an attribute writer that allow setting a integer value" do
47
- object = described_class.new(0)
48
- object.price = 10
49
-
50
- expect(object.price).to eq(Money.new(10))
51
- end
52
-
53
- it "generates an attribute writer that allow setting a float value" do
54
- object = described_class.new(0)
55
- object.price = 10.12
56
-
57
- expect(object.price).to eq(Money.new(10.12))
58
- end
59
-
60
- it "generates an attribute writer that allow setting a nil value" do
61
- object = described_class.new(0)
62
- object.price = nil
63
-
64
- expect(object.price).to eq(nil)
65
- end
66
-
67
- it "generates an attribute writer that allow setting a blank value" do
68
- object = described_class.new(0)
69
- object.price = ''
70
-
71
- expect(object.price).to eq(nil)
72
- end
73
- end
74
-
75
- RSpec.describe NormalObject do
76
- it_behaves_like "an object with a money accessor"
77
- end
78
-
79
- RSpec.describe StructObject do
80
- it_behaves_like "an object with a money accessor"
81
-
82
- it 'does not generate an ivar to store the price value' do
83
- object = described_class.new(10.00)
84
-
85
- expect(object.instance_variable_get(:@price)).to eq(nil)
86
- end
87
- end