Pull to refresh

Как правильно считать чужие деньги

Reading time3 min
Views2.3K
Для рельс уже написан миллион и один туториал про то, что делать, если вдруг приходится писать приложение, которое работает с деньгами.

Обычно все сводится к советам не использовать Float, использовать Decimal, транзакции там всякие и прочее. И в большей части случаев этих советов вполне достаточно для того, чтобы разработчик чувствовал себя сухо и комфортно.

А сталкивались ли вы с ситуацией, когда, скажем, приложение должно обслуживать жителей более чем одной страны?





Вот гипотетическая ситауция: приложение — сервис по подписке для любителей онаниз, например, эээ, платных мультфильмов. У нас каждый мультфильм стоит определенное decimal-количество рублей, которое хранится в базе. И у пользователя есть определенное 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
Tags:
Hubs:
Total votes 49: ↑45 and ↓4+41
Comments17

Articles