use crate::conventions::Usd;
use crate::event::{EventPayload, LedgerEvent};
use crate::identity::EventId;
use crate::state::{LedgerState, Term};
use std::collections::BTreeSet;
const PART_I_DESCRIPTION: &str = "basis estimated at the minimum daily closing price over the \
attested acquisition window (Cohan; the bearing-heavily minimum)";
const NO_LOSS_SUFFIX: &str = "; limited so as not to report a loss from the estimate";
const RISK_PARAGRAPH: &str = "Penalty exposure — if an exam determines a different basis, the penalty \
is 20% ordinary / 40% worst-case of the resulting additional tax (the underpayment attributable \
to the misstatement), plus interest; the Form 8275 disclosure and the good-faith window-low-close \
methodology mitigate, they do not eliminate, that exposure; adequate disclosure does NOT protect \
against the \u{00a7}6662(e)/(h) valuation-misstatement penalty (Woods v. Commissioner); for \
charitable-deduction property, \u{00a7}6664(c)(2) removes the reasonable-cause defense.";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Part1Item {
pub form: String,
pub line: String,
pub description: String,
pub amount: Usd,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Disclosure8275 {
pub part_i: Vec<Part1Item>,
pub part_ii: String,
pub incomplete: bool,
}
fn part_ii_narrative_for(events: &[LedgerEvent], target: &EventId) -> Option<String> {
let voided: BTreeSet<EventId> = events
.iter()
.filter_map(|e| match &e.payload {
EventPayload::VoidDecisionEvent(v) => Some(v.target_event_id.clone()),
_ => None,
})
.collect();
events.iter().find_map(|e| match &e.payload {
EventPayload::PromoteTranche(p) if p.target == *target && !voided.contains(&e.id) => {
Some(p.part_ii_narrative.clone())
}
_ => None,
})
}
pub fn disclosure_8275(
events: &[LedgerEvent],
state: &LedgerState,
year: i32,
) -> Option<Disclosure8275> {
let mut part_i: Vec<Part1Item> = Vec::new();
let mut targets: BTreeSet<EventId> = BTreeSet::new();
for d in state
.disposals
.iter()
.filter(|d| d.disposed_at.year() == year)
{
for leg in d
.legs
.iter()
.filter(|l| state.promoted_origins.contains(&l.lot_id.origin_event_id))
{
targets.insert(leg.lot_id.origin_event_id.clone());
let mut description = PART_I_DESCRIPTION.to_string();
if leg.basis >= leg.proceeds {
description.push_str(NO_LOSS_SUFFIX);
}
let line = match leg.term {
Term::ShortTerm => "Part I \u{2014} column (e)",
Term::LongTerm => "Part II \u{2014} column (e)",
}
.to_string();
part_i.push(Part1Item {
form: "8949".to_string(),
line,
description,
amount: leg.basis, });
}
}
if part_i.is_empty() {
return None;
}
let part_ii = targets
.iter()
.filter_map(|t| part_ii_narrative_for(events, t))
.collect::<Vec<_>>()
.join("\n\n");
let incomplete = part_ii.trim().is_empty();
Some(Disclosure8275 {
part_i,
part_ii,
incomplete,
})
}
impl Disclosure8275 {
pub fn render(&self) -> String {
let mut out = String::from("Form 8275 \u{2014} Disclosure Statement\n\n");
out.push_str("Part I \u{2014} Disclosure of Positions Taken\n\n");
for item in &self.part_i {
out.push_str(&format!(
" \u{2022} Form {form}, {line}: ${amount:.2} \u{2014} {desc}\n",
form = item.form,
line = item.line,
amount = item.amount,
desc = item.description,
));
}
out.push_str("\nPart II \u{2014} Detailed Explanation\n\n");
if self.incomplete {
out.push_str("[INCOMPLETE \u{2014} no Part II narrative on record]\n");
} else {
out.push_str(&self.part_ii);
out.push('\n');
}
out.push('\n');
out.push_str(RISK_PARAGRAPH);
out.push('\n');
out
}
}