use crate::{CliError, Session};
use btctax_core::event::ConsentTerm;
use btctax_core::Usd;
use btctax_store::Passphrase;
use std::collections::BTreeSet;
use std::path::Path;
use time::OffsetDateTime;
use crate::eventref::parse_event_id;
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
pub enum ProvenanceKind {
Purchase,
Gift,
Inheritance,
Mining,
Earned,
Airdrop,
Fork,
}
impl ProvenanceKind {
pub const ALL: &[ProvenanceKind] = &[
ProvenanceKind::Purchase,
ProvenanceKind::Gift,
ProvenanceKind::Inheritance,
ProvenanceKind::Mining,
ProvenanceKind::Earned,
ProvenanceKind::Airdrop,
ProvenanceKind::Fork,
];
pub fn label(self) -> &'static str {
match self {
ProvenanceKind::Purchase => "purchase",
ProvenanceKind::Gift => "gift",
ProvenanceKind::Inheritance => "inheritance",
ProvenanceKind::Mining => "mining",
ProvenanceKind::Earned => "staking/earning",
ProvenanceKind::Airdrop => "airdrop",
ProvenanceKind::Fork => "fork",
}
}
}
pub const PROMOTE_ACK_PHRASE: &str = "I understand and accept this estimated-basis risk";
pub const PROVENANCE_TEXT: &str = "these units were acquired by purchase within the declared window — \
not by gift, inheritance, mining, staking/earning, airdrop, fork, or any acquisition other than \
purchase";
pub const PROVENANCE_VERSION: &str = "v1";
const CONSENT_INTRO: &str = "Promoting this tranche is a KNOWING choice to file a >$0 basis floor \
(the minimum daily closing price over the attested acquisition window) instead of the IRS-fallback \
$0. If an exam determines the correct basis is $0, 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 this exposure, \
but do not eliminate it and do not guarantee immunity from penalty.";
fn render_term(term: &ConsentTerm, gift_only_years: &BTreeSet<i32>) -> String {
match term {
ConsentTerm::ComputedTax {
year,
delta_usd,
deduction_delta_usd,
} => {
let mut line = if *delta_usd >= Usd::ZERO {
format!(
"Year {year}: promoting this tranche SAVES ~${} in computed federal tax.",
delta_usd.round_dp(2)
)
} else {
format!(
"Year {year}: promoting this tranche INCREASES computed federal tax by ~${}.",
(-*delta_usd).round_dp(2)
)
};
if let Some(d) = deduction_delta_usd {
line.push_str(&format!(
" The computed tax figure does NOT capture this charitable-deduction change (priced \
only on the full return); its own Δ is ~${}.",
d.round_dp(2)
));
if gift_only_years.contains(year) {
line.push_str(
" That Δ is a donee-basis (§1015) documentation change; the donor's 1040 is \
unaffected — NOT a Schedule-A deduction.",
);
}
}
line
}
ConsentTerm::Uncomputable {
year,
gain_delta_usd,
deduction_delta_usd,
} => {
let mut line = format!(
"Year {year}: tax not computable here (no table/profile/blocked) — promoting changes the \
reported gain by ~${} and the deduction/basis by ~${}.",
gain_delta_usd.round_dp(2),
deduction_delta_usd.round_dp(2)
);
if *deduction_delta_usd != Usd::ZERO && gift_only_years.contains(year) {
line.push_str(
" The deduction/basis figure is a donee-basis (§1015) documentation change; the \
donor's 1040 is unaffected — NOT a Schedule-A deduction.",
);
}
line
}
ConsentTerm::CascadeNamed { year } => format!(
"Year {year}: this promote's cross-year effects may also shift that year's §1212(b)/§170(d) \
carryover-in (named here, not separately quantified)."
),
ConsentTerm::Unrealized {
sat,
hypothetical_reduction,
as_of,
} => match (hypothetical_reduction, as_of) {
(Some(r), Some(d)) => format!(
"{sat} sat remain undisposed: at the {d} close, promoting would reduce a future sale's \
reported gain by up to ~${} (hypothetical, not a filed figure) — saving and exposure \
accrue only at disposal.",
r.round_dp(2)
),
_ => format!(
"{sat} sat remain undisposed: no current bundled price is available — the filed floor \
itself is the maximum possible gain reduction on a future sale (hypothetical, not a \
filed figure) — saving and exposure accrue only at disposal."
),
},
}
}
pub fn render_consent(terms: &[ConsentTerm], gift_only_years: &BTreeSet<i32>) -> String {
let mut out = String::new();
out.push_str(CONSENT_INTRO);
for term in terms {
out.push('\n');
out.push_str(&render_term(term, gift_only_years));
}
out
}
pub fn promote_tranche(
vault_path: &Path,
pp: &Passphrase,
target_ref: &str,
provenance: ProvenanceKind,
part_ii: String,
acknowledge: Option<&str>,
now: OffsetDateTime,
) -> Result<btctax_core::EventId, CliError> {
let target_event_id = parse_event_id(target_ref)?;
let mut session = Session::open(vault_path, pp)?;
let events = btctax_core::persistence::load_all(session.conn())?;
let cfg = session.config()?.to_projection();
let plan = crate::chokepoint::plan_promote(
&events,
session.prices(),
&cfg,
&target_event_id,
provenance,
&part_ii,
now,
)
.map_err(CliError::from)?;
println!("{}", crate::chokepoint::render_consent(&plan));
let event_id = crate::chokepoint::apply_promote(&mut session, plan, acknowledge, now)?;
eprint!("\n⚠ {}", btctax_core::experimental::NOTICE.plain_text());
Ok(event_id)
}
#[cfg(test)]
mod tests {
use super::*;
use clap::ValueEnum;
#[test]
fn provenance_all_covers_every_clap_value_variant() {
let variants = ProvenanceKind::value_variants();
assert_eq!(
ProvenanceKind::ALL.len(),
variants.len(),
"ProvenanceKind::ALL must list EVERY variant (the closed BG-D5 enumeration): {:?} vs {:?}",
ProvenanceKind::ALL,
variants
);
for v in variants {
assert!(
ProvenanceKind::ALL.contains(v),
"ProvenanceKind::ALL is missing {v:?} — a driver could never offer it to the filer"
);
}
assert_eq!(
ProvenanceKind::ALL[0],
ProvenanceKind::Purchase,
"Purchase (the only gate-passing value) stays first so a driver's own ordering is stable"
);
}
#[test]
fn every_provenance_label_is_distinct_and_non_empty() {
let mut seen = std::collections::BTreeSet::new();
for k in ProvenanceKind::ALL {
let l = k.label();
assert!(!l.trim().is_empty(), "{k:?} has an empty label");
assert!(seen.insert(l), "duplicate provenance label: {l}");
}
}
}