shopify-money 0.16.0 → 1.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f9919cf2d14833f204c05a9642efb7a48bb23822c70c48cf523ccbc68c3e7491
4
- data.tar.gz: d73bd65e6cb49ee8f5be7add57683274bb006fbfd0ec61aec566385fd92d50f6
3
+ metadata.gz: ddeb1f02ca7fda02ed007433687bf2fdbce36e9b11d64724753cf2e2420507c0
4
+ data.tar.gz: de0de61cbe5c6ae02b2fbf333e4136edbfcd1c2388441c219ffb1a00e2d0a963
5
5
  SHA512:
6
- metadata.gz: b3dc0c02cee6598d2369ce472a5df01dfb3528cfb45f517394ab7c29b059338b8d65d10c03ae397a4ecdff98a7987a61e0312b894947b728e1ed32078b726086
7
- data.tar.gz: 1e7cec90da29c1327fdf64984f34e7b6fa01282eb7a0c2e3eda6482d688b06d148b7b7bff760555e3a7d7302ec8b20079ad468cd7c02dabb83c7e9a0aeb0a1f2
6
+ metadata.gz: 86ef224eb66a9d8d6249b921cdd9162594babac0e5fa7525f2e9e5125ad94ab02e395bef9af508d4f6d08622d2c3fa6628b902aac3de803ef81fd30fcae0464c
7
+ data.tar.gz: 7332ed67b6d7389123131538c7c05faae5b9e67a4b77ae66c21f0d2bbd7031f4994576fdaa14c54294ad2969c70ffe1e70958528f16116e944fa6457d5c80d6e
data/README.md CHANGED
@@ -20,6 +20,10 @@ money_column expects a DECIMAL(21,3) database field.
20
20
 
21
21
  gem 'shopify-money', require: 'money'
22
22
 
23
+ ## Upgrading to v1.0
24
+
25
+ see instructions and breaking changes: https://github.com/Shopify/money/blob/master/UPGRADING.md
26
+
23
27
  ## Usage
24
28
 
25
29
  ``` ruby
@@ -77,7 +81,9 @@ By default `Money` defaults to Money::NullCurrency as its currency. This is a
77
81
  global variable that can be changed using:
78
82
 
79
83
  ``` ruby
