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 newmoney_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.
About the Author