Money and Ruby - An exercise in metaprogramming
While developing a small personal project, I got intrigued about using the money gem and integrating it with mongoid. And I found that for almost all the models that I needed to use currencies and monetary values, I always wrote the same set of codes. And after a long hiatus from the beauty of metaprogramming, I finally found enough time to put this one set of codes in a single location, and simply do some magic on the models, and then voila, I gots monetized fields in my models!
module Mongoid::Currency
extend ActiveSupport::Concern
module ClassMethods
def apply_currency(*args)
options = args.extract_options!
currency_enabled_fields = args
raise ArgumentError, "Define fields to apply currency to." if
currency_enabled_fields.empty?
currency_field = options[:field] || "currency"
field :"#{currency_field}_id", type: String, default: (options[:default] || "usd")
currency_enabled_fields.each do |field_name|
field :"#{field_name}_in_cents", type: Integer, default: 0
end
attr_accessor :apply_currency_exchange_rates
before_update :convert_currencies
validates :"#{currency_field}_id", presence: true,
inclusion: { in: Money::Currency::TABLE.keys.map(&:to_s) }
currency_enabled_fields.each do |field_name|
validates :"#{field_name}_in_cents", presence: true, numericality: true
end
delegate :symbol, to: :"#{currency_field}", prefix: true
define_method currency_field do
if instance_variable_defined? "@#{currency_field}"
instance_variable_get "@#{currency_field}"
else
instance_variable_set "@#{currency_field}",
(Money::Currency.new(send "#{currency_field}_id") rescue nil)
end
end
define_method "#{currency_field}=" do |value|
value = Money::Currency.new(value) unless value.is_a? Money::Currency
write_attribute("#{currency_field}_id", value.id)
instance_variable_set("@#{currency_field}", value).tap do |currency|
currency_enabled_fields.each do |field_name|
instance_variable_set "@#{field_name}",
Money.new(send("#{field_name}_in_cents"), currency)
end
end
end
define_method "#{currency_field}_id=" do |value|
write_attribute("#{currency_field}_id", value).tap do |currency_id|
send "#{currency_field}=", Money::Currency.new(currency_id) rescue nil
end
end
currency_enabled_fields.each do |field_name|
define_method field_name do
if instance_variable_defined? "@#{field_name}"
instance_variable_get "@#{field_name}"
else
instance_variable_set "@#{field_name}",
Money.new(send("#{field_name}_in_cents"), send(currency_field))
end
end
define_method "#{field_name}=" do |value|
if value.is_a? Money
send "#{field_name}_in_cents=",
value.exchange_to(send(currency_field)).cents
else
send "#{field_name}_in_cents=", value.to_f * 100
end
instance_variable_set "@#{field_name}",
Money.new(send("#{field_name}_in_cents"), send(currency_field))
end
end
define_method :convert_currencies do
return true unless send("#{currency_field}_id_changed?") and
!!apply_currency_exchange_rates
currency_enabled_fields.each do |field_name|
original_value = Money.new(send("#{field_name}_in_cents"),
Money::Currency.new(send "#{currency_field}_id_was"))
new_value = original_value.exchange_to(
Money::Currency.new(send "#{currency_field}_id"))
send "#{field_name}_in_cents=", new_value.cents
instance_variable_set "@#{field_name}", new_value
end
end
end
end
end
And in my Mongoid models, I can simply do the following:
class Account
include Mongoid::Document
include Mongoid::Currency
apply_currency :balance, :balance_reconciled
end
# account = Account.new
# account.balance = 10.00
# account.balance.format #=> $10.00
# account.balance_in_cents #=> 1000
# account.balance_reconciled #=> 25.00
# account.currency_id #=> "usd"
# account.currency #=> #<Money::Currency id: usd, name: United States Dollars ...>
# account.currency = Money::Currency.new(:php)
# account.currency #=> #<Money::Currency id: php, name: Philippine Peso ...>
# account.currency = :gbp
# account.currency #=> #<Money::Currency id: gbp, name: British Pound ...>
# account.currency_id = :eur
# account.currency #=> #<Money::Currency id: eur, name: Euro ...>
# account.save
# account.balance.format #=> 10.00 €
# account.currency_id = :usd
# account.apply_currency_exchange_rates = true
# account.save
# account.balance.format #=> $0.23 (using google_currency gem with money)
class Transaction
include Mongoid::Document
include Mongoid::Currency
apply_currency :amount, field: :unit
end
# transaction = Transaction.new
# transaction.unit = :php
# transaction.unit_id #=> "php"
# transaction.unit #=> #<Money::Currency id: php, name: Philippine Peso ...>
# transaction.amount #=> #<Money cents:0 currency:PHP>
# transaction.amount.format #=> ₱0.00
Next step: Creating a gem out of this. It's been a while since I've published one.

