Handling Money in Rails – Part I

This is Part I in a series of posts that will address benefits of using an actual Money type in your Ruby or Rails projects.

So, you have a working eCommerce application with customers, products, shopping carts, and line items, now you need to start collecting and managing money in droves and droves.

No problem, right? Just add a Payment class with an amount, and start solving the next problem! But what type of attribute should #amount be?

There are many ways to handle representing money in your system: Fixnum, Float, String, but which will work best?

Most tutorials and examples that I see regarding handling money suggest that you use a Fixnum integer to represent the amount in cents of a product price, a payment amount, or any other object that may need to represent money.

It’s also been suggested to use a Float so we get a closer representation of what money typically looks like. However, anytime this approach is used you’ll see extra code needed to handle precision, and to correct rounding errors.

In almost all cases, when displaying this information to a user, the Rails view helper number_to_currency is used. This should be our first hint that something is a bit off. We just asked Rails to display a number as currency… Shouldn’t there be a way to have a currency object that knows how it should display itself?

Also, when dealing with user input, we can expect to receive a wide range of values that all represent the same amount.

  • 15
  • $15
  • 15.00
  • $15.00

Instead of imposing strict input validations, and using something along the lines of validates_numericality_of our users should be able to enter any of the above and have it just work.

There just seem to be a mountain of issues and edge cases that come with using primitive Ruby constructs to represent a non-primitive object.

This is without even mentioning the possibility of needing to handle multiple currencies, or dealing with exchanging one currency for another.

Luckily, we do have access to Money objects in Ruby; there is a widely accepted library [Money][1]. Similar to how you’re already probably handling amounts, this object accepts an amount in cents, and an optional currency type.

A Failing Test

So, let’s get started by writing a quick test to describe how a future Payment class would handle money amounts. We’re going to test various forms of input that we listed earlier that may make their way through our application at some point.


# test/unit/payment_test.rb

require 'test_helper'

class PaymentTest < MiniTest::Unit::TestCase
  def setup
    @payment = Payment.new
    @fifteen_dollars = Money.new '1500', 'USD'
  end

  def test_string
    @payment.amount = '15'
    assert_equal @fifteen_dollars, @payment.amount
  end

  def test_string_with_symbol
    @payment.amount = '$15'
    assert_equal @fifteen_dollars, @payment.amount
  end

  def test_string_decimal
    @payment.amount = '15.00'
    assert_equal @fifteen_dollars, @payment.amount
  end

  def test_string_decimal_with_symbol
    @payment.amount = '$15.00'
    assert_equal @fifteen_dollars, @payment.amount
  end

  def test_integer
    @payment.amount = 15
    assert_equal @fifteen_dollars, @payment.amount
  end

  def test_decimal
    @payment.amount = 15.00
    assert_equal @fifteen_dollars, @payment.amount
  end
end

A fairly straight forward test that should most obviously fail. Our job is, just as obviously, to get it to pass.

Making It Pass

We're going to start with the most basic Ruby object to represent payments in our system and the behavior they need to encapsulate.


# app/models/payment.rb

class Payment attr_reader :amount_cents, :amount_currency

  def initialize
    @amount_cents = 0
    @amount_currency = 'USD'
  end

  def amount
  end

  def amount=(amount_value)
  end
end

This class should get us passed the first rule of TDD: make the test green or change the error message. We have changed the error message, but now it's time to make it green.

Our first step will be to make our application "money aware" by requiring the gem. As always, this is as simple as adding:


# Gemfile

gem 'money', '~> 5.1.0'

Now to implement #amount=. This needs to take an input value, and set the appropriate instance variables to be used when retrieving the amount.

Again the Money gem will handle this for us in most cases. They have very kindly provided #to_money to coerce any Numeric or String object into money. With this help, our method becomes trivial to implement.


# payment.rb

def amount=(amount_value)
  raise ArgumentError unless amount_value.respond_to? :to_money
  money = amount_value.to_money
  @amount_cents = money.cents
  @amount_currency = money.currency_as_string money
end

Since we also know that a Money object just needs an amount in cents and a currency type, again our amount method becomes simple to implement as well.


# payment.rb

def amount
  Money.new amount_cents, amount_currency
end

Just like that, we should have passing tests and an object that knows how to handle money!

That's it for Part I.