money-rails 0.2.0 → 0.3.0

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.
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0
4
+
5
+ - Add support for model-wise currency.
6
+ - Fix conversion of monetized attribute value, whether a currency
7
+ table column exists or not.
8
+ - Add configuration options for currency exchange (config.add_rate,
9
+ config.default_bank)
10
+
3
11
  ## 0.2.0
4
12
 
5
13
  - Add new generator to install money.rb initializer
data/README.md CHANGED
@@ -10,6 +10,11 @@ Use 'monetize' to specify which fields you want to be backed by
10
10
  Money objects and helpers provided by the [money](http://github.com/Rubymoney/money)
11
11
  gem.
12
12
 
13
+ Currently, this library is in active development mode, so if you would
14
+ like to have a new feature feel free to open a new issue
15
+ [here](https://github.com/RubyMoney/money-rails/issues). You are also
16
+ welcome to contribute to the project.
17
+
13
18
  ## Installation
14
19
 
15
20
  Add this line to your application's Gemfile:
@@ -64,16 +69,108 @@ Now the model objects will have a ```discount``` attribute which
64
69
  is a Money object, wrapping the value of ```discount_subunit``` column to a
65
70
  Money instance.
66
71
 
67
- ### Field currencies
72
+ ### Currencies
73
+
74
+ Money-rails supports a set of options to handle currencies for your
75
+ monetized fields. The default option for every conversion is to use
76
+ the global default currency of Money library, as given in the configuration
77
+ initializer of money-rails:
78
+
79
+ ```ruby
80
+ # config/initializers/money.rb
81
+ MoneyRails.configure do |config|
82
+
83
+ # set the default currency
84
+ config.default_currency = :usd
85
+
86
+ end
87
+ ```
88
+
89
+ In many cases this is not enough, so there are some other options to
90
+ satisfy your needs.
91
+
92
+ #### Model Currency
93
+
94
+ You can define a specific currency for an activerecord model. This currency is
95
+ used for the creation and conversions of the Money objects referring to
96
+ every monetized attributes of the specific model. This means it overrides
97
+ the global default currency of Money library. To attach a currency to a
98
+ model use the ```register_currency``` macro:
99
+
100
+ ```ruby
101
+ # app/models/product.rb
102
+ class Product < ActiveRecord::Base
103
+
104
+ # Use EUR as model level currency
105
+ register_currency :eur
106
+
107
+ monetize :discount_subunit, :as => "discount"
108
+ monetize :bonus_cents
109
+
110
+ end
111
+ ```
112
+
113
+ Now ```product.discount``` and ```product.bonus``` will return a Money
114
+ object using EUR as currency, instead of the default USD.
115
+
116
+ #### Attribute Currency (:with_currency)
68
117
 
69
- You can define a specific currency per monetized field:
118
+ By using the key ```:with_currency``` with a currency symbol value in
119
+ the ```monetize``` macro call, you can define a currency in a more
120
+ granular way. This way you attach a currency only to the specific monetized
121
+ model attribute. It also allows to override both the model level
122
+ and the global default currency:
70
123
 
71
124
  ```ruby
72
- monetize :discount_subunit, :as => "discount", :with_currency => :eur
125
+ # app/models/product.rb
126
+ class Product < ActiveRecord::Base
127
+
128
+ # Use EUR as model level currency
129
+ register_currency :eur
130
+
131
+ monetize :discount_subunit, :as => "discount"
132
+ monetize :bonus_cents, :with_currency => :gbp
133
+
134
+ end
73
135
  ```
74
136
 
75
- Now ```discount_subunit``` will give you a Money object using EUR as
76
- currency.
137
+ In this case the ```product.bonus``` will return a Money object of GBP
138
+ currency, whereas ```product.discount.currency_as_string # => EUR ```
139
+
140
+ #### Instance Currencies
141
+
142
+ All the previous options do not require any extra model field to hold
143
+ currency values. If you need to provide differrent currency per model
144
+ instance, then you need to add a column with the name ```currency```
145
+ in your db table. Money-rails will discover this automatically,
146
+ and will use this knowledge to override the model level and global
147
+ default values. Attribute currency cannot be combined with instance
148
+ currency!
149
+
150
+ ```ruby
151
+ class Transaction < ActiveRecord::Base
152
+
153
+ # This model has a separate currency column
154
+ attr_accessible :amount_cents, :currency, :tax_cents
155
+
156
+ # Use model level currency
157
+ register_currency :gbp
158
+
159
+ monetize :amount_cents
160
+ monetize :tax_cents
161
+
162
+ end
163
+
164
+ # Now instantiating with a specific currency overrides the
165
+ # the model and global currencies
166
+ t = Transaction.new(:amount_cents => 2500, :currency => "CAD")
167
+ t.amount == Money.new(2500, "CAD") # true
168
+ ```
169
+
170
+ WARNING: In this case :with_currency is not permitted and the usage
171
+ of this parameter will cause an ArgumentError exception.
172
+
173
+ In general, the use of this strategy is discouraged unless there is a reason.
77
174
 
78
175
  ### Configuration parameters
79
176
 
@@ -86,6 +183,10 @@ MoneyRails.configure do |config|
86
183
  #
87
184
  config.default_currency = :usd
88
185
 
186
+ # Add custom exchange rates
187
+ config.add_rate "USD", "CAD", 1.24515
188
+ config.add_rate "CAD", "USD", 0.803115
189
+
89
190
  # To handle the inclusion of validations for monetized fields
90
191
  # The default value is true
91
192
  #
@@ -114,6 +215,10 @@ end
114
215
  used more than once to set more custom currencies. The value should be
115
216
  a hash of all the necessary key/value pairs (important keys: :priority, :iso_code, :name,
116
217
  :symbol, :symbol_first, :subunit, :subunit_to_unit, :thousands_separator, :decimal_mark).
218
+ * add_rate: Provide custom exchange rate for currencies in one direction
219
+ only! This rate is added to the attached bank object.
220
+ * default_bank: The default bank object holding exchange rates etc.
221
+ (https://github.com/RubyMoney/money#currency-exchange)
117
222
 
118
223
  ## Maintainers
119
224
 
@@ -6,6 +6,18 @@ MoneyRails.configure do |config|
6
6
  #
7
7
  #config.default_currency = :usd
8
8
 
9
+ # Set default bank object
10
+ #
11
+ # Example:
12
+ # config.default_bank = EuCentralBank.new
13
+
14
+ # Add exchange rates to current money bank object.
15
+ # (The conversion rate refers to one direction only)
16
+ #
17
+ # Example:
18
+ # config.add_rate "USD", "CAD", 1.24515
19
+ # config.add_rate "CAD", "USD", 0.803115
20
+
9
21
  # To handle the inclusion of validations for monetized fields
10
22
  # The default value is true
11
23
  #
@@ -21,11 +21,14 @@ module MoneyRails
21
21
  ":with_currency or :with_model_currency")
22
22
  end
23
23
 
24
- # Model currency field name
24
+ # Optional table column which holds currency iso_codes
25
+ # It allows per row currency values
26
+ # Overrides default currency
25
27
  model_currency_name = options[:with_model_currency] ||
26
28
  options[:model_currency] || "currency"
27
29
 
28
- # Override Model and default currency
30
+ # This attribute allows per column currency values
31
+ # Overrides row and default currency
29
32
  field_currency_name = options[:with_currency] ||
30
33
  options[:field_currency] || nil
31
34
 
@@ -44,27 +47,44 @@ module MoneyRails
44
47
  name = subunit_name << "_money"
45
48
  end
46
49
 
47
- class_eval do
48
- composed_of name.to_sym,
49
- :class_name => "Money",
50
- :mapping => [[subunit_name, "cents"], [model_currency_name, "currency_as_string"]],
51
- :constructor => Proc.new { |cents, currency|
52
- Money.new(cents || 0, field_currency_name || currency ||
53
- Money.default_currency)
54
- },
55
- :converter => Proc.new { |value|
56
- if value.respond_to?(:to_money)
57
- if field_currency_name
58
- value.to_money(field_currency_name)
59
- else
60
- value.to_money
61
- end
50
+ has_currency_table_column = self.attribute_names.include? model_currency_name
51
+
52
+ if has_currency_table_column
53
+ raise(ArgumentError, ":with_currency should not be used with tables" \
54
+ " which contain a column for currency values") if field_currency_name
55
+
56
+ mappings = [[subunit_name, "cents"], [model_currency_name, "currency_as_string"]]
57
+ constructor = Proc.new { |cents, currency|
58
+ Money.new(cents || 0, currency || self.respond_to?(:currency) &&
59
+ self.currency || Money.default_currency)
60
+ }
61
+ converter = Proc.new { |value|
62
+ raise(ArgumentError, "Only Money objects are allowed for assignment")
63
+ }
64
+ else
65
+ mappings = [[subunit_name, "cents"]]
66
+ constructor = Proc.new { |cents|
67
+ Money.new(cents || 0, field_currency_name || self.respond_to?(:currency) &&
68
+ self.currency || Money.default_currency)
69
+ }
70
+ converter = Proc.new { |value|
71
+ if value.respond_to?(:to_money)
72
+ value.to_money(field_currency_name || self.respond_to?(:currency) &&
73
+ self.currency)
62
74
  else
63
75
  raise(ArgumentError, "Can't convert #{value.class} to Money")
64
76
  end
65
77
  }
66
78
  end
67
79
 
80
+ class_eval do
81
+ composed_of name.to_sym,
82
+ :class_name => "Money",
83
+ :mapping => mappings,
84
+ :constructor => constructor,
85
+ :converter => converter
86
+ end
87
+
68
88
  # Include numericality validation if needed
69
89
  if MoneyRails.include_validations
70
90
  class_eval do
@@ -72,6 +92,20 @@ module MoneyRails
72
92
  end
73
93
  end
74
94
  end
95
+
96
+ def register_currency(currency_name)
97
+ # Lookup the given currency_name and raise exception if
98
+ # no currency is found
99
+ currency_object = Money::Currency.find currency_name
100
+ raise(ArgumentError, "Can't find #{currency_name} currency code") unless currency_object
101
+
102
+ class_eval do
103
+ @currency = currency_object
104
+ class << self
105
+ attr_reader :currency
106
+ end
107
+ end
108
+ end
75
109
  end
76
110
  end
77
111
  end
@@ -26,6 +26,17 @@ module MoneyRails
26
26
  Money::Currency.register(currency_options)
27
27
  end
28
28
 
29
+ # Set default bank object
30
+ #
31
+ # example (given that eu_central_bank is in Gemfile):
32
+ # MoneyRails.configure do |config|
33
+ # config.default_bank = EuCentralBank.new
34
+ # end
35
+ delegate :default_bank=, :to => :Money
36
+
37
+ # Provide exchange rates
38
+ delegate :add_rate, :to => :Money
39
+
29
40
  # Use (by default) validation of numericality for each monetized field.
30
41
  mattr_accessor :include_validations
31
42
  @@include_validations = true
@@ -1,3 +1,3 @@
1
1
  module MoneyRails
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -10,7 +10,7 @@ Gem::Specification.new do |s|
10
10
  s.email = ["alup.rubymoney@gmail.com"]
11
11
  s.description = "This library provides integration of RubyMoney - Money gem with Rails"
12
12
  s.summary = "Money gem integration with Rails"
13
- s.homepage = "https://github.com/RubyMoney/money"
13
+ s.homepage = "https://github.com/RubyMoney/money-rails"
14
14
 
15
15
  s.files = Dir.glob("{lib,spec}/**/*")
16
16
  s.files += %w(CHANGELOG.md LICENSE README.md)
@@ -28,4 +28,5 @@ Gem::Specification.new do |s|
28
28
  s.add_development_dependency "sqlite3", "~> 1.3.6"
29
29
  s.add_development_dependency "rspec", "~> 2.9.0"
30
30
  s.add_development_dependency "rspec-rails", "~> 2.9.0"
31
+ s.add_development_dependency "guard-rspec", "~> 0.7.2"
31
32
  end
@@ -0,0 +1,138 @@
1
+ require 'spec_helper'
2
+
3
+ describe MoneyRails::ActiveRecord::Monetizable do
4
+
5
+ describe "monetize" do
6
+ before :each do
7
+ @product = Product.create(:price_cents => 3000, :discount => 150,
8
+ :bonus_cents => 200)
9
+ @service = Service.create(:charge_cents => 2000, :discount_cents => 120)
10
+ end
11
+
12
+ it "attaches a Money object to model field" do
13
+ @product.price.should be_an_instance_of(Money)
14
+ @product.discount_value.should be_an_instance_of(Money)
15
+ @product.bonus.should be_an_instance_of(Money)
16
+ end
17
+
18
+ it "returns the expected money amount as a Money object" do
19
+ @product.price.should == Money.new(3000, "USD")
20
+ end
21
+
22
+ it "assigns the correct value from a Money object" do
23
+ @product.price = Money.new(3210, "USD")
24
+ @product.save.should be_true
25
+ @product.price_cents.should == 3210
26
+ end
27
+
28
+ it "respects :as argument" do
29
+ @product.discount_value.should == Money.new(150, "USD")
30
+ end
31
+
32
+ it "uses numericality validation" do
33
+ @product.price_cents = "foo"
34
+ @product.save.should be_false
35
+
36
+ @product.price_cents = 2000
37
+ @product.save.should be_true
38
+ end
39
+
40
+ it "uses Money default currency if :with_currency has not been used" do
41
+ @service.discount.currency.should == Money::Currency.find(:eur)
42
+ end
43
+
44
+ it "overrides default currency with the currency registered for the model" do
45
+ @product.price.currency.should == Money::Currency.find(:usd)
46
+ end
47
+
48
+ it "overrides default currency with the value of :with_currency argument" do
49
+ @service.charge.currency.should == Money::Currency.find(:usd)
50
+ @product.bonus.currency.should == Money::Currency.find(:gbp)
51
+ end
52
+
53
+ it "assigns correctly Money objects to the attribute" do
54
+ @product.price = Money.new(2500, :USD)
55
+ @product.save.should be_true
56
+ @product.price.cents.should == 2500
57
+ @product.price.currency_as_string.should == "USD"
58
+ end
59
+
60
+ it "assigns correctly Fixnum objects to the attribute" do
61
+ @product.price = 25
62
+ @product.save.should be_true
63
+ @product.price.cents.should == 2500
64
+ @product.price.currency_as_string.should == "USD"
65
+ end
66
+
67
+ it "overrides default, model currency with the value of :with_currency in fixnum assignments" do
68
+ @product.bonus = 25
69
+ @product.save.should be_true
70
+ @product.bonus.cents.should == 2500
71
+ @product.bonus.currency_as_string.should == "GBP"
72
+ end
73
+
74
+ it "overrides default currency with model currency, in fixnum assignments" do
75
+ @product.discount_value = 5
76
+ @product.save.should be_true
77
+ @product.discount_value.cents.should == 500
78
+ @product.discount_value.currency_as_string.should == "USD"
79
+ end
80
+
81
+ it "falls back to default currency, in fixnum assignments" do
82
+ @service.discount = 5
83
+ @service.save.should be_true
84
+ @service.discount.cents.should == 500
85
+ @service.discount.currency_as_string.should == "EUR"
86
+ end
87
+
88
+ context "for model with currency column:" do
89
+ before :each do
90
+ @transaction = Transaction.create(:amount_cents => 2400, :tax_cents => 600,
91
+ :currency => :usd)
92
+ @dummy_product1 = DummyProduct.create(:price_cents => 2400, :currency => :usd)
93
+ @dummy_product2 = DummyProduct.create(:price_cents => 2600) # nil currency
94
+ end
95
+
96
+ it "overrides default currency with the value of row currency" do
97
+ @transaction.amount.currency.should == Money::Currency.find(:usd)
98
+ end
99
+
100
+ it "overrides default currency with the currency registered for the model" do
101
+ @dummy_product2.price.currency.should == Money::Currency.find(:gbp)
102
+ end
103
+
104
+ it "overrides default and model currency with the row currency" do
105
+ @dummy_product1.price.currency.should == Money::Currency.find(:usd)
106
+ end
107
+
108
+ it "constructs the money attribute from the stored mapped attribute values" do
109
+ @transaction.amount.should == Money.new(2400, :usd)
110
+ end
111
+
112
+ it "instantiates correctly Money objects from the mapped attributes" do
113
+ t = Transaction.new(:amount_cents => 2500, :currency => "CAD")
114
+ t.amount.should == Money.new(2500, "CAD")
115
+ end
116
+
117
+ it "assigns correctly Money objects to the attribute" do
118
+ @transaction.amount = Money.new(2500, :eur)
119
+ @transaction.save.should be_true
120
+ @transaction.amount.cents.should == Money.new(2500, :eur).cents
121
+ @transaction.amount.currency_as_string.should == "EUR"
122
+ end
123
+
124
+ it "raises exception if a non Money object is assigned to the attribute" do
125
+ expect { @transaction.amount = "not a Money object" }.to raise_error(ArgumentError)
126
+ expect { @transaction.amount = 234 }.to raise_error(ArgumentError)
127
+ end
128
+
129
+ end
130
+ end
131
+
132
+ describe "register_currency" do
133
+ it "attaches currency at model level" do
134
+ Product.currency.should == Money::Currency.find(:usd)
135
+ DummyProduct.currency.should == Money::Currency.find(:gbp)
136
+ end
137
+ end
138
+ end