Skip to main content

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}