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.
- 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
|