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.
data/lib/money/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  class Money
3
- VERSION = "0.14.6"
3
+ VERSION = "1.0.0.pre"
4
4
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  module MoneyColumn
3
+ class CurrencyReadOnlyError < StandardError; end
4
+
3
5
  module ActiveRecordHooks
4
6
  def self.included(base)
5
7
  base.extend(ClassMethods)
@@ -49,16 +51,19 @@ module MoneyColumn
49
51
  return self[column] = nil
50
52
  end
51
53
 
52
- currency_raw_source = options[:currency] || (send(options[:currency_column]) rescue nil)
53
-
54
- if !money.is_a?(Money)
55
- return self[column] = Money.new(money, currency_raw_source).value
54
+ unless money.is_a?(Money)
55
+ return self[column] = Money::Helpers.value_to_decimal(money)
56
56
  end
57
57
 
58
58
  if options[:currency_read_only]
59
- currency_source = Money::Helpers.value_to_currency(currency_raw_source)
60
- if currency_raw_source && !money.currency.compatible?(currency_source)
61
- Money.deprecate("[money_column] currency mismatch between #{currency_source} and #{money.currency} in column #{column}.")
59
+ currency = options[:currency] || try(options[:currency_column])
60
+ if currency && !money.currency.compatible?(Money::Helpers.value_to_currency(currency))
61
+ msg = "[money_column] currency mismatch between #{currency} and #{money.currency} in column #{column}."
62
+ if Money.config.legacy_deprecations
63
+ Money.deprecate(msg)
64
+ else
65
+ raise MoneyColumn::CurrencyReadOnlyError, msg
66
+ end
62
67
  end
63
68
  else
64
69
  self[options[:currency_column]] = money.currency.to_s unless money.no_currency?
@@ -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")
@@ -20,8 +20,10 @@ RSpec.describe AccountingMoneyParser do
20
20
  end
21
21
 
22
22
  it "parses an invalid string to $0" do
23
- expect(Money).to receive(:deprecate).once
24
- expect(@parser.parse("no money")).to eq(Money.new)
23
+ configure(legacy_deprecations: true) do
24
+ expect(Money).to receive(:deprecate).once
25
+ expect(@parser.parse("no money", 'USD')).to eq(Money.new(0, 'USD'))
26
+ end
25
27
  end
26
28
 
27
29
  it "parses a single digit integer string" do
@@ -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
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+ require 'spec_helper'
3
+
4
+ RSpec.describe "Money::Config" do
5
+ describe 'legacy_deprecations' do
6
+ it "respects the default currency" do
7
+ configure(default_currency: 'USD', legacy_deprecations: true) do
8
+ expect(Money.default_currency).to eq("USD")
9
+ end
10
+ end
11
+
12
+ it 'defaults to not opt-in to v1' do
13
+ expect(Money::Config.new.legacy_deprecations).to eq(false)
14
+ end
15
+
16
+ it 'legacy_deprecations returns true when opting in to v1' do
17
+ configure(legacy_deprecations: true) do
18
+ expect(Money.config.legacy_deprecations).to eq(true)
19
+ end
20
+ end
21
+
22
+ it 'sets the deprecations to raise' do
23
+ configure(legacy_deprecations: true) do
24
+ expect { Money.deprecate("test") }.to raise_error(ActiveSupport::DeprecationException)
25
+ end
26
+ end
27
+
28
+ it 'legacy_deprecations defaults to NULL_CURRENCY' do
29
+ configure(legacy_default_currency: true) do
30
+ expect(Money.config.default_currency).to eq(Money::NULL_CURRENCY)
31
+ end
32
+ end
33
+ end
34
+
35
+ describe 'parser' do
36
+ it 'defaults to MoneyParser' do
37
+ expect(Money::Config.new.parser).to eq(MoneyParser)
38
+ end
39
+
40
+ it 'can be set to a new parser' do
41
+ configure(parser: AccountingMoneyParser) do
42
+ expect(Money.config.parser).to eq(AccountingMoneyParser)
43
+ end
44
+ end
45
+ end
46
+
47
+ describe 'default_currency' do
48
+ it 'defaults to nil' do
49
+ configure do
50
+ expect(Money.config.default_currency).to eq(nil)
51
+ end
52
+ end
53
+
54
+ it 'can be set to a new currency' do
55
+ configure(default_currency: 'USD') do
56
+ expect(Money.config.default_currency).to eq('USD')
57
+ end
58
+ end
59
+ end
60
+ end
@@ -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)
data/spec/helpers_spec.rb CHANGED
@@ -46,8 +46,10 @@ RSpec.describe Money::Helpers do
46
46
  end
47
47
 
48
48
  it 'invalid string returns zero' do
49
- expect(Money).to receive(:deprecate).once
50
- expect(subject.value_to_decimal('invalid')).to eq(0)
49
+ configure(legacy_deprecations: true) do
50
+ expect(Money).to receive(:deprecate).once
51
+ expect(subject.value_to_decimal('invalid')).to eq(0)
52
+ end
51
53
  end
52
54
 
53
55
  it 'raises on invalid object' do
@@ -70,7 +72,9 @@ RSpec.describe Money::Helpers do
70
72
  end
71
73
 
72
74
  it 'returns the default currency when value is empty' do
73
- expect(subject.value_to_currency('')).to eq(Money.default_currency)
75
+ configure(legacy_deprecations: true, default_currency: 'USD') do
76
+ expect(subject.value_to_currency('')).to eq(Money::Currency.new('USD'))
77
+ end
74
78
  end
75
79
 
76
80
  it 'returns the default currency when value is xxx' do
@@ -86,8 +90,10 @@ RSpec.describe Money::Helpers do
86
90
  end
87
91
 
88
92
  it 'returns the null currency when invalid iso is passed' do
89
- expect(Money).to receive(:deprecate).once
90
- expect(subject.value_to_currency('invalid')).to eq(Money::NULL_CURRENCY)
93
+ configure(legacy_deprecations: true) do
94
+ expect(Money).to receive(:deprecate).once
95
+ expect(subject.value_to_currency('invalid')).to eq(Money::NULL_CURRENCY)
96
+ end
91
97
  end
92
98
 
93
99
  it 'raises on invalid object' do
@@ -30,13 +30,18 @@ end
30
30
 
31
31
  class MoneyWithDelegatedCurrency < ActiveRecord::Base
32
32
  self.table_name = 'money_records'
33
- attr_accessor :delegated_record
34
33
  delegate :currency, to: :delegated_record
35
34
  money_column :price, currency_column: 'currency', currency_read_only: true
36
35
  money_column :prix, currency_column: 'currency2', currency_read_only: true
37
36
  def currency2
38
37
  delegated_record.currency
39
38
  end
39
+
40
+ private
41
+
42
+ def delegated_record
43
+ MoneyRecord.new(currency: 'USD')
44
+ end
40
45
  end
41
46
 
42
47
  class MoneyWithCustomAccessors < ActiveRecord::Base
@@ -97,11 +102,13 @@ RSpec.describe 'MoneyColumn' do
97
102
  end
98
103
 
99
104
  it 'returns money with null currency when the currency in the DB is invalid' do
100
- expect(Money).to receive(:deprecate).once
101
- record.update_columns(currency: 'invalid')
102
- record.reload
103
- expect(record.price.currency).to be_a(Money::NullCurrency)
104
- expect(record.price.value).to eq(1.23)
105
+ configure(legacy_deprecations: true) do
106
+ expect(Money).to receive(:deprecate).once
107
+ record.update_columns(currency: 'invalid')
108
+ record.reload
109
+ expect(record.price.currency).to be_a(Money::NullCurrency)
110
+ expect(record.price.value).to eq(1.23)
111
+ end
105
112
  end
106
113
 
107
114
  it 'handles legacy support for saving floats' do
@@ -114,12 +121,12 @@ RSpec.describe 'MoneyColumn' do
114
121
  expect(record.prix.currency.to_s).to eq('CAD')
115
122
  end
116
123
 
117
- it 'handles legacy support for saving floats with correct currency rounding' do
124
+ it 'handles legacy support for saving floats as provided' do
118
125
  record.update(price: 3.2112, prix: 3.2156)
119
- expect(record.attributes['price']).to eq(3.21)
126
+ expect(record.attributes['price']).to eq(3.2112)
120
127
  expect(record.price.value).to eq(3.21)
121
128
  expect(record.price.currency.to_s).to eq(currency)
122
- expect(record.attributes['prix']).to eq(3.22)
129
+ expect(record.attributes['prix']).to eq(3.2156)
123
130
  expect(record.prix.value).to eq(3.22)
124
131
  expect(record.prix.currency.to_s).to eq('CAD')
125
132
  end
@@ -136,7 +143,7 @@ RSpec.describe 'MoneyColumn' do
136
143
 
137
144
  it 'does not overwrite a currency column with a default currency when saving zero' do
138
145
  expect(record.currency.to_s).to eq('EUR')
139
- record.update(price: Money.zero)
146
+ record.update(price: Money.new(0, Money::NULL_CURRENCY))
140
147
  expect(record.currency.to_s).to eq('EUR')
141
148
  end
142
149
 
@@ -165,8 +172,8 @@ RSpec.describe 'MoneyColumn' do
165
172
  describe 'garbage amount' do
166
173
  let(:amount) { 'foo' }
167
174
 
168
- it 'raises a deprecation warning' do
169
- expect { subject }.to raise_error(ActiveSupport::DeprecationException)
175
+ it 'raises an ArgumentError' do
176
+ expect { subject }.to raise_error(ArgumentError)
170
177
  end
171
178
  end
172
179
 
@@ -174,7 +181,7 @@ RSpec.describe 'MoneyColumn' do
174
181
  let(:currency) { 'foo' }
175
182
 
176
183
  it 'raises an UnknownCurrency error' do
177
- expect { subject }.to raise_error(ActiveSupport::DeprecationException)
184
+ expect { subject }.to raise_error(Money::Currency::UnknownCurrency)
178
185
  end
179
186
  end
180
187
 
@@ -217,11 +224,20 @@ RSpec.describe 'MoneyColumn' do
217
224
  describe 'read_only_currency true' do
218
225
  it 'does not write the currency to the db' do
219
226
  record = MoneyWithReadOnlyCurrency.create
220
- record.update_columns(price: 1, currency: 'USD')
221
- expect(Money).to receive(:deprecate).once
222
- record.update(price: Money.new(4, 'CAD'))
223
- expect(record.price.value).to eq(4)
224
- expect(record.price.currency.to_s).to eq('USD')
227
+ record.update_columns(currency: 'USD')
228
+ expect { record.update(price: Money.new(4, 'CAD')) }.to raise_error(MoneyColumn::CurrencyReadOnlyError)
229
+ end
230
+
231
+ it 'legacy_deprecations does not write the currency to the db' do
232
+ configure(legacy_deprecations: true) do
233
+ record = MoneyWithReadOnlyCurrency.create
234
+ record.update_columns(currency: 'USD')
235
+
236
+ expect(Money).to receive(:deprecate).once
237
+ record.update(price: Money.new(4, 'CAD'))
238
+ expect(record.price.value).to eq(4)
239
+ expect(record.price.currency.to_s).to eq('USD')
240
+ end
225
241
  end
226
242
 
227
243
  it 'reads the currency that is already in the db' do
@@ -233,12 +249,14 @@ RSpec.describe 'MoneyColumn' do
233
249
  end
234
250
 
235
251
  it 'reads an invalid currency from the db and generates a no currency object' do
236
- expect(Money).to receive(:deprecate).once
237
- record = MoneyWithReadOnlyCurrency.create
238
- record.update_columns(currency: 'invalid', price: 1)
239
- record.reload
240
- expect(record.price.value).to eq(1)
241
- expect(record.price.currency.to_s).to eq('')
252
+ configure(legacy_deprecations: true) do
253
+ expect(Money).to receive(:deprecate).once
254
+ record = MoneyWithReadOnlyCurrency.create
255
+ record.update_columns(currency: 'invalid', price: 1)
256
+ record.reload
257
+ expect(record.price.value).to eq(1)
258
+ expect(record.price.currency.to_s).to eq('')
259
+ end
242
260
  end
243
261
 
244
262
  it 'sets the currency correctly when the currency is changed' do
@@ -248,12 +266,12 @@ RSpec.describe 'MoneyColumn' do
248
266
  end
249
267
 
250
268
  it 'handle cases where the delegate allow_nil is false' do
251
- record = MoneyWithDelegatedCurrency.new(price: Money.new(10, 'USD'), delegated_record: MoneyRecord.new(currency: 'USD'))
269
+ record = MoneyWithDelegatedCurrency.new(price: Money.new(10, 'USD'))
252
270
  expect(record.price.currency.to_s).to eq('USD')
253
271
  end
254
272
 
255
273
  it 'handle cases where a manual delegate does not allow nil' do
256
- record = MoneyWithDelegatedCurrency.new(prix: Money.new(10, 'USD'), delegated_record: MoneyRecord.new(currency: 'USD'))
274
+ record = MoneyWithDelegatedCurrency.new(prix: Money.new(10, 'USD'))
257
275
  expect(record.price.currency.to_s).to eq('USD')
258
276
  end
259
277
  end
@@ -370,10 +388,7 @@ RSpec.describe 'MoneyColumn' do
370
388
 
371
389
  describe 'default_currency = nil' do
372
390
  around do |example|
373
- default_currency = Money.default_currency
374
- Money.default_currency = nil
375
- example.run
376
- Money.default_currency = default_currency
391
+ configure(default_currency: nil) { example.run }
377
392
  end
378
393
 
379
394
  it 'writes currency from input value to the db' do
@@ -384,11 +399,17 @@ RSpec.describe 'MoneyColumn' do
384
399
  expect(record.price.currency.to_s).to eq('GBP')
385
400
  end
386
401
 
387
- it 'raises missing currency error when input is not a money object' do
388
- record.update(currency: nil)
402
+ it 'raises missing currency error reading a value that was saved using legacy non-money object' do
403
+ record.update(currency: nil, price: 3)
404
+ expect { record.price }.to raise_error(ArgumentError, 'missing currency')
405
+ end
389
406
 
390
- expect { record.update(price: 3) }
391
- .to raise_error(ArgumentError, 'missing currency')
407
+ it 'handles legacy support for saving price and currency separately' do
408
+ record.update(currency: nil)
409
+ record.update(price: 7, currency: 'GBP')
410
+ record.reload
411
+ expect(record.price.value).to eq(7)
412
+ expect(record.price.currency.to_s).to eq('GBP')
392
413
  end
393
414
  end
394
415
  end