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,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
|