Skip to content

Commit

Permalink
Refactor income tax calculator. (#43)
Browse files Browse the repository at this point in the history
* Add callable gem.

* Break up income tax calculator into multiple, smaller services.

* Refactor incomes methods in concern.

* Uninstall callable gem, add callable module, configure to be used in application.

* Refactor downstream.

* Lint.

* Add test for federal tax calculator.

* Add test for fica and state tax calculators.

* Lint.

* Specs for net income calculator

* Refactor with use of new services.

* Remove commented out code.

* Fix service.

* Fix net income service.

* Add standard deduction to federal tax calculator.
  • Loading branch information
neb417 authored Jul 15, 2024
1 parent 930c193 commit c4ee2dc
Show file tree
Hide file tree
Showing 18 changed files with 252 additions and 52 deletions.
4 changes: 2 additions & 2 deletions app/controllers/concerns/dashboard_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions app/controllers/concerns/guilt_free.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 14 additions & 23 deletions app/controllers/concerns/save_income.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
28 changes: 19 additions & 9 deletions app/controllers/concerns/taxed_income.rb
Original file line number Diff line number Diff line change
@@ -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
27 changes: 27 additions & 0 deletions app/services/federal_tax_calculator.rb
Original file line number Diff line number Diff line change
@@ -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
15 changes: 15 additions & 0 deletions app/services/fica_tax_calculator.rb
Original file line number Diff line number Diff line change
@@ -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
32 changes: 32 additions & 0 deletions app/services/net_income_calculator.rb
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions app/services/savings_calculator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions app/services/state_tax_calculator.rb
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions app/views/shared/_taxed_income.html.erb
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<% 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 %>
<div class="px-5"><%= humanized_money_with_symbol(tax) %></div>
<% else %>
<div class="px-5"><%= humanized_money_with_symbol(tax / 26) %></div>
<% end %>
<% end %>
<% end %>
4 changes: 4 additions & 0 deletions config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions lib/modules/callable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module Callable
extend ActiveSupport::Concern

class_methods do
def call(**args)
new(**args).call
end
end
end
16 changes: 9 additions & 7 deletions spec/factories/federal_tax_brackets.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions spec/factories/incomes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions spec/services/federal_tax_calculator_spec.rb
Original file line number Diff line number Diff line change
@@ -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
23 changes: 23 additions & 0 deletions spec/services/fica_tax_calculator_spec.rb
Original file line number Diff line number Diff line change
@@ -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
23 changes: 23 additions & 0 deletions spec/services/net_income_calculator_spec.rb
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit c4ee2dc

Please sign in to comment.