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
There are many ways to handle representing money in your system:
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.
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]. 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.
Money gem will handle this for us in most cases. They have very kindly provided
#to_money to coerce any
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.