Friday, February 5, 2010

Getting started with Money and Act_As_Money

Why?
I consider myself still fairly new to Rails after about a year of learning and I still seem to get caught up on some things that I shouldn't.  I'm working on an application that needs to care about money to some extent and I was hoping to avoid the pain of dealing with floating point issues.  I've been there numerous times in the past and it is never enjoyable and sometimes results in embarrassing situations as you try to explain why somethings don't add up correctly or compare well.  These experiences have me leaning towards storing money as integer values in the database and working with them as pennies. So I was happy to see someone has already done most of the work in Ruby and there is a nice plug-in for Rails to simplify the code in the model.

Overview
If you're dealing with money in your Rails application the first thing you'll need is the Money gem and then you'll likely want the act_as_money plug-in.  It seems that most things in the Ruby and Rails communities these days have forked all over God's green earth and it is often a challenge trying to figure out what I should use.  (Am I the only one seeing and thinking this?)  I dug for an hour or so and read through numerous posts before making my decision.  Because I'm using Ruby 1.8.7 and Rails 2.3.5, I decided to go with the original Money gem (currently 2.1.5 updated November 2009) instead of the collectiveidea Money gem.   For those of you with Rails 2.1 or earlier you might want to consider using the collectiveidea-money gem as it appears from my reading that you'll have more success there. It's that or bite the bullet and move to a more recent version of Rails.  I also opted to use the act_as_money plug-in from collectiveidea (updated 11-19-2008).  Again, for anyone on Rails 2.1 you will want to pull that plug-in from the repository using the 2.1 tag.

The play by play
  • First install the Money gem.
gem install money

  • Then from inside your Rails project install the act_as_money plug-in
script/plugin install git://github.com/collectiveidea/acts_as_money.git
 
  • The next thing you'll want to do is create a migration if your existing models with non-integer money values.  In my case I was using decimal instead of integer.
  • def self.up
          change_column(:invoice_items, :rate, :integer)
          change_column(:invoice_items, :amount, :integer)
          change_column(:invoices, :taxable_amount, :integer)
          change_column(:invoices, :non_taxable_amount, :integer)
          change_column(:invoices, :taxes, :integer)
          change_column(:invoices, :total, :integer)
    
          rename_column(:invoice_items, :rate, :rate_in_cents)
          rename_column(:invoice_items, :amount, :amount_in_cents)
          rename_column(:invoices, :taxable_amount, :taxable_amount_in_cents )
          rename_column(:invoices, :non_taxable_amount,:non_taxable_amount_in_cents )
          rename_column(:invoices, :taxes, :taxes_in_cents)
          rename_column(:invoices, :total, :total_in_cents)
    end
    
  • Here are a couple methods I used in my migration to covert the data in the columns being changed.
  • class ModifyMoneyFields < ActiveRecord::Migration
    
      def ModifyMoneyFields::fixDollarsToCents(model, fields)
          records = model.find(:all)
          records.each do |rec|
              fields.each do |field|
                  d = rec.send(field)
                  d = d * 100
                  rec.update_attribute(field,d)
              end
          end
      end
    
      def ModifyMoneyFields::fixCentsToDollars(model, fields)
          records = model.find(:all)
          records.each do |rec|
              fields.each do |field|
                  d = rec.send(field)
                  d = d / 100.0
                  rec.update_attribute(field,d)
              end
          end
      end
    
      def self.up
          fixDollarsToCents(Invoice, [:taxable_amount, :non_taxable_amount, :taxes, :total])
          fixDollarsToCents(InvoiceItem, [:rate, :amount])
    
  • After creating your migrations you'll want to adjust your models.  The plug-in will make this easy to do. In fact, it's easier than most of the posts show if you named your column with a suffix of "_in_cents" as shown in the previous step.  The "_in_cents" is the default assumed by the plug-in when using the composed_of method for creating the Money object.
  • class Invoice < Blameable
    
        belongs_to :term
    
        has_many :invoice_items
    
        # Note: The :currency => false means NO currency field in the model, if not supplied the plugin defaults to US dollars and you need a field in your model
        # Note: You can also provide :cents => :total_pennies if you opted for different field names
    
        money :taxable_amount, :currency => false
        money :non_taxable_amount, :currency => false
        money :taxes, :currency => false
        money :total, :currency => false
    
  • Finally, run your migration.

    rake db:migrate
     
    Something that tripped me up for a few minutes...

    In some of my validation code for the models I was comparing the "new" Money fields to a fixed number such as:
    def validate
         if rate == 0
            errors.add(:rate, "must not be zero.")
         end
    end
    
    This caused my code to freak out tossing the following exception:

    undefined method `cents' for 0:Fixnum
    C:/Ruby1.8.7/lib/ruby/gems/1.8/gems/money-2.1.5/lib/money/money.rb:92:in `=='

    To fix this just change your code to:
    def validate
         if rate == 0.to_money
            errors.add(:rate, "must not be zero.")
         end
    end
    
    The Money gem has core extensions for Number and String which adds the to_money() method.

    Summary
    Once I decided to make the switch I was able to make changes related 22 fields and 14 models in about 1 hour.  All my model tests confirmed things were working correctly.  Hopefully you found this helpful. Good luck!


    References
    The documentation for the Money gem
    The Act_As_Money plugin
    post on act_as_money
    An good simple post on composed_of also referencing Money and act_as_money