Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

RubyMoney/money-rails

Repository files navigation

RubyMoney - Money-Rails

Gem Version Ruby License

Introduction

This library provides integration of the money gem with Rails.

Use monetize to specify which fields you want to be backed by Money objects and helpers provided by the money gem.

Currently, this library is in active development mode, so if you would like to have a new feature, feel free to open a new issue here. You are also welcome to contribute to the project.

Installation

Add it to your application’s Gemfile using:

bundle add money-rails

Or install it yourself using:

$ gem install money-rails

You can also use the money configuration initializer:

$ bin/rails generate money_rails:initializer

There, you can define the default currency value and set other configuration parameters for the Rails app.

Without Rails in rack-based applications, call during initialization:

MoneyRails::Hooks.init

Usage

ActiveRecord

Usage example

For example, we create a Product model which has an integer column called price_cents and we want to handle it using a Money object instead:

class Product < ActiveRecord::Base
 monetize :price_cents
end

Now each Product object will also have an attribute called price which is a Money object, and can be used for money comparisons, conversions etc.

In this case the name of the money attribute is created automagically by removing the _cents suffix from the column name.

If you are using another database column name, or you prefer another name for the money attribute, then you can provide an as argument with a string value to the monetize macro:

monetize :discount_subunit, as: "discount"

Now the model objects will have a discount attribute which is a Money object, wrapping the value of the discount_subunit column with a Money instance.

Migration helpers

If you want to add a money field to a product model you can use the add_monetize helper. This helper can be customized inside a MoneyRails.configure block. You should customize the add_monetize helper to match the most common use case and utilize it across all migrations.

class MonetizeProduct < ActiveRecord::Migration
 def change
 add_monetize :products, :price
 # OR
 change_table :products do |t|
 t.monetize :price
 end
 end
end

Another example, where the currency column is not included:

class MonetizeItem < ActiveRecord::Migration
 def change
 add_monetize :items, :price, currency: { present: false }
 end
end

Notice: Default value of currency field, generated by migration’s helper, is USD. To override these defaults, you need change the default_currency in an initializer and run migrations.

The add_monetize helper is reversible, so you can use it inside change migrations. If you’re writing separate up and down methods, you can use the remove_monetize helper.

Allow nil values

If you want to allow nil and/or blank values to a specific monetized field, you can use the :allow_nil parameter:

# in Product model
monetize :optional_price_cents, allow_nil: true
# in Migration
def change
 add_monetize :products,
 :optional_price,
 amount: { null: true, default: nil },
 currency: { null: true, default: nil }
end
# now blank assignments are permitted
product.optional_price = nil
product.save # returns without errors
product.optional_price # => nil
product.optional_price_cents # => nil

Allow large numbers

If you foresee that you will be saving large values (range is -2147483648 to +2147483647 for Postgres), increase your integer column limit to bigint:

def change
 change_column :products, :price_cents, :integer, limit: 8
end

Numericality validation options

You can also pass along numericality validation options such as this:

monetize :price_in_a_range_cents,
 allow_nil: true,
 numericality: {
 greater_than_or_equal_to: 0,
 less_than_or_equal_to: 10000
 }

Or, if you prefer, you can skip validations entirely for the attribute. This is useful if chosen attributes are aggregate methods and you wish to avoid executing them on every record save.

monetize :price_in_a_range_cents, disable_validation: true

You can also skip validations independently from each other by simply passing false to the validation you are willing to skip, like this:

monetize :price_in_a_range_cents, numericality: false

And you can also use subunit_numericality for subunit:

monetize :price_in_a_range_cents,
 allow_nil: true,
 subunit_numericality: {
 greater_than_or_equal_to: 0,
 less_than_or_equal_to: 100_00
 }

Mongoid

Money is available as a field type to supply during a field definition:

class Product
 include Mongoid::Document
 field :price, type: Money
end
obj = Product.new
# => #<Product _id: 4fe865699671383656000001, _type: nil, price: nil>
obj.price
# => nil
obj.price = Money.new(100, 'EUR')
# => #<Money cents:100 currency:EUR>
obj.price
#=> #<Money cents:100 currency:EUR>
obj.save
# => true
obj
# => #<Product _id: 4fe865699671383656000001, _type: nil, price: {cents: 100, currency_iso: "EUR"}>
obj.price
#=> #<Money cents:100 currency:EUR>
## You can access the money hash too:
obj[:price]
# => {cents: 100, currency_iso: "EUR"}

The usual options on field as index, default, ..., are available.

Method conversion

Method return values can be monetized in the same way attributes are monetized. For example:

class Transaction < ActiveRecord::Base
 monetize :price_cents
 monetize :tax_cents
 monetize :total_cents
 def total_cents
 price_cents + tax_cents
 end
end

Now each Transaction object has a method called total which returns a Money object.

Currencies

money-rails supports a set of options to handle currencies for your monetized fields. The default option for every conversion is to use the global default currency of the Money library, as given in the configuration initializer of money-rails:

# config/initializers/money.rb
MoneyRails.configure do |config|
 # set the default currency
 config.default_currency = :usd
end

For a complete list of available currencies: ISO 4217

If you need to set the default currency on a per-request basis, such as in a multi-tenant application, you may use a lambda to lazy-load the default currency from a field in a configuration model called Tenant in this example:

# config/initializers/money.rb
ActiveSupport::Reloader.to_prepare do
 MoneyRails.configure do |config|
 # set the default currency based on client configuration
 config.default_currency = -> { Tenant.current.default_currency }
 end
end

In many cases this is not enough, so there are some other options to meet your needs.

Model Currency

You can override the global default currency within a specific ActiveRecord model using the register_currency macro:

# app/models/product.rb
class Product < ActiveRecord::Base
 # Use EUR as model level currency
 register_currency :eur
 monetize :discount_subunit, as: "discount"
 monetize :bonus_cents
end

Now product.discount and product.bonus will return a Money object using EUR as their currency, instead of the default USD.

(This is not available in Mongoid).

Attribute Currency (:with_currency)

By passing the option :with_currency to the monetize macro call, with a currency code (symbol or string) or a callable object (object that responds to the call method) that returns a currency code, as its value, you can define a currency in a more granular way. This will let you attach the given currency only to the specified monetized model attribute (allowing you to, for example, monetize different attributes of the same model with different currencies).

This allows you to override both the model level and the global default currencies:

# app/models/product.rb
class Product < ActiveRecord::Base
 # Use EUR as the model level currency
 register_currency :eur
 monetize :discount_subunit, as: "discount"
 monetize :bonus_cents, with_currency: :gbp
end

In this case product.bonus will return a Money object with GBP as its currency, whereas product.discount.currency.to_s # => EUR

As mentioned earlier you can use an object that responds to the method call and accepts the model instance as a parameter. That means you can use a Proc or lambda (we would recommend lambda over Proc because of their different control flow characteristics) or even define a separate class with an instance or class method (maybe even a module) to return the currency code:

class DeliveryFee
 def call(product)
 # some logic here that will return a currency code
 end
end
module OptionalPrice
 def self.call(product)
 # some logic here that will return a currency code
 end
end
class Product < ActiveRecord::Base
 monetize :price_cents, with_currency: ->(_product) { :gbp }
 monetize :delivery_fee_cents, with_currency: DeliveryFee.new
 monetize :optional_price_cents, with_currency: OptionalPrice
end

Instance Currencies

All the previous options do not require any extra model fields to hold the currency values. If the currency of a field will vary from one model instance to another, then you should add a column called currency to your database table and pass the option with_model_currency to the monetize macro.

money-rails will use this knowledge to override the model level and global default values. Non-nil instance currency values also override attribute currency values, so they have the highest precedence.

class Transaction < ActiveRecord::Base
 # Use model level currency
 register_currency :gbp
 monetize :amount_cents, with_model_currency: :currency
 monetize :tax_cents, with_model_currency: :currency
end
# Now instantiating with a specific currency overrides
# the model and global currencies
t = Transaction.new(amount_cents: 2500, currency: "CAD")
t.amount == Money.new(2500, "CAD") # true

Configuration parameters

You can handle a bunch of configuration params through money.rb initializer:

MoneyRails.configure do |config|
 # To set the default currency
 #
 # config.default_currency = :usd
 # Set default bank object
 #
 # Example:
 # config.default_bank = EuCentralBank.new
 # Add exchange rates to current money bank object.
 # (The conversion rate refers to one direction only)
 #
 # Example:
 # config.add_rate "USD", "CAD", 1.24515
 # config.add_rate "CAD", "USD", 0.803115
 # To handle the inclusion of validations for monetized fields
 # The default value is true
 #
 # config.include_validations = true
 # Default ActiveRecord migration configuration values for columns:
 #
 # config.amount_column = {
 # prefix: '', # column name prefix
 # postfix: '_cents', # column name postfix
 # column_name: nil, # full column name (overrides prefix, postfix and accessor name)
 # type: :integer, # column type
 # present: true, # column will be created
 # null: false, # other options will be treated as column options
 # default: 0
 # }
 #
 # config.currency_column = {
 # prefix: '',
 # postfix: '_currency',
 # column_name: nil,
 # type: :string,
 # present: true,
 # null: false,
 # default: 'USD'
 # }
 # Register a custom currency
 #
 # Example:
 # config.register_currency = {
 # priority: 1,
 # iso_code: "EU4",
 # name: "Euro with subunit of 4 digits",
 # symbol: "€",
 # symbol_first: true,
 # subunit: "Subcent",
 # subunit_to_unit: 10000,
 # thousands_separator: ".",
 # decimal_mark: ","
 # }
 # Specify a rounding mode
 # Any one of:
 #
 # BigDecimal::ROUND_UP,
 # BigDecimal::ROUND_DOWN,
 # BigDecimal::ROUND_HALF_UP,
 # BigDecimal::ROUND_HALF_DOWN,
 # BigDecimal::ROUND_HALF_EVEN,
 # BigDecimal::ROUND_CEILING,
 # BigDecimal::ROUND_FLOOR
 #
 # set to BigDecimal::ROUND_HALF_EVEN by default
 #
 # config.rounding_mode = BigDecimal::ROUND_HALF_UP
 # Set default money format globally.
 # Default value is nil meaning "ignore this option".
 # Example:
 #
 # config.default_format = {
 # no_cents_if_whole: nil,
 # symbol: nil,
 # sign_before_symbol: nil
 # }
 # Set whether an error should be raised when parsing money values
 # This includes assigning to a monetized field with the wrong currency
 # Default value is false
 #
 # config.raise_error_on_money_parsing = true
end
  • default_currency: Set the default (application wide) currency (USD is the default)
  • include_validations: Permit the inclusion of a validates_numericality_of validation for each monetized field (the default is true)
  • register_currency: Register one custom currency. This option can be used more than once to set more custom currencies. The value should be a hash of all the necessary key/value pairs (important keys: :priority, :iso_code, :name, :symbol, :symbol_first, :subunit, :subunit_to_unit, :thousands_separator, :decimal_mark).
  • add_rate: Provide custom exchange rate for currencies in one direction only! This rate is added to the attached bank object.
  • default_bank: The default bank object holding exchange rates etc. (https://github.com/RubyMoney/money#currency-exchange)
  • default_format: Force Money#format to use these options for formatting.
  • amount_column: Provide values for the amount column (holding the fractional part of a money object).
  • currency_column: Provide default values or even disable (present: false) the currency column.
  • rounding_mode: Set Money.rounding_mode to one of the BigDecimal constants.
  • raise_error_on_money_parsing: Set whether errors should be raised when parsing money values

Helpers

For examples below, @money_object == <Money fractional:650 currency:USD>

Helper Result
currency_symbol <span class="currency_symbol">$</span>
humanized_money @money_object 6.50
humanized_money_with_symbol @money_object 6ドル.50
money_without_cents @money_object 6
money_without_cents_and_with_symbol @money_object 6ドル
money_only_cents @money_object 50

no_cents_if_whole configuration param

humanized_money and humanized_money_with_symbol will not render the cents part if it contains only zeros, unless config.no_cents_if_whole is set to false in the money.rb configuration (default: true). Note that the config.default_format will be overwritten by config.no_cents_if_whole. So humanized_money will ignore config.default_format = { no_cents_if_whole: false } if you don't set config.no_cents_if_whole = false.

Testing

The test helpers work with both RSpec and Minitest.

RSpec

If you use RSpec, just require the test helpers in spec_helper.rb:

require "money-rails/test_helpers"

The helpers will automatically be included in your RSpec tests.

Minitest

If you use Minitest, require the test helpers and include them in your test class:

require "money-rails/test_helpers"
class ProductTest < Minitest::Test
 include MoneyRails::TestHelpers
 def test_monetizes_price
 matcher = monetize(:price_cents)
 assert matcher.matches?(Product)
 end
end

The monetize matcher

is_expected.to monetize(:price)

This will ensure that a column called price_cents is being monetized.

is_expected.to monetize(:price).allow_nil

By using allow_nil you can specify money attributes that accept nil values.

is_expected.to monetize(:price).as(:discount_value)

By using as chain you can specify the exact name to which a monetized column is being mapped.

is_expected.to monetize(:price).with_currency(:gbp)

By using the with_currency chain you can specify the expected currency for the chosen money attribute. (You can also combine all the chains.)

is_expected.to monetize(:price).with_model_currency(:currency)

By using the with_model_currency chain you can specify the attribute that contains the currency to be used for the chosen money attribute.

For examples on using the test_helpers look at test_helpers_spec.rb

Supported ORMs/ODMs

  • ActiveRecord (>= 7.0)
  • Mongoid (>= 7.x)

Supported Ruby interpreters

  • MRI Ruby >= 3.1

You can see a full list of the currently supported interpreters in ruby.yml

Contributing

Steps

  1. Fork the repo
  2. Run the tests
  3. Make your changes
  4. Test your changes
  5. Create a Pull Request

How to run the tests

Our tests are executed with several ORMs - see Rakefile for details. To install all required gems run rake spec:all. That command will take care of installing all required gems for all the different Gemfiles and then running the test suite with the installed bundle.

You can also run the test suite against a specific ORM or Rails version, rake -T will give you an idea of the possible task (take a look at the tasks under the spec: namespace).

If you are testing against mongoid, make sure to have a MongoDB server running before executing the suite (e.g. sudo mongod --quiet or docker run --rm -p 27017:27017 mongo:latest).

About

Integration of RubyMoney - Money with Rails

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 138

Languages

AltStyle によって変換されたページ (->オリジナル) /