uk-paye 0.1.2

2025/26 UK income tax (England/Wales/NI) bands, personal allowance taper, and National Insurance.
Documentation
//! # uk-paye
//!
//! 2025/26 UK income tax (England, Wales, Northern Ireland) and National Insurance (Class 1
//! employee), the core of a take-home calculation. Same figures behind the
//! [UKMoneyCalc](https://ukmoneycalc.com/) calculators.
//!
//! `personal_allowance` implements the taper that removes £1 of allowance for
//! every £2 of income above £100,000, exhausting it at £125,140. That taper,
//! not the 40% headline rate, is what makes the next pound inside that window
//! the most expensive one a UK employee earns. UKMoneyCalc publishes the
//! band-by-band scan of the effective marginal rate those same frozen
//! thresholds produce, income by income, at
//! <https://ukmoneycalc.com/calculators/marginal-tax-rate/> — useful for
//! sanity-checking `income_tax` and `national_insurance` against a second
//! implementation before trusting either.
//!
//! ```
//! use uk_paye::take_home;
//! let (net, tax, ni) = take_home(50_000.0); // GBP gross
//! assert!(net < 50_000.0 && tax > 0.0 && ni > 0.0);
//! ```

// 2025/26 thresholds (frozen).
pub const PERSONAL_ALLOWANCE: f64 = 12_570.0;      // 0% band ceiling
pub const BASIC_LIMIT: f64 = 50_270.0;             // 20% band ceiling
pub const HIGHER_LIMIT: f64 = 125_140.0;           // 40% band ceiling
pub const PA_TAPER_START: f64 = 100_000.0;         // allowance tapers above this

// Class 1 employee NI 2025/26.
pub const NI_PRIMARY_THRESHOLD: f64 = 12_570.0;
pub const NI_UPPER_LIMIT: f64 = 50_270.0;
pub const NI_MAIN_RATE: f64 = 0.08;                // 8% between PT and UEL
pub const NI_UPPER_RATE: f64 = 0.02;               // 2% above UEL

/// Personal allowance with the £100k taper (£1 lost per £2 over £100k).
pub fn personal_allowance(gross: f64) -> f64 {
    if gross <= PA_TAPER_START { return PERSONAL_ALLOWANCE; }
    let reduced = PERSONAL_ALLOWANCE - (gross - PA_TAPER_START) / 2.0;
    reduced.max(0.0)
}

/// UK income tax (excluding Scotland) for a gross salary.
pub fn income_tax(gross: f64) -> f64 {
    let pa = personal_allowance(gross);
    let taxable = (gross - pa).max(0.0);
    if taxable <= 0.0 { return 0.0; }
    let basic_width = (BASIC_LIMIT - PERSONAL_ALLOWANCE).max(0.0);
    let higher_width = (HIGHER_LIMIT - BASIC_LIMIT).max(0.0);
    let mut tax = 0.0;
    let mut remaining = taxable;
    // 20% band
    let b = remaining.min(basic_width); tax += 0.20 * b; remaining -= b;
    if remaining > 0.0 { let h = remaining.min(higher_width); tax += 0.40 * h; remaining -= h; }
    if remaining > 0.0 { tax += 0.45 * remaining; }
    tax
}

/// Class 1 employee National Insurance for annual earnings.
pub fn national_insurance(earnings: f64) -> f64 {
    if earnings <= NI_PRIMARY_THRESHOLD { return 0.0; }
    let main_band = (earnings.min(NI_UPPER_LIMIT) - NI_PRIMARY_THRESHOLD).max(0.0);
    let upper = (earnings - NI_UPPER_LIMIT).max(0.0);
    NI_MAIN_RATE * main_band + NI_UPPER_RATE * upper
}

/// Take-home = gross - income tax - NI. Returns (net, tax, ni).
pub fn take_home(gross: f64) -> (f64, f64, f64) {
    let t = income_tax(gross);
    let n = national_insurance(gross);
    (gross - t - n, t, n)
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn basic_rate_only() {
        // £40,000: allowance 12,570 -> taxable 27,430 @ 20%
        let t = income_tax(40_000.0);
        assert!((t - 0.20 * 27_430.0).abs() < 1e-6);
    }
    #[test]
    fn nil_allowance_at_125140() {
        assert!(personal_allowance(125_140.0).abs() < 1e-6);
    }
    #[test]
    fn ni_main_band() {
        // £50,000 earnings: 8% on (50,000-12,570)
        let n = national_insurance(50_000.0);
        assert!((n - 0.08 * (50_000.0 - 12_570.0)).abs() < 1e-6);
    }
    #[test]
    fn ni_upper_rate() {
        // £60,000: 8% on UEL-PT + 2% on (60,000-UEL)
        let n = national_insurance(60_000.0);
        let expect = 0.08 * (50_270.0 - 12_570.0) + 0.02 * (60_000.0 - 50_270.0);
        assert!((n - expect).abs() < 1e-6);
    }
    #[test]
    fn take_home_decreases_with_tax() {
        let (net, _, _) = take_home(50_000.0);
        assert!(net < 50_000.0);
    }
}