Most of the apps that I work on involve dealing with money in some form. I’m a big fan of the Money gem, which allows you to
store currency values in cents in an integer column in the database, then turn it into a easy-to-user Money object.
One problem I have with the Money gem is that when it converts strings to a Money object it doesn’t store the original
value, which makes it hard to show friendly validation messages to users.
In this post I’ll explain how you can make the Money gem a bit friendlier to use. The story goes something like this:
As a customer
When I enter "$21.045" in to a money form field
I want to see a validation error saying it's an invalid amount
And I want to see "$21.045" in the form field
Because my credit card cannot be charged fractional cents
And I most likely made an error
Extend money
First, extend money and add a new field to store the original value:
# config/initializers/money_ext.rb
Money.class_eval do
attr_accessor :original_value
end
Configure the ActiveRecord objects
Then, craft a composed_of declaration that stores the original value when it’s being set, which can be used by ActiveRecord objects:
class Product < ActiveRecord::Base
composed_of :price,
:class_name => "Money",
:mapping => ["price_in_cents", "cents"],
:converter => proc { |value|
money = value.to_money
money.original_value = value
money
}
end
Now when you assign a price to a Product, you can access its original value:
Product.new(:price => "$21.567").price.original_value # => "$21.567"
If you need this in multiple models, you can easily extract it to a module:
module SmartMoney
def smart_money(column)
composed_of column,
:class_name => "Money",
:mapping => ["#{column}_in_cents", "cents"],
:converter => proc { |value|
money = value.to_money
money.original_value = value
money
}
end
end
class Product < ActiveRecord::Base
extend SmartMoney
smart_money :price
end
Expose the original value in forms
To show the users the original value in their forms, you can create a custom form builder for your app, and add a new
money_field method that will do the right thing, like so:
# app/helpers/my_custom_form_builder.rb
class MyCustomFormBuilder < ActionView::Helpers::FormBuilder
def money_field(method, options = {})
value = @object.send(method)
formatted_value = value.original_value.presence || value.format
text_field method, options.merge(:value => (formatted_value))
end
end
# config/initializers/default_form_builder.rb
ActionView::Base.default_form_builder = MyCustomFormBuilder
The money_field method first checks for the presence of an original_value and shows it if it’s there, then
defaults to the format method if original_value is not present. You can now use this money_field like any other
form helper:
# in any view
<%= form_for @product do |f| %>
<%= f.money_field :price %>
<% end %>
Add validations
Now that the Money object, the model and the view are configured properly, you can add custom validations that can access the
original value that the user entered. This Rails 3 validator is an example of one that only allows user input with up to 2 decimal places:
# app/validators/whole_cent_validator.rb
class WholeCentValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
_, cents = value.original_value.to_s.gsub(/[^0-9.]/, '').split(".")
if cents && (cents.length > 2)
record.errors[attribute] << (options[:message] || "must be a valid dollar value")
end
end
end
You can add this to the Product class like so:
class Product < ActiveRecord::Base
validates :price,
:whole_cent => {
:message => "must be a valid dollar amount between $1.00 and $10,000.00"
}
end
Summary
Even though it involves extending Money, adding custom form builder methods, creating custom validations and crafting
a non-standard composed_of declaration, it’s relatively simple to add user-friendly validations to Money fields in such
a way that it’s easy to use for all of your money fields app-wide.
“One problem I have with the Money gem is that when it converts strings to a Money object it doesn’t store the original value, which makes it hard to show friendly validation messages to users.”
I think you’re supposed to assign price_before_type_cast the original string value. Then the form helpers will display the original value and that variable can be accessed from validations.
May 23, 2011 at 3:32 am
If `price` were a field in the database, you would be correct. But since I used `composed_of`, Rails does not store the the value before type cast (before conversion):
product = Product.new(:price => “$21.045″)
product.price_in_cents_before_type_cast # => 2105
product.price_before_type_cast # => undefined method
I didn’t think of it before, but I could have created a custom `composed_of` (or patched Rails) to stored the value before conversion, which would have been a bit more general-purpose. Sounds like a great addition to Rails!
May 23, 2011 at 7:53 am
Always nice to google a problem and find a Pivot on the other end with a solution.
I am also using the money gem and was hoping I could trick the model into letting me store off the user-entered value without having to patch it, but without alias-method-chaining, I think this looks pretty gnarly. I’ll just go ahead and implement the monkey patch you’ve got for the money gem.
FWIW, I’m using the following regex format validation on my user-entered string:
/^$?(?:d+)(?:.d{1,2}){0,1}/
Thanks Jeff!
I’m adding the following as helpful search keywords:
validation composed_of rails validate
July 9, 2011 at 3:45 pm
Oh, also, I agree that composed_of should *definitely* be storing this off as a before_type_cast. I’ll +1 any patch you write for this in a heartbeat.
July 9, 2011 at 3:46 pm
Oops, one last note: that regex needs an end of string to prevent wayward matches:
/^$?(?:d+)(?:.d{1,2}){0,1}$/
July 9, 2011 at 4:26 pm