Для рельс уже написан миллион и один туториал про то, что делать, если вдруг приходится писать приложение, которое работает с деньгами.
Обычно все сводится к советам не использовать Float, использовать Decimal, транзакции там всякие и прочее. И в большей части случаев этих советов вполне достаточно для того, чтобы разработчик чувствовал себя сухо и комфортно.
А сталкивались ли вы с ситуацией, когда, скажем, приложение должно обслуживать жителей более чем одной страны?
Вот гипотетическая ситауция: приложение — сервис по подписке для любителейонаниз, например, эээ, платных мультфильмов. У нас каждый мультфильм стоит определенное decimal-количество рублей, которое хранится в базе. И у пользователя есть определенное decimal-количество рублей, которое он может тратить на просмотр. И все круто, пока мы не решаем, что было бы выгодно показывать мультфильмы еще и гражданам Украины. Украинцы ведь тоже любят смотреть мультфильмы и тратить деньги. Вот только гражданам этим неудобно иметь счет в рублях. Потому что зарплату им платят в гривнах, и на карточке у них наверняка гривны. И вообще, каждый раз в уме переводить, сколько каждый рублевый мультик будет стоить, как-то невесело.
И тут в сияющем арморе спасителя дня выруливает гем
Вобщем, красота да и только.
Единственно, что омрачает праздник — весьма нетривиальный механизм подключения к рельсовым моделям. Примерно как-то так:
И это только в одну модель. А еще надо не забыть инициализатор написать, чтобы валюты друг в друга конвертировались:
Короче, когда я копипастил этот код в третью по счету модель, пришла в мою светлую голову мысль написать простецкий гем, слегка упрощающий все это дело. Ну я и написал.
Называется counterfeit.
Работает просто как валенки:
Гугл в качестве обменника проставляется сам, но только при первой неудачной попытке конвертнуть валюты стандартным конвентором. Стандартному нужно руками прописывать курсы — врядли вы станете этим заниматься, правда ведь?
Вобщем, если вам когда-нибудь придется писать приложение с платными мультиками, попробуйте counterfeit. И найдете баги — пишите, а то я уже в продакшен запустил.
Что-то подсказывало мне, что мало кто с первого раза напечатает слово counterfeit без ошибок, поэтому специально для таких ребят был сделан волшебный алиас:
Обычно все сводится к советам не использовать Float, использовать Decimal, транзакции там всякие и прочее. И в большей части случаев этих советов вполне достаточно для того, чтобы разработчик чувствовал себя сухо и комфортно.
А сталкивались ли вы с ситуацией, когда, скажем, приложение должно обслуживать жителей более чем одной страны?
Вот гипотетическая ситауция: приложение — сервис по подписке для любителей
Money
И тут в сияющем арморе спасителя дня выруливает гем
money
. Он умеет как раз все что нужно:# 5 баксов
dollars = Money.new(500, 'USD')
# 10 евро!
euros = Money.new(1000, 'EUR') #
# можно сравнивать
euros > dollars # => true
# конвертировать
euros.exchange_to('USD') # => #<Money cents:1408 currency:USD>
# и даже складывать!!11
Money.new(1000, 'USD') + Money.new(1000, 'EUR') # => #<Money cents:2408 currency:USD>
Вобщем, красота да и только.
Единственно, что омрачает праздник — весьма нетривиальный механизм подключения к рельсовым моделям. Примерно как-то так:
# Gemfile
gem 'money'
gem 'google_currency', :require => 'money/bank/google_currency'
# cartoon.rb
class Cartoon < ActiveRecord::Base
composed_of :price,
:class_name => 'Money',
:mapping => [[ 'price_in_cents', 'cents' ], [ 'currency', 'currency_as_string' ]],
:constructor => Proc.new { |cents, currency| Money.new(cents || 0, currency || Money.default_currency) },
:converter => Proc.new { |value| value.respond_to?(:to_money) ? value.to_money : raise(ArgumentError, "Can't convert #{value.class} to Money") }
end
# migration
create_table :cartoons do |t|
# ...
t.integer :price_in_cents, :default => 0, :null => false
t.string :price_currency, :limit => 3, :null => false
# ...
end
И это только в одну модель. А еще надо не забыть инициализатор написать, чтобы валюты друг в друга конвертировались:
# intializers/money.rb
Money.default_bank = Money::Bank::GoogleCurrency.new
Counterfeit
Короче, когда я копипастил этот код в третью по счету модель, пришла в мою светлую голову мысль написать простецкий гем, слегка упрощающий все это дело. Ну я и написал.
Называется counterfeit.
Работает просто как валенки:
# Gemfile
gem 'counterfeit'
# cartoon.rb
class Cartoon < ActiveRecord::Base
has_counterfeit :price
# тут можно прописать валюту по умолчанию ключем
# :currency => 'RUB'
# и даже настроить аттрибуты, куда будет все сохраняться .
# :currency_attribute => :price_currency,
# :amount_attribute => :price_in_cents
end
# migration
create_table :cartoons do |t|
# ...
t.money :price
# ...
end
Гугл в качестве обменника проставляется сам, но только при первой неудачной попытке конвертнуть валюты стандартным конвентором. Стандартному нужно руками прописывать курсы — врядли вы станете этим заниматься, правда ведь?
Вобщем, если вам когда-нибудь придется писать приложение с платными мультиками, попробуйте counterfeit. И найдете баги — пишите, а то я уже в продакшен запустил.
But wait, there is more
Что-то подсказывало мне, что мало кто с первого раза напечатает слово counterfeit без ошибок, поэтому специально для таких ребят был сделан волшебный алиас:
class Cartoon < ActiveRecord::Base
has_money :price # так же проще, да?
end