obbba_breaks/lib.rs
1//! # obbba-breaks
2//!
3//! Estimate the **One Big Beautiful Bill Act (OBBBA)** individual income-tax
4//! breaks: the **increased (and permanent) standard deduction** plus the
5//! temporary **above-the-line deductions for qualified overtime and tips**
6//! (tax years **2025–2028**).
7//!
8//! ## Important: these are estimates pending final IRS guidance
9//!
10//! OBBBA (signed July 2025) made the larger TCJA standard deduction permanent,
11//! added a small temporary boost for 2025–2028, and introduced the "no tax on
12//! overtime" / "no tax on tips" above-the-line deductions. The headline numbers
13//! below are widely reported, but the IRS was still finalizing implementation
14//! details (exact MAGI definitions, which tips/OT amounts qualify, inflation
15//! indexing for future years) through late 2025 and into 2026. **Every figure
16//! here is an estimate / model input**, not a guarantee of any taxpayer's actual
17//! deduction. Confirm against final IRS guidance and a qualified tax
18//! professional.
19//!
20//! Default estimated parameters used:
21//!
22//! | Parameter | Single / HoH | Married Filing Jointly |
23//! |------------------------------------|--------------|------------------------|
24//! | 2025 base standard deduction | `$15,750` | `$31,500` |
25//! | Temporary add-on (2025–2028) | `$750` | `$1,500` |
26//! | Overtime / tips deduction cap | `$12,500` | `$25,000` |
27//! | Overtime / tips MAGI phase-out | begins `$150,000` (single) / `$300,000` (joint); `$1` lost per `$10` MAGI over |
28//!
29//! ## Quick example
30//! ```
31//! use obbba_breaks::{standard_deduction, overtime_deduction, FilingStatus};
32//!
33//! // 2025 standard deduction for a single filer
34//! let sd = standard_deduction(FilingStatus::Single, 2025);
35//! assert!((sd - 16_500.0).abs() < 1e-6); // 15,750 + 750
36//!
37//! // Qualified overtime deduction (full, under the MAGI threshold)
38//! let od = overtime_deduction(8_000.0, 90_000.0, FilingStatus::Single);
39//! assert!((od - 8_000.0).abs() < 1e-6);
40//! ```
41
42/// Filing status.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum FilingStatus {
45 Single,
46 Joint,
47}
48
49/// Filing-status- and year-dependent **standard deduction** parameters
50/// (estimated OBBBA amounts).
51#[derive(Debug, Clone, Copy, PartialEq)]
52pub struct StandardDeduction {
53 /// Base (permanent, TCJA-level) standard deduction.
54 pub base: f64,
55 /// Temporary OBBBA add-on available 2025–2028 inclusive.
56 pub temporary_addon: f64,
57}
58
59impl StandardDeduction {
60 /// Estimated **single / head-of-household** standard deduction for 2025.
61 pub const SINGLE_2025: StandardDeduction = StandardDeduction {
62 base: 15_750.0,
63 temporary_addon: 750.0,
64 };
65 /// Estimated **married filing jointly** standard deduction for 2025.
66 pub const JOINT_2025: StandardDeduction = StandardDeduction {
67 base: 31_500.0,
68 temporary_addon: 1_500.0,
69 };
70
71 /// Total standard deduction: `base + temporary_addon`.
72 #[inline]
73 pub fn total(&self) -> f64 {
74 self.base + self.temporary_addon
75 }
76}
77
78/// Whether the temporary OBBBA add-on applies for the given tax year (2025–2028).
79pub fn addon_applies(year: u32) -> bool {
80 (2025..=2028).contains(&year)
81}
82
83/// Estimated **total standard deduction** for a filing status and tax year.
84///
85/// For years inside the 2025–2028 window the temporary add-on is included;
86/// outside the window (e.g., 2029 onward, once the add-on sunsets) only the
87/// permanent base is returned. Amounts are the **2025** estimates and are **not**
88/// inflation-indexed here — callers needing future-year precision should supply
89/// their own indexed [`StandardDeduction`].
90pub fn standard_deduction(status: FilingStatus, year: u32) -> f64 {
91 standard_deduction_with(status, year, |s, _| s.default_2025())
92}
93
94/// Same as [`standard_deduction`] but with a caller-supplied lookup for the
95/// base/addon parameters (e.g., to plug in IRS-inflation-indexed figures).
96pub fn standard_deduction_with<F>(status: FilingStatus, year: u32, lookup: F) -> f64
97where
98 F: Fn(FilingStatus, u32) -> StandardDeduction,
99{
100 let sd = lookup(status, year);
101 if addon_applies(year) {
102 sd.total()
103 } else {
104 sd.base
105 }
106}
107
108impl FilingStatus {
109 /// Default estimated 2025 standard-deduction parameters for this status.
110 pub fn default_2025(self) -> StandardDeduction {
111 match self {
112 FilingStatus::Single => StandardDeduction::SINGLE_2025,
113 FilingStatus::Joint => StandardDeduction::JOINT_2025,
114 }
115 }
116}
117
118// ---------------------------------------------------------------------------
119// Above-the-line overtime / tips deductions
120// ---------------------------------------------------------------------------
121
122/// Parameters for the above-the-line overtime/tips deduction (estimates).
123#[derive(Debug, Clone, Copy, PartialEq)]
124pub struct QualifiedDeductionParams {
125 /// Maximum deduction when fully eligible.
126 pub cap: f64,
127 /// MAGI at which the phase-out begins.
128 pub phase_out_start: f64,
129 /// Dollars of deduction lost per dollar of MAGI over the threshold
130 /// (`$1 per $10` → `0.1`).
131 pub phase_out_rate: f64,
132}
133
134impl QualifiedDeductionParams {
135 pub const SINGLE: QualifiedDeductionParams = QualifiedDeductionParams {
136 cap: 12_500.0,
137 phase_out_start: 150_000.0,
138 phase_out_rate: 0.1,
139 };
140 pub const JOINT: QualifiedDeductionParams = QualifiedDeductionParams {
141 cap: 25_000.0,
142 phase_out_start: 300_000.0,
143 phase_out_rate: 0.1,
144 };
145
146 #[inline]
147 pub fn phase_out_end(&self) -> f64 {
148 self.phase_out_start + self.cap / self.phase_out_rate
149 }
150}
151
152impl FilingStatus {
153 /// Default estimated overtime/tips deduction parameters for this status.
154 pub fn default_qualified_params(self) -> QualifiedDeductionParams {
155 match self {
156 FilingStatus::Single => QualifiedDeductionParams::SINGLE,
157 FilingStatus::Joint => QualifiedDeductionParams::JOINT,
158 }
159 }
160}
161
162/// Effective cap on a qualified (overtime/tips) deduction at a given MAGI,
163/// before considering how much the taxpayer actually earned.
164pub fn qualified_effective_cap(magi: f64, p: QualifiedDeductionParams) -> f64 {
165 if magi <= p.phase_out_start {
166 p.cap
167 } else if magi >= p.phase_out_end() {
168 0.0
169 } else {
170 let over = magi - p.phase_out_start;
171 (p.cap - over * p.phase_out_rate).max(0.0)
172 }
173}
174
175/// Estimated **qualified overtime** deduction — the lesser of overtime earned
176/// and the effective cap at the taxpayer's MAGI. (Estimates pending final IRS
177/// guidance.)
178pub fn overtime_deduction(qualified_overtime: f64, magi: f64, status: FilingStatus) -> f64 {
179 qualified_deduction(qualified_overtime, magi, status)
180}
181
182/// Estimated **qualified tips** deduction — same structure as overtime, capped
183/// and phased out identically. (Estimates pending final IRS guidance.)
184pub fn tips_deduction(qualified_tips: f64, magi: f64, status: FilingStatus) -> f64 {
185 qualified_deduction(qualified_tips, magi, status)
186}
187
188fn qualified_deduction(amount: f64, magi: f64, status: FilingStatus) -> f64 {
189 let amt = amount.max(0.0);
190 let cap = qualified_effective_cap(magi, status.default_qualified_params());
191 amt.min(cap)
192}
193
194/// Estimated **total** OBBBA deduction = standard deduction + overtime deduction
195/// + tips deduction, all for one filing status and year. This treats the
196/// above-the-line deductions as stacking on top of the standard deduction
197/// (which is how above-the-line deductions work).
198pub fn total_deduction(
199 status: FilingStatus,
200 year: u32,
201 qualified_overtime: f64,
202 qualified_tips: f64,
203 magi: f64,
204) -> f64 {
205 let sd = standard_deduction(status, year);
206 let ot = overtime_deduction(qualified_overtime, magi, status);
207 let tips = tips_deduction(qualified_tips, magi, status);
208 sd + ot + tips
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 #[test]
216 fn standard_deduction_single_2025_includes_addon() {
217 // 15,750 + 750 = 16,500
218 assert!((standard_deduction(FilingStatus::Single, 2025) - 16_500.0).abs() < 1e-6);
219 }
220
221 #[test]
222 fn standard_deduction_joint_2025_includes_addon() {
223 // 31,500 + 1,500 = 33,000
224 assert!((standard_deduction(FilingStatus::Joint, 2025) - 33_000.0).abs() < 1e-6);
225 }
226
227 #[test]
228 fn addon_sunsets_after_2028() {
229 // 2029 → base only, no temporary add-on
230 assert!((standard_deduction(FilingStatus::Single, 2029) - 15_750.0).abs() < 1e-6);
231 assert!((standard_deduction(FilingStatus::Joint, 2029) - 31_500.0).abs() < 1e-6);
232 }
233
234 #[test]
235 fn addon_window_boundaries() {
236 assert!(addon_applies(2025));
237 assert!(addon_applies(2028));
238 assert!(!addon_applies(2024));
239 assert!(!addon_applies(2029));
240 }
241
242 #[test]
243 fn overtime_deduction_under_threshold_full() {
244 let d = overtime_deduction(8_000.0, 90_000.0, FilingStatus::Single);
245 assert!((d - 8_000.0).abs() < 1e-6);
246 }
247
248 #[test]
249 fn overtime_deduction_capped() {
250 // $40k OT single → cap at $12,500
251 let d = overtime_deduction(40_000.0, 90_000.0, FilingStatus::Single);
252 assert!((d - 12_500.0).abs() < 1e-6);
253 }
254
255 #[test]
256 fn tips_deduction_capped_joint() {
257 // $40k tips joint → cap at $25,000
258 let d = tips_deduction(40_000.0, 200_000.0, FilingStatus::Joint);
259 assert!((d - 25_000.0).abs() < 1e-6);
260 }
261
262 #[test]
263 fn qualified_phase_out_linear() {
264 let p = QualifiedDeductionParams::SINGLE;
265 // $10k over → lose $1,000 → cap $11,500
266 assert!((qualified_effective_cap(160_000.0, p) - 11_500.0).abs() < 1e-6);
267 // end at 150k + 125k = 275k
268 assert!((p.phase_out_end() - 275_000.0).abs() < 1e-6);
269 assert_eq!(qualified_effective_cap(300_000.0, p), 0.0);
270 }
271
272 #[test]
273 fn total_deduction_stacks_all_three() {
274 // Single 2025, $8k OT + $5k tips, MAGI $90k
275 // 16,500 + 8,000 + 5,000 = 29,500
276 let t = total_deduction(FilingStatus::Single, 2025, 8_000.0, 5_000.0, 90_000.0);
277 assert!((t - 29_500.0).abs() < 1e-6);
278 }
279
280 #[test]
281 fn total_deduction_respects_caps() {
282 // Single 2025, $40k OT + $40k tips (both capped), MAGI $90k
283 // 16,500 + 12,500 + 12,500 = 41,500
284 let t = total_deduction(FilingStatus::Single, 2025, 40_000.0, 40_000.0, 90_000.0);
285 assert!((t - 41_500.0).abs() < 1e-6);
286 }
287
288 #[test]
289 fn negative_amounts_clamped() {
290 assert_eq!(overtime_deduction(-1_000.0, 90_000.0, FilingStatus::Single), 0.0);
291 }
292}