I am using the Money gem for handling transaction amounts. I'd like to use different currencies for different transactions (but without conversions). The default currency is set in the money.rb:
config.default_currency = :usd
Even though I can set different currencies when creating a Transaction, the view always shows the amount in US dollars. For example this 12.00 transaction with RUB as currency:
<Transaction id: 100, amount_cents: 1200, currency: "RUB", created_at: "2013-12-11 09:32:52", updated_at: "2013-12-11 09:32:52">
Is being shown as a USD transaction in my views.
<% transactions.each do |transaction| %>
<%= transaction.amount_cents.to_money.format %>
...
=> $12.00
Here's the code from my Transaction.rb (just in case I'm missing something)
composed_of :amount_cents,
class_name: 'Money',
mapping: %w(amount_cents cents),
converter: Proc.new { |value| value.respond_to?(:to_money) ? value.to_money : Money.empty }
monetize :amount_cents, as: "amount"
Any ideas on how to override the default? I'll be thankful for any advice.
Resolved the issue using amount instead of amount_cents.
Related
I am building and Rails 5 API where I am trying to send money amount and store it in PostgresQL database. I am sending amount 2.4 but I see in database only 2 is stored. what I am doing wrong?
my migration:
class CreateTransactions < ActiveRecord::Migration[5.1]
def change
create_table :transactions do |t|
t.monetize :transaction_price, amount: { null: true, default: nil }
t.timestamps
end
end
end
my model is:
class Transaction < ApplicationRecord
monetize :transaction_price_cents
end
my controller:
class TransactionsController < ApiController
def create
transaction = Transaction.new(transaction_param)
if transaction.save
render json: { status: 'SUCCESS', data:transaction }, status: :ok
end
end
private
def transaction_param
params.require(:transaction).permit(:transaction_price_cents)
end
end
I am sending this json with postman:
{
"transaction_price_cents": 345.23
}
What I am getting in response:
{
"status": "SUCCESS",
"data": {
"id": 1,
"transaction_price_cents": 345,
"transaction_price_currency": "USD",
}
}
I either want 345.23 or 34523 but its giving me only 345!
Your price in cents! And that's ok!
Handling money in cents is a common pattern. It will also save your life when it comes to rounding errors with taxes or currency exchange. Like in their docs mentioned you should use a helper to output the price in a human readable form:
humanized_money #money_object # => 6.50
humanized_money_with_symbol #money_object # => $6.50
money_without_cents_and_with_symbol #money_object # => $6
If you accessing the data via an API you could add a human_readable field in your api
def transaction_price_human_readable
return humanized_money_with_symbol(#money_object) # or self or...
end
Save/Create model: If you get a floating number you could change the floating point into cents before_save
before_save :convert_transaction_price
def convert_transaction_price
self.transaction_price = (self.transaction_price * 100).to_i
end
I had the same problem.
(EDITED NEW AND CORRECT ANSWER):
All I had to do was to use the provided attribute from the money-rails gem. In my case I had an amount_cents attribute, and I had to use the provided :amount attribute in the form.
<%= f.label :amount %>
<%= f.text_field :amount %>
NOTE: I converted the value of :amount_cents to a float string in the edit.html.erb as followed:
<%= f.label :amount %>
<%= f.text_field :amount, value: number_with_precision(f.object.amount_cents / 100, precision: 2).gsub(/\./, ',') %>
(also note that I had configured money-rails to use EUROs which use "," as delimiter, thats why i have to use .gsbu(/\./, ','))
And here ist the IMPORTANT PART, I had to update my strong_parameters in the controller to permit :amount, and not :amount_cents
private
def invoice_params
params.require(:invoice).permit(…, :amount, …)
end
--
(OLD ANSWER):
I came to the conclusion that it is best to change the input value directly in the Frontend to cents. And then send the cents to the backend.
Here is a nice Stimulus Controller which does exactly that: https://gist.github.com/psergi/72f99b792a967525ffe2e319cf746101
(You may need to update that gist to your liking, and also it expects that you use Stimulus in your rails project)
(I leave the old answer in here, because I think it is a good practice to send _cents from the frontend to the backend, but in the moment it is not necessary [for me]. If you want to support more than one currency, you probably want to do it like that and use a .js framework to handle the input conversion -> s.th. like http://autonumeric.org/)
I am using the Money gem and composed_of as per this answer: https://stackoverflow.com/a/3843805/4162458
I have an integer column in my database, :discount
product.rb
class Product < ActiveRecord::Base
composed_of :discount_display,
:class_name => 'Money',
:mapping => %w(discount cents),
:converter => Proc.new { |value| Money.new(value) }
And in my form:
<%= f.number_field :discount_display, class: 'form-control', min: "0" %>
However, if I enter 12, it saves in the database as 12 and displays on the form when I refresh as 0.12.
How can I have it so that when you enter "12" it is saved as 1200 in the db and displays properly, as that answers seems to say should happen?
As per documentation,
Represents monetary values as integers, in cents. This avoids floating
point rounding errors.
Hence, if you expect collect dollars from the user, then, you need to convert it to cents before saving in DB. Perhaps, your converter proc should be
Proc.new { |value| Money.new(value * 100) }
or
Proc.new { |value| Money.from_amount(value, "USD") }
Does the money-rails gem require a specific order of price and currency when initializing an object? For example, take a look at the following:
Object.new(currency: 'JPY', price: 25)
=> #<Object id: nil, price_cents: 25, currency: "JPY">
If we specify the price first, we get an incorrect value (2500) for the price:
Object.new(price: 25, currency: 'JPY')
=> #<Object id: nil, price_cents: 2500, currency: "JPY">
Object contains the following: monetize :price_cents.
It looks like the order matters for certain currencies (including JPY because it doesn't have cents). This may not be the best solution but if anyone is stuck, here is what I did.
I added the following to the self.monetize method in money-rails to override the initialize methods for classes that use it:
define_method "initialize" do |opts = {}|
opts = opts.deep_symbolize_keys
opts = {currency: opts[:currency]}.merge(opts)
super(opts)
end
This way it will send the currency first.
I use money-rails gem in my rails application.
I have model Transaction.
class Transaction < ActiveRecord::Base
monetize :amount_money, as: :amount, with_model_currency: :currency, numericality: {greater_than_or_equal_to: 0}
end
And form for adding new transaction.
= simple_form_for [:post_manager, #transaction] do |f|
= f.label t('activerecord.attributes.transaction.amount')
= f.input :amount, class: 'form-control'
= f.submit t('views.transactions.submit.create'), class: 'btn btn-md btn-success'
In my controller action:
def create
#transaction = Transaction.new(transaction_params)
#transaction.save
respond_with(#transaction, location: post_manager_transactions_path)
end
In my money initializer:
MoneyRails.configure do |config|
config.register_currency = {
priority: 1,
iso_code: 'BYR',
name: 'Belarusian Ruble',
symbol: 'Br',
disambiguate_symbol: 'BYR',
subunit_to_unit: 1,
symbol_first: false,
decimal_mark: '.',
thousands_separator: ' ',
iso_numeric: '974',
smallest_denomination: 100
}
end
When I try to add new transaction:
In my controller action:
[1] pry(#<PostManager::TransactionsController>)> #transaction
=> #<Transaction:0x000001018ebfa0 id: nil, kind: "withdraw", amount_money: 12300, note: "vf", approved: nil, wallet_id: 1, category_id: 1, created_at: nil, updated_at: nil>
[2] pry(#<PostManager::TransactionsController>)> params
=> {"utf8"=>"✓",
"authenticity_token"=>"hAHFdamHK7CI41zXiHUCSb+RUg+57JR9sZTIhi2frcLEQELakQuOvhs8xaWMwK32XbxTsTfplCQJA7XigsueLQ==",
"transaction"=>{"kind"=>"withdraw", "category_id"=>"1", "amount"=>"123", "note"=>"vf"},
"commit"=>"Создать операцию",
"controller"=>"post_manager/transactions",
"action"=>"create"}
So. In my params :amount is 123, but in my new transaction :amount is 12300, so i have 2 extra zeros in my amount money field.
And I really don't know how to fix it. Maybe someone have had such problem before?
This is the expected behaviour of the money-rails gem. It will save the currency amount in it's lowest denomination in order to prevent rounding errors.
Since you have not specified the currency of the amount you're entering, it defaults to USD and converts it to cents, which is why you're seeing that result:
$123 * 100 = 12300
The gem is smart enough to convert currency amounts into their lowest denomination. However, it needs to know the currency it is working with, since not all currencies behave the same.
The way you approach this is dependent on your application logic. However, as a test you could add the following hidden field to your form that would yield the result you expect.
= f.input :amount_currency, as: :hidden, input_html: { value: "BYR" }
L
I have the following rails model:
class Product < ActiveRecord::Base
end
class CreateProducts < ActiveRecord::Migration
def self.up
create_table :products do |t|
t.decimal :price
t.timestamps
end
end
def self.down
drop_table :products
end
end
But when I do the following in the rails console:
ruby-1.9.2-p180 :001 > product = Product.new
=> #<Product id: nil, price: nil, created_at: nil, updated_at: nil>
ruby-1.9.2-p180 :002 > product.price = 'a'
=> "a"
ruby-1.9.2-p180 :003 > product.save
=> true
ruby-1.9.2-p180 :004 > p product
#<Product id: 2, price: #<BigDecimal:39959f0,'0.0',9(9)>, created_at: "2011-05-18 02:48:10", updated_at: "2011-05-18 02:48:10">
=> #<Product id: 2, price: #<BigDecimal:3994ca8,'0.0',9(9)>, created_at: "2011-05-18 02:48:10", updated_at: "2011-05-18 02:48:10">
As you can see, I wrote 'a' and it saved 0.0 in the database. Why is that? This is particularly annoying because it bypasses my validations e.g.:
class Product < ActiveRecord::Base
validates :price, :format => /\d\.\d/
end
anything that is invalid gets cast to 0.0 if you call to_f on it
"a".to_f #=> 0.0
you would need to check it with validations in the model
validates_numericality_of :price # at least in rails 2 i think
i dont know what validating by format does, so i cant help you there, but try to validate that it is a number, RegExs are only checked against strings, so if the database is a number field it might be messing up
:format is for stuff like email addresses, logins, names, etc to check for illegeal characters and such
You need to re-look at what is your real issue is. It is a feature of Rails that a string is auto-magically converted into either the appropriate decimal value or into 0.0 otherwise.
What's happening
1) You can store anything into an ActiveRecord field. It is then converted into the appropriate type for database.
>> product.price = "a"
=> "a"
>> product.price
=> #<BigDecimal:b63f3188,'0.0',4(4)>
>> product.price.to_s
=> "0.0"
2) You should use the correct validation to make sure that only valid data is stored. Is there anything wrong with storing the value 0? If not, then you don't need a validation.
3) You don't have to validate that a number will be stored in the database. Since you declared the db field to be a decimal field, it will ONLY hold decimals (or null if you let the field have null values).
4) Your validation was a string-oriented validation. So the validation regexp changed the 0.0 BigDecimal into "0.0" and it passed your validation. Why do you think that your validation was bypassed?
5) Why, exactly, are you worried about other programmers storing strings into your price field?
Are you trying to avoid products being set to zero price by mistake? There are a couple of ways around that. You could check the value as it comes in (before it is converted to a decimal) to see if its format is right. See AR Section "Overwriting default accessors"
But I think that would be messy and error prone. You'd have to set the record's Error obj from a Setter, or use a flag. And simple class checking wouldn't work, remember that form data always comes in as a string.
Recommended Instead, make the user confirm that they meant to set the price to 0 for the product by using an additional AR-only field (a field that is not stored in the dbms).
Eg
attr_accessor :confirm_zero_price
# Validate that when the record is created, the price
# is either > 0 or (price is <= 0 && confirm_zero_price)
validates_numericality_of :price, :greater_than => 0,
:unless => Proc.new { |s| s.confirm_zero_price},
:on => :create
Notes The above is the sort of thing that is VERY important to include in your tests.
Also I've had similar situations in the past. As a result of my experiences, I now record, in the database, the name of the person who said that the value should indeed be $0 (or negative) and let them have a 255 char reason field for their justification. Saves a lot of time later on when people are wondering what was the reason.