mudrat_projector 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +2 -0
- data/.ruby-version +1 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +49 -0
- data/README.md +46 -0
- data/Rakefile +26 -0
- data/bin/testrb +3 -0
- data/finance_fu.txt +41 -0
- data/lib/mudrat_projector/account.rb +75 -0
- data/lib/mudrat_projector/amortizer.rb +103 -0
- data/lib/mudrat_projector/banker_rounding.rb +11 -0
- data/lib/mudrat_projector/chart_of_accounts.rb +119 -0
- data/lib/mudrat_projector/date_diff.rb +169 -0
- data/lib/mudrat_projector/projection.rb +45 -0
- data/lib/mudrat_projector/projector.rb +71 -0
- data/lib/mudrat_projector/schedule.rb +45 -0
- data/lib/mudrat_projector/scheduled_transaction.rb +45 -0
- data/lib/mudrat_projector/tax_calculation.rb +154 -0
- data/lib/mudrat_projector/tax_calculator.rb +144 -0
- data/lib/mudrat_projector/tax_values_by_year.yml +102 -0
- data/lib/mudrat_projector/transaction.rb +71 -0
- data/lib/mudrat_projector/transaction_entry.rb +126 -0
- data/lib/mudrat_projector/transaction_handler.rb +19 -0
- data/lib/mudrat_projector/validator.rb +49 -0
- data/lib/mudrat_projector/version.rb +3 -0
- data/lib/mudrat_projector.rb +27 -0
- data/mudrat_projector.gemspec +28 -0
- data/test/integrations/long_term_projection_test.rb +42 -0
- data/test/integrations/mortgage_test.rb +85 -0
- data/test/integrations/self_employed_tax_calculation_test.rb +44 -0
- data/test/models/accounts_test.rb +39 -0
- data/test/models/chart_of_accounts_test.rb +170 -0
- data/test/models/date_diff_test.rb +117 -0
- data/test/models/projection_test.rb +62 -0
- data/test/models/projector_test.rb +168 -0
- data/test/models/schedule_test.rb +68 -0
- data/test/models/scheduled_transaction_test.rb +47 -0
- data/test/models/tax_calculator_test.rb +145 -0
- data/test/models/transaction_entry_test.rb +37 -0
- data/test/models/transaction_handler_test.rb +67 -0
- data/test/models/transaction_test.rb +66 -0
- data/test/models/validator_test.rb +45 -0
- data/test/test_helper.rb +69 -0
- metadata +204 -0
@@ -0,0 +1,45 @@
|
|
1
|
+
module MudratProjector
|
2
|
+
class Projection
|
3
|
+
attr :range
|
4
|
+
|
5
|
+
SequenceEntry = Struct.new :transaction, :batch_id do
|
6
|
+
def sort_key
|
7
|
+
[transaction.date, batch_id]
|
8
|
+
end
|
9
|
+
|
10
|
+
def <=> other_entry
|
11
|
+
sort_key <=> other_entry.sort_key
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize range: date_range, chart: chart_of_accounts
|
16
|
+
@chart = chart
|
17
|
+
@batch_id = 0
|
18
|
+
@range = range
|
19
|
+
@transaction_sequence = []
|
20
|
+
end
|
21
|
+
|
22
|
+
def << transaction
|
23
|
+
@transaction_sequence.push SequenceEntry.new(transaction, @batch_id)
|
24
|
+
end
|
25
|
+
|
26
|
+
def add_transaction_batch batch
|
27
|
+
batch.each do |transaction|
|
28
|
+
self << transaction
|
29
|
+
@batch_id += 1
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def project!
|
34
|
+
freeze
|
35
|
+
transaction_sequence.each do |transaction|
|
36
|
+
@chart.apply_transaction transaction
|
37
|
+
yield transaction if block_given?
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def transaction_sequence
|
42
|
+
@transaction_sequence.tap(&:sort!).map &:transaction
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module MudratProjector
|
2
|
+
class Projector
|
3
|
+
extend Forwardable
|
4
|
+
extend BankerRounding
|
5
|
+
|
6
|
+
AccountDoesNotExist = Class.new StandardError
|
7
|
+
AccountExists = Class.new ArgumentError
|
8
|
+
BalanceError = Class.new StandardError
|
9
|
+
InvalidAccount = Class.new ArgumentError
|
10
|
+
InvalidTransaction = Class.new StandardError
|
11
|
+
|
12
|
+
attr :from
|
13
|
+
|
14
|
+
def initialize params = {}
|
15
|
+
@chart = params.fetch :chart, ChartOfAccounts.new
|
16
|
+
@from = params.fetch :from, ABSOLUTE_START
|
17
|
+
@transactions = []
|
18
|
+
@validator = Validator.new projector: self, chart: @chart
|
19
|
+
end
|
20
|
+
|
21
|
+
def_delegators :@chart, *%i(accounts account_balance apply_transaction balance fetch net_worth split_account)
|
22
|
+
def_delegators :@validator, *%i(must_be_balanced! validate_account! validate_transaction!)
|
23
|
+
|
24
|
+
def accounts= accounts
|
25
|
+
accounts.each do |account_id, params|
|
26
|
+
add_account account_id, params
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def account_exists? account_id
|
31
|
+
@chart.exists? account_id
|
32
|
+
end
|
33
|
+
|
34
|
+
def add_account account_id, **params
|
35
|
+
validate_account! account_id, params
|
36
|
+
@chart.add_account account_id, params
|
37
|
+
end
|
38
|
+
|
39
|
+
def add_transaction params
|
40
|
+
if params.is_a? Transaction
|
41
|
+
transaction = params
|
42
|
+
else
|
43
|
+
klass = params.has_key?(:schedule) ? ScheduledTransaction : Transaction
|
44
|
+
transaction = klass.new params
|
45
|
+
validate_transaction! transaction
|
46
|
+
end
|
47
|
+
@transactions.push transaction
|
48
|
+
end
|
49
|
+
|
50
|
+
def balanced?
|
51
|
+
balance.zero?
|
52
|
+
end
|
53
|
+
|
54
|
+
def project to: end_of_projection, build_next: false, &block
|
55
|
+
must_be_balanced!
|
56
|
+
projection = Projection.new range: (from..to), chart: @chart
|
57
|
+
handler = TransactionHandler.new projection: projection
|
58
|
+
if build_next
|
59
|
+
handler.next_projector = self.class.new from: to + 1, chart: @chart
|
60
|
+
end
|
61
|
+
@transactions.each do |transaction| handler << transaction; end
|
62
|
+
projection.project! &block
|
63
|
+
handler.next_projector
|
64
|
+
end
|
65
|
+
|
66
|
+
def transactions= transactions
|
67
|
+
transactions.each do |transaction| add_transaction transaction; end
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module MudratProjector
|
2
|
+
class Schedule
|
3
|
+
attr :count, :scalar, :unit
|
4
|
+
|
5
|
+
def initialize params = {}
|
6
|
+
@count = params.fetch :count, nil
|
7
|
+
@scalar = params.fetch :scalar
|
8
|
+
@unit = params.fetch :unit
|
9
|
+
end
|
10
|
+
|
11
|
+
def advance_over range
|
12
|
+
split_count_over(range).reduce range.begin do |date, factor|
|
13
|
+
@count -= factor if @count
|
14
|
+
yield [date, factor]
|
15
|
+
DateDiff.advance intervals: factor, unit: unit, from: date
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def finished?
|
20
|
+
(count.nil? || count > 0) ? false : true
|
21
|
+
end
|
22
|
+
|
23
|
+
def split_count_over range
|
24
|
+
diff = DateDiff.date_diff unit: unit, from: range.begin, to: range.end
|
25
|
+
full_units, final_prorate = [@count, diff].compact.min.divmod 1
|
26
|
+
([1] * full_units).tap do |list|
|
27
|
+
list.push final_prorate unless final_prorate.zero?
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def serialize
|
32
|
+
{
|
33
|
+
scalar: scalar,
|
34
|
+
unit: unit,
|
35
|
+
}.tap { |h| h[:count] = count if count }
|
36
|
+
end
|
37
|
+
|
38
|
+
def slice range, &block
|
39
|
+
bits = []
|
40
|
+
advance_over range do |date, factor| bits.push [date, factor]; end
|
41
|
+
leftover = finished? ? nil : serialize
|
42
|
+
[bits, leftover]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module MudratProjector
|
2
|
+
class ScheduledTransaction < Transaction
|
3
|
+
attr :schedule
|
4
|
+
|
5
|
+
def initialize params = {}
|
6
|
+
@schedule = Schedule.new params.fetch :schedule
|
7
|
+
super params
|
8
|
+
end
|
9
|
+
|
10
|
+
def build_transactions intervals
|
11
|
+
intervals.map do |date, prorate|
|
12
|
+
clone_transaction new_date: date, multiplier: prorate
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def clone_transaction new_date: date, new_schedule: nil, multiplier: 1
|
17
|
+
params = build_cloned_tranasction_params new_date, multiplier
|
18
|
+
return Transaction.new(params) if new_schedule.nil?
|
19
|
+
params[:schedule] = new_schedule
|
20
|
+
ScheduledTransaction.new params
|
21
|
+
end
|
22
|
+
|
23
|
+
def multiply entries, by: 1
|
24
|
+
entries.map do |entry| entry * by; end
|
25
|
+
end
|
26
|
+
|
27
|
+
def slice slice_date
|
28
|
+
intervals, next_schedule = schedule.slice(date..slice_date)
|
29
|
+
if next_schedule
|
30
|
+
leftover = clone_transaction new_date: slice_date + 1, new_schedule: next_schedule
|
31
|
+
end
|
32
|
+
[build_transactions(intervals), leftover]
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def build_cloned_tranasction_params new_date, multiplier
|
38
|
+
{
|
39
|
+
date: new_date,
|
40
|
+
debits: multiply(debits, by: multiplier),
|
41
|
+
credits: multiply(credits, by: multiplier),
|
42
|
+
}
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
module MudratProjector
|
2
|
+
class TaxCalculation
|
3
|
+
attr :values_hash
|
4
|
+
attr :w2_gross, :se_gross, :pretax_deductions, :other_gross
|
5
|
+
attr :projector, :household, :taxes_withheld
|
6
|
+
|
7
|
+
def initialize projector, household, values_hash
|
8
|
+
@values_hash = values_hash
|
9
|
+
@projector = projector
|
10
|
+
@w2_gross = 0
|
11
|
+
@se_gross = 0
|
12
|
+
@pretax_deductions = 0
|
13
|
+
@hsa_contribs = Hash.new { |h,k| h[k] = 0 }
|
14
|
+
@household = household
|
15
|
+
@itemized_deduction = 0
|
16
|
+
@taxes_withheld = 0
|
17
|
+
@other_gross = 0
|
18
|
+
extend_year_shim
|
19
|
+
end
|
20
|
+
|
21
|
+
def method_missing method_name, *args
|
22
|
+
if args.empty? && values_hash.has_key?(method_name)
|
23
|
+
values_hash.fetch method_name
|
24
|
+
else
|
25
|
+
super
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def << transaction
|
30
|
+
@w2_gross += transaction.salaries_and_wages
|
31
|
+
@se_gross += transaction.business_profit
|
32
|
+
@other_gross += transaction.dividend_income
|
33
|
+
@itemized_deduction += transaction.charitable_contributions
|
34
|
+
@itemized_deduction += transaction.interest_paid
|
35
|
+
@itemized_deduction += transaction.taxes_paid
|
36
|
+
@taxes_withheld += transaction.taxes_withheld
|
37
|
+
transaction.debits.each do |entry|
|
38
|
+
account = projector.fetch entry.account_id
|
39
|
+
if account.type == :asset && account.tag?(:hsa)
|
40
|
+
revenue = transaction.credits.select { |e| a = projector.fetch(e.account_id); a.type == :revenue && a.tag?(:salary)}
|
41
|
+
@pretax_deductions += revenue.reduce 0 do |s,e| s + e.amount; end
|
42
|
+
@hsa_contribs[account] += [entry.amount - pretax_deductions, 0].max
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def adjustments
|
48
|
+
(self_employment_tax / 2) + hsa_contributions
|
49
|
+
end
|
50
|
+
|
51
|
+
def agi
|
52
|
+
total_income - adjustments
|
53
|
+
end
|
54
|
+
|
55
|
+
def brackets
|
56
|
+
bracket_caps = super.tap do |list|
|
57
|
+
list.push Float::INFINITY
|
58
|
+
end
|
59
|
+
values_hash.fetch(:bracket_rates).zip bracket_caps
|
60
|
+
end
|
61
|
+
|
62
|
+
def deduction
|
63
|
+
[itemized_deduction, standard_deduction].max
|
64
|
+
end
|
65
|
+
|
66
|
+
def effective_rate
|
67
|
+
(((gross - net) / gross) * 100).round 2
|
68
|
+
end
|
69
|
+
|
70
|
+
def exemption
|
71
|
+
personal_exemption * household.exemptions
|
72
|
+
end
|
73
|
+
|
74
|
+
def gross
|
75
|
+
w2_gross + se_gross + other_gross
|
76
|
+
end
|
77
|
+
|
78
|
+
def income_tax
|
79
|
+
amount_taxed = 0
|
80
|
+
brackets.reduce 0 do |sum, (rate, cap)|
|
81
|
+
if cap == Float::INFINITY
|
82
|
+
in_bracket = taxable_income - amount_taxed
|
83
|
+
else
|
84
|
+
in_bracket = [cap, taxable_income].min - amount_taxed
|
85
|
+
end
|
86
|
+
amount_taxed += in_bracket
|
87
|
+
t = (in_bracket * (rate / 100))
|
88
|
+
sum + t
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def hsa_contributions
|
93
|
+
@hsa_contribs.reduce 0 do |sum, (account, amount)|
|
94
|
+
type = [:individual, :family, :senior].detect { |tag| account.tag? tag }
|
95
|
+
max = hsa_limit.fetch type
|
96
|
+
sum + [amount, max].min
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def itemized_deduction
|
101
|
+
@itemized_deduction
|
102
|
+
end
|
103
|
+
|
104
|
+
def medicare_withheld
|
105
|
+
w2_gross * (hi_rate / 100)
|
106
|
+
end
|
107
|
+
|
108
|
+
def net
|
109
|
+
gross - taxes
|
110
|
+
end
|
111
|
+
|
112
|
+
def self_employment_tax
|
113
|
+
rate = (oasdi_rate + hi_rate) / 100
|
114
|
+
se_gross * (1 - rate) * (rate * 2)
|
115
|
+
end
|
116
|
+
|
117
|
+
def social_security_withheld
|
118
|
+
[w2_gross, oasdi_wage_base].min * (oasdi_rate / 100)
|
119
|
+
end
|
120
|
+
|
121
|
+
def taxable_income
|
122
|
+
agi - deduction - exemption
|
123
|
+
end
|
124
|
+
|
125
|
+
def taxes
|
126
|
+
withholding_tax + income_tax + self_employment_tax
|
127
|
+
end
|
128
|
+
|
129
|
+
def taxes_owed
|
130
|
+
taxes - taxes_withheld
|
131
|
+
end
|
132
|
+
|
133
|
+
def total_income
|
134
|
+
gross - pretax_deductions
|
135
|
+
end
|
136
|
+
|
137
|
+
def year
|
138
|
+
projector.from.year
|
139
|
+
end
|
140
|
+
|
141
|
+
def withholding_tax
|
142
|
+
medicare_withheld + social_security_withheld
|
143
|
+
end
|
144
|
+
|
145
|
+
private
|
146
|
+
|
147
|
+
def extend_year_shim
|
148
|
+
shim_module = "Year#{year}".to_sym
|
149
|
+
if TaxCalculator.const_defined? shim_module
|
150
|
+
extend TaxCalculator.const_get shim_module
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
module MudratProjector
|
2
|
+
class TaxCalculator
|
3
|
+
module Year2012
|
4
|
+
# see http://en.wikipedia.org/wiki/Tax_Relief,_Unemployment_Insurance_Reauthorization,_and_Job_Creation_Act_of_2010
|
5
|
+
def social_security_withheld
|
6
|
+
super * (4.2.to_d / 6.2.to_d)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
attr :household, :projector
|
11
|
+
|
12
|
+
HOUSEHOLD_TYPES = %i(married_filing_jointly married_filing_separately single
|
13
|
+
head_of_household)
|
14
|
+
EXPENSE_ACCOUNT_ID = :united_states_treasury
|
15
|
+
|
16
|
+
Household = Struct.new :filing_status, :exemptions do
|
17
|
+
def initialize *args
|
18
|
+
super
|
19
|
+
unless HOUSEHOLD_TYPES.include? filing_status
|
20
|
+
raise "Invalid filing status #{filing_status.inspect}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.project projector, to: end_date, household: nil
|
26
|
+
tax_calculator = new projector: projector, household: household
|
27
|
+
(projector.from.year..to.year).each do
|
28
|
+
calculation = tax_calculator.calculate!
|
29
|
+
yield calculation if block_given?
|
30
|
+
end
|
31
|
+
tax_calculator.projector
|
32
|
+
end
|
33
|
+
|
34
|
+
class TransactionWrapper
|
35
|
+
INCOME_VALUES = %i(business_profit dividend_income salaries_and_wages)
|
36
|
+
ADJUSTMENTS_VALUES = %i(other_adjustments)
|
37
|
+
DEDUCTIONS_VALUES = %i(charitable_contributions interest_paid taxes_paid)
|
38
|
+
CREDITS_VALUES = %i()
|
39
|
+
|
40
|
+
VALUES = [INCOME_VALUES, ADJUSTMENTS_VALUES, DEDUCTIONS_VALUES, CREDITS_VALUES].flatten 1
|
41
|
+
|
42
|
+
attr :calculator, :taxes_withheld
|
43
|
+
private :calculator
|
44
|
+
|
45
|
+
def initialize calculator, transaction
|
46
|
+
@calculator = calculator
|
47
|
+
@taxes_withheld = 0
|
48
|
+
@transaction = transaction
|
49
|
+
VALUES.each do |attr_name| instance_variable_set "@#{attr_name}", 0; end
|
50
|
+
end
|
51
|
+
|
52
|
+
VALUES.each do |attr_name| attr attr_name; end
|
53
|
+
|
54
|
+
def calculate!
|
55
|
+
@transaction.entries.each do |entry|
|
56
|
+
account = calculator.projector.fetch entry.account_id
|
57
|
+
if account.type == :revenue
|
58
|
+
@salaries_and_wages += entry.delta if account.tag? :salary
|
59
|
+
@business_profit += entry.delta if account.tag? :self_employed
|
60
|
+
@dividend_income += entry.delta if account.tag? :dividend
|
61
|
+
elsif account.type == :expense
|
62
|
+
@business_profit -= entry.delta if account.tag? :self_employed
|
63
|
+
@charitable_contributions +=
|
64
|
+
entry.delta if account.tag? "501c".to_sym
|
65
|
+
@interest_paid += entry.delta if account.tag? :mortgage_interest
|
66
|
+
@taxes_paid += entry.delta if account.tag? :tax
|
67
|
+
@taxes_withheld += entry.delta if entry.account_id == EXPENSE_ACCOUNT_ID
|
68
|
+
elsif account.type == :asset
|
69
|
+
@other_adjustments += entry.delta if account.tag?(:hsa) && entry.debit?
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def debits
|
75
|
+
@transaction.debits
|
76
|
+
end
|
77
|
+
|
78
|
+
def credits
|
79
|
+
@transaction.credits
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def initialize projector: nil, household: {}
|
84
|
+
@household = Household.new(
|
85
|
+
household.fetch(:filing_status),
|
86
|
+
household.fetch(:exemptions),
|
87
|
+
)
|
88
|
+
@projector = projector
|
89
|
+
@values_hash = parse_yaml
|
90
|
+
unless projector.account_exists? EXPENSE_ACCOUNT_ID
|
91
|
+
projector.add_account EXPENSE_ACCOUNT_ID, type: :expense
|
92
|
+
end
|
93
|
+
unless projector.account_exists? :owed_taxes
|
94
|
+
projector.add_account :owed_taxes, type: :liability
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def calculate!
|
99
|
+
end_of_calendar_year = Date.new year, 12, 31
|
100
|
+
calculation = TaxCalculation.new projector, household, @values_hash
|
101
|
+
next_projector = projector.project to: end_of_calendar_year, build_next: true do |transaction|
|
102
|
+
calculation << TransactionWrapper.new(self, transaction).tap(&:calculate!)
|
103
|
+
end
|
104
|
+
final_transaction = Transaction.new(
|
105
|
+
date: end_of_calendar_year,
|
106
|
+
debit: { amount: calculation.taxes_owed, account_id: EXPENSE_ACCOUNT_ID },
|
107
|
+
credit: { amount: calculation.taxes_owed, account_id: :owed_taxes },
|
108
|
+
)
|
109
|
+
projector.apply_transaction final_transaction
|
110
|
+
@projector = next_projector
|
111
|
+
@values_hash = parse_yaml
|
112
|
+
calculation
|
113
|
+
end
|
114
|
+
|
115
|
+
def year
|
116
|
+
projector.from.year
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
def parse_yaml
|
122
|
+
yaml = File.expand_path '../tax_values_by_year.yml', __FILE__
|
123
|
+
by_year = YAML.load File.read(yaml)
|
124
|
+
max_year = by_year.keys.max
|
125
|
+
parsed = by_year.fetch([year, max_year].min).tap do |hash|
|
126
|
+
recursively_symbolize_keys! hash
|
127
|
+
end
|
128
|
+
hash = parsed.fetch household.filing_status
|
129
|
+
parsed.each_with_object hash do |(key, value), hash|
|
130
|
+
hash[key] = value unless HOUSEHOLD_TYPES.include? key
|
131
|
+
end
|
132
|
+
hash
|
133
|
+
end
|
134
|
+
|
135
|
+
def recursively_symbolize_keys! hash
|
136
|
+
hash.keys.each do |key|
|
137
|
+
value = hash.delete key
|
138
|
+
recursively_symbolize_keys! value if value.is_a? Hash
|
139
|
+
key = key.respond_to?(:to_sym) ? key.to_sym : key
|
140
|
+
hash[key] = value
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
2012:
|
2
|
+
oasdi_rate: 6.2
|
3
|
+
oasdi_wage_base: 110_100
|
4
|
+
hi_rate: 1.45
|
5
|
+
personal_exemption: 3_800
|
6
|
+
hsa_limit:
|
7
|
+
individual: 3_100
|
8
|
+
family: 6_250
|
9
|
+
senior: 1000
|
10
|
+
bracket_rates:
|
11
|
+
- 10.0
|
12
|
+
- 15.0
|
13
|
+
- 25.0
|
14
|
+
- 28.0
|
15
|
+
- 33.0
|
16
|
+
- 35.0
|
17
|
+
married_filing_jointly:
|
18
|
+
standard_deduction: 11_900
|
19
|
+
brackets:
|
20
|
+
- 17_400
|
21
|
+
- 70_700
|
22
|
+
- 142_700
|
23
|
+
- 217_450
|
24
|
+
- 388_350
|
25
|
+
married_filing_separately:
|
26
|
+
standard_deduction: 5_950
|
27
|
+
brackets:
|
28
|
+
- 8_700
|
29
|
+
- 35_350
|
30
|
+
- 71_350
|
31
|
+
- 108_725
|
32
|
+
- 194_175
|
33
|
+
single:
|
34
|
+
standard_deduction: 5_950
|
35
|
+
brackets:
|
36
|
+
- 8_700
|
37
|
+
- 35_350
|
38
|
+
- 85_650
|
39
|
+
- 178_650
|
40
|
+
- 388_350
|
41
|
+
head_of_household:
|
42
|
+
standard_deduction: 8_700
|
43
|
+
brackets:
|
44
|
+
- 12_400
|
45
|
+
- 47_350
|
46
|
+
- 122_300
|
47
|
+
- 190_050
|
48
|
+
- 388_350
|
49
|
+
|
50
|
+
2013:
|
51
|
+
oasdi_rate: 6.2
|
52
|
+
oasdi_wage_base: 113_700
|
53
|
+
hi_rate: 1.45
|
54
|
+
personal_exemption: 3_900
|
55
|
+
hsa_limit:
|
56
|
+
individual: 3_250
|
57
|
+
family: 6_450
|
58
|
+
senior: 1000
|
59
|
+
bracket_rates:
|
60
|
+
- 10.0
|
61
|
+
- 15.0
|
62
|
+
- 25.0
|
63
|
+
- 28.0
|
64
|
+
- 33.0
|
65
|
+
- 35.0
|
66
|
+
- 39.6
|
67
|
+
married_filing_jointly:
|
68
|
+
standard_deduction: 12_200
|
69
|
+
brackets:
|
70
|
+
- 17_850
|
71
|
+
- 72_500
|
72
|
+
- 146_400
|
73
|
+
- 223_050
|
74
|
+
- 398_350
|
75
|
+
- 450_000
|
76
|
+
married_filing_separately:
|
77
|
+
standard_deduction: 6_100
|
78
|
+
brackets:
|
79
|
+
- 8_925
|
80
|
+
- 36_250
|
81
|
+
- 73_200
|
82
|
+
- 111_525
|
83
|
+
- 199_175
|
84
|
+
- 225_000
|
85
|
+
single:
|
86
|
+
standard_deduction: 6_100
|
87
|
+
brackets:
|
88
|
+
- 8_925
|
89
|
+
- 36_250
|
90
|
+
- 87_850
|
91
|
+
- 183_250
|
92
|
+
- 398_350
|
93
|
+
- 400_000
|
94
|
+
head_of_household:
|
95
|
+
standard_deduction: 8_950
|
96
|
+
brackets:
|
97
|
+
- 12_750
|
98
|
+
- 48_600
|
99
|
+
- 125_450
|
100
|
+
- 203_150
|
101
|
+
- 398_350
|
102
|
+
- 425_000
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module MudratProjector
|
2
|
+
class Transaction
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
attr :credits, :date, :debits
|
6
|
+
|
7
|
+
def initialize params = {}
|
8
|
+
@date = params.fetch :date
|
9
|
+
self.credits = extract_entry_params :credit, params
|
10
|
+
self.debits = extract_entry_params :debit, params
|
11
|
+
end
|
12
|
+
|
13
|
+
def balanced?
|
14
|
+
sum_credits = build_set_for_balance credits
|
15
|
+
sum_debits = build_set_for_balance debits
|
16
|
+
(sum_credits ^ sum_debits).empty?
|
17
|
+
end
|
18
|
+
|
19
|
+
def credits= credits
|
20
|
+
@credits = build_entries :credit, credits
|
21
|
+
end
|
22
|
+
|
23
|
+
def debits= debits
|
24
|
+
@debits = build_entries :debit, debits
|
25
|
+
end
|
26
|
+
|
27
|
+
def each &block
|
28
|
+
credits.each &block
|
29
|
+
debits.each &block
|
30
|
+
end
|
31
|
+
|
32
|
+
def slice slice_date
|
33
|
+
if date > slice_date
|
34
|
+
[[], self]
|
35
|
+
else
|
36
|
+
[[self], nil]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def extract_entry_params credit_or_debit, params
|
43
|
+
entries = Array params["#{credit_or_debit}s".to_sym]
|
44
|
+
return entries unless params.has_key? credit_or_debit
|
45
|
+
unless entries.empty?
|
46
|
+
raise ArgumentError, "You cannot supply both #{credit_or_debit} and "\
|
47
|
+
"#{credit_or_debit}s"
|
48
|
+
end
|
49
|
+
[params.fetch(credit_or_debit)]
|
50
|
+
end
|
51
|
+
|
52
|
+
def build_entries credit_or_debit, entries
|
53
|
+
entries.map do |entry_params|
|
54
|
+
if entry_params.is_a? TransactionEntry
|
55
|
+
entry_params
|
56
|
+
else
|
57
|
+
TransactionEntry.public_send "new_#{credit_or_debit}", entry_params
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def build_set_for_balance entries
|
63
|
+
hash = Hash.new { |h,k| h[k] = 0 }
|
64
|
+
entries.each do |entry|
|
65
|
+
balance_key = entry.class == TransactionEntry ? :fixed : entry.other_account_id
|
66
|
+
hash[balance_key] += entry.scalar
|
67
|
+
end
|
68
|
+
hash.reduce Set.new do |set, (_, value)| set << value; end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|