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.
- data/CHANGELOG.md +8 -0
- data/README.md +110 -5
- data/lib/generators/templates/money.rb +12 -0
- data/lib/money-rails/active_record/monetizable.rb +51 -17
- data/lib/money-rails/configuration.rb +11 -0
- data/lib/money-rails/version.rb +1 -1
- data/money-rails.gemspec +2 -1
- data/spec/active_record/monetizable_spec.rb +138 -0
- data/spec/{config_spec.rb → configuration_spec.rb} +5 -0
- data/spec/dummy/app/models/dummy_product.rb +10 -0
- data/spec/dummy/app/models/product.rb +5 -2
- data/spec/dummy/app/models/service.rb +7 -0
- data/spec/dummy/app/models/transaction.rb +8 -0
- data/spec/dummy/config/initializers/money.rb +4 -0
- data/spec/dummy/db/development.sqlite3 +0 -0
- data/spec/dummy/db/migrate/20120524052716_create_services.rb +10 -0
- data/spec/dummy/db/migrate/20120528181002_create_transactions.rb +11 -0
- data/spec/dummy/db/migrate/20120528210103_create_dummy_products.rb +10 -0
- data/spec/dummy/db/schema.rb +22 -1
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/development.log +64 -17
- data/spec/dummy/log/test.log +881 -327
- metadata +89 -103
- data/spec/dummy/db/migrate/20120402080614_add_currency_to_product.rb +0 -6
- data/spec/monetize_spec.rb +0 -41
data/CHANGELOG.md
CHANGED
@@ -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
|
-
###
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
#
|
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
|
-
#
|
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
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
data/lib/money-rails/version.rb
CHANGED
data/money-rails.gemspec
CHANGED
@@ -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
|