diff --git a/Gemfile b/Gemfile index ee809b9f..f6e21c12 100644 --- a/Gemfile +++ b/Gemfile @@ -27,6 +27,7 @@ gem "bcrypt", "~> 3.1.7" gem "inline_svg" gem "jbuilder" gem "tzinfo-data", platforms: %i[ windows jruby ] +gem "money-rails", "~> 1.12" group :development, :test do gem "debug", platforms: %i[ mri windows ] diff --git a/Gemfile.lock b/Gemfile.lock index c0746429..6efd9773 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -202,6 +202,15 @@ GEM matrix (0.4.2) mini_mime (1.1.5) minitest (5.21.2) + monetize (1.13.0) + money (~> 6.12) + money (6.16.0) + i18n (>= 0.6.4, <= 2) + money-rails (1.15.0) + activesupport (>= 3.0) + monetize (~> 1.9) + money (~> 6.13) + railties (>= 3.0) msgpack (1.7.2) net-imap (0.4.10) date @@ -382,6 +391,7 @@ DEPENDENCIES inline_svg jbuilder letter_opener + money-rails (~> 1.12) pg (~> 1.1) propshaft puma (>= 5.0) diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 035a8330..9336af14 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -30,7 +30,7 @@ class AccountsController < ApplicationController private def account_params - params.require(:account).permit(:name, :accountable_type, :balance, :subtype) + params.require(:account).permit(:name, :accountable_type, :balance, :balance_cents, :subtype) end def account_type_class diff --git a/app/models/account.rb b/app/models/account.rb index f42ef47b..f5140ab1 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -4,4 +4,6 @@ class Account < ApplicationRecord delegated_type :accountable, types: %w[ Account::Credit Account::Depository Account::Investment Account::Loan Account::OtherAsset Account::OtherLiability Account::Property Account::Vehicle], dependent: :destroy delegate :type_name, to: :accountable + + monetize :balance_cents end diff --git a/app/views/accounts/index.html.erb b/app/views/accounts/index.html.erb index 39bc4efe..a0597914 100644 --- a/app/views/accounts/index.html.erb +++ b/app/views/accounts/index.html.erb @@ -11,7 +11,7 @@ <%= account.accountable %>

- <%= number_to_currency account.balance %> + <%= humanized_money_with_symbol account.balance %>

<% end %> diff --git a/app/views/accounts/new.html.erb b/app/views/accounts/new.html.erb index 2e6e7ddf..db7aa8c8 100644 --- a/app/views/accounts/new.html.erb +++ b/app/views/accounts/new.html.erb @@ -33,7 +33,7 @@
- <%= f.number_field :balance, placeholder: "$0.00", in: 0.00..100000000.00, required: 'required', class: "p-0 mt-1 bg-transparent border-none opacity-50 focus:outline-none focus:ring-0 focus-within:opacity-100" %> + <%= f.number_field :balance, placeholder: "$0.00", in: 0.00..100000000.00, step: :any, required: 'required', class: "p-0 mt-1 bg-transparent border-none opacity-50 focus:outline-none focus:ring-0 focus-within:opacity-100" %>
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index f8bd7290..53dcff9f 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -70,7 +70,7 @@ <%= account.name %>

- <%= number_to_currency account.balance %> + <%= humanized_money_with_symbol account.balance %>

<% end %> diff --git a/config/initializers/money.rb b/config/initializers/money.rb new file mode 100644 index 00000000..557b6bac --- /dev/null +++ b/config/initializers/money.rb @@ -0,0 +1,114 @@ +# encoding : utf-8 + +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 + # } + + # If you would like to use I18n localization (formatting depends on the + # locale): + config.locale_backend = :i18n + # + # Example (using default localization from rails-i18n): + # + # I18n.locale = :en + # Money.new(10_000_00, 'USD').format # => $10,000.00 + # I18n.locale = :es + # Money.new(10_000_00, 'USD').format # => $10.000,00 + # + # For the legacy behaviour of "per currency" localization (formatting depends + # only on currency): + # config.locale_backend = :currency + # + # Example: + # Money.new(10_000_00, 'USD').format # => $10,000.00 + # Money.new(10_000_00, 'EUR').format # => €10.000,00 + # + # In case you don't need localization and would like to use default values + # (can be redefined using config.default_format): + # config.locale_backend = nil + + # Set default raise_error_on_money_parsing option + # It will be raise error if assigned different currency + # The default value is false + # + # Example: + # config.raise_error_on_money_parsing = false +end diff --git a/db/migrate/20240206031739_replace_money_field.rb b/db/migrate/20240206031739_replace_money_field.rb new file mode 100644 index 00000000..dfe2b732 --- /dev/null +++ b/db/migrate/20240206031739_replace_money_field.rb @@ -0,0 +1,14 @@ +class ReplaceMoneyField < ActiveRecord::Migration[7.2] + def change + add_monetize :accounts, :balance + change_column :accounts, :balance_cents, :integer, limit: 8 + + Account.reset_column_information + + Account.find_each do |account| + account.update_columns(balance_cents: Money.from_amount(account.balance, account.currency).cents) + end + + remove_column :accounts, :balance + end +end diff --git a/db/schema.rb b/db/schema.rb index 45b6b395..98483a6f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2024_02_03_050018) do +ActiveRecord::Schema[7.2].define(version: 2024_02_06_031739) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql" @@ -59,12 +59,13 @@ ActiveRecord::Schema[7.2].define(version: 2024_02_03_050018) do t.string "subtype" t.uuid "family_id", null: false t.string "name" - t.bigint "balance", default: 0 t.string "currency", default: "USD" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.string "accountable_type" t.uuid "accountable_id" + t.bigint "balance_cents", default: 0, null: false + t.string "balance_currency", default: "USD", null: false t.index ["accountable_type"], name: "index_accounts_on_accountable_type" t.index ["family_id"], name: "index_accounts_on_family_id" end diff --git a/test/fixtures/accounts.yml b/test/fixtures/accounts.yml index 9f410649..3f31f421 100644 --- a/test/fixtures/accounts.yml +++ b/test/fixtures/accounts.yml @@ -1,14 +1,14 @@ dylan_checking: family: dylan_family name: Bob's Checking - balance: 1200 + balance_cents: 1200 dylan_roth: family: dylan_family name: Bob's Roth IRA - balance: 1200 + balance_cents: 1200 richards_savings: family: richards_family name: Keef's HYSA - balance: 1500 \ No newline at end of file + balance_cents: 1500 diff --git a/test/models/account_test.rb b/test/models/account_test.rb index 8db20847..a67ed495 100644 --- a/test/models/account_test.rb +++ b/test/models/account_test.rb @@ -1,11 +1,31 @@ require "test_helper" class AccountTest < ActiveSupport::TestCase - test "new account should be valid" do + def setup depository = Account::Depository.create! - account = Account.create!(family: families(:dylan_family), name: "Explicit Checking", balance: 1200, accountable: depository) - assert account.valid? - assert_not_nil account.accountable_id - assert_not_nil account.accountable + @account = Account.create!(family: families(:dylan_family), name: "Explicit Checking", balance_cents: 1200, accountable: depository) + end + + test "new account should be valid" do + assert @account.valid? + assert_not_nil @account.accountable_id + assert_not_nil @account.accountable + end + + test "balance returns Money object" do + @account.balance = 10 + assert_instance_of Money, @account.balance + assert_equal :usd, @account.balance.currency.id + end + + test "correctly assigns Money objects to the attribute" do + @account.balance = Money.new(2500, "USD") + assert_equal 2500, @account.balance_cents + end + + test "balance_cents can be updated" do + new_balance = Money.new(10000, "USD") + @account.balance = new_balance + assert_equal new_balance, @account.balance end end