money-rails 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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