use crate::conventions::Usd;
use crate::tax::return_inputs::{
CarryProvenance, CharitableCarryItem, CharitableClass, CharitableGift,
};
use rust_decimal_macros::dec;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CharitableResult {
pub allowed_cash: Usd,
pub allowed_noncash: Usd,
pub allowed_carryover: Usd,
pub allowed: Usd,
pub carryover_out: Vec<CharitableCarryItem>,
}
fn is_expired(origin: i32, year: i32) -> bool {
year - origin > 5
}
struct ClassAlloc {
current_allowed: Usd,
carryover_allowed: Usd,
carry_out: Vec<CharitableCarryItem>,
}
fn allocate_class(
class: CharitableClass,
current_total: Usd,
carryover: &[CharitableCarryItem],
ceiling: Usd,
year: i32,
) -> ClassAlloc {
let current_allowed = current_total.min(ceiling);
let mut room = ceiling - current_allowed;
let mut carryover_allowed = Usd::ZERO;
let mut carry_out = Vec::new();
let current_excess = current_total - current_allowed;
if current_excess > Usd::ZERO {
carry_out.push(CharitableCarryItem {
class,
amount: current_excess,
origin_year: year,
provenance: CarryProvenance::default(),
});
}
for item in carryover {
let used = item.amount.min(room);
carryover_allowed += used;
room -= used;
let remaining = item.amount - used;
if remaining > Usd::ZERO {
carry_out.push(CharitableCarryItem {
class,
amount: remaining,
origin_year: item.origin_year,
provenance: item.provenance, });
}
}
ClassAlloc {
current_allowed,
carryover_allowed,
carry_out,
}
}
pub fn apply_170b(
agi: Usd,
gifts: &[CharitableGift],
carryover_in: &[CharitableCarryItem],
year: i32,
) -> CharitableResult {
use CharitableClass::{CapGainProp30, Cash60, OrdinaryProp50};
let agi = agi.max(Usd::ZERO);
let pct = |p: Usd| p * agi;
let nonneg = |v: Usd| v.max(Usd::ZERO);
let current = |class: CharitableClass| -> Usd {
gifts
.iter()
.filter(|g| g.class == class)
.map(|g| g.amount)
.sum()
};
let carry_for = |class: CharitableClass| -> Vec<CharitableCarryItem> {
let mut v: Vec<CharitableCarryItem> = carryover_in
.iter()
.filter(|c| c.class == class && !is_expired(c.origin_year, year))
.cloned()
.collect();
v.sort_by_key(|c| c.origin_year);
v
};
let cash60 = allocate_class(
Cash60,
current(Cash60),
&carry_for(Cash60),
pct(dec!(0.60)),
year,
);
let allowed_cash_tier = cash60.current_allowed + cash60.carryover_allowed;
let ord50 = allocate_class(
OrdinaryProp50,
current(OrdinaryProp50),
&carry_for(OrdinaryProp50),
nonneg(pct(dec!(0.50)) - allowed_cash_tier),
year,
);
let allowed_ord_tier = ord50.current_allowed + ord50.carryover_allowed;
let cgp30_ceiling = pct(dec!(0.30)).min(nonneg(
pct(dec!(0.50)) - allowed_cash_tier - allowed_ord_tier,
));
let cgp30 = allocate_class(
CapGainProp30,
current(CapGainProp30),
&carry_for(CapGainProp30),
cgp30_ceiling,
year,
);
let allowed_cash = cash60.current_allowed;
let allowed_noncash = ord50.current_allowed + cgp30.current_allowed;
let allowed_carryover =
cash60.carryover_allowed + ord50.carryover_allowed + cgp30.carryover_allowed;
let carryover_out = [cash60, ord50, cgp30]
.into_iter()
.flat_map(|a| a.carry_out)
.collect();
CharitableResult {
allowed_cash,
allowed_noncash,
allowed_carryover,
allowed: allowed_cash + allowed_noncash + allowed_carryover,
carryover_out,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn gift(class: CharitableClass, amount: Usd) -> CharitableGift {
CharitableGift { class, amount }
}
fn carry(class: CharitableClass, amount: Usd, origin_year: i32) -> CharitableCarryItem {
CharitableCarryItem {
class,
amount,
origin_year,
provenance: CarryProvenance::default(),
}
}
#[test]
fn deep04_worked_example_cent_exact() {
let gifts = [
gift(CharitableClass::Cash60, dec!(5000)),
gift(CharitableClass::CapGainProp30, dec!(70000)),
];
let r = apply_170b(dec!(200000), &gifts, &[], 2024);
assert_eq!(r.allowed_cash, dec!(5000)); assert_eq!(r.allowed_noncash, dec!(60000)); assert_eq!(r.allowed_carryover, Usd::ZERO); assert_eq!(r.allowed, dec!(65000)); assert_eq!(
r.carryover_out,
vec![carry(CharitableClass::CapGainProp30, dec!(10000), 2024)]
);
}
#[test]
fn thirty_percent_class_capped_by_overall_fifty_room() {
let gifts = [
gift(CharitableClass::Cash60, dec!(40000)),
gift(CharitableClass::CapGainProp30, dec!(50000)),
];
let r = apply_170b(dec!(100000), &gifts, &[], 2024);
assert_eq!(r.allowed_cash, dec!(40000));
assert_eq!(r.allowed_noncash, dec!(10000));
assert_eq!(r.carryover_out[0].amount, dec!(40000));
}
#[test]
fn kat17_same_year_short_and_long_crypto() {
let gifts = [
gift(CharitableClass::OrdinaryProp50, dec!(20000)),
gift(CharitableClass::CapGainProp30, dec!(40000)),
];
let r = apply_170b(dec!(100000), &gifts, &[], 2024);
assert_eq!(r.allowed_noncash, dec!(50000));
assert_eq!(r.allowed, dec!(50000));
assert_eq!(
r.carryover_out,
vec![carry(CharitableClass::CapGainProp30, dec!(10000), 2024)]
);
}
#[test]
fn carryover_in_consumed_after_current_year() {
let gifts = [gift(CharitableClass::Cash60, dec!(50000))];
let cin = [carry(CharitableClass::Cash60, dec!(20000), 2022)];
let r = apply_170b(dec!(100000), &gifts, &cin, 2024);
assert_eq!(r.allowed_cash, dec!(50000)); assert_eq!(r.allowed_carryover, dec!(10000)); assert_eq!(r.allowed, dec!(60000)); assert_eq!(
r.carryover_out,
vec![carry(CharitableClass::Cash60, dec!(10000), 2022)]
);
}
#[test]
fn expired_carryover_is_dropped() {
let cin = [
carry(CharitableClass::Cash60, dec!(5000), 2018), carry(CharitableClass::Cash60, dec!(3000), 2019), ];
let r = apply_170b(dec!(100000), &[], &cin, 2024);
assert_eq!(r.allowed_carryover, dec!(3000)); assert!(r.carryover_out.is_empty()); }
#[test]
fn kat13_carryover_ages_across_a_std_year() {
let cin = [carry(CharitableClass::Cash60, dec!(30000), 2022)];
let r = apply_170b(dec!(40000), &[], &cin, 2024);
assert_eq!(r.allowed_carryover, dec!(24000)); assert_eq!(
r.carryover_out,
vec![carry(CharitableClass::Cash60, dec!(6000), 2022)]
);
}
#[test]
fn oldest_vintage_consumed_first() {
let cin = [
carry(CharitableClass::Cash60, dec!(10000), 2023),
carry(CharitableClass::Cash60, dec!(10000), 2020),
];
let r = apply_170b(dec!(20000), &[], &cin, 2024);
assert_eq!(r.allowed_carryover, dec!(12000));
assert_eq!(
r.carryover_out,
vec![carry(CharitableClass::Cash60, dec!(8000), 2023)]
);
}
#[test]
fn negative_agi_clamped_to_zero_ceilings() {
let gifts = [gift(CharitableClass::Cash60, dec!(5000))];
let r = apply_170b(dec!(-10000), &gifts, &[], 2024);
assert_eq!(r.allowed_cash, Usd::ZERO);
assert_eq!(r.allowed_noncash, Usd::ZERO);
assert_eq!(r.allowed_carryover, Usd::ZERO);
assert_eq!(r.allowed, Usd::ZERO);
assert_eq!(
r.carryover_out,
vec![carry(CharitableClass::Cash60, dec!(5000), 2024)]
);
}
}