A money field supports/depends upon a currency and that introduces the question of what exchange_rate to use!
The simple answer is 1.00000 meaning that the currency gets 'traded' on par - which will satisfy the 'one currency' use case (commonly exchange_rates are listed with 5 decimals - but some currencies requires 7 or more decimals to allowing an accurate calculation, like the Afghani, currently traded against the USD at 0.0206954). One currency use cases could be set using a simple configuration option - like config.money_rails.one_currency_only = true
A somewhat more complex use case is the 'two currencies' application where all money fields are listed (and calculated) in a 'preferred' currency but has to be presented in a secondary currency as well. Some EU-countries have not implemented the Euro as their national currency, like Poland. Their national currency has an almost fixed exchange_rate towards the Euro - it sees fluctuations but only marginally. Money fields are rarely converted but even though, should display in two currencies. This use case will require an exchange rate (exchange quote to be exact) - which could be cached and used when displaying money fields in the alternate currency (a view helper could be provided an extra argument to display the amount using the alternate currency exchange rate).
Increasingly complex, the 'multilateral' use case will persist money in a number of different currencies - and each transaction will see a (slightly) different exchange rate. Amounts are converted using sell/buy rates - depending on whether the participating bank is selling the amount to the user or buying it from the user! The difference usually is neglectable - except when amounts get big - or the number of transactions are considerable (loosing half-a-cent two million times a day still sets you back 10K a day!!). This use case has two options - use a means exchange rate (which will usually be the case - eg. the nordea gem provides such a means exchange rate ) or go with both a sell and a buy rate. Both options requires at least one accurate exchange rate which adds yet another challenge: time
Digressing on the subject of time: money amounts are registered at time t1 (at exchange rate x1) but rarely are executed at that exact point in time. Hence the user will see the conversion taking place at some later point in time - say t2 - at rate x2, and once concluded, the amount will have transformed from, say euro's, to dollars with a margin (and usually some surcharge which we will have to leave for the developer to implement ways of handling). Businesses operating in multiple currencies solve this in one of two ways 1) have a number of accounts, one in each currency regularly being used or 2) hedges exposure to risks through either defensively insuring or aggressively trading currencies. Either way they will 'know' the exchange rate at the time (or within the hour) of registering money fields and the exchange_rates table will hold adjusted conversion rates.
On a totally different note, money fields persists amounts and a currency - but not the purpose :) This will inhibit the accuracy of the exchange rate - as banks sets different rates on commercial transactions versus cash buy/sell - but again, that will have to abstracted away IMHO
Summing up:
Supporting exchange rates above the obvious one currency and alternate currency use cases would require a configuration which by popular convention methodology could utilise the nordea gem entirely - it converts amounts to other currencies using a cached exchange rate as provided by Nordea, but configurable to allow for persisted exchange rates in an exchange_rateables:
exchange_rateable: (ID +) CURRENCY_ID + EXCHANGE_RATE + EXCHANGE_RATEABLE_ID + EXCHANGE_RATEABLE_TYPE + EXCHANGE_RATEABLE_FIELDNAME + CREATED_AT
This table would record the currency used at the time of registering the amount in some table/field and the exchange_rate used - it could be a cached exchange rate or the present rate - depending on the implementation -
Persisting the exchange_rates could be in this table:
exchange_rates: ID + CURRENCY_ID + MIDDLE_RATE + SELL_RATE + BUY_RATE + QUOTE_AT + CREATED_AT
Now, using this could look a little (the following is part pipe dream part pseudo code, and please excuse my mixing it all together in ways it should not, hoping that you get the general idea) like
# config/initializers/money_rails.rb
require 'nordea'
Money.default_bank = Nordea::Bank.new
MoneyRails.setup do |config|
config.one_currency = false
config.persist_exchange_quotes = :table # :file | :cache
config.exchange_rate = :middle # :sell | :buy | :day_mean | :week_mean | :month_mean
end
# lib/money_rails.rb
require 'delayed_job'
require 'delayed_job_active_record'
require 'delayed_job_croned' # non-existing CRON-like 'runner' - every so often
class MoneyRails
def week_mean(wednesday=nil)
wednesday ||= 1.week.ago.at_beginning_of_week + 2.days
rates = exchange_rates.where{ (quote_at >= wednesday - 2.days) & (quote_at <= wednesday + 2.days) }
rates.sum(exchange_rate) / rates.count
end
Cron.add_job('Money.default_bank.update_rates',[ '09:00:00','12:00:00','16:30:00']) if %w{ table cache file }.include? config.persist_exchange_quotes.to_s
end
# lib/nordea.rb
module Nordea
class Bank
# the default_bank #update_rates should be 'changed/implemented' to use the config.exchange_rate
def update_rates
end
def exchange_with_rate( to_currency, rate)
me=self
rate = ExchangeRateable.where{ exchangeable_id == me.id & exchange_rateable_type == me.class.to_s }[0] || e_w_r(to_currency, rate)
rate = rate.exchange_rate if rate.class == ExchangeRateable
Money.new((from.cents * rate).round, to_currency)
end
def exchange_with_rate_locked( to_currency, rate )
me = self
rate = exchange_with_rate( to_currency, rate )
ExchangeRateable.create( currency: to_currency, exchange_rate: rate, exchangeable_id: me.id, exchange_rateable_type = me.class.to_s )
rate
end
private
# exchange_with_rate should return a specific rate
def e_w_r(to_currency, rate)
case rate.class
when Symbol
rates = ExchangeRate.joins(:currencies).where{ currencies.iso_code==to_currency }.order{ quote_at desc}
return 1.0 if rates.empty?
case rate
when :sell
rates[0].sell_rate
end
when String
rate.to_f
end
end
end
end
# app/models/account.rb
class Account
monetize :amount_cents
end
irb >> acc=Account.new( amount: 100 )
irb >> acc.amount.exchange_to("SEK") # use what_ever exchange_rate is 'in stock'
irb >> acc.amount.exchange_with_rate("SEK", :week_mean) # use the :week_mean to convert the amount
irb >> acc.amount.exchange_with_rate("SEK", :sell) # use the present or latest :sell rate to convert the amount
irb >> acc.amount.exchange_with_rate("SEK", 1.25416) # use this exchange rate
irb >> acc.amount.exchange_with_rate_locked("SEK", :buy) # use the present or latest :buy rate to convert the amount - and lock it for future use!