Creating user-friendly validation messages with the Money gem

May 23, 2011 Pivotal Labs

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.

About the Author

Biography

Previous
Tracker going all HTTPS
Tracker going all HTTPS

About six months ago, a certain Firefox extension made headlines by making it incredibly easy for people to...

Next
Creating strongly-typed, app-wide, user-editable settings
Creating strongly-typed, app-wide, user-editable settings

I recently worked on an app where an admin user needed to be able to tweak an app-wide configuration settin...

×

Subscribe to our Newsletter

!
Thank you!
Error - something went wrong!