shopify-money 0.16.0 → 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.
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