use super::funejson::Value; use super::dates_uk_tax_year::uk_tax_year; use super::invest_capital_gains_tax_data::{ CGT_RATES, CGT_RATES_HISTORY, CGT_RATES_HORIZON, CGT_YEARS, CGT_YEARS_HISTORY, CGT_YEARS_HORIZON, }; use super::money_amount::{money_from_value, money_to_value, Money}; use super::money_apply_rate::apply_rate; /// Gains that share a pair of rates; they are interchangeable for every step below. struct Bucket { lower: i64, higher: i64, amount: i64, } fn gbp(minor: i64) -> Money { Money { minor, currency: "GBP".to_string(), } } fn check_money(value: &Money, name: &str) { if value.currency != "GBP" { panic!("{} must be in GBP, received {}", name, value.currency); } } fn pruned(history: &str, horizon: Option<&str>, date: &str) -> String { match horizon { Some(h) if date < h => format!( ": this build was installed with history={}, so it only carries rules from {}. Reinstall with history=full for earlier years.", history, h ), _ => String::new(), } } fn rate_for(disposal: &CgtDisposal) -> (i64, i64) { for rule in CGT_RATES { if rule.kind != disposal.kind || disposal.date.as_str() < rule.valid_from { continue; } if let Some(to) = rule.valid_to { if disposal.date.as_str() > to { continue; } } return (rule.lower_basis_points, rule.higher_basis_points); } panic!( "no CGT rate for {} gains on {}{}", disposal.kind, disposal.date, pruned(CGT_RATES_HISTORY, CGT_RATES_HORIZON, &disposal.date) ); } /// Capital Gains Tax for an individual for one UK tax year. /// /// The year's losses come off its gains in full, even when that wastes the /// annual exempt amount; losses brought forward only bring the net gains down /// to the annual exempt amount. Losses and the exempt amount go against the /// gains charged at the highest rate first, and what taxable income leaves of /// the basic rate band goes to the gains whose two rates are furthest apart: /// that is the "most beneficial" order TCGA 1992 s1F and s1K allow. /// /// # Panics /// Panics on a tax year or disposal date the rules do not cover, a disposal /// outside the tax year, amounts not in GBP, or negative income or losses /// brought forward. pub fn capital_gains_tax( tax_year_date: &str, disposals: &[CgtDisposal], taxable_income: &Money, losses_brought_forward: &Money, ) -> CgtResult { let year = uk_tax_year(tax_year_date); let rules = CGT_YEARS .iter() .find(|row| row.valid_from <= year.start.as_str() && row.valid_to.map(|to| year.start.as_str() <= to).unwrap_or(true)); let rules = match rules { Some(r) => r, None => panic!( "no CGT rules for the {} tax year{}", year.label, pruned(CGT_YEARS_HISTORY, CGT_YEARS_HORIZON, &year.start) ), }; check_money(taxable_income, "taxableIncome"); check_money(losses_brought_forward, "lossesBroughtForward"); if taxable_income.minor < 0 { panic!("taxableIncome must not be negative, received {}", taxable_income.minor); } if losses_brought_forward.minor < 0 { panic!("lossesBroughtForward must not be negative, received {}", losses_brought_forward.minor); } let mut buckets: Vec = Vec::new(); let mut gains: i64 = 0; let mut losses: i64 = 0; for disposal in disposals { check_money(&disposal.gain, "gain"); if disposal.kind != "residential" && disposal.kind != "other" { panic!("kind must be residential or other, received \"{}\"", disposal.kind); } if disposal.date < year.start || disposal.date > year.end { panic!("the disposal on {} is outside the {} tax year", disposal.date, year.label); } let (lower, higher) = rate_for(disposal); if disposal.gain.minor <= 0 { losses -= disposal.gain.minor; continue; } gains += disposal.gain.minor; match buckets.iter_mut().find(|b| b.lower == lower && b.higher == higher) { Some(bucket) => bucket.amount += disposal.gain.minor, None => buckets.push(Bucket { lower, higher, amount: disposal.gain.minor, }), } } let net = gains - losses; let annual_exempt_amount = rules.annual_exempt_amount; let mut brought_forward_used = 0; let mut exempt_used = 0; let mut taxable = 0; let mut carried = losses_brought_forward.minor; if net > 0 { brought_forward_used = losses_brought_forward.minor.min((net - annual_exempt_amount).max(0)); exempt_used = annual_exempt_amount.min(net - brought_forward_used); taxable = net - brought_forward_used - exempt_used; carried -= brought_forward_used; } else { carried += -net; } // Deductions against the gains taxed hardest first. let mut deductions = gains - taxable; buckets.sort_by(|a, b| b.higher.cmp(&a.higher).then(b.lower.cmp(&a.lower))); for bucket in buckets.iter_mut() { let used = bucket.amount.min(deductions); bucket.amount -= used; deductions -= used; } // The basic rate band where it saves the most: the widest gap between the two rates. let band_available = (rules.basic_rate_band - taxable_income.minor).max(0); let mut band = band_available; buckets.sort_by(|a, b| (b.higher - b.lower).cmp(&(a.higher - a.lower)).then(b.higher.cmp(&a.higher))); let mut charged: Vec<(i64, i64)> = Vec::new(); let mut charge = |rate: i64, amount: i64| { if amount > 0 { match charged.iter_mut().find(|(r, _)| *r == rate) { Some(entry) => entry.1 += amount, None => charged.push((rate, amount)), } } }; for bucket in &buckets { let lower = bucket.amount.min(band); band -= lower; charge(bucket.lower, lower); charge(bucket.higher, bucket.amount - lower); } charged.sort_by_key(|(rate, _)| *rate); let lines: Vec = charged .iter() .map(|(rate, amount)| CgtLine { rate_basis_points: *rate, gains: gbp(*amount), tax: apply_rate(&gbp(*amount), *rate, "down"), }) .collect(); let tax: i64 = lines.iter().map(|line| line.tax.minor).sum(); CgtResult { tax_year: year.label.clone(), gains: gbp(gains), losses: gbp(losses), losses_brought_forward_used: gbp(brought_forward_used), annual_exempt_amount: gbp(annual_exempt_amount), annual_exempt_amount_used: gbp(exempt_used), taxable_gains: gbp(taxable), basic_rate_band_available: gbp(band_available), lines, tax: gbp(tax), losses_carried_forward: gbp(carried), } } pub fn cgt_line_to_value(line: &CgtLine) -> Value { Value::obj(vec![ ("rateBasisPoints", Value::Int(line.rate_basis_points)), ("gains", money_to_value(&line.gains)), ("tax", money_to_value(&line.tax)), ]) } pub fn cgt_result_to_value(r: &CgtResult) -> Value { Value::obj(vec![ ("taxYear", Value::str(&r.tax_year)), ("gains", money_to_value(&r.gains)), ("losses", money_to_value(&r.losses)), ("lossesBroughtForwardUsed", money_to_value(&r.losses_brought_forward_used)), ("annualExemptAmount", money_to_value(&r.annual_exempt_amount)), ("annualExemptAmountUsed", money_to_value(&r.annual_exempt_amount_used)), ("taxableGains", money_to_value(&r.taxable_gains)), ("basicRateBandAvailable", money_to_value(&r.basic_rate_band_available)), ("lines", Value::Arr(r.lines.iter().map(cgt_line_to_value).collect())), ("tax", money_to_value(&r.tax)), ("lossesCarriedForward", money_to_value(&r.losses_carried_forward)), ]) } /// Refuse a fractional number of pence with the wording TypeScript and Python use. fn whole_money(v: &Value, name: &str) -> Money { if let Value::Float(f) = v.get("minor") { if f.fract() != 0.0 { panic!("{} must be a whole number of pence, received {}", name, f); } } money_from_value(v) } pub fn cgt_disposal_from_value(v: &Value) -> CgtDisposal { CgtDisposal { date: v.get("date").as_str().to_string(), kind: v.get("kind").as_str().to_string(), gain: whole_money(v.get("gain"), "gain"), } } pub fn fune_vector(args: &[Value]) -> Value { let disposals: Vec = args[1].as_arr().iter().map(cgt_disposal_from_value).collect(); cgt_result_to_value(&capital_gains_tax( args[0].as_str(), &disposals, &whole_money(&args[2], "taxableIncome"), &whole_money(&args[3], "lossesBroughtForward"), )) }