EasyRailsMoney
“Young people, nowadays, imagine that money is everything.
Yes, murmured Lord Henry, settling his button-hole in his coat; and when they grow older they know it.”
― Oscar Wilde, The Picture of Dorian Gray and Other Writings
This library provides integration of money gem with Rails.
It provides migration helpers to define a schema with either a single
currency column or a currency column per-money object.
It also provides a ActiveRecord DSL to define that an attribute is a Money object and that it has a default currency
Please open a new issue in the github project issues tracker. You are also more than welcome to contribute to the project :-)
Credits
Have stolen lots of code from money-rails
But database schema, API and tests are written from scratch
money-rails is much more popular and full-featured. Definately try it out. I have actually submitted a PR to that project and it is actively maintained.
I have tried to create a simpler version of money-rails
With a better API and database schema, in my opinion.
I created this project to scratch my itch.
Installation
Add this line to your application's Gemfile:
gem 'easy_rails_money'
And then execute:
$ bundle
Or install it yourself as:
$ gem install easy_rails_money
Contributing
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request
Why use the Money gem
In Ruby, Integer and Floats are native types
BigDecimal is like a wrapper over a string
Float is imprecise. storing Money as float will give us round-off
errors. i think will have problems comparing two floats as well
BigDecimal is precise and can handle arbitrary precision
but it is slower
Check https://gist.github.com/deepak/1275050 for a benchmark
So, to represent money we have:
- store as String
- store as decimal in database which Rails typecasts to BigDecimal
- store as integer. Which is what the Money gem does
Money gem wraps:
- currency of a amount
- list of currency conversion rates
- actually convert one currency to another
flipkart.com has a version of the flipkart-money Money gem which uses BigDecimal
check the Money readme as well
Rationale
Let us say you want to store a Rupee Money object in the database
principal = Money.new(100, "inr")
To serialize the values in the database Option 1:
class CreateLoan < ActiveRecord::Migration
def change
create_table :loans do |t|
t.integer :principal_money
t.string :principal_currency
end
end
end
Option 2: Another option would be
class CreateLoan < ActiveRecord::Migration
def change
create_table :loans do |t|
t.integer :principal_as_paise
end
end
end
Note that we are storing the base unit in the database. If the amount is in dollars we store in cents or if the amount is in Indian Rupees we store in paise and so on. This is done because FLoats do not have a accurate representation but Integers do. Can store BigDecimal as well but it is slower. This is why the Money gem stores amounts as integer
Watch Rubyconf 2011 Float-is-legacy for more details and read What Every Computer Scientist Should Know About Floating-Point Arithmetic, by David Goldberg, published in March, 1991
We have encoded the currency in the column name. I like it because there is no need to define another column and it is simple. But the disadvantage is that it is inflexible ie. cannot store two currencies and changing the column name in MySQL might require downtime for a big table
So let us go with the first option. The disadvantage is that currency is stored as a string. Integer might be better for storing in the database
Now let us say we want to store multiple columns:
principal = Money.new(100, "inr")
repaid = Money.new(20, "inr")
npa = Money.new(10, "inr")
Now we would represent it as
class CreateLoan < ActiveRecord::Migration
def change
create_table :loans do |t|
t.integer :principal_money
t.string :principal_currency
t.integer :repaid_money
t.string :repaid_currency
t.integer :npa_money
t.string :npa_currency
end
end
end
We are repeating ourself and mostly all currencies for a record will be the same. So we can configure the currency on a per-record basis and write
Option 3:
class CreateLoan < ActiveRecord::Migration
def change
create_table :loans do |t|
t.string :currency
t.integer :principal_money
t.integer :repaid_money
t.integer :npa_money
end
end
end
It might be possible that we set a currency once for the whole app and never change it. But this seems like a nice tradeoff api-wise
Also the column names are suffixed with _money
and _currency
We need this for now, to reflect on the database scheme. I
Ideally should be able to read the metadata from rails scheme cache.
Usage
ActiveRecord
Only ActiveRecord is supported for now. And has been tested on ActiveRecord 3.x
Migration helpers
If you want to create a table which has some money columns, then you can use the money
migration helper
class CreateLoanWithCurrency < ActiveRecord::Migration
def change
create_table :loans, force: true do |t|
t.string :name
t.money :principal
t.money :repaid
t.money :npa
t.currency
end
end
end
If you want to add a money column to an existing table then you can
again use the money
migration helper
class AddPrincipalToLoan < ActiveRecord::Migration
def change
change_table :loans do |t|
t.money :principal
end
end
end
Another option is to use add_money
migration helper
It is a different DSL style, similar to create_table
class AddPrincipalToLoan < ActiveRecord::Migration
def up
add_money :loans, :principal, :repaid, :npa
end
def down
remove_money :loans, :principal, :repaid, :npa
end
end
add_money
helper is revertable, so you may use it inside change
migrations.
If you writing separate up
and down
methods, you may use
the remove_money
migration helper.
The above statements for money
and add_money
will create
two columns. An integer column to store the lower denomination as an
integer and a string column to store the currency name.
eg. if we say add_money :loans, :principal
Then the following two
columns will be created:
- integer column called
principal_money
- string column called
principal_currency
If we want to store $ 100
in this column then:
- column
principal_money
will contain the unit in the lower denomination ie. cents in this case. So for$100
it will store100 * 100 => 100_000 cents
- column
principal_currency
will store the currency name ie.usd
Both the amount and currency is needed to create a Money
object
Now if we have multiple money columns, then you can choose to have a single currency column
class CreateLoanWithCurrency < ActiveRecord::Migration
def change
create_table :loans, force: true do |t|
t.string :name
t.money :principal
t.money :repaid
t.money :npa
t.currency
end
end
end
This will create a single column for currency:
- It creates three columns for each of the money columns
principal_money
,repaid_money
andnpa_money
- note that it does not create a currency column for each of the
money columns. But a common currency column is created.
It is boringly enough called
currency
Note that columns are prefixed with _money
and _currency
And the common currency column is called currency
.
It is used to reflect on the database schema ie. to find out the
money and currency columns defined.
Right now, none of these choices are customizable.
Defining the Model
If every money column has its own currency column, then we cn define
the model as:
class Loan < ActiveRecord::Base
attr_accessible :name
money :principal
money :repaid
money :npa
end
The corresponding migration (given above) is:
class CreateLoanWithCurrency < ActiveRecord::Migration
def change
create_table :loans, force: true do |t|
t.string :name
t.money :principal
t.money :repaid
t.money :npa
end
end
end
Now if you want a single currency column then:
class Loan < ActiveRecord::Base
attr_accessible :name
with_currency(:inr) do
money :principal
money :repaid
money :npa
end
end
The corresponding migration (given above) is:
class CreateLoanWithCurrency < ActiveRecord::Migration
def change
create_table :loans, force: true do |t|
t.string :name
t.money :principal
t.money :repaid
t.money :npa
t.currency
end
end
end
For such a record, where the single currency is defined. calling currency on a new record will give us the currency. And can define a common currency per-record while creating it
eg:
class Loan < ActiveRecord::Base
attr_accessible :name
with_currency(:inr) do
money :principal
money :repaid
money :npa
end
end
loan = Loan.new
loan.currency # equals Money::Currency.new(:inr)
loan_usd = Loan.new(currency: :usd)
loan_usd.currency # equals Money::Currency.new(:usd)
TODO's
- Proof-read docs
- currency is stored as a string. Integer might be better for storing in the database
- store a snapshot of the exchange rate as well when the record was inserted or if we want to "freeze" the exchange rate per-record
- specs for migration test the same thing in multiple ways. have a spec helper
- add Gemfil to test on ActiveRecord 4.x ie. with Rails4 . Add to travis.yml as well
- configure the
_money
and_currency
prefix and the name of the commoncurrency
column - check specs tagged as "fixme"
- cryptographically sign gem
- test if Memoization in ```MoneyDsl#money`` will make any difference and add a performance test to catch regressions
- will it make sense to define the
money
dsl onActiveModel
? - the column names are suffixed with
_money
and_currency
We need this for now, to reflect on the database scheme. Ideally should be able to read the metadata from rails scheme cache. - see spec tagged with
migration
.
if we define a ActiveRecord object with a money column
"before" the table is defined. Then it will throw
an error and we will assume that a single
currency is defined. So always restart the app after the
migrations are run.
Any better way ?
Also the error handling EasyRailsMoney::ActiveRecord::MoneyDsl.single_currency?
is dependent on the database adapter
being used, which sucks. can test on other database adapters
or handle a generic error - add typecast for currency column
right now, it is always a string
do we want a Money::Currency object back?
not decided
see currency_persistence_spec.rb - make sure re-opening and redefining money dsl methods work eg. moving from individual currencies to single currency
- document. methods defined inside activerecord's scope move to a helper, no that its namespace is not polluted
- two specs tagged with fixme in validates_money_spec failing
- a version of inclusion_in validator that can compare Symbol and string
- code parser to lint that multiple money statements are used
without a with_currency statement. does cane (rubygem) have plugins?