Handling Money in Rails – Part II

This is Part II in a series of posts that will address the problems and solutions that I have encountered while building an eCommerce application. Part I

If you’re following our series, we’ve built a Payment class that has methods to set and get amount which is a Money type. Now we’ve reached the point of needing to persist data.

To review, our current implementation of Payment is:


# app/models/payment.rb

class Payment
  attr_reader :amount_cents, :amount_currency

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

  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

  def amount
    Money.new amount_cents, amount_currency
  end
end

A Migration

The first thing we’ll need is a quick migration to actually be able to persist our payments.


class CreatePayments < ActiveRecord::Migration
  def up
    create_table :payments do |t|
      t.integer :amount_cents,    null: false, default: 0
      t.string  :amount_currency, null: false

      t.timestamps
    end
  end

  def down
    drop_table :payments
  end
end

Composition with composed_of

Ok, now we just need to inherit from ActiveRecord::Base and get to saving and retriving this data from our database. We’re also going to make use of composed_of to eliminate our setters and getters. the full documentation is also available at api.rubyonrails.org.

In essense the method is passed 2 parameters:

  1. The name of the attribute, as a symbol
  2. An options hash

The attribute param should be obvious in our case (:amount), and we’ll look at the options in a little more detail.

The first option we’ll look at is the :class_name, which again should fairly obviously be “Money” since we always want our amount to be an instance of Money.


# app/models/payment.rb

composed_of :amount, class_name: 'Money'

Mapping the Attributes

Next, we’ll look at the :mapping option. This one is a little less apparent from the start, but makes a bit of sense as we examine it more closely.

The :mapping key takes an array of arrays. Each array is a one-to-one mapping with the first item being the attribute of the model class, and the second item being the attribute of the target class, in our case: Money.

The 2 attributes we need to set on our Payment class are amount_cents and amount_currency. These match-up with the cents and currency_as_string attributes on the Money class, respectively.

Attribute in Payment Attribute in Money
amount_cents cents
amount_currency currency_as_string

So, about halfway through, our method call should look like this:


# app/models/payment.rb

composed_of :amount,
  class_name: 'Money',
  mapping: [ %w(amount_cents cents), %w(amount_currency currency_as_string) ]

Getter = Constructor

The next option that we need to set is, :constructor. This key takes either a method name or a Proc that is called when getting our amount. The method or Proc is passed arguments for each of the attributes set in our :mapping arrays (amount_cents, and amount_currency).

Notice that the constructor is essentially the same implementation as the original amount method definition.


# app/models/payment.rb

composed_of :amount,
  class_name: 'Money',
  mapping: [ %w(amount_cents cents), %w(amount_currency currency_as_string) ],
  constructor: Proc.new { |cents, currency| Money.new(cents, currency) }

Setter = Converter

The final option that we need to set is, :converter. This key takes either the name of a method on the class that was set in our earlier :class_name option, or a Proc that is called when setting the amount attribute. This Proc will be called with one argument (the new value to assign to amount), and only if the argument passed is not an instance of :class_name (Money).

Ok, we’re finally ready to finish off our method call and have a fully monetized amount attribute for Payment. Again, notice that this is the same implementation as our original amount= method.


# app/models/payment.rb

composed_of :amount,
  class_name: 'Money',
  mapping: [ %w(amount_cents cents), %w(amount_currency currency_as_string) ],
  constructor: Proc.new { |cents, currency| Money.new(cents, currency) },
  converter: Proc.new { |value| value.respond_to?(:to_money) ? value.to_money : raise(
    ArgumentError, "Can't convert #{value.class} to `Money`"
  ) }

Success! We have a Payment class that knows how to represent an amount as actual money, but can save to any database that only knows about integers and strings.

All of the tests from our earlier suite should still be green, and we’ve pushed all of the converting to ActiveRecord.

Hopefully, you’ve learned a little about the not commonly discussed composed_of method, and are already envisioning ways to compose your models of multiple different types of classes without worrying about serialzing data into the database.

While this is can come in very handily, it can also be cumbersome to use in multiple models. For example, we would most definitely be violating DRY if we had classes representing Payment, Product, LineItem, Order that all had essentially the same composed_of method call. This doesn’t even mention the possibility of having multiple attributes on a model that need to monetized.

So, next time we’ll look into creating our own monetize class method to take care of all of this for us.