diff --git a/Gemfile b/Gemfile index 863d16f..21e78cf 100644 --- a/Gemfile +++ b/Gemfile @@ -88,3 +88,5 @@ group :test do # Code coverage; started in test/test_helper.rb only when COVERAGE=1 gem "simplecov", require: false end + +gem "money-rails", "~> 3.0" diff --git a/Gemfile.lock b/Gemfile.lock index ecea679..2a19f5b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -226,6 +226,16 @@ GEM minitest (6.0.6) drb (~> 2.0) prism (~> 1.5) + monetize (2.0.0) + money (~> 7.0) + money (7.1.1) + bigdecimal + i18n (~> 1.9) + money-rails (3.0.0) + activesupport (>= 7.0) + monetize (~> 2.0) + money (~> 7.0) + railties (>= 7.0) msgpack (1.8.4) net-imap (0.6.6) date @@ -489,6 +499,7 @@ DEPENDENCIES json (>= 2.21.2) kamal letter_opener + money-rails (~> 3.0) nokogiri (>= 1.19.4) postmark-rails (~> 0.22) propshaft @@ -597,6 +608,9 @@ CHECKSUMS mini_magick (5.3.1) sha256=29395dfd76badcabb6403ee5aff6f681e867074f8f28ce08d78661e9e4a351c4 mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef minitest (6.0.6) sha256=153ea36d1d987a62942382b61075745042a2b3123b1cd48f4c3675af9cc7d6f1 + monetize (2.0.0) sha256=152980cadff9fd11a95817d56e3d7d69b07998c4506f15d971edf05851f82aba + money (7.1.1) sha256=8ed5cd99dfbc45e82688ea711cb59ea3c396c2608cd88876502b164c90269d4e + money-rails (3.0.0) sha256=f4bd1c8e1c98d221e7bf452eeb52e240f41224e43da2a477a5659a87c0c95bae msgpack (1.8.4) sha256=4411c22d350dd1c20250f7eada3cca2695438c2f769cf0782f0cd065d90a3e7b net-imap (0.6.6) sha256=96aa4ee50df3060203e649efc341f53480b791d49e150f2fdebf68beb141a8df net-pop (0.1.2) sha256=848b4e982013c15b2f0382792268763b748cce91c9e91e36b0f27ed26420dff3 diff --git a/app/models/allocation.rb b/app/models/allocation.rb index e5cc0f8..c449ec6 100644 --- a/app/models/allocation.rb +++ b/app/models/allocation.rb @@ -1,4 +1,6 @@ class Allocation < ApplicationRecord + monetize :amount_cents, allow_nil: true + belongs_to :scenario belongs_to :allocation_category, optional: true diff --git a/app/models/allocation/one_time.rb b/app/models/allocation/one_time.rb index d81eee1..386dafa 100644 --- a/app/models/allocation/one_time.rb +++ b/app/models/allocation/one_time.rb @@ -1,5 +1,5 @@ class Allocation::OneTime < Allocation - validates :amount, + validates :amount_cents, presence: true, numericality: { only_integer: true, greater_than: 0 } validate :within_total_giving_amount @@ -16,7 +16,7 @@ def share_percentage total = scenario.one_time_giving_amount return 0 if total.zero? - (amount.to_i / total.to_f * 100).round + (amount.cents / total.cents.to_f * 100).round end # The most this allocation can be set to while staying within the scenario's @@ -34,21 +34,18 @@ def max_amount(scenario = self.scenario) private def within_total_giving_amount - return if amount.blank? || scenario&.total_giving_amount.blank? + return if amount_cents.blank? || scenario&.total_giving_amount_cents.blank? - remaining = budget_remaining(scenario) - return if remaining.nil? - - if amount > remaining - others = scenario.total_giving_amount - remaining - errors.add(:amount, "would bring one-time giving to #{others + amount}, over the total giving amount of #{scenario.total_giving_amount.to_i}") + others = scenario.one_time_allocations.where.not(id: id).sum(:amount_cents) + if others + amount_cents > scenario.total_giving_amount_cents + errors.add(:amount, "would bring one-time giving to #{(others + amount_cents) / 100.0}, over the total giving amount of #{(scenario.total_giving_amount_cents / 100.0).to_i}") end end def budget_remaining(scenario = self.scenario) return nil if scenario.blank? || scenario.total_giving_amount.blank? - others = scenario.one_time_allocations.where.not(id: id).sum(:amount) - scenario.total_giving_amount - others + others = scenario.one_time_allocations.where.not(id: id).sum(:amount_cents) + (scenario.total_giving_amount_cents - others) / 100 end end diff --git a/app/models/allocation/ongoing.rb b/app/models/allocation/ongoing.rb index 35335bd..c44789a 100644 --- a/app/models/allocation/ongoing.rb +++ b/app/models/allocation/ongoing.rb @@ -7,11 +7,11 @@ class Allocation::Ongoing < Allocation validate :within_ongoing_percentage_total def dollar_amount - (percentage.to_i / 100.0 * scenario.ongoing_giving_amount).round + scenario.ongoing_giving_amount * (percentage.to_i / 100.0) end def perpetuity_annual_amount - (dollar_amount * PERPETUITY_PAYOUT_RATE).round + dollar_amount * PERPETUITY_PAYOUT_RATE end def ongoing? diff --git a/app/models/scenario.rb b/app/models/scenario.rb index 1c69ede..300d8a7 100644 --- a/app/models/scenario.rb +++ b/app/models/scenario.rb @@ -1,6 +1,8 @@ class Scenario < ApplicationRecord include UserSearchable + monetize :total_giving_amount_cents, allow_nil: true + belongs_to :organization belongs_to :user has_many :allocations, dependent: :destroy @@ -33,11 +35,11 @@ def ongoing_percentage_total end def one_time_giving_amount - one_time_allocations.sum { |allocation| allocation.amount.to_i } + one_time_allocations.sum(Money.new(0)) { |allocation| allocation.amount } end def ongoing_giving_amount - total_giving_amount.to_i - one_time_giving_amount + (total_giving_amount || Money.new(0)) - one_time_giving_amount end private diff --git a/app/views/admin/scenarios/index.html.erb b/app/views/admin/scenarios/index.html.erb index 92b7394..38affea 100644 --- a/app/views/admin/scenarios/index.html.erb +++ b/app/views/admin/scenarios/index.html.erb @@ -41,7 +41,7 @@ <%= link_to scenario_path(scenario), data: { turbo_frame: "_top" }, class: "block" do %> - <%= scenario.total_giving_amount.present? ? number_to_currency(scenario.total_giving_amount, precision: 0) : content_tag(:span, "—", class: "text-ink-faint") %> + <%= scenario.total_giving_amount.present? ? number_to_currency(scenario.total_giving_amount) : content_tag(:span, "—", class: "text-ink-faint") %> <% end %> diff --git a/app/views/public/scenarios/show.html.erb b/app/views/public/scenarios/show.html.erb index aa1237a..35b9026 100644 --- a/app/views/public/scenarios/show.html.erb +++ b/app/views/public/scenarios/show.html.erb @@ -6,9 +6,9 @@

<%= @scenario.name %>

Shared by <%= @scenario.user.display_name %>

- <% if @scenario.total_giving_amount.to_i.positive? %> + <% if @scenario.total_giving_amount&.positive? %>

- Total giving <%= number_to_currency(@scenario.total_giving_amount, precision: 0) %> + Total giving <%= number_to_currency(@scenario.total_giving_amount) %>

<% end %>
diff --git a/app/views/scenarios/_allocation.html.erb b/app/views/scenarios/_allocation.html.erb index d3e8082..b09fa82 100644 --- a/app/views/scenarios/_allocation.html.erb +++ b/app/views/scenarios/_allocation.html.erb @@ -4,7 +4,7 @@ <% slider_limit = remaining_ongoing_percentage(scenario, allocation) %>
px-4 py-3 transition hover:shadow-sm"> @@ -25,7 +25,7 @@
<%= allocation.percentage %>% - <%= number_to_currency(allocation.dollar_amount, precision: 0) %> + <%= number_to_currency(allocation.dollar_amount) %>
<% unless allocation.greatest_community_need? %>

- Est. <%= number_to_currency(allocation.perpetuity_annual_amount, precision: 0) %> + Est. <%= number_to_currency(allocation.perpetuity_annual_amount) %> annually in perpetuity (<%= (Allocation::Ongoing::PERPETUITY_PAYOUT_RATE * 100).to_i %>% of principal)

@@ -89,7 +89,7 @@
- <%= number_to_currency(allocation.amount, precision: 0) %> + <%= number_to_currency(allocation.amount) %>
diff --git a/app/views/scenarios/index.html.erb b/app/views/scenarios/index.html.erb index 946b7d3..19c2e58 100644 --- a/app/views/scenarios/index.html.erb +++ b/app/views/scenarios/index.html.erb @@ -19,7 +19,7 @@ <%= link_to scenario.name, scenario_path(scenario), class: "font-serif font-medium text-lg text-ink hover:text-brand" %>

- <%= scenario.total_giving_amount.present? ? number_to_currency(scenario.total_giving_amount, precision: 0) : "No total set" %> + <%= scenario.total_giving_amount.present? ? number_to_currency(scenario.total_giving_amount) : "No total set" %>

<%= link_to "Open", scenario_path(scenario), class: "text-brand hover:underline" %> diff --git a/app/views/scenarios/total_giving_amounts/_form.html.erb b/app/views/scenarios/total_giving_amounts/_form.html.erb index 103723a..51fe458 100644 --- a/app/views/scenarios/total_giving_amounts/_form.html.erb +++ b/app/views/scenarios/total_giving_amounts/_form.html.erb @@ -6,9 +6,8 @@ <%= form.label :total_giving_amount, "Total giving amount", class: "w-40 text-ink-soft" %>
$ - <%= form.number_field :total_giving_amount, step: "1", min: 0, autofocus: true, placeholder: "0", - inputmode: "numeric", - data: { controller: "integer-input", action: "input->integer-input#transform" }, + <%= form.number_field :total_giving_amount, value: scenario.total_giving_amount&.to_i, step: "1", min: 0, autofocus: true, placeholder: "0", + inputmode: "decimal", class: "block w-full rounded-md border border-line bg-surface-soft pl-7 pr-3 py-2 shadow-sm focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/10" %>
diff --git a/app/views/scenarios/total_giving_amounts/_total_giving_amount.html.erb b/app/views/scenarios/total_giving_amounts/_total_giving_amount.html.erb index 6f59843..5463890 100644 --- a/app/views/scenarios/total_giving_amounts/_total_giving_amount.html.erb +++ b/app/views/scenarios/total_giving_amounts/_total_giving_amount.html.erb @@ -5,7 +5,7 @@ Total giving amount <% if scenario.total_giving_amount.present? %> <%= link_to edit_scenario_total_giving_amount_path(scenario), class: "group inline-flex items-center gap-2" do %> - <%= number_to_currency(scenario.total_giving_amount, precision: 0) %> + <%= number_to_currency(scenario.total_giving_amount) %>