mudrat_projector 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|