80
- Money.default_currency = Money::Currency.new("USD")
84
+ Money.configure do |config|
85
+ config.default_currency = Money::Currency.new("USD")
86
+ end
81
87
  ```
82
88
 
83
89
  In web apps you might want to set the default currency on a per request basis.
data/UPGRADING.md ADDED
@@ -0,0 +1,57 @@
1
+ ## Upgrading to v1.0
2
+
3
+ In an initializer add the following
4
+ ```ruby
5
+ Money.configure do |config|
6
+ config.legacy_default_currency!
7
+ config.legacy_deprecations!
8
+ config.legacy_json_format!
9
+ #...
10
+ end
11
+ ```
12
+
13
+ Remove each legacy setting making sure your app functions as expected.
14
+
15
+ ### Legacy support
16
+
17
+ #### legacy_default_currency!
18
+
19
+ By enabling this setting your app will accept money object that are missing a currency
20
+
21
+ ```ruby
22
+ Money.new(1) #=> value: 1, currency: XXX
23
+ ```
24
+
25
+ #### legacy_deprecations!
26
+
27
+ invalid money values return zero
28
+ ```ruby
29
+ Money.new('a', 'USD') #=> Money.new(0, 'USD')
30
+ ```
31
+
32
+ invalid currency is ignored
33
+ ```ruby
34
+ Money.new(1, 'ABCD') #=> Money.new(1)
35
+ ```
36
+
37
+ mathematical operations between objects are allowed
38
+ ```ruby
39
+ Money.new(1, 'USD') + Money.new(1, 'CAD') #=> Money.new(2, 'USD')
40
+ ```
41
+
42
+ parsing a string with invalid delimiters
43
+ ```ruby
44
+ Money.parse('123*12') #=> Money.new(123)
45
+ ```
46
+
47
+ #### legacy_json_format!
48
+
49
+ to_json will return only the value (no currency)
50
+ ```ruby
51
+ # with legacy_json_format!
52
+ money.to_json #=> "1"
53
+
54
+ # without
55
+ money.to_json #=> { value: 1, currency: 'USD' }
56
+ ```
57
+
data/lib/money.rb CHANGED
@@ -4,6 +4,7 @@ require_relative 'money/helpers'
4
4
  require_relative 'money/currency'
5
5
  require_relative 'money/null_currency'
6
6
  require_relative 'money/allocator'
7
+ require_relative 'money/config'
7
8
  require_relative 'money/money'
8
9
  require_relative 'money/errors'
9
10
  require_relative 'money/deprecations'
@@ -7,6 +7,8 @@ class Money
7
7
  super
8
8
  end
9
9
 
10
+ ONE = BigDecimal("1")
11
+
10
12
  # Allocates money between different parties without losing pennies.
11
13
  # After the mathematically split has been performed, left over pennies will
12
14
  # be distributed round-robin amongst the parties. This means that parties
@@ -43,7 +45,7 @@ class Money
43
45
  splits.map!(&:to_r)
44
46
  allocations = splits.inject(0, :+)
45
47
 
46
- if (allocations - BigDecimal("1")) > Float::EPSILON
48
+ if (allocations - ONE) > Float::EPSILON
47
49
  raise ArgumentError, "splits add to more than 100%"
48
50
  end
49
51
 
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Money
4
+ class Config
5
+ attr_accessor :parser, :default_currency, :legacy_json_format, :legacy_deprecations
6
+
7
+ def legacy_default_currency!
8
+ @default_currency ||= Money::NULL_CURRENCY
9
+ end
10
+
11
+ def legacy_deprecations!
12
+ @legacy_deprecations = true
13
+ end
14
+
15
+ def legacy_json_format!
16
+ @legacy_json_format = true
17
+ end
18
+
19
+ def initialize
20
+ @parser = MoneyParser
21
+ @default_currency = nil
22
+ @legacy_json_format = false
23
+ @legacy_deprecations = false
24
+ end
25
+ end
26
+ end
data/lib/money/helpers.rb CHANGED
@@ -28,7 +28,7 @@ class Money
28
28
  when Rational
29
29
  BigDecimal(num, MAX_DECIMAL)
30
30
  when String
31
- decimal = BigDecimal(num, exception: false)
31
+ decimal = BigDecimal(num, exception: !Money.config.legacy_deprecations)
32
32
  return decimal if decimal
33
33
 
34
34
  Money.deprecate("using Money.new('#{num}') is deprecated and will raise an ArgumentError in the next major release")
@@ -46,7 +46,7 @@ class Money
46
46
  currency
47
47
  when nil, ''
48
48
  default = Money.current_currency || Money.default_currency
49
- raise(ArgumentError, 'missing currency') if default.nil? || default == ''
49
+ raise(Money::Currency::UnknownCurrency, 'missing currency') if default.nil? || default == ''
50
50
  value_to_currency(default)
51
51
  when 'xxx', 'XXX'
52
52
  Money::NULL_CURRENCY
@@ -54,8 +54,12 @@ class Money
54
54
  begin
55
55
  Currency.find!(currency)
56
56
  rescue Money::Currency::UnknownCurrency => error
57
- Money.deprecate(error.message)
58
- Money::NULL_CURRENCY
57
+ if Money.config.legacy_deprecations
58
+ Money.deprecate(error.message)
59
+ Money::NULL_CURRENCY
60
+ else
61
+ raise error
62
+ end
59
63
  end
60
64
  else
61
65
  raise ArgumentError, "could not parse as currency #{currency.inspect}"
data/lib/money/money.rb CHANGED
@@ -11,7 +11,14 @@ class Money
11
11
  def_delegators :@value, :zero?, :nonzero?, :positive?, :negative?, :to_i, :to_f, :hash
12
12
 
13
13
  class << self
14
- attr_accessor :parser, :default_currency
14
+ extend Forwardable
15
+ attr_accessor :config
16
+ def_delegators :@config, :parser, :parser=, :default_currency, :default_currency=
17
+
18
+ def configure
19
+ self.config ||= Config.new
20
+ yield(config) if block_given?
21
+ end
15
22
 
16
23
  def new(value = 0, currency = nil)
17
24
  value = Helpers.value_to_decimal(value)
@@ -73,13 +80,8 @@ class Money
73
80
  Money.current_currency = old_currency
74
81
  end
75
82
  end
76
-
77
- def default_settings
78
- self.parser = MoneyParser
79
- self.default_currency = Money::NULL_CURRENCY
80
- end
81
83
  end
82
- default_settings
84
+ configure
83
85
 
84
86
  def initialize(value, currency)
85
87
  raise ArgumentError if value.nan?
@@ -138,7 +140,11 @@ class Money
138
140
 
139
141
  def *(numeric)
140
142
  unless numeric.is_a?(Numeric)
141
- Money.deprecate("Multiplying Money with #{numeric.class.name} is deprecated and will be removed in the next major release.")
143
+ if Money.config.legacy_deprecations
144
+ Money.deprecate("Multiplying Money with #{numeric.class.name} is deprecated and will be removed in the next major release.")
145
+ else
146
+ raise ArgumentError, "Money objects can only be multiplied by a Numeric"
147
+ end
142
148
  end
143
149
  Money.new(value.to_r * numeric, currency)
144
150
  end
@@ -198,8 +204,12 @@ class Money
198
204
 
199
205
  curr = Helpers.value_to_currency(curr)
200
206
  unless currency.compatible?(curr)
201
- Money.deprecate("mathematical operation not permitted for Money objects with different currencies #{curr} and #{currency}. " \
202
- "A Money::IncompatibleCurrencyError will raise in the next major release")
207
+ msg = "mathematical operation not permitted for Money objects with different currencies #{curr} and #{currency}"
208
+ if Money.config.legacy_deprecations
209
+ Money.deprecate("#{msg}. A Money::IncompatibleCurrencyError will raise in the next major release")
210
+ else
211
+ raise Money::IncompatibleCurrencyError, msg
212
+ end
203
213
  end
204
214
 
205
215
  self
@@ -230,11 +240,19 @@ class Money
230
240
  end
231
241
 
232
242
  def to_json(options = {})
233
- to_s
243
+ if options.delete(:legacy_format) || Money.config.legacy_json_format
244
+ to_s
245
+ else
246
+ as_json(options).to_json
247
+ end
234
248
  end
235
249
 
236
- def as_json(*args)
237
- to_s
250
+ def as_json(options = {})
251
+ if options.delete(:legacy_format) || Money.config.legacy_json_format
252
+ to_s
253
+ else
254
+ { value: to_s(:amount), currency: currency.to_s }
255
+ end
238
256
  end
239
257
 
240
258
  def abs
@@ -82,9 +82,12 @@ class MoneyParser
82
82
  number = number.to_s.strip
83
83
 
84
84
  if number.empty?
85
- raise MoneyFormatError, "invalid money string: #{input}" if strict
86
- Money.deprecate("invalid money strings will raise in the next major release \"#{input}\"")
87
- return '0'
85
+ if Money.config.legacy_deprecations && !strict
86
+ Money.deprecate("invalid money strings will raise in the next major release \"#{input}\"")
87
+ return '0'
88
+ else
89
+ raise MoneyFormatError, "invalid money string: #{input}"
90
+ end
88
91
  end
89
92
 
90
93
  marks = number.scan(/[#{ESCAPED_MARKS}]/).flatten
@@ -107,8 +110,11 @@ class MoneyParser
107
110
  return amount.tr(ESCAPED_NON_COMMA_MARKS, '').sub(',', '.')
108
111
  end
109
112
 
110
- raise MoneyFormatError, "invalid money string: #{input}" if strict
111
- Money.deprecate("invalid money strings will raise in the next major release \"#{input}\"")
113
+ if Money.config.legacy_deprecations && !strict
114
+ Money.deprecate("invalid money strings will raise in the next major release \"#{input}\"")
115
+ else
116
+ raise MoneyFormatError, "invalid money string: #{input}"
117
+ end
112
118
 
113
119
  normalize_number(number, marks, currency)
114
120
  end
data/lib/money/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  class Money
3
- VERSION = "0.16.0"
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?
@@ -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
@@ -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
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
@@ -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
@@ -12,9 +12,11 @@ RSpec.describe MoneyParser do
12
12
  end
13
13
 
14
14
  it "parses an invalid string when not strict" do
15
- expect(Money).to receive(:deprecate).twice
16
- expect(@parser.parse("no money")).to eq(Money.new(0, Money::NULL_CURRENCY))
17
- expect(@parser.parse("1..")).to eq(Money.new(1))
15
+ configure(legacy_deprecations: true) do
16
+ expect(Money).to receive(:deprecate).twice
17
+ expect(@parser.parse("no money", 'USD')).to eq(Money.new(0, 'USD'))
18
+ expect(@parser.parse("1..", 'USD')).to eq(Money.new(1, 'USD'))
19
+ end
18
20
  end
19
21
 
20
22
  it "parses raise with an invalid string and strict option" do
@@ -22,6 +24,12 @@ RSpec.describe MoneyParser do
22
24
  expect { @parser.parse("1..1", strict: true) }.to raise_error(MoneyParser::MoneyFormatError)
23
25
  end
24
26
 
27
+ it "parses raise with an invalid when a currency is missing" do
28
+ configure do
29
+ expect { @parser.parse("1") }.to raise_error(Money::Currency::UnknownCurrency)
30
+ end
31
+ end
32
+
25
33
  it "parses a single digit integer string" do
26
34
  expect(@parser.parse("1")).to eq(Money.new(1.00))
27
35
  end
@@ -144,8 +152,10 @@ RSpec.describe MoneyParser do
144
152
  end
145
153
 
146
154
  it "parses amount with multiple inconsistent thousands delimiters" do
147
- expect(Money).to receive(:deprecate).once
148
- expect(@parser.parse("1.1.11.111")).to eq(Money.new(1_111_111))
155
+ configure(legacy_deprecations: true) do
156
+ expect(Money).to receive(:deprecate).once
157
+ expect(@parser.parse("1.1.11.111", 'USD')).to eq(Money.new(1_111_111, 'USD'))
158
+ end
149
159
  end
150
160
 
151
161
  it "parses raises with multiple inconsistent thousands delimiters and strict option" do
@@ -216,8 +226,10 @@ RSpec.describe MoneyParser do
216
226
  end
217
227
 
218
228
  it "parses amount with multiple inconsistent thousands delimiters" do
219
- expect(Money).to receive(:deprecate).once
220
- expect(@parser.parse("1,1,11,111")).to eq(Money.new(1_111_111))
229
+ configure(legacy_deprecations: true) do
230
+ expect(Money).to receive(:deprecate).once
231
+ expect(@parser.parse("1,1,11,111", 'USD')).to eq(Money.new(1_111_111, 'USD'))
232
+ end
221
233
  end
222
234
 
223
235
  it "parses raises with multiple inconsistent thousands delimiters and strict option" do
data/spec/money_spec.rb CHANGED
@@ -3,23 +3,16 @@ require 'spec_helper'
3
3
  require 'yaml'
4
4
 
5
5
  RSpec.describe "Money" do
6
-
7
6
  let (:money) { Money.new(1) }
8
7
  let (:amount_money) { Money.new(1.23, 'USD') }
9
8
  let (:non_fractional_money) { Money.new(1, 'JPY') }
10
9
  let (:zero_money) { Money.new(0) }
11
10
 
12
11
  context "default currency not set" do
13
- before(:each) do
14
- @default_currency = Money.default_currency
15
- Money.default_currency = nil
16
- end
17
- after(:each) do
18
- Money.default_currency = @default_currency
19
- end
20
-
21
12
  it "raises an error" do
22
- expect { money }.to raise_error(ArgumentError)
13
+ configure(default_currency: nil) do
14
+ expect { money }.to raise_error(ArgumentError)
15
+ end
23
16
  end
24
17
  end
25
18
 
@@ -39,9 +32,15 @@ RSpec.describe "Money" do
39
32
  expect(Money.new(1).to_money('CAD')).to eq(Money.new(1, 'CAD'))
40
33
  end
41
34
 
42
- it "#to_money doesn't overwrite the money object's currency" do
43
- expect(Money).to receive(:deprecate).once
44
- expect(Money.new(1, 'USD').to_money('CAD')).to eq(Money.new(1, 'USD'))
35
+ it "legacy_deprecations #to_money doesn't overwrite the money object's currency" do
36
+ configure(legacy_deprecations: true) do
37
+ expect(Money).to receive(:deprecate).once
38
+ expect(Money.new(1, 'USD').to_money('CAD')).to eq(Money.new(1, 'USD'))
39
+ end
40
+ end
41
+
42
+ it "#to_money raises when changing currency" do
43
+ expect{ Money.new(1, 'USD').to_money('CAD') }.to raise_error(Money::IncompatibleCurrencyError)
45
44
  end
46
45
 
47
46
  it "defaults to 0 when constructed with no arguments" do
@@ -52,9 +51,15 @@ RSpec.describe "Money" do
52
51
  expect(Money.new('')).to eq(Money.new(0))
53
52
  end
54
53
 
55
- it "defaults to 0 when constructed with an invalid string" do
56
- expect(Money).to receive(:deprecate).once
57
- expect(Money.new('invalid')).to eq(Money.new(0.00))
54
+ it "legacy_deprecations defaults to 0 when constructed with an invalid string" do
55
+ configure(legacy_deprecations: true) do
56
+ expect(Money).to receive(:deprecate).once
57
+ expect(Money.new('invalid', 'USD')).to eq(Money.new(0.00, 'USD'))
58
+ end
59
+ end
60
+
61
+ it "raises when constructed with an invalid string" do
62
+ expect{ Money.new('invalid') }.to raise_error(ArgumentError)
58
63
  end
59
64
 
60
65
  it "to_s correctly displays the right number of decimal places" do
@@ -99,8 +104,19 @@ RSpec.describe "Money" do
99
104
  expect{ money.to_s(:some_weird_style) }.to raise_error(ArgumentError)
100
105
  end
101
106
 
102
- it "as_json as a float with 2 decimal places" do
103
- expect(money.as_json).to eq("1.00")
107
+ it "legacy_json_format makes as_json return the legacy format" do
108
+ configure(legacy_json_format: true) do
109
+ expect(Money.new(1, 'CAD').as_json).to eq("1.00")
110
+ end
111
+ end
112
+
113
+ it "legacy_format correctly sets the json format" do
114
+ expect(Money.new(1, 'CAD').as_json(legacy_format: true)).to eq("1.00")
115
+ expect(Money.new(1, 'CAD').to_json(legacy_format: true)).to eq("1.00")
116
+ end
117
+
118
+ it "as_json as a json containing the value and currency" do
119
+ expect(money.as_json).to eq(value: "1.00", currency: "CAD")
104
120
  end
105
121
 
106
122
  it "is constructable with a BigDecimal" do
@@ -139,9 +155,15 @@ RSpec.describe "Money" do
139
155
  expect((Money.new + Money.new)).to eq(Money.new)
140
156
  end
141
157
 
142
- it "adds inconsistent currencies" do
143
- expect(Money).to receive(:deprecate).once
144
- expect(Money.new(5, 'USD') + Money.new(1, 'CAD')).to eq(Money.new(6, 'USD'))
158
+ it "legacy_deprecations adds inconsistent currencies" do
159
+ configure(legacy_deprecations: true) do
160
+ expect(Money).to receive(:deprecate).once
161
+ expect(Money.new(5, 'USD') + Money.new(1, 'CAD')).to eq(Money.new(6, 'USD'))
162
+ end
163
+ end
164
+
165
+ it "raises when adding inconsistent currencies" do
166
+ expect{ Money.new(5, 'USD') + Money.new(1, 'CAD') }.to raise_error(Money::IncompatibleCurrencyError)
145
167
  end
146
168
 
147
169
  it "is subtractable" do
@@ -157,8 +179,10 @@ RSpec.describe "Money" do
157
179
  end
158
180
 
159
181
  it "logs a deprecation warning when adding across currencies" do
160
- expect(Money).to receive(:deprecate)
161
- expect(Money.new(10, 'USD') - Money.new(1, 'JPY')).to eq(Money.new(9, 'USD'))
182
+ configure(legacy_deprecations: true) do
183
+ expect(Money).to receive(:deprecate)
184
+ expect(Money.new(10, 'USD') - Money.new(1, 'JPY')).to eq(Money.new(9, 'USD'))
185
+ end
162
186
  end
163
187
 
164
188
  it "keeps currency when doing a computation with a null currency" do
@@ -254,9 +278,15 @@ RSpec.describe "Money" do
254
278
  expect(((1.0 / 12) * Money.new(3.3))).to eq(Money.new(0.28))
255
279
  end
256
280
 
257
- it "is multipliable by a money object" do
258
- expect(Money).to receive(:deprecate).once
259
- expect((Money.new(3.3) * Money.new(1))).to eq(Money.new(3.3))
281
+ it "legacy_deprecations is multipliable by a money object" do
282
+ configure(legacy_deprecations: true) do
283
+ expect(Money).to receive(:deprecate).once
284
+ expect((Money.new(3.3, 'USD') * Money.new(1, 'USD'))).to eq(Money.new(3.3, 'USD'))
285
+ end
286
+ end
287
+
288
+ it "raises when multiplied by a money object" do
289
+ expect{ (Money.new(3.3) * Money.new(1)) }.to raise_error(ArgumentError)
260
290
  end
261
291
 
262
292
  it "rounds multiplication result with fractional penny of 5 or higher up" do
@@ -294,7 +324,13 @@ RSpec.describe "Money" do
294
324
  end
295
325
 
296
326
  it "returns cents in to_json" do
297
- expect(Money.new(1.00).to_json).to eq("1.00")
327
+ configure(legacy_json_format: true) do
328
+ expect(Money.new('1.23', 'USD').to_json).to eq('1.23')
329
+ end
330
+ end
331
+
332
+ it "returns value and currency in to_json" do
333
+ expect(Money.new(1.00).to_json).to eq('{"value":"1.00","currency":"CAD"}')
298
334
  end
299
335
 
300
336
  it "supports absolute value" do
@@ -352,9 +388,12 @@ RSpec.describe "Money" do
352
388
  end
353
389
 
354
390
  it "generates a true rational" do
355
- expect(Money.rational(Money.new(10.0), Money.new(15.0))).to eq(Rational(2,3))
356
- expect(Money).to receive(:deprecate).once
357
- expect(Money.rational(Money.new(10.0, 'USD'), Money.new(15.0, 'JPY'))).to eq(Rational(2,3))
391
+ expect(Money.rational(Money.new(10.0, 'USD'), Money.new(15.0, 'USD'))).to eq(Rational(2,3))
392
+
393
+ configure(legacy_deprecations: true) do
394
+ expect(Money).to receive(:deprecate).once
395
+ expect(Money.rational(Money.new(10.0, 'USD'), Money.new(15.0, 'JPY'))).to eq(Rational(2,3))
396
+ end
358
397
  end
359
398
 
360
399
  describe "frozen with amount of $1" do
@@ -413,9 +452,11 @@ RSpec.describe "Money" do
413
452
  end
414
453
 
415
454
  it "<=> issues deprecation warning when comparing incompatible currency" do
416
- expect(Money).to receive(:deprecate).twice
417
- expect(Money.new(1000, 'USD') <=> Money.new(2000, 'JPY')).to eq(-1)
418
- expect(Money.new(2000, 'JPY') <=> Money.new(1000, 'USD')).to eq(1)
455
+ configure(legacy_deprecations: true) do
456
+ expect(Money).to receive(:deprecate).twice
457
+ expect(Money.new(1000, 'USD') <=> Money.new(2000, 'JPY')).to eq(-1)
458
+ expect(Money.new(2000, 'JPY') <=> Money.new(1000, 'USD')).to eq(1)
459
+ end
419
460
  end
420
461
 
421
462
  describe('same values') do
@@ -437,15 +478,22 @@ RSpec.describe "Money" do
437
478
  it { expect(cad_10 < nil_10).to(eq(false)) }
438
479
  end
439
480
 
440
- describe('different currencies') do
481
+ describe('legacy different currencies') do
482
+ around(:each) do |test|
483
+ configure(legacy_deprecations: true) { test.run }
484
+ end
485
+
441
486
  it { expect(Money).to(receive(:deprecate).once); expect(cad_10 <=> usd_10).to(eq(0)) }
442
487
  it { expect(Money).to(receive(:deprecate).once); expect(cad_10 > usd_10).to(eq(false)) }
443
488
  it { expect(Money).to(receive(:deprecate).once); expect(cad_10 >= usd_10).to(eq(true)) }
444
- it { expect(cad_10 == usd_10).to(eq(false)) }
445
489
  it { expect(Money).to(receive(:deprecate).once); expect(cad_10 <= usd_10).to(eq(true)) }
446
490
  it { expect(Money).to(receive(:deprecate).once); expect(cad_10 < usd_10).to(eq(false)) }
447
491
  end
448
492
 
493
+ describe('different currencies') do
494
+ it { expect(cad_10 == usd_10).to(eq(false)) }
495
+ end
496
+
449
497
  describe('to_money types') do
450
498
  it { expect(cad_10 <=> 10.00).to(eq(0)) }
451
499
  it { expect(cad_10 > 10.00).to(eq(false)) }
@@ -710,8 +758,7 @@ RSpec.describe "Money" do
710
758
  end
711
759
 
712
760
  describe "parser dependency injection" do
713
- before(:each) { Money.parser = AccountingMoneyParser }
714
- after(:each) { Money.parser = MoneyParser }
761
+ around(:each) { |test| configure(parser: AccountingMoneyParser, default_currency: 'CAD') { test.run }}
715
762
 
716
763
  it "keeps AccountingMoneyParser class on new money objects" do
717
764
  expect(Money.new.class.parser).to eq(AccountingMoneyParser)
@@ -875,8 +922,7 @@ RSpec.describe "Money" do
875
922
  end
876
923
 
877
924
  context "with .default_currency set" do
878
- before(:each) { Money.default_currency = Money::Currency.new('EUR') }
879
- after(:each) { Money.default_currency = Money::NULL_CURRENCY }
925
+ around(:each) { |test| configure(default_currency: Money::Currency.new('EUR')) { test.run }}
880
926
 
881
927
  it "can be nested and falls back to default_currency outside of the blocks" do
882
928
  money2, money3 = nil
data/spec/spec_helper.rb CHANGED
@@ -68,3 +68,18 @@ RSpec::Matchers.define :quack_like do
68
68
  expected.instance_methods - actual.instance_methods
69
69
  end
70
70
  end
71
+
72
+
73
+ def configure(default_currency: nil, legacy_json_format: nil, legacy_deprecations: nil, legacy_default_currency: nil, parser: nil)
74
+ old_config = Money.config
75
+ Money.config = Money::Config.new.tap do |config|
76
+ config.default_currency = default_currency if default_currency
77
+ config.parser = parser if parser
78
+ config.legacy_json_format! if legacy_json_format
79
+ config.legacy_deprecations! if legacy_deprecations
80
+ config.legacy_default_currency! if legacy_default_currency
81
+ end
82
+ yield
83
+ ensure
84
+ Money.config = old_config
85
+ 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.16.0
4
+ version: 1.0.0.pre
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify Inc
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-03-03 00:00:00.000000000 Z
11
+ date: 2021-05-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -110,6 +110,7 @@ files:
110
110
  - LICENSE.txt
111
111
  - README.md
112
112
  - Rakefile
113
+ - UPGRADING.md
113
114
  - bin/console
114
115
  - config/currency_historic.yml
115
116
  - config/currency_iso.yml
@@ -118,6 +119,7 @@ files:
118
119
  - lib/money.rb
119
120
  - lib/money/accounting_money_parser.rb
120
121
  - lib/money/allocator.rb
122
+ - lib/money/config.rb
121
123
  - lib/money/core_extensions.rb
122
124
  - lib/money/currency.rb
123
125
  - lib/money/currency/loader.rb
@@ -139,6 +141,7 @@ files:
139
141
  - money.gemspec
140
142
  - spec/accounting_money_parser_spec.rb
141
143
  - spec/allocator_spec.rb
144
+ - spec/config_spec.rb
142
145
  - spec/core_extensions_spec.rb
143
146
  - spec/currency/loader_spec.rb
144
147
  - spec/currency_spec.rb
@@ -168,9 +171,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
168
171
  version: '2.6'
169
172
  required_rubygems_version: !ruby/object:Gem::Requirement
170
173
  requirements:
171
- - - ">="
174
+ - - ">"
172
175
  - !ruby/object:Gem::Version
173
- version: '0'
176
+ version: 1.3.1
174
177
  requirements: []
175
178
  rubygems_version: 3.0.3
176
179
  signing_key:
@@ -179,6 +182,7 @@ summary: Shopify's money gem
179
182
  test_files:
180
183
  - spec/accounting_money_parser_spec.rb
181
184
  - spec/allocator_spec.rb
185
+ - spec/config_spec.rb
182
186
  - spec/core_extensions_spec.rb
183
187
  - spec/currency/loader_spec.rb
184
188
  - spec/currency_spec.rb