shopify-money 0.14.6 → 1.0.0.pre

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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