mudrat_projector 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/.ruby-version +1 -0
  4. data/.travis.yml +3 -0
  5. data/Gemfile +4 -0
  6. data/Gemfile.lock +49 -0
  7. data/README.md +46 -0
  8. data/Rakefile +26 -0
  9. data/bin/testrb +3 -0
  10. data/finance_fu.txt +41 -0
  11. data/lib/mudrat_projector/account.rb +75 -0
  12. data/lib/mudrat_projector/amortizer.rb +103 -0
  13. data/lib/mudrat_projector/banker_rounding.rb +11 -0
  14. data/lib/mudrat_projector/chart_of_accounts.rb +119 -0
  15. data/lib/mudrat_projector/date_diff.rb +169 -0
  16. data/lib/mudrat_projector/projection.rb +45 -0
  17. data/lib/mudrat_projector/projector.rb +71 -0
  18. data/lib/mudrat_projector/schedule.rb +45 -0
  19. data/lib/mudrat_projector/scheduled_transaction.rb +45 -0
  20. data/lib/mudrat_projector/tax_calculation.rb +154 -0
  21. data/lib/mudrat_projector/tax_calculator.rb +144 -0
  22. data/lib/mudrat_projector/tax_values_by_year.yml +102 -0
  23. data/lib/mudrat_projector/transaction.rb +71 -0
  24. data/lib/mudrat_projector/transaction_entry.rb +126 -0
  25. data/lib/mudrat_projector/transaction_handler.rb +19 -0
  26. data/lib/mudrat_projector/validator.rb +49 -0
  27. data/lib/mudrat_projector/version.rb +3 -0
  28. data/lib/mudrat_projector.rb +27 -0
  29. data/mudrat_projector.gemspec +28 -0
  30. data/test/integrations/long_term_projection_test.rb +42 -0
  31. data/test/integrations/mortgage_test.rb +85 -0
  32. data/test/integrations/self_employed_tax_calculation_test.rb +44 -0
  33. data/test/models/accounts_test.rb +39 -0
  34. data/test/models/chart_of_accounts_test.rb +170 -0
  35. data/test/models/date_diff_test.rb +117 -0
  36. data/test/models/projection_test.rb +62 -0
  37. data/test/models/projector_test.rb +168 -0
  38. data/test/models/schedule_test.rb +68 -0
  39. data/test/models/scheduled_transaction_test.rb +47 -0
  40. data/test/models/tax_calculator_test.rb +145 -0
  41. data/test/models/transaction_entry_test.rb +37 -0
  42. data/test/models/transaction_handler_test.rb +67 -0
  43. data/test/models/transaction_test.rb +66 -0
  44. data/test/models/validator_test.rb +45 -0
  45. data/test/test_helper.rb +69 -0
  46. metadata +204 -0
@@ -0,0 +1,117 @@
1
+ require 'test_helper'
2
+
3
+ class DateDiffTest < Minitest::Test
4
+ def test_date_diff_via_keyword_args
5
+ via_keyword_args = date_diff(
6
+ unit: :year,
7
+ from: jan_1_2000,
8
+ to: jan_1_2000,
9
+ )
10
+ via_positional_params = date_diff(
11
+ :year,
12
+ jan_1_2000,
13
+ jan_1_2000,
14
+ )
15
+ assert_equal via_positional_params, via_keyword_args
16
+ end
17
+
18
+ def test_years_between_a_few_days
19
+ assert_equal (1.0 / 366.0), date_diff(:year, jan_1_2000, jan_1_2000)
20
+ assert_equal (31.0 / 366.0), date_diff(:year, jan_1_2000, jan_31_2000)
21
+ assert_equal (32.0 / 366.0), date_diff(:year, jan_1_2000, feb_1_2000)
22
+ assert_equal (2/366.0 + 2/365.0), date_diff(:year, dec_30_2000, jan_2_2001)
23
+ end
24
+
25
+ def test_quarters_between_a_few_days
26
+ q1_2000 = (31 + 29 + 31).to_f
27
+ q4_2000 = (31 + 30 + 31).to_f
28
+ q1_2001 = (31 + 28 + 31).to_f
29
+ assert_equal (1.0 / q1_2000), date_diff(:quarter, jan_1_2000, jan_1_2000)
30
+ assert_equal (31.0 / q1_2000), date_diff(:quarter, jan_1_2000, jan_31_2000)
31
+ assert_equal (32.0 / q1_2000), date_diff(:quarter, jan_1_2000, feb_1_2000)
32
+ assert_equal (2/q4_2000 + 2/q1_2001), date_diff(:quarter, dec_30_2000, jan_2_2001)
33
+ end
34
+
35
+ def test_months_between_a_few_days
36
+ assert_equal (1.0 / 31.0), date_diff(:month, jan_1_2000, jan_1_2000)
37
+ assert_equal (31.0 / 31.0), date_diff(:month, jan_1_2000, jan_31_2000)
38
+ assert_equal (1.0 + 1/29.0), date_diff(:month, jan_1_2000, feb_1_2000)
39
+ assert_equal (2.0/31.0 + 2.0/31.0), date_diff(:month, dec_30_2000, jan_2_2001)
40
+ end
41
+
42
+ def test_weeks_between_a_few_days
43
+ assert_equal (1.0 / 7.0), date_diff(:week, jan_1_2000, jan_1_2000)
44
+ assert_equal (31.0 / 7.0), date_diff(:week, jan_1_2000, jan_31_2000)
45
+ assert_equal (32.0 / 7.0), date_diff(:week, jan_1_2000, feb_1_2000)
46
+ assert_equal (2/7.0 + 2/7.0), date_diff(:week, dec_30_2000, jan_2_2001)
47
+ end
48
+
49
+ def test_days_between_a_few_days
50
+ assert_equal 1.0, date_diff(:day, jan_1_2000, jan_1_2000)
51
+ assert_equal 31.0, date_diff(:day, jan_1_2000, jan_31_2000)
52
+ assert_equal 32.0, date_diff(:day, jan_1_2000, feb_1_2000)
53
+ assert_equal 4.0, date_diff(:day, dec_30_2000, jan_2_2001)
54
+ end
55
+
56
+ def test_six_months
57
+ assert_equal ((1 + 29 + 31 + 30 + 31 + 2) / 366.0), date_diff(:year, jan_31_2000, jun_2_2000)
58
+ assert_equal (1.0/31.0 + 4.0 + 2.0/30.0), date_diff(:month, jan_31_2000, jun_2_2000)
59
+ assert_equal ((1 + 29 + 31 + 30 + 31 + 2) / 7.0), date_diff(:week, jan_31_2000, jun_2_2000)
60
+ assert_equal (1 + 29 + 31 + 30 + 31 + 2), date_diff(:day, jan_31_2000, jun_2_2000)
61
+ end
62
+
63
+ def test_a_single_year
64
+ assert_equal 1.0, date_diff(:year, jan_1_2000, dec_31_2000)
65
+ assert_equal 4.0, date_diff(:quarter, jan_1_2000, dec_31_2000)
66
+ assert_equal 12.0, date_diff(:month, jan_1_2000, dec_31_2000)
67
+ end
68
+
69
+ def test_full_years
70
+ dec_12_2004 = Date.new 2004, 12, 31
71
+ assert_equal 5.0, date_diff(:year, jan_1_2000, dec_12_2004)
72
+ assert_equal 20.0, date_diff(:quarter, jan_1_2000, dec_12_2004)
73
+ assert_equal 60.0, date_diff(:month, jan_1_2000, dec_12_2004)
74
+ end
75
+
76
+ def test_a_year_and_four_days_across_a_leap_year
77
+ assert_equal (2/366.0 + 1.0 + 2/365.0), date_diff(:year, dec_30_2000, Date.new(2002, 1, 2))
78
+ assert_equal (2/31.0 + 12.0 + 2/31.0), date_diff(:month, dec_30_2000, Date.new(2002, 1, 2))
79
+ end
80
+
81
+ def test_advancing_days
82
+ assert_equal Date.new(2000, 1, 2), advance(from: jan_1_2000, unit: :day, intervals: 1)
83
+ assert_equal Date.new(2000, 1, 3), advance(from: jan_1_2000, unit: :day, intervals: 2)
84
+ assert_equal Date.new(2000, 1, 3), advance(from: jan_1_2000, unit: :day, intervals: 1.5)
85
+ end
86
+
87
+ def test_advancing_weeks
88
+ assert_equal Date.new(2000, 1, 8), advance(from: jan_1_2000, unit: :week, intervals: 1)
89
+ assert_equal Date.new(2000, 1, 15), advance(from: jan_1_2000, unit: :week, intervals: 2)
90
+ end
91
+
92
+ def test_advancing_months
93
+ assert_equal Date.new(2000, 2, 1), advance(from: jan_1_2000, unit: :month, intervals: 1)
94
+ assert_equal Date.new(2000, 3, 1), advance(from: jan_1_2000, unit: :month, intervals: 2)
95
+ assert_equal Date.new(2000, 4, 16), advance(from: apr_1_2000, unit: :month, intervals: 0.5)
96
+ end
97
+
98
+ def test_advancing_quarters
99
+ assert_equal Date.new(2000, 4, 1), advance(from: jan_1_2000, unit: :quarter, intervals: 1)
100
+ assert_equal Date.new(2000, 7, 1), advance(from: jan_1_2000, unit: :quarter, intervals: 2)
101
+ end
102
+
103
+ def test_advancing_years
104
+ assert_equal Date.new(2001, 1, 1), advance(from: jan_1_2000, unit: :year, intervals: 1)
105
+ assert_equal Date.new(2002, 1, 1), advance(from: jan_1_2000, unit: :year, intervals: 2)
106
+ end
107
+
108
+ private
109
+
110
+ def date_diff *args
111
+ DateDiff.date_diff *args
112
+ end
113
+
114
+ def advance *args
115
+ DateDiff.advance *args
116
+ end
117
+ end
@@ -0,0 +1,62 @@
1
+ require 'test_helper'
2
+
3
+ class ProjectionTest < Minitest::Test
4
+ def setup
5
+ @chart = ChartOfAccounts.new.tap do |c|
6
+ c.add_account :checking, type: :asset
7
+ c.add_account :job, type: :revenue
8
+ end
9
+ @projection = Projection.new range: (jan_1_2000..dec_31_2000), chart: @chart
10
+ end
11
+
12
+ def test_adding_transaction
13
+ @projection << valid_transaction
14
+ assert_equal 1, @projection.transaction_sequence.size
15
+ end
16
+
17
+ def test_adding_transactions_out_of_order
18
+ @projection << (first = valid_transaction(date: jan_3_2000))
19
+ @projection << (second = valid_transaction(date: jan_2_2000))
20
+ @projection << (third = valid_transaction(date: jan_2_2000))
21
+ @projection << (fourth = valid_transaction(date: jan_1_2000))
22
+
23
+ assert_equal [fourth, second, third, first].map(&:date),
24
+ @projection.transaction_sequence.map(&:date)
25
+ end
26
+
27
+ def test_project_freezes_projection_and_plays_transactions_through_chart
28
+ add_valid_transactions
29
+ assert_equal 0, @chart.net_worth
30
+
31
+ @projection.project!
32
+ assert @projection.frozen?
33
+
34
+ assert_equal 3032, @chart.net_worth
35
+ end
36
+
37
+ def test_project_will_yield_to_block
38
+ add_valid_transactions
39
+
40
+ dates = []
41
+ @projection.project! { |t| dates << t.date }
42
+
43
+ assert_equal [jan_1_2000, feb_2_2000, mar_3_2000, apr_4_2000], dates
44
+ end
45
+
46
+ private
47
+
48
+ def add_valid_transactions
49
+ @projection << valid_transaction(date: jan_1_2000)
50
+ @projection << valid_transaction(date: feb_2_2000, amount: 32)
51
+ @projection << valid_transaction(date: mar_3_2000)
52
+ @projection << valid_transaction(date: apr_4_2000)
53
+ end
54
+
55
+ def valid_transaction date: jan_1_2000, amount: 1000
56
+ Transaction.new(
57
+ date: date,
58
+ debit: { amount: amount, account_id: :checking },
59
+ credit: { amount: amount, account_id: :job },
60
+ )
61
+ end
62
+ end
@@ -0,0 +1,168 @@
1
+ require 'test_helper'
2
+
3
+ class ProjectorTest < Minitest::Test
4
+ def setup
5
+ @projector = Projector.new from: jan_1_2000
6
+ @projector.add_account :checking, type: :asset
7
+ @projector.add_account :nustartup_inc, type: :revenue
8
+ end
9
+
10
+ def test_next_projector_includes_deferred_transactions_from_projector
11
+ @projector.add_transaction(
12
+ date: jan_1_2000,
13
+ debit: { amount: 1000, account_id: :checking },
14
+ credit: { amount: 1000, account_id: :nustartup_inc },
15
+ schedule: every_month(until: jun_30_2001),
16
+ )
17
+ next_projector = @projector.project to: dec_31_2000, build_next: true
18
+ assert_equal jan_1_2001, next_projector.from
19
+ next_projector.project to: dec_31_2001
20
+
21
+ assert_equal 18000, next_projector.net_worth
22
+ end
23
+
24
+ def test_project_yields_to_block
25
+ @projector.add_transaction(
26
+ date: jan_1_2000,
27
+ debit: { amount: 1000, account_id: :checking },
28
+ credit: { amount: 1000, account_id: :nustartup_inc },
29
+ schedule: every_month(until: apr_30_2000),
30
+ )
31
+
32
+ expected_dates = [jan_1_2000, feb_1_2000, mar_1_2000, apr_1_2000]
33
+ actual_dates = []
34
+
35
+ @projector.project to: dec_31_2000 do |transaction|
36
+ actual_dates.push transaction.date
37
+ end
38
+
39
+ assert_equal expected_dates, actual_dates
40
+ end
41
+ end
42
+
43
+ class ProjectorAccountsTest < Minitest::Test
44
+ def setup
45
+ @projector = Projector.new from: jan_1_2000
46
+ end
47
+
48
+ def test_add_account_refuses_to_overwrite_account
49
+ @projector.add_account :checking, type: :asset
50
+ assert_raises Projector::AccountExists do
51
+ @projector.add_account :checking, type: :asset
52
+ end
53
+ end
54
+
55
+ def test_add_account_refuses_to_add_account_without_valid_type
56
+ assert_raises Projector::InvalidAccount do
57
+ @projector.add_account :checking, type: nil
58
+ end
59
+ assert_raises Projector::InvalidAccount do
60
+ @projector.add_account :checking, type: :foozle
61
+ end
62
+ end
63
+
64
+ def test_supplying_an_opening_balance
65
+ assert_equal 0, @projector.net_worth
66
+
67
+ @projector.add_account(
68
+ :checking,
69
+ open_date: jan_1_2000,
70
+ opening_balance: 500,
71
+ type: :asset,
72
+ )
73
+ refute_equal 0, @projector.balance
74
+
75
+ assert_raises Projector::InvalidAccount do
76
+ @projector.add_account(
77
+ :savings,
78
+ open_date: jan_2_2000,
79
+ opening_balance: 500,
80
+ type: :asset,
81
+ )
82
+ end
83
+
84
+ @projector.add_account(
85
+ :estate,
86
+ open_date: jan_1_2000,
87
+ opening_balance: 500,
88
+ type: :equity,
89
+ )
90
+ assert_equal 0, @projector.balance
91
+ assert_equal 500, @projector.net_worth
92
+ end
93
+
94
+ def test_accounts_must_be_balanced_to_run_projection
95
+ @projector.add_account(
96
+ :checking,
97
+ open_date: jan_1_2000,
98
+ opening_balance: 500,
99
+ type: :asset,
100
+ )
101
+ refute_equal 0, @projector.balance
102
+
103
+ assert_raises Projector::BalanceError do
104
+ @projector.project to: dec_31_2000
105
+ end
106
+ end
107
+
108
+ def test_add_accounts_passes_account_hashes_to_add_account
109
+ assert_equal [], @projector.accounts
110
+ @projector.accounts = { checking: { type: :asset } }
111
+ assert_equal [:checking], @projector.accounts
112
+ end
113
+ end
114
+
115
+ class ProjectorTransactionTest < Minitest::Test
116
+ def setup
117
+ @projector = Projector.new from: jan_1_2000
118
+ @projector.add_account :checking, type: :asset
119
+ @projector.add_account :nustartup_inc, type: :revenue
120
+ end
121
+
122
+ def test_single_transaction
123
+ add_simple_transaction
124
+
125
+ @projector.project to: dec_31_2000
126
+
127
+ assert_equal 1000, @projector.net_worth
128
+ assert_equal 1000, @projector.account_balance(:checking)
129
+ assert_equal 1000, @projector.account_balance(:nustartup_inc)
130
+ end
131
+
132
+ def test_scheduled_transaction
133
+ add_scheduled_transaction
134
+
135
+ @projector.project to: dec_31_2000
136
+
137
+ assert_equal 12000, @projector.net_worth
138
+ assert_equal 12000, @projector.account_balance(:checking)
139
+ assert_equal 12000, @projector.account_balance(:nustartup_inc)
140
+ end
141
+
142
+ def test_future_transaction
143
+ add_simple_transaction jan_1_2010
144
+
145
+ @projector.project to: dec_31_2000
146
+
147
+ assert_equal 0, @projector.net_worth
148
+ end
149
+
150
+ private
151
+
152
+ def add_scheduled_transaction date = jan_1_2000
153
+ @projector.add_transaction(
154
+ date: date,
155
+ credit: { amount: 1000, account_id: :nustartup_inc },
156
+ debit: { amount: 1000, account_id: :checking },
157
+ schedule: every_month,
158
+ )
159
+ end
160
+
161
+ def add_simple_transaction date = jan_1_2000
162
+ @projector.add_transaction(
163
+ date: date,
164
+ credit: { amount: 1000, account_id: :nustartup_inc },
165
+ debit: { amount: 1000, account_id: :checking },
166
+ )
167
+ end
168
+ end
@@ -0,0 +1,68 @@
1
+ require 'test_helper'
2
+
3
+ class ScheduledTransactionTest < Minitest::Test
4
+ def test_slice_does_not_return_leftover_if_ending_within_slice_date
5
+ @schedule = Schedule.new every_month until: dec_31_2000
6
+ _, leftover = @schedule.slice(jan_1_2000..dec_31_2000)
7
+ assert_nil leftover
8
+ end
9
+
10
+ def test_slice_returns_leftover_if_schedule_has_no_end
11
+ @schedule = Schedule.new every_month
12
+ _, leftover = @schedule.slice(jan_1_2000..dec_31_2000)
13
+
14
+ expected_schedule = { scalar: 1, unit: :month }
15
+ assert_equal expected_schedule, leftover
16
+ end
17
+
18
+ def test_slice_returns_leftover_if_ending_after_slice_date
19
+ @schedule = Schedule.new every_month until: jun_30_2001
20
+ _, leftover = @schedule.slice(jan_1_2000..dec_31_2000)
21
+
22
+ expected_schedule = { scalar: 1, unit: :month, count: 6 }
23
+ assert_equal expected_schedule, leftover
24
+ end
25
+
26
+ def test_slice_yields_advancing_dates
27
+ @schedule = Schedule.new every_month until: apr_15_2000
28
+
29
+ expected_dates = [jan_1_2000, feb_1_2000, mar_1_2000, apr_1_2000]
30
+ actual_dates, _ = @schedule.slice(jan_1_2000..apr_15_2000)
31
+ actual_dates.map! &:first
32
+
33
+ assert_equal expected_dates, actual_dates
34
+ end
35
+
36
+ def test_slice_prorates_final_period
37
+ @schedule = Schedule.new every_month until: apr_15_2000
38
+
39
+ expected_prorates = [1, 1, 1, 0.5]
40
+ actual_prorates, _ = @schedule.slice(jan_1_2000..dec_31_2000)
41
+ actual_prorates.map! &:last
42
+
43
+ assert_equal expected_prorates, actual_prorates
44
+ end
45
+
46
+ def test_handles_start_after_projection
47
+ @schedule = Schedule.new every_month(from: apr_1_2000, until: jun_15_2000)
48
+
49
+ expected_prorates = [1, 1, 0.5]
50
+ actual_prorates, _ = @schedule.slice(apr_1_2000..jun_15_2000)
51
+ actual_prorates.map! &:last
52
+
53
+ assert_equal expected_prorates, actual_prorates
54
+ end
55
+
56
+ def test_handles_start_during_range_and_end_after_range
57
+ @schedule = Schedule.new every_month(from: jun_16_2000, until: apr_15_2001)
58
+
59
+ expected_prorates = [1, 1, 1, 1, 1, 0.5 ]
60
+ expected_leftover = { scalar: 1, unit: :month, count: 4.5 }
61
+
62
+ actual_prorates, actual_leftover = @schedule.slice(jun_16_2000..nov_30_2000)
63
+ actual_prorates.map! &:last
64
+
65
+ assert_equal expected_prorates, actual_prorates
66
+ assert_equal expected_leftover, actual_leftover
67
+ end
68
+ end
@@ -0,0 +1,47 @@
1
+ require 'test_helper'
2
+
3
+ class ScheduledTransactionTest < Minitest::Test
4
+ def setup
5
+ @chart = ChartOfAccounts.new
6
+ @chart.add_account :checking, type: :asset
7
+ @chart.add_account :job, type: :revenue
8
+ end
9
+
10
+ def test_prorating
11
+ in_range, _ = scheduled_transaction.slice apr_15_2000
12
+ assert_equal [1000, 1000, 1000, 500], in_range.map { |t| t.credits.first.scalar }
13
+
14
+ in_range, _ = scheduled_transaction_with_percentage.slice apr_15_2000
15
+ assert_equal [0.25, 0.25, 0.25, 0.125], in_range.map { |t| t.credits.first.scalar }
16
+ end
17
+
18
+ def test_leftover
19
+ _, leftover = scheduled_transaction(apr_15_2000).slice apr_15_2000
20
+ assert_nil leftover
21
+
22
+ _, leftover = scheduled_transaction(apr_16_2000).slice apr_15_2000
23
+ refute_nil leftover
24
+
25
+ assert_equal apr_16_2000, leftover.date
26
+ end
27
+
28
+ private
29
+
30
+ def scheduled_transaction end_date = nil
31
+ ScheduledTransaction.new(
32
+ date: jan_1_2000,
33
+ debit: { account_id: :checking, amount: 1000 },
34
+ credit: { account_id: :job, amount: 1000 },
35
+ schedule: every_month(until: end_date),
36
+ )
37
+ end
38
+
39
+ def scheduled_transaction_with_percentage
40
+ ScheduledTransaction.new(
41
+ date: jan_1_2000,
42
+ debit: { account_id: :checking, percent: 0.25, of: :job },
43
+ credit: { account_id: :job, percent: 0.25, of: :job },
44
+ schedule: every_month,
45
+ )
46
+ end
47
+ end
@@ -0,0 +1,145 @@
1
+ require 'test_helper'
2
+
3
+ class TaxCalculatorTest < Minitest::Test
4
+ def setup
5
+ @projector = Projector.new from: jan_1_2012
6
+ @projector.add_account :checking, type: :asset
7
+ @projector.add_account :job, type: :revenue, tags: %i(salary)
8
+
9
+ @projector.add_transaction(
10
+ date: jan_1_2012,
11
+ credit: { amount: 6000, account_id: :job },
12
+ debit: { amount: 6000, account_id: :checking },
13
+ schedule: every_month,
14
+ )
15
+ @projector.add_transaction(
16
+ date: jun_30_2013,
17
+ credit: { amount: 3000, account_id: :job },
18
+ debit: { amount: 3000, account_id: :checking },
19
+ )
20
+ end
21
+
22
+ def test_basic_1040
23
+ @tax_calculator = TaxCalculator.new projector: @projector, household: single
24
+ calculation = @tax_calculator.calculate!
25
+
26
+ assert_equal 2012, calculation.year
27
+ assert_equal 72000, calculation.gross
28
+ assert_equal 72000, calculation.total_income
29
+ assert_equal 0, calculation.adjustments
30
+ assert_equal 72000, calculation.agi
31
+ assert_equal 5950, calculation.deduction
32
+ assert_equal 3800, calculation.exemption
33
+ assert_equal 4068, calculation.withholding_tax
34
+ assert_equal 62250, calculation.taxable_income
35
+ assert_equal 11592.5, calculation.income_tax
36
+ assert_equal 15660.5, calculation.taxes
37
+ assert_equal 15660.5, calculation.taxes_owed
38
+ assert_equal 56339.5, calculation.net
39
+ assert_equal 21.75, calculation.effective_rate
40
+ end
41
+
42
+ def test_basic_1040_with_withholding
43
+ @projector.add_transaction(
44
+ date: jan_1_2012,
45
+ credit: { amount: 1000, account_id: :checking },
46
+ debit: { amount: 1000, account_id: TaxCalculator::EXPENSE_ACCOUNT_ID },
47
+ schedule: every_month,
48
+ )
49
+
50
+ @tax_calculator = TaxCalculator.new projector: @projector, household: single
51
+ calculation = @tax_calculator.calculate!
52
+ assert_equal 3660.5, calculation.taxes_owed
53
+ end
54
+
55
+ def test_basic_1040_married
56
+ @tax_calculator = TaxCalculator.new projector: @projector, household: married
57
+ calculation = @tax_calculator.calculate!
58
+
59
+ assert_equal 11900, calculation.deduction
60
+ assert_equal 3800 * 4, calculation.exemption
61
+ end
62
+
63
+ def test_charity_contribution
64
+ @projector.add_account :public_radio, type: :expense, tags: %i(501c)
65
+ @projector.add_transaction(
66
+ date: jun_30_2012,
67
+ debit: { amount: 15000, account_id: :public_radio },
68
+ credit: { amount: 15000, account_id: :checking },
69
+ )
70
+ @tax_calculator = TaxCalculator.new projector: @projector, household: single
71
+ calculation = @tax_calculator.calculate!
72
+
73
+ assert_equal 15000, calculation.deduction
74
+ end
75
+
76
+ def test_basic_1040_2013
77
+ @projector = @projector.project to: dec_31_2012, build_next: true
78
+ @tax_calculator = TaxCalculator.new projector: @projector, household: single
79
+ calculation = @tax_calculator.calculate!
80
+
81
+ assert_equal 75000, calculation.total_income
82
+ assert_equal 5737.5, calculation.withholding_tax
83
+ end
84
+
85
+ def test_hsa_deduction
86
+ @projector.add_account :hsa, type: :asset, tags: %i(hsa individual)
87
+ @tax_calculator = TaxCalculator.new projector: @projector, household: single
88
+ @projector.add_transaction(
89
+ date: jun_30_2012,
90
+ debit: { amount: 4000, account_id: :hsa },
91
+ credit: { amount: 4000, account_id: :checking },
92
+ )
93
+ calculation = @tax_calculator.calculate!
94
+
95
+ assert_equal 72000, calculation.gross
96
+ assert_equal 72000, calculation.total_income
97
+ assert_equal 3100, calculation.adjustments
98
+ assert_equal 68900, calculation.agi
99
+ end
100
+
101
+ def test_pretax_hsa_deduction
102
+ @projector.add_account :hsa, type: :asset, tags: %i(hsa family)
103
+ @projector.add_transaction(
104
+ date: jun_30_2012,
105
+ debit: { amount: 7000, account_id: :hsa },
106
+ credit: { amount: 7000, account_id: :job },
107
+ )
108
+ @tax_calculator = TaxCalculator.new projector: @projector, household: single
109
+ calculation = @tax_calculator.calculate!
110
+
111
+ assert_equal 79000, calculation.gross
112
+ assert_equal 72000, calculation.total_income
113
+ assert_equal 0, calculation.adjustments
114
+ assert_equal 72000, calculation.agi
115
+ end
116
+
117
+ def test_mortgage_interest_and_property_taxes_paid
118
+ @projector.add_account :interest, type: :expense, tags: %i(mortgage_interest)
119
+ @projector.add_account :property_taxes, type: :expense, tags: %i(property tax)
120
+ @projector.add_transaction(
121
+ date: jun_30_2012,
122
+ debit: { amount: 7500, account_id: :interest },
123
+ credit: { amount: 7500, account_id: :job },
124
+ )
125
+ @projector.add_transaction(
126
+ date: jun_30_2012,
127
+ debit: { amount: 5000, account_id: :property_taxes },
128
+ credit: { amount: 5000, account_id: :job },
129
+ )
130
+ @tax_calculator = TaxCalculator.new projector: @projector, household: single
131
+ calculation = @tax_calculator.calculate!
132
+
133
+ assert_equal 12500, calculation.deduction
134
+ end
135
+
136
+ private
137
+
138
+ def single
139
+ { filing_status: :single, exemptions: 1 }
140
+ end
141
+
142
+ def married
143
+ { filing_status: :married_filing_jointly, exemptions: 4 }
144
+ end
145
+ end
@@ -0,0 +1,37 @@
1
+ require 'test_helper'
2
+
3
+ class TransactionEntryTest < Minitest::Test
4
+ def test_must_supply_type_of_debit_or_credit
5
+ assert_raises ArgumentError do
6
+ TransactionEntry.new credit_or_debit: :foo, amount: 1, account_id: :checking
7
+ end
8
+ end
9
+
10
+ def test_must_supply_a_nonzero_amount
11
+ assert_raises ArgumentError do
12
+ TransactionEntry.new credit_or_debit: :credit, amount: 0, account_id: :checking
13
+ end
14
+ end
15
+
16
+ def test_must_supply_an_account
17
+ assert_raises KeyError do
18
+ TransactionEntry.new credit_or_debit: :credit, amount: 1
19
+ end
20
+ end
21
+
22
+ def test_calculate_amount_sets_amount_and_delta
23
+ chart = ChartOfAccounts.new
24
+ chart.add_account :checking, type: :asset, opening_balance: 1000
25
+ chart.add_account :job, type: :revenue, opening_balance: 1000
26
+
27
+ t1 = TransactionEntry.new credit_or_debit: :credit, amount: 1, account_id: :checking
28
+ t1.calculate chart
29
+ assert_equal 1, t1.amount
30
+ assert_equal -1, t1.delta
31
+
32
+ t2 = TransactionEntry.new credit_or_debit: :debit, percent: 0.5, of: :checking, account_id: :checking
33
+ t2.calculate chart
34
+ assert_equal 500, t2.amount
35
+ assert_equal 500, t2.delta
36
+ end
37
+ end