Multi-currency support: Money + Currency class improvements #553

Merged
zachgoll merged 2 commits from money-and-currency-class into main 2024-03-18 23:21:00 +08:00
zachgoll commented 2024-03-18 21:43:33 +08:00 (Migrated from github.com)

This PR is a precursor to #543, which was growing too big and tackling too many things at once.

Overview

Below are some explanations of changes and sample usage for documentation purposes.

Money lib

Adds Money, Money::Currency, and Money::Arithmetic to lib as these represent generic concepts that could apply to any app that deals with money.

  • Money packages up all the information required to represent a monetary value (amount, currency)
  • Money::Currency stores information about all global currencies and can provide a list of these through Money::Currency.all or Money::Currency.popular
  • Money::Arithmetic allows us to add, subtract, multiply, divide, and compare monetary values without any special syntax. In other words, we can do Money.new(0) == Money.new(0) or Money.new(100) > Money.new(5)

Why not money and money-rails?

There are two main reasons I decided to go with a custom implementation:

  1. The money and money-rails gems are somewhat opinionated about a project's money storage strategy. Throughout most of its API, it assumes that money is being stored in minor currency units. Our project does not do that. We store in Decimal{19,4}, which was chosen to make the data a lot more intuitive (1 less layer of indirection). Furthermore, the money-rails gem integrates at the model level (with migration helpers), which as we saw with a first attempt at using it, caused unnecessary confusion for new contributors.
  2. The overall implementation here is fairly simple, so we get the benefit of owning our implementation without a huge maintenance cost.

Monetizable Concern + Form Helper

In an attempt to keep things simple and intuitive, as an alternative to the money-rails strategy that "magically" turns fields like Account.balance into Money instances, I've created a Monetizable concern with a straightforward API:

class Account
  include Monetizable

  monetize :balance # creates `balance_money` getter method, which returns instance of Money
end

This unobtrusively adds a field called balance_money, which returns Money.new(balance, currency).

Furthermore, I've created a form helper with the following API:

<%= form_with model: @transaction do |f| %>
  <%= f.money_field :amount_money %>
  <%= f.submit %>
<% end %>

f.money_field will read amount_money and create an :amount and :currency input that is updated on submission.

The overall goal here is to give the developer flexibility when working with money and making it explicit when a field deals with money and when it doesn't.

This PR is a precursor to #543, which was growing too big and tackling too many things at once. ## Overview Below are some explanations of changes and sample usage for documentation purposes. ### Money lib Adds `Money`, `Money::Currency`, and `Money::Arithmetic` to `lib` as these represent generic concepts that could apply to any app that deals with money. - `Money` packages up all the information required to represent a monetary value (amount, currency) - `Money::Currency` stores information about all global currencies and can provide a list of these through `Money::Currency.all` or `Money::Currency.popular` - `Money::Arithmetic` allows us to add, subtract, multiply, divide, and compare monetary values without any special syntax. In other words, we can do `Money.new(0) == Money.new(0)` or `Money.new(100) > Money.new(5)` #### Why not `money` and `money-rails`? There are two main reasons I decided to go with a custom implementation: 1. The `money` and `money-rails` gems are somewhat opinionated about a project's money storage strategy. Throughout most of its API, it assumes that money is being stored in _minor currency units_. Our project does not do that. We store in `Decimal{19,4}`, which was chosen to make the data a lot more intuitive (1 less layer of indirection). Furthermore, the `money-rails` gem integrates at the model level (with migration helpers), which as we saw with a first attempt at using it, caused unnecessary confusion for new contributors. 3. The overall implementation here is fairly simple, so we get the benefit of owning our implementation without a huge maintenance cost. ### Monetizable Concern + Form Helper In an attempt to keep things simple and intuitive, as an alternative to the `money-rails` strategy that "magically" turns fields like `Account.balance` into `Money` instances, I've created a `Monetizable` concern with a straightforward API: ```rb class Account include Monetizable monetize :balance # creates `balance_money` getter method, which returns instance of Money end ``` This unobtrusively adds a field called `balance_money`, which returns `Money.new(balance, currency)`. Furthermore, I've created a form helper with the following API: ```erb <%= form_with model: @transaction do |f| %> <%= f.money_field :amount_money %> <%= f.submit %> <% end %> ``` `f.money_field` will read `amount_money` and create an `:amount` and `:currency` input that is updated on submission. The overall goal here is to give the developer _flexibility_ when working with money and making it _explicit_ when a field deals with money and when it doesn't.
Sign in to join this conversation.