obbba_overtime/lib.rs
1//! # obbba-overtime
2//!
3//! Estimate the **One Big Beautiful Bill Act (OBBBA)** above-the-line deduction
4//! for *qualified overtime compensation* — the so-called "no tax on overtime" —
5//! including its Modified Adjusted Gross Income (MAGI) phase-out.
6//!
7//! ## Important: these are estimates pending final IRS guidance
8//!
9//! OBBBA created a temporary (tax years **2025–2028**) above-the-line deduction
10//! for FLSA-qualified overtime pay. The headline parameters below are widely
11//! reported from the statute and early IRS guidance, but the IRS was still
12//! issuing implementation details (e.g., exactly how MAGI is computed for this
13//! purpose, treatment of bonuses, and the precise phase-out slope) through late
14//! 2025 and into 2026. **Every figure here is an estimate / model input — not a
15//! guarantee of any taxpayer's actual deduction.** Always confirm against the
16//! final IRS guidance and a qualified tax professional before relying on a
17//! number.
18//!
19//! The figures this crate uses by default:
20//!
21//! | Parameter | Single / HoH | Married Filing Jointly |
22//! |---------------------------------|--------------|------------------------|
23//! | Maximum deduction (cap) | `$12,500` | `$25,000` |
24//! | MAGI phase-out begins | `$150,000` | `$300,000` |
25//! | Phase-out reduction | `$1` of deduction lost per `$10` of MAGI over the threshold |
26//!
27//! ## Quick example
28//! ```
29//! use obbba_overtime::{deduction, FilingStatus};
30//!
31//! let d = deduction(8_000.0, 90_000.0, FilingStatus::Single);
32//! // $8,000 of qualified OT, MAGI well under the $150k threshold → full amount
33//! assert!((d - 8_000.0).abs() < 1e-6);
34//!
35//! let capped = deduction(40_000.0, 90_000.0, FilingStatus::Single);
36//! // OT above the $12,500 single cap is not deductible
37//! assert!((capped - 12_500.0).abs() < 1e-6);
38//! ```
39
40/// Filing-status-dependent deduction parameters.
41///
42/// All fields are the **estimated** statutory values; they are configurable so
43/// the crate can track IRS guidance as it finalizes.
44#[derive(Debug, Clone, Copy, PartialEq)]
45pub struct Params {
46 /// Maximum deduction for the full (under-threshold) overtime amount.
47 pub cap: f64,
48 /// MAGI at which the phase-out *begins* (deduction starts shrinking).
49 pub phase_out_start: f64,
50 /// Phase-out rate: dollars of deduction lost **per dollar** of MAGI above
51 /// the threshold. The widely reported "$1 per $10 of MAGI" → `0.1`.
52 pub phase_out_rate: f64,
53}
54
55impl Params {
56 /// Estimated parameters for **Single / Head of Household** filers.
57 pub const SINGLE: Params = Params {
58 cap: 12_500.0,
59 phase_out_start: 150_000.0,
60 phase_out_rate: 0.1, // $1 deduction lost per $10 MAGI over threshold
61 };
62
63 /// Estimated parameters for **Married Filing Jointly**.
64 pub const JOINT: Params = Params {
65 cap: 25_000.0,
66 phase_out_start: 300_000.0,
67 phase_out_rate: 0.1,
68 };
69
70 /// MAGI at which the deduction is fully phased out (reaches $0) given the
71 /// configured rate.
72 #[inline]
73 pub fn phase_out_end(&self) -> f64 {
74 self.phase_out_start + self.cap / self.phase_out_rate
75 }
76}
77
78/// Filing status, used to select default [`Params`].
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum FilingStatus {
81 Single,
82 Joint,
83}
84
85impl FilingStatus {
86 /// Default (estimated) parameters for this filing status.
87 pub fn default_params(self) -> Params {
88 match self {
89 FilingStatus::Single => Params::SINGLE,
90 FilingStatus::Joint => Params::JOINT,
91 }
92 }
93}
94
95/// The maximum deduction available to a taxpayer with the given MAGI, *before*
96/// considering how much overtime they actually earned (i.e., the lesser of the
97/// statutory cap and what the phase-out leaves available).
98///
99/// Returns the **effective cap** at that income. Below the phase-out start it
100/// equals `params.cap`; above the phase-out end it equals `0`.
101pub fn effective_cap(magi: f64, params: Params) -> f64 {
102 if magi <= params.phase_out_start {
103 params.cap
104 } else if magi >= params.phase_out_end() {
105 0.0
106 } else {
107 let over = magi - params.phase_out_start;
108 (params.cap - over * params.phase_out_rate).max(0.0)
109 }
110}
111
112/// Estimated **overtime deduction** for a taxpayer.
113///
114/// `qualified_overtime` is the FLSA overtime compensation received (typically
115/// the *premium* half-time portion, 0.5 × OT hours × hourly rate, but callers
116/// should follow the IRS definition). `magi` is Modified Adjusted Gross Income.
117/// The deduction is the **lesser** of the overtime earned and the effective cap
118/// at the taxpayer's MAGI.
119///
120/// All inputs are clamped to be non-negative. Result is the estimated
121/// above-the-line deduction in dollars.
122pub fn deduction(qualified_overtime: f64, magi: f64, status: FilingStatus) -> f64 {
123 deduction_with(qualified_overtime, magi, status.default_params())
124}
125
126/// Same as [`deduction`] but with explicit [`Params`] (override the defaults).
127pub fn deduction_with(qualified_overtime: f64, magi: f64, params: Params) -> f64 {
128 let ot = qualified_overtime.max(0.0);
129 let cap = effective_cap(magi, params);
130 ot.min(cap)
131}
132
133/// Estimated federal income-tax savings from the overtime deduction, given a
134/// **marginal tax rate** (e.g., 0.22 for the 22% bracket).
135///
136/// `savings = deduction × marginal_rate`. This is a simplified estimate: it
137/// assumes the deduction reduces income taxed entirely at the marginal rate and
138/// ignores interactions with other phase-outs, AMT, and state tax.
139pub fn tax_savings(qualified_overtime: f64, magi: f64, marginal_rate: f64, status: FilingStatus) -> f64 {
140 deduction(qualified_overtime, magi, status) * marginal_rate.max(0.0)
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn under_threshold_full_ot() {
149 // $8k OT, $90k MAGI single → deduct all $8k
150 let d = deduction(8_000.0, 90_000.0, FilingStatus::Single);
151 assert!((d - 8_000.0).abs() < 1e-6);
152 }
153
154 #[test]
155 fn ot_above_cap_capped() {
156 // $40k OT, $90k MAGI single → cap at $12,500
157 let d = deduction(40_000.0, 90_000.0, FilingStatus::Single);
158 assert!((d - 12_500.0).abs() < 1e-6);
159 }
160
161 #[test]
162 fn joint_cap_is_25k() {
163 let d = deduction(50_000.0, 200_000.0, FilingStatus::Joint);
164 assert!((d - 25_000.0).abs() < 1e-6);
165 }
166
167 #[test]
168 fn phase_out_reduces_linearly() {
169 // Single, MAGI $160k → $10k over threshold → lose $1,000 → cap $11,500
170 let p = Params::SINGLE;
171 assert!((effective_cap(160_000.0, p) - 11_500.0).abs() < 1e-6);
172 // $20k over → lose $2,000 → cap $10,500
173 assert!((effective_cap(170_000.0, p) - 10_500.0).abs() < 1e-6);
174 }
175
176 #[test]
177 fn phase_out_end_is_zero() {
178 let p = Params::SINGLE;
179 // cap / rate = 12,500 / 0.1 = 125,000 over threshold → end at $275k
180 assert!((p.phase_out_end() - 275_000.0).abs() < 1e-6);
181 assert!(effective_cap(275_000.0, p).abs() < 1e-6);
182 assert_eq!(effective_cap(300_000.0, p), 0.0);
183 }
184
185 #[test]
186 fn deduction_at_phase_out_is_min_of_ot_and_effective_cap() {
187 // Single, $15k OT, $160k MAGI → effective cap $11,500 → deduct $11,500
188 let d = deduction(15_000.0, 160_000.0, FilingStatus::Single);
189 assert!((d - 11_500.0).abs() < 1e-6);
190 }
191
192 #[test]
193 fn negative_inputs_clamped() {
194 assert_eq!(deduction(-1_000.0, 90_000.0, FilingStatus::Single), 0.0);
195 }
196
197 #[test]
198 fn tax_savings_uses_marginal_rate() {
199 // $10k OT fully deductible at 22% → $2,200 savings
200 let s = tax_savings(10_000.0, 80_000.0, 0.22, FilingStatus::Single);
201 assert!((s - 2_200.0).abs() < 1e-6);
202 }
203
204 #[test]
205 fn custom_params_override() {
206 let custom = Params {
207 cap: 5_000.0,
208 phase_out_start: 100_000.0,
209 phase_out_rate: 0.2,
210 };
211 // $20k OT, $90k MAGI → capped at custom $5k
212 assert!((deduction_with(20_000.0, 90_000.0, custom) - 5_000.0).abs() < 1e-6);
213 }
214}