1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
use serde::{Deserialize, Serialize};
use super::Dollars;
use super::escalation::{ColaAnchor, ColaSpec};
use super::is_default;
use super::triggers::Trigger;
/// What kind of income a source is; the kind decides its tax treatment.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum IncomeKind {
/// Wages; ordinary income, source of contributions, paying payroll tax.
Salary,
/// Net earnings from self-employment; ordinary income, paying
/// self-employment tax and building an earnings record.
SelfEmployment,
/// Defined-benefit pension; ordinary income.
Pension,
/// Annuity payout; treated as fully ordinary income.
Annuity,
/// Net rental income; ordinary income.
Rental,
/// Social Security benefit; taxed by the provisional-income rules.
SocialSecurity,
/// One-time untaxed receipt (inheritance, gift, sale proceeds).
Windfall,
/// Anything else; treated as ordinary income.
Other,
}
impl IncomeKind {
/// Every kind, in the order the schema declares them.
pub const ALL: &'static [Self] = &[
Self::Salary,
Self::SelfEmployment,
Self::Pension,
Self::Annuity,
Self::Rental,
Self::SocialSecurity,
Self::Windfall,
Self::Other,
];
/// The kind as a plan file spells it.
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Salary => "salary",
Self::SelfEmployment => "self-employment",
Self::Pension => "pension",
Self::Annuity => "annuity",
Self::Rental => "rental",
Self::SocialSecurity => "social-security",
Self::Windfall => "windfall",
Self::Other => "other",
}
}
/// Whether an income of this kind is earned - a salary or
/// self-employment - and so builds an earnings record and may be
/// covered by a workplace plan.
#[must_use]
pub const fn is_earned(self) -> bool {
matches!(self, Self::Salary | Self::SelfEmployment)
}
/// Whether the work an income of this kind is paid for can have a
/// workplace plan that covers its owner: any earned income's.
#[must_use]
pub const fn can_be_covered(self) -> bool {
self.is_earned()
}
/// Whether an income of this kind may state its amount as its first
/// year's: any but a Social Security benefit, which SSA states in
/// today's dollars and adjusts whether or not it is claimed.
#[must_use]
pub const fn can_escalate_from_start(self) -> bool {
!matches!(self, Self::SocialSecurity)
}
}
/// An income source, in annual today's dollars unless its `cola_from` says
/// its first year's.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Income {
/// Unique id, which scenarios and references match the income by; a
/// plan without one is refused by validation.
#[serde(default)]
pub id: String,
/// Display name; defaults to the id.
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
/// The kind, which decides tax treatment.
pub kind: IncomeKind,
/// Owning person id.
pub owner: String,
/// Annual amount (for `windfall`, the one-time amount): in today's
/// dollars, or under `cola_from = "start"` what its first year pays. A
/// `social-security` income may leave it out, and its annual benefit at
/// the claim is then computed from the owner's earnings record and the
/// salary the plan projects; stated, it is used as is.
#[serde(skip_serializing_if = "Option::is_none")]
pub amount: Option<Dollars>,
/// First year received; absent means from plan start.
#[serde(skip_serializing_if = "Option::is_none")]
pub start: Option<Trigger>,
/// Last year received; absent means through the horizon.
#[serde(skip_serializing_if = "Option::is_none")]
pub end: Option<Trigger>,
/// One-time receipt in the trigger's year; excludes `start`/`end`.
#[serde(skip_serializing_if = "Option::is_none")]
pub on: Option<Trigger>,
/// How the amount escalates: plan inflation (`true`, default), frozen
/// nominal (`false`), or a fixed annual rate of its own.
#[serde(default)]
pub cola: ColaSpec,
/// Where `cola` runs from: the plan's start (default), or the income's
/// first year, which the amount is then stated for. Not for
/// `social-security`, whose amount is always today's dollars.
#[serde(default, skip_serializing_if = "is_default")]
pub cola_from: ColaAnchor,
/// For a `salary`: the job's workplace plan covers the owner while it
/// pays, contributing or not, which decides whether a traditional IRA
/// contribution is deductible.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub covered: bool,
/// For a `salary`: what the employer takes out before tax each year -
/// health premiums, a flexible spending account - in the dollars the
/// amount is stated in, growing as it does. It reaches neither cash
/// nor any tax, and is not recorded as covered earnings.
#[serde(skip_serializing_if = "Option::is_none")]
pub pretax: Option<Dollars>,
/// For a `salary`: whether the job pays into Social Security. One that
/// does not - a non-covered public employee's - pays no Social Security
/// tax, Medicare's still, and is not recorded as covered earnings.
#[serde(default = "yes", skip_serializing_if = "Clone::clone")]
pub social_security: bool,
}
fn yes() -> bool {
true
}
impl Income {
/// Whether the income's benefit is computed rather than stated: a
/// `social-security` income with no `amount`.
#[must_use]
pub fn is_derived(&self) -> bool {
self.kind == IncomeKind::SocialSecurity && self.amount.is_none()
}
/// Whether the income is `person`'s Social Security benefit.
#[must_use]
pub fn is_benefit_of(&self, person: &str) -> bool {
self.kind == IncomeKind::SocialSecurity && self.owner == person
}
}