fortuneteller 0.1.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 +10 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +21 -0
- data/README.md +130 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/fortuneteller.gemspec +26 -0
- data/lib/fortuneteller.rb +29 -0
- data/lib/fortuneteller/account.rb +51 -0
- data/lib/fortuneteller/cashflow.rb +18 -0
- data/lib/fortuneteller/inflating_int.rb +13 -0
- data/lib/fortuneteller/job.rb +78 -0
- data/lib/fortuneteller/moment_struct.rb +122 -0
- data/lib/fortuneteller/person.rb +11 -0
- data/lib/fortuneteller/savings_plans.rb +13 -0
- data/lib/fortuneteller/simulator.rb +114 -0
- data/lib/fortuneteller/social_security.rb +71 -0
- data/lib/fortuneteller/spending_strategy.rb +70 -0
- data/lib/fortuneteller/state.rb +121 -0
- data/lib/fortuneteller/transform_base.rb +14 -0
- data/lib/fortuneteller/transform_generator.rb +59 -0
- data/lib/fortuneteller/utils/social_security.rb +261 -0
- data/lib/fortuneteller/version.rb +3 -0
- metadata +112 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
module FortuneTeller
|
|
2
|
+
# We extend `state` Hash with this module for readability
|
|
3
|
+
class State
|
|
4
|
+
attr_reader :date, :accounts, :cashflow, :from, :to
|
|
5
|
+
|
|
6
|
+
def self.cashflow_base
|
|
7
|
+
FortuneTeller::Cashflow.new(
|
|
8
|
+
pretax_gross: 0,
|
|
9
|
+
pretax_salary: 0,
|
|
10
|
+
pretax_savings_withdrawal: 0,
|
|
11
|
+
pretax_savings: 0,
|
|
12
|
+
pretax_savings_matched: 0,
|
|
13
|
+
pretax_adjusted: 0,
|
|
14
|
+
tax_withholding: 0,
|
|
15
|
+
take_home_pay: 0
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def initialize(start_date:, previous: nil)
|
|
20
|
+
@from = start_date.dup
|
|
21
|
+
@date = start_date
|
|
22
|
+
@accounts = {}
|
|
23
|
+
unless previous.nil?
|
|
24
|
+
previous.accounts.each { |k, a| @accounts[k] = a.dup }
|
|
25
|
+
end
|
|
26
|
+
@cashflow = {
|
|
27
|
+
primary: Array.new(12) { self.class.cashflow_base },
|
|
28
|
+
partner: Array.new(12) { self.class.cashflow_base }
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def add_account(key:, account:)
|
|
33
|
+
@accounts[key] = account.initial_state(start_date: @date)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def pass_time(to:)
|
|
37
|
+
@date = to
|
|
38
|
+
@to = to
|
|
39
|
+
@accounts.each_value { |a| a.pass_time(to: to) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def apply_pretax_savings_withdrawal(date:, holder:, amount:, source:)
|
|
43
|
+
@accounts[source].debit(amount: amount, on: date)
|
|
44
|
+
c = FortuneTeller::Cashflow.new(pretax_gross: amount, pretax_savings_withdrawal: amount)
|
|
45
|
+
c.line_items[:pretax_adjusted] = amount
|
|
46
|
+
c.line_items[:tax_withholding] = 0
|
|
47
|
+
c.line_items[:take_home_pay] = amount
|
|
48
|
+
apply_cashflow(date: date, holder: holder, cashflow: c)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def apply_w2_income(date:, holder:, income:, account_credits:)
|
|
52
|
+
c = generate_w2_cashflow(date, income)
|
|
53
|
+
apply_cashflow(date: date, holder: holder, cashflow: c)
|
|
54
|
+
account_credits.each do |k, amount|
|
|
55
|
+
@accounts[k].credit(amount: amount, on: date)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def apply_ss_income(date:, holder:, income:)
|
|
60
|
+
c = generate_ss_cashflow(date, income)
|
|
61
|
+
apply_cashflow(date: date, holder: holder, cashflow: c)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def init_next
|
|
65
|
+
self.class.new(start_date: @date, previous: self)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def merged_cashflow(holder:)
|
|
69
|
+
@cashflow[holder].reduce(FortuneTeller::Cashflow.new, :merge!)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def as_json(_options = nil)
|
|
73
|
+
{
|
|
74
|
+
date: @date,
|
|
75
|
+
# cashflow: {
|
|
76
|
+
# primary: merged_cashflow(holder: :primary).as_json(options),
|
|
77
|
+
# partner: merged_cashflow(holder: :partner).as_json(options)
|
|
78
|
+
# },
|
|
79
|
+
accounts: @accounts.as_json
|
|
80
|
+
}
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def generate_w2_cashflow(date, income)
|
|
86
|
+
c = FortuneTeller::Cashflow.new(
|
|
87
|
+
pretax_gross: (income[:wages] + income[:matched]),
|
|
88
|
+
pretax_salary: income[:wages],
|
|
89
|
+
pretax_savings: income[:saved],
|
|
90
|
+
pretax_savings_matched: income[:matched],
|
|
91
|
+
pretax_adjusted: (income[:wages] - income[:saved])
|
|
92
|
+
)
|
|
93
|
+
c.line_items[:tax_withholding] = calculate_w2_withholding(
|
|
94
|
+
date: date,
|
|
95
|
+
adjusted_income: c.line_items[:pretax_adjusted],
|
|
96
|
+
pay_period: income[:pay_period]
|
|
97
|
+
)
|
|
98
|
+
c.line_items[:take_home_pay] = c.line_items[:pretax_adjusted] - c.line_items[:tax_withholding]
|
|
99
|
+
c
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def generate_ss_cashflow(date, income)
|
|
103
|
+
FortuneTeller::Cashflow.new(
|
|
104
|
+
pretax_gross: income[:ss],
|
|
105
|
+
pretax_ss: income[:ss],
|
|
106
|
+
pretax_adjusted: income[:ss],
|
|
107
|
+
tax_withholding: 0,
|
|
108
|
+
take_home_pay: income[:ss]
|
|
109
|
+
)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def calculate_w2_withholding(date:, adjusted_income:, pay_period:)
|
|
113
|
+
# Ideally, use state to determine w-4 allowances
|
|
114
|
+
(adjusted_income * 0.3).floor
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def apply_cashflow(date:, holder:, cashflow:)
|
|
118
|
+
@cashflow[holder][(date.month - 1)].merge!(cashflow)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module FortuneTeller
|
|
2
|
+
# Provides base functionality to inheriting transform classes
|
|
3
|
+
class TransformBase
|
|
4
|
+
attr_reader :date, :holder
|
|
5
|
+
def initialize(date:, holder:)
|
|
6
|
+
@date = date
|
|
7
|
+
@holder = holder
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def <=>(other)
|
|
11
|
+
date <=> other.date
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
module FortuneTeller
|
|
2
|
+
# Base class for FortuneTeller objects that generate transforms
|
|
3
|
+
class TransformGenerator
|
|
4
|
+
attr_reader :holder, :start_date, :end_date
|
|
5
|
+
|
|
6
|
+
def initialize(holder: nil, start_date: nil, end_date: nil, **data)
|
|
7
|
+
@holder = holder
|
|
8
|
+
@start_date = start_date
|
|
9
|
+
@end_date = end_date
|
|
10
|
+
@scheduled = []
|
|
11
|
+
@data = MomentStruct.new data
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def method_missing(name, *args)
|
|
15
|
+
if @data.respond_to? name
|
|
16
|
+
@data.send(name, *args)
|
|
17
|
+
else
|
|
18
|
+
super
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def respond_to_missing?(name, include_private = false)
|
|
23
|
+
@data.respond_to?(name) || super
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def bounded_gen_transforms(from:, to:, plan:)
|
|
27
|
+
return [] if out_of_range?(from: from, to: to)
|
|
28
|
+
from = tighten_from(from)
|
|
29
|
+
to = tighten_to(to)
|
|
30
|
+
gen_transforms(from: from, to: to, plan: plan)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def state_reader
|
|
36
|
+
@data.to_reader
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def states_in_range(from:, to:)
|
|
40
|
+
@data.read_range(from: from, to: to)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def out_of_range?(from:, to:)
|
|
44
|
+
return true if !@start_date.nil? && (to < @start_date)
|
|
45
|
+
return true if !@end_date.nil? && (from >= @end_date)
|
|
46
|
+
false
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def tighten_from(from)
|
|
50
|
+
return from if @start_date.nil?
|
|
51
|
+
[from, @start_date].max
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def tighten_to(to)
|
|
55
|
+
return to if @end_date.nil?
|
|
56
|
+
[to, @end_date].min
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
module FortuneTeller
|
|
2
|
+
module Utils
|
|
3
|
+
|
|
4
|
+
# Calculates adjusted benefit from PIA
|
|
5
|
+
# Based on https://www.ssa.gov/oact/quickcalc/early_late.html 11/16/2017
|
|
6
|
+
class SocialSecurity
|
|
7
|
+
attr_accessor :pia
|
|
8
|
+
|
|
9
|
+
def initialize(dob:, start_month:)
|
|
10
|
+
@dob = dob
|
|
11
|
+
@adjusted_dob = (dob.day == 1 ? dob.yesterday : dob)
|
|
12
|
+
@start_month = start_month.at_beginning_of_month
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def estimate_pia(current_salary:, annual_raise:)
|
|
16
|
+
year = Date.today.year
|
|
17
|
+
last_year = @start_month.year
|
|
18
|
+
salary_history = {year => current_salary}
|
|
19
|
+
((@dob.year+18)..(year-1)).reverse_each do |y|
|
|
20
|
+
salary_history[y] = (salary_history[y+1]/annual_raise).floor
|
|
21
|
+
end
|
|
22
|
+
if(last_year > year)
|
|
23
|
+
((year+1)..last_year).each do |y|
|
|
24
|
+
salary_history[y] = (salary_history[y-1]*annual_raise).floor
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
salaries = salary_history.map{|y, s| s*indexing_factors[y]}
|
|
28
|
+
aime = (salaries.sort.last(35).reduce(:+)/(35.0*12)).floor
|
|
29
|
+
puts "AIME #{aime}"
|
|
30
|
+
|
|
31
|
+
if aime > bend_points[1]
|
|
32
|
+
pia_62 = (0.9*bend_points[0]+0.32*(bend_points[1]-bend_points[0])+0.15*(aime-bend_points[1])).floor
|
|
33
|
+
elsif aime > bend_points[0]
|
|
34
|
+
pia_62 = (0.9*bend_points[0]+0.32*(aime-bend_points[0])).floor
|
|
35
|
+
else
|
|
36
|
+
pia_62 = (0.9*aime).floor
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
pia_adjusted = pia_62
|
|
40
|
+
((@dob.year+63)..@start_month.year).each do |y|
|
|
41
|
+
pia_adjusted = (pia_adjusted*(100+COLA_1975_START[y-1-1975])/100.0).floor
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
@pia = pia_adjusted
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def fra_pia=(fra_pia)
|
|
48
|
+
pia_adjusted = fra_pia
|
|
49
|
+
if @start_month.year < full_retirement_month.year
|
|
50
|
+
(@start_month.year..(full_retirement_month.year-1)).each do |y|
|
|
51
|
+
pia_adjusted = (pia_adjusted/((100+COLA_1975_START[y-1975])/100.0)).floor
|
|
52
|
+
end
|
|
53
|
+
elsif @start_month.year > full_retirement_month.year
|
|
54
|
+
puts "START GREATER"
|
|
55
|
+
((full_retirement_month.year+1)..@start_month.year).each do |y|
|
|
56
|
+
puts "START: #{@start_month.year}, YEAR: #{y}"
|
|
57
|
+
puts "PIA ADJ = #{pia_adjusted}"
|
|
58
|
+
pia_adjusted = (pia_adjusted*(100+COLA_1975_START[y-1-1975])/100.0).floor
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
@pia = pia_adjusted
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def calculate_benefit
|
|
65
|
+
frm = full_retirement_month
|
|
66
|
+
puts "FRM: #{frm}"
|
|
67
|
+
return @pia if @start_month == frm
|
|
68
|
+
|
|
69
|
+
if(@start_month < frm)
|
|
70
|
+
min_rm = min_retirement_month
|
|
71
|
+
raise bounds_error(start: @start_month, min: min_rm) if @start_month < min_rm
|
|
72
|
+
early_benefit( months: month_diff(@start_month, frm) )
|
|
73
|
+
else
|
|
74
|
+
max_rm = max_retirement_month
|
|
75
|
+
raise bounds_error(start: @start_month, max: max_rm) if @start_month > max_rm
|
|
76
|
+
late_benefit( months: month_diff(frm, @start_month) )
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
TRANSITION_YEARS = {
|
|
83
|
+
1938 => [65, 2], 1939 => [65, 4], 1940 => [65, 6], 1941 => [65, 8],
|
|
84
|
+
1942 => [65, 10], 1955 => [66, 2], 1956 => [66, 4], 1957 => [66, 6],
|
|
85
|
+
1958 => [66, 8], 1959 => [66, 10],
|
|
86
|
+
}
|
|
87
|
+
DELAY_RATES = {
|
|
88
|
+
1925 => (7/24.0), 1926 => (7/24.0), 1927 => (8/24.0), 1928 => (8/24.0),
|
|
89
|
+
1929 => (9/24.0), 1930 => (9/24.0), 1931 => (10/24.0), 1932 => (10/24.0),
|
|
90
|
+
1933 => (11/24.0), 1934 => (11/24.0), 1935 => (12/24.0), 1936 => (12/24.0),
|
|
91
|
+
1937 => (13/24.0), 1938 => (13/24.0), 1939 => (14/24.0), 1940 => (14/24.0),
|
|
92
|
+
1941 => (15/24.0), 1942 => (15/24.0),
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
AWI_1951_START = [
|
|
96
|
+
# Final (1951-2016)
|
|
97
|
+
# https://www.ssa.gov/oact/COLA/AWI.html
|
|
98
|
+
279916, 297332, 313944, 315564, 330144, 353236, 364172, 367380, 385580,
|
|
99
|
+
400712, 408676, 429140, 439664, 457632, 465872, 493836, 521344, 557176,
|
|
100
|
+
589376, 618624, 649708, 713380, 758016, 803076, 863092, 922648, 977944,
|
|
101
|
+
1055603, 1147946, 1251346, 1377310, 1453134, 1523924, 1613507, 1682251,
|
|
102
|
+
1732182, 1842651, 1933404, 2009955, 2102798, 2181160, 2293542, 2313267,
|
|
103
|
+
2375353, 2470566, 2591390, 2742600, 2886144, 3046984, 3215482, 3292192,
|
|
104
|
+
3325209, 3406495, 3564855, 3695294, 3865141, 4040548, 4133497, 4071161,
|
|
105
|
+
4167383, 4297961, 4432167, 4488816, 4648152, 4809863, 4866473,
|
|
106
|
+
# Estimates (2017-2070)
|
|
107
|
+
# https://www.ssa.gov/oact/TR/TRassum.html (see awi_projector)
|
|
108
|
+
5056265, 5298965, 5537418, 5775526, 6018098, 6252803, 6484156, 6730553,
|
|
109
|
+
6986314, 7251793, 7527361, 7813400, 8110309, 8418500, 8738403, 9070462,
|
|
110
|
+
9415139, 9772914, 10144284, 10529766, 10929897, 11345233, 11776351,
|
|
111
|
+
12223852, 12688358, 13170515, 13670994, 14190491, 14729729, 15289458,
|
|
112
|
+
15870457, 16473534, 17099528, 17749310, 18423783, 19123886, 19850593,
|
|
113
|
+
20604915, 21387901, 22200641, 23044265, 23919947, 24828904, 25772402,
|
|
114
|
+
26751753, 27768319, 28823515, 29918808, 31055722, 32235839, 33460800,
|
|
115
|
+
34732310, 36052137, 37422118
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
COLA_1975_START = [
|
|
119
|
+
# Final (1975-2017)
|
|
120
|
+
# https://www.ssa.gov/oact/COLA/colaseries.html
|
|
121
|
+
8.0, 6.4, 5.9, 6.5, 9.9, 14.3, 11.2, 7.4, 3.5, 3.5, 3.1, 1.3, 4.2,
|
|
122
|
+
4.0, 4.7, 5.4, 3.7, 3.0, 2.6, 2.8, 2.6, 2.9, 2.1, 1.3, 2.5, 3.5,
|
|
123
|
+
2.6, 1.4, 2.1, 2.7, 4.1, 3.3, 2.3, 5.8, 0.0, 0.0, 3.6, 1.7, 1.5,
|
|
124
|
+
1.7, 0.0, 0.3, 2.0,
|
|
125
|
+
# Estimates (2018-2070)
|
|
126
|
+
# https://www.ssa.gov/oact/TR/TRassum.html
|
|
127
|
+
3.1, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6,
|
|
128
|
+
2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6,
|
|
129
|
+
2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6,
|
|
130
|
+
2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6, 2.6,
|
|
131
|
+
2.6,
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
def bend_points
|
|
135
|
+
return @bend_points unless @bend_points.nil?
|
|
136
|
+
|
|
137
|
+
age_62_year = @dob.year+62
|
|
138
|
+
age_62_index = AWI_1951_START[(age_62_year-1951)]
|
|
139
|
+
year_1979_index = AWI_1951_START[(1979-1951)]
|
|
140
|
+
year_1979_bends = [18000, 108500]
|
|
141
|
+
@bend_points = [
|
|
142
|
+
(year_1979_bends[0].to_f*(age_62_index/year_1979_index)).floor,
|
|
143
|
+
(year_1979_bends[1].to_f*(age_62_index/year_1979_index)).floor,
|
|
144
|
+
]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def indexing_factors
|
|
148
|
+
return @indexing_factors unless @indexing_factors.nil?
|
|
149
|
+
|
|
150
|
+
age_60_year = @dob.year+60
|
|
151
|
+
age_60_index = AWI_1951_START[(age_60_year-1951)]
|
|
152
|
+
@indexing_factors = {}
|
|
153
|
+
(18..60).each do |age|
|
|
154
|
+
year = @dob.year+age
|
|
155
|
+
@indexing_factors[year] = age_60_index.to_f/AWI_1951_START[(year-1951)]
|
|
156
|
+
end
|
|
157
|
+
(61..70).each do |age|
|
|
158
|
+
year = @dob.year+age
|
|
159
|
+
@indexing_factors[year] = 1.0
|
|
160
|
+
end
|
|
161
|
+
@indexing_factors
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def full_retirement_month
|
|
165
|
+
return @frm unless @frm.nil?
|
|
166
|
+
|
|
167
|
+
year = @adjusted_dob.year
|
|
168
|
+
frm = @adjusted_dob.at_beginning_of_month
|
|
169
|
+
if year <= 1938
|
|
170
|
+
@frm = frm.years_since(65)
|
|
171
|
+
elsif (year >= 1943 and year <= 1954)
|
|
172
|
+
@frm = frm.years_since(66)
|
|
173
|
+
elsif year >= 1960
|
|
174
|
+
@frm = frm.years_since(67)
|
|
175
|
+
else
|
|
176
|
+
t = TRANSITION_YEARS[year][0]
|
|
177
|
+
@frm = frm.years_since(t[0]).years_since(t[1])
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def max_retirement_month
|
|
182
|
+
@adjusted_dob.at_beginning_of_month.years_since(70)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def min_retirement_month
|
|
186
|
+
if @adjusted_dob.day == 1
|
|
187
|
+
@adjusted_dob.years_since(62)
|
|
188
|
+
else
|
|
189
|
+
@adjusted_dob.at_beginning_of_month.years_since(62).months_since(1)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def early_benefit(months:)
|
|
194
|
+
if months <= 36
|
|
195
|
+
multiplier = 100.0 - ((5.0 * months) / 9.0)
|
|
196
|
+
else
|
|
197
|
+
multiplier = 100.0 - 20.0 - ((5.0 * months) / 12.0)
|
|
198
|
+
end
|
|
199
|
+
puts "EARLY PIA: #{(pia*multiplier/100.0).floor}"
|
|
200
|
+
(@pia*multiplier/100.0).floor
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def late_benefit(months:)
|
|
204
|
+
year = @adjusted_dob.year
|
|
205
|
+
if year <= 1924
|
|
206
|
+
monthly = 6/24.0
|
|
207
|
+
elsif year <= 1942
|
|
208
|
+
monthly = DELAY_RATES[year]
|
|
209
|
+
else
|
|
210
|
+
monthly = 16.0/24.0
|
|
211
|
+
end
|
|
212
|
+
multiplier = 100.0 + (monthly*months)
|
|
213
|
+
puts "LATE PIA: #{(pia*multiplier/100.0).floor}"
|
|
214
|
+
(@pia*multiplier/100.0).floor
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def month_diff(a, b)
|
|
218
|
+
(b.year * 12 + b.month) - (a.year * 12 + a.month)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def bounds_error(start:, min: nil, max: nil)
|
|
222
|
+
self.class::StartDateBounds.new(start: start, min: min, max: max)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def self.awi_projector
|
|
226
|
+
projected = []
|
|
227
|
+
projected_increases = {
|
|
228
|
+
# From https://www.ssa.gov/oact/TR/TRassum.html
|
|
229
|
+
2017 => 3.9,
|
|
230
|
+
2018 => 4.8,
|
|
231
|
+
2019 => 4.5,
|
|
232
|
+
2020 => 4.3,
|
|
233
|
+
2021 => 4.2,
|
|
234
|
+
2022 => 3.9,
|
|
235
|
+
2023 => 3.7,
|
|
236
|
+
2024 => 3.8,
|
|
237
|
+
2025 => 3.8,
|
|
238
|
+
2026 => 3.8
|
|
239
|
+
}
|
|
240
|
+
last_awi = AWI_1951_START[2016-1951]
|
|
241
|
+
(2017..2070).each do |year|
|
|
242
|
+
increase = projected_increases[[2026, year].min]
|
|
243
|
+
last_awi = (last_awi*((100+increase)/100.0)).floor
|
|
244
|
+
projected << last_awi
|
|
245
|
+
end
|
|
246
|
+
projected
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
class StartDateBounds < StandardError
|
|
250
|
+
def initialize(**kargs)
|
|
251
|
+
if not kargs[:max].nil?
|
|
252
|
+
super("Start #{kargs[:start]} is greater than max #{kargs[:max]}")
|
|
253
|
+
elsif not kargs[:min].nil?
|
|
254
|
+
super("Start #{kargs[:start]} is less than min #{kargs[:min]}")
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|