use crate::conventions::{tax_date, TaxDate};
use crate::identity::{EventId, WalletId};
use crate::project::pools::{pool_key, PoolKey};
use crate::state::{BlockerKind, LedgerState};
use crate::LedgerEvent;
use std::collections::{BTreeMap, BTreeSet};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Shortfall {
pub event: EventId,
pub wallet: Option<WalletId>,
pub date: TaxDate,
pub short_sat: i64,
pub fee_sat: i64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Triage {
DeclareCandidate(Shortfall),
ResolveFirst {
shortfall: Shortfall,
blocker: BlockerKind,
},
DataFix(EventId),
}
pub fn shortfalls(state: &LedgerState) -> Vec<Shortfall> {
let mut by_event: BTreeMap<EventId, (Option<WalletId>, TaxDate, i64, i64)> = BTreeMap::new();
for r in &state.shortfalls {
let entry = by_event
.entry(r.event.clone())
.or_insert_with(|| (r.wallet.clone(), r.date, 0, 0));
entry.2 += r.principal_sat + r.fee_sat;
entry.3 += r.fee_sat;
}
by_event
.into_iter()
.map(|(event, (wallet, date, short_sat, fee_sat))| Shortfall {
event,
wallet,
date,
short_sat,
fee_sat,
})
.collect()
}
fn is_open_acquisition_kind(kind: BlockerKind) -> bool {
matches!(
kind,
BlockerKind::UnknownBasisInbound
| BlockerKind::Unclassified
| BlockerKind::ImportConflict
| BlockerKind::UnmatchedOutflows
)
}
pub fn triage(events: &[LedgerEvent], state: &LedgerState) -> Vec<Triage> {
let by_id: BTreeMap<&EventId, &LedgerEvent> = events.iter().map(|e| (&e.id, e)).collect();
let open: Vec<(PoolKey, TaxDate, BlockerKind)> = state
.blockers
.iter()
.filter(|b| is_open_acquisition_kind(b.kind))
.filter_map(|b| {
let ev_id = b.event.as_ref()?;
let le = by_id.get(ev_id)?;
let w = le.wallet.as_ref()?;
let d = tax_date(le.utc_timestamp, le.original_tz);
Some((pool_key(d, w), d, b.kind))
})
.collect();
let sf = shortfalls(state);
let sf_events: BTreeSet<EventId> = sf.iter().map(|s| s.event.clone()).collect();
let mut out = Vec::new();
for b in &state.blockers {
if b.kind == BlockerKind::UncoveredDisposal {
if let Some(ev_id) = &b.event {
if !sf_events.contains(ev_id) {
out.push(Triage::DataFix(ev_id.clone()));
}
}
}
}
for s in sf {
let Some(w) = s.wallet.clone() else {
out.push(Triage::DataFix(s.event.clone()));
continue;
};
let pool = pool_key(s.date, &w);
match open.iter().find(|(p, d, _)| *p == pool && *d <= s.date) {
Some((_, _, kind)) => out.push(Triage::ResolveFirst {
shortfall: s,
blocker: *kind,
}),
None => out.push(Triage::DeclareCandidate(s)),
}
}
out
}