diff --git a/app/controllers/concerns/dashboard_builder.rb b/app/controllers/concerns/dashboard_builder.rb index eaa0644..6b7358a 100644 --- a/app/controllers/concerns/dashboard_builder.rb +++ b/app/controllers/concerns/dashboard_builder.rb @@ -12,8 +12,8 @@ def build_dashboard_variables! @fixed_expenses = FixedExpense.get_ordered @savings_rate = SavingsRate.savings @investing_rate = SavingsRate.investing - build_taxed_income_vars! - build_savings_vars! + build_income_tax_variables! + build_savings_vars!(salary_income: @salary_taxed.net_income, hourly_income: @hourly_taxed.net_income) build_guilt_free_vars! build_total_cost_vars! end diff --git a/app/controllers/concerns/guilt_free.rb b/app/controllers/concerns/guilt_free.rb index 8b2432d..b632a5b 100644 --- a/app/controllers/concerns/guilt_free.rb +++ b/app/controllers/concerns/guilt_free.rb @@ -2,8 +2,8 @@ module GuiltFree def build_guilt_free_vars! - @guilt_free_salary = GuiltFreeCalculator.new(@salary_taxed.total_net_income, salary_savings_totalizer) - @guilt_free_hourly = GuiltFreeCalculator.new(@hourly_taxed.total_net_income, hourly_savings_totalizer) + @guilt_free_salary = GuiltFreeCalculator.new(@salary_taxed.net_income, salary_savings_totalizer) + @guilt_free_hourly = GuiltFreeCalculator.new(@hourly_taxed.net_income, hourly_savings_totalizer) end def salary_savings_totalizer diff --git a/app/controllers/concerns/save_income.rb b/app/controllers/concerns/save_income.rb index 4e0ffb7..a1e69b1 100644 --- a/app/controllers/concerns/save_income.rb +++ b/app/controllers/concerns/save_income.rb @@ -2,38 +2,29 @@ module SaveIncome extend ActiveSupport::Concern - include TaxedIncome - def build_savings_vars! - @hourly_saving = hourly_saving - @hourly_invest = hourly_investing - @salary_saving = salary_saving - @salary_invest = salary_investing + def build_savings_vars!(salary_income:, hourly_income:) + @salary_saving = savings(income: salary_income) + @salary_invest = investing(income: salary_income) + @hourly_saving = savings(income: hourly_income) + @hourly_invest = investing(income: hourly_income) end - def salary_investing - SavingsCalculator.new(tax_on_salary, set_invest_rate) - end - - def salary_saving - SavingsCalculator.new(tax_on_salary, set_save_rate) - end + private - def hourly_investing - SavingsCalculator.new(tax_on_hourly, set_invest_rate) + def savings(income:) + SavingsCalculator.new(income, savings_rate) end - def hourly_saving - SavingsCalculator.new(tax_on_hourly, set_save_rate) + def investing(income:) + SavingsCalculator.new(income, investing_rate) end - private - - def set_save_rate - SavingsRate.savings.rate + def savings_rate + @savings_rate ||= SavingsRate.savings_rate end - def set_invest_rate - SavingsRate.investing.rate + def investing_rate + @investing_rate ||= SavingsRate.investing_rate end end diff --git a/app/controllers/concerns/taxed_income.rb b/app/controllers/concerns/taxed_income.rb index 09ff25c..a1e59cc 100644 --- a/app/controllers/concerns/taxed_income.rb +++ b/app/controllers/concerns/taxed_income.rb @@ -1,18 +1,28 @@ # frozen_string_literal: true module TaxedIncome - def tax_on_salary - income = Income.find_by(income_type: "Salary") - IncomeTaxCalculatorService.new(income: income) + extend ActiveSupport::Concern + + def build_income_tax_variables! + @salary_taxed = build_income_tax_object(income: salary_income) + @hourly_taxed = build_income_tax_object(income: hourly_income) + end + + private + + def salary_income + @salary_income = Income.find_by(income_type: "Salary").weekly_income * 52 end - def tax_on_hourly - income = Income.find_by(income_type: "Hourly") - IncomeTaxCalculatorService.new(income: income) + def hourly_income + @hourly_income = Income.find_by(income_type: "Hourly").weekly_income * 52 end - def build_taxed_income_vars! - @salary_taxed = tax_on_salary - @hourly_taxed = tax_on_hourly + def build_income_tax_object(income:) + federal_tax = FederalTaxCalculator.call(income: income) + fica_tax = FicaTaxCalculator.call(income: income) + state_tax = StateTaxCalculator.call(income: income) + net_income = income - (fica_tax + federal_tax + state_tax) + OpenStruct.new(federal_tax: federal_tax, fica_tax: fica_tax, state_tax: state_tax, net_income: net_income) end end diff --git a/app/services/federal_tax_calculator.rb b/app/services/federal_tax_calculator.rb new file mode 100644 index 0000000..0ff5c4e --- /dev/null +++ b/app/services/federal_tax_calculator.rb @@ -0,0 +1,27 @@ +class FederalTaxCalculator + include Callable + + def initialize(income:) + self.income = income + end + + def call + calculate + end + + private + + attr_accessor :income + + def calculate + bracket = FederalTaxBracket.where("bottom_range_cents <= ?", taxable_income.fractional).order(:bottom_range_cents).last + taxable_at_bracket_rate = Money.new(taxable_income - bracket.bottom_range) + rated = bracket.rate * taxable_at_bracket_rate + rated + bracket.cumulative + end + + def taxable_income + # 2024 standard deduction = 13,850 + @taxable_income ||= income - Money.new(13_850_00) + end +end diff --git a/app/services/fica_tax_calculator.rb b/app/services/fica_tax_calculator.rb new file mode 100644 index 0000000..abe190f --- /dev/null +++ b/app/services/fica_tax_calculator.rb @@ -0,0 +1,15 @@ +class FicaTaxCalculator + include Callable + + def initialize(income:) + self.income = income + end + + def call + income * 0.0765 + end + + private + + attr_accessor :income +end diff --git a/app/services/net_income_calculator.rb b/app/services/net_income_calculator.rb new file mode 100644 index 0000000..c7a845c --- /dev/null +++ b/app/services/net_income_calculator.rb @@ -0,0 +1,32 @@ +class NetIncomeCalculator + attr_reader :annual_income, :daily_income, :weekly_income, :monthly_income, :quarterly_income, :biannual_income + + def initialize(annual_income:) + @annual_income = annual_income + @biannual_income = calculate_biannual_income + @quarterly_income = calculate_quarterly_income + @monthly_income = calculate_monthly_income + @weekly_income = calculate_weekly_income + @daily_income = calculate_daily_income + end + + def calculate_biannual_income + @annual_income / 2 + end + + def calculate_quarterly_income + @annual_income / 4 + end + + def calculate_monthly_income + @annual_income / 12 + end + + def calculate_weekly_income + @annual_income / 52 + end + + def calculate_daily_income + @annual_income / 365 + end +end diff --git a/app/services/savings_calculator.rb b/app/services/savings_calculator.rb index 7cfe974..9a943f6 100644 --- a/app/services/savings_calculator.rb +++ b/app/services/savings_calculator.rb @@ -11,8 +11,8 @@ class SavingsCalculator :daily_saving def initialize(income_type, saving_rate) - @annual_income = income_type.annual_income - @saving_rate = saving_rate + @annual_income = income_type + @saving_rate = saving_rate.rate @annual_saving = calculate_savings @bi_weekly_saving = calculate_bi_weekly_saving @daily_saving = calculate_daily_saving diff --git a/app/services/state_tax_calculator.rb b/app/services/state_tax_calculator.rb new file mode 100644 index 0000000..d50a3b6 --- /dev/null +++ b/app/services/state_tax_calculator.rb @@ -0,0 +1,16 @@ +class StateTaxCalculator + include Callable + + def initialize(income:) + self.income = income + end + + # Colorado state tax rate is 4.4% + def call + income * 0.044 + end + + private + + attr_accessor :income +end diff --git a/app/views/shared/_taxed_income.html.erb b/app/views/shared/_taxed_income.html.erb index 97bf581..da19282 100644 --- a/app/views/shared/_taxed_income.html.erb +++ b/app/views/shared/_taxed_income.html.erb @@ -1,4 +1,4 @@ -<% paid_taxes = [ taxed_income.federal_income_tax, taxed_income.fica_tax, taxed_income.state_tax, taxed_income.total_net_income ] %> +<% paid_taxes = [ taxed_income.federal_tax, taxed_income.fica_tax, taxed_income.state_tax, taxed_income.net_income ] %> <% paid_taxes.each do |tax| %> <% if annual %> @@ -6,4 +6,4 @@ <% else %>
<%= humanized_money_with_symbol(tax / 26) %>
<% end %> -<% end %> \ No newline at end of file +<% end %> diff --git a/config/application.rb b/config/application.rb index f800e9e..cf4b42d 100644 --- a/config/application.rb +++ b/config/application.rb @@ -23,6 +23,10 @@ class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. config.load_defaults 7.0 + # Load in all modules + config.autoload_paths << Rails.root.join("lib", "modules") + config.eager_load_paths << Rails.root.join("lib", "modules") + # Configuration for the application, engines, and railties goes here. # # These settings can be overridden in specific environments using the files diff --git a/lib/modules/callable.rb b/lib/modules/callable.rb new file mode 100644 index 0000000..d1c0740 --- /dev/null +++ b/lib/modules/callable.rb @@ -0,0 +1,9 @@ +module Callable + extend ActiveSupport::Concern + + class_methods do + def call(**args) + new(**args).call + end + end +end diff --git a/spec/factories/federal_tax_brackets.rb b/spec/factories/federal_tax_brackets.rb index 1b0433d..991b9fe 100644 --- a/spec/factories/federal_tax_brackets.rb +++ b/spec/factories/federal_tax_brackets.rb @@ -15,27 +15,29 @@ # updated_at :datetime not null # FactoryBot.define do + # tax_brackets = 10% on first $1,000, 15% from $1,001 to $100,000, 25% from $100,001 to $500,000 + factory :federal_tax_bracket do tier { "Tier 1" } bottom_range_cents { 0 } - top_range_cents { 100_000 } + top_range_cents { 100_000 } # $1,000.00 rate { 0.1 } cumulative_cents { 0 } trait :tier_2 do tier { "Tier 2" } - bottom_range_cents { 1_000_100 } - top_range_cents { 10_000_000 } + bottom_range_cents { 100_100 } # $1,001.00 + top_range_cents { 10_000_000 } # $100,000.00 rate { 0.15 } - cumulative_cents { 200_000 } + cumulative_cents { 10_000 } # $100.00 end trait :tier_3 do tier { "Tier 2" } - bottom_range_cents { 10_000_100 } - top_range_cents { 50_000_000 } + bottom_range_cents { 10_000_100 } # $100,001.00 + top_range_cents { 50_000_000 } # $500,000.00 rate { 0.25 } - cumulative_cents { 500_000 } + cumulative_cents { 1_485_000 } # $14,850.00 end trait :with_all_tiers do diff --git a/spec/factories/incomes.rb b/spec/factories/incomes.rb index 7c697cd..7d6a877 100644 --- a/spec/factories/incomes.rb +++ b/spec/factories/incomes.rb @@ -26,11 +26,11 @@ weekly_income_cents { 300_000 * 40 } end - # trait :with_all_types do - # after :create do |_record| - # create(:income, :hourly) - # end - # end + trait :with_all_types do + after :create do |_record| + create(:income, :hourly) + end + end # to_create do |instance| # instance.id = Income.find_or_create_by(income_type: instance.income_type).id diff --git a/spec/services/federal_tax_calculator_spec.rb b/spec/services/federal_tax_calculator_spec.rb new file mode 100644 index 0000000..334cd24 --- /dev/null +++ b/spec/services/federal_tax_calculator_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require("rails_helper") + +RSpec.describe FederalTaxCalculator, type: :service do + subject(:service) do + described_class.call( + income: salary_income.rate + ) + end + + let!(:salary_income) { create(:income) } + let!(:tax_brackets) { create(:federal_tax_bracket, :with_all_tiers) } + + it { expect(service).to be_a Money } + + it "calculates federal tax" do + # salary_income = $50,000 + # standard_deduction = $13,850 + # tax_brackets = 10% on first $1,000, 15% from $1,001 to $100,000, 25% from $100,001 to $500,000 + + expect(service.format).to eq("$5,372.35") + expect((salary_income.rate - service).format).to eq("$44,627.65") + end +end diff --git a/spec/services/fica_tax_calculator_spec.rb b/spec/services/fica_tax_calculator_spec.rb new file mode 100644 index 0000000..4afba86 --- /dev/null +++ b/spec/services/fica_tax_calculator_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require("rails_helper") + +RSpec.describe FicaTaxCalculator, type: :service do + subject(:service) do + described_class.call( + income: salary_income.rate + ) + end + + let!(:salary_income) { create(:income) } + let!(:tax_brackets) { create(:federal_tax_bracket, :with_all_tiers) } + + it { expect(service).to be_a Money } + + it "calculates FICA tax" do + # salary_income = $50,000 + + expect(service.format).to eq("$3,825.00") + expect((salary_income.rate - service).format).to eq("$46,175.00") + end +end diff --git a/spec/services/net_income_calculator_spec.rb b/spec/services/net_income_calculator_spec.rb new file mode 100644 index 0000000..776439b --- /dev/null +++ b/spec/services/net_income_calculator_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require("rails_helper") + +RSpec.describe NetIncomeCalculator, type: :service do + subject(:service) do + described_class.new( + annual_income: salary_income.rate + ) + end + + let!(:salary_income) { create(:income) } + let!(:tax_brackets) { create(:federal_tax_bracket, :with_all_tiers) } + + it { expect(service).to be_a NetIncomeCalculator } + + it { expect(service.annual_income.format).to eq("$50,000.00") } + it { expect(service.biannual_income.format).to eq("$25,000.00") } + it { expect(service.quarterly_income.format).to eq("$12,500.00") } + it { expect(service.monthly_income.format).to eq("$4,166.67") } + it { expect(service.weekly_income.format).to eq("$961.54") } + it { expect(service.daily_income.format).to eq("$136.99") } +end diff --git a/spec/services/state_tax_calculator_spec.rb b/spec/services/state_tax_calculator_spec.rb new file mode 100644 index 0000000..f9be4f7 --- /dev/null +++ b/spec/services/state_tax_calculator_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require("rails_helper") + +RSpec.describe StateTaxCalculator, type: :service do + subject(:service) do + described_class.call( + income: salary_income.rate + ) + end + + let!(:salary_income) { create(:income) } + let!(:tax_brackets) { create(:federal_tax_bracket, :with_all_tiers) } + + it { expect(service).to be_a Money } + + it "calculates state tax" do + # salary_income = $50,000 + + expect(service.format).to eq("$2,200.00") + expect((salary_income.rate - service).format).to eq("$47,800.00") + end +end