use crate::conventions::{tax_date, TaxDate, TRANSITION_DATE};
use crate::event::{EventPayload, LedgerEvent};
use crate::identity::{EventId, WalletId};
use crate::project::resolve::{method_election_is_forward, resolve_election, ElectionRec};
use crate::state::LedgerState;
use std::collections::{BTreeMap, BTreeSet};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ComplianceStatus {
StandingOrder { effective_from: TaxDate },
Contemporaneous,
AttestedRecording,
NonCompliant,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DisposalCompliance {
pub disposal: EventId,
pub wallet: WalletId,
pub date: TaxDate,
pub status: ComplianceStatus,
}
fn collect_elections(events: &[LedgerEvent], voided: &BTreeSet<EventId>) -> Vec<ElectionRec> {
let mut out = Vec::new();
for e in events {
let EventId::Decision { seq } = e.id else {
continue;
};
if voided.contains(&e.id) {
continue;
}
if let EventPayload::MethodElection(me) = &e.payload {
let made = tax_date(e.utc_timestamp, e.original_tz);
if method_election_is_forward(me, made) {
out.push(ElectionRec {
effective_from: me.effective_from,
method: me.method,
decision_seq: seq,
wallet: me.wallet.clone(),
});
}
}
}
out
}
pub fn disposal_compliance(events: &[LedgerEvent], state: &LedgerState) -> Vec<DisposalCompliance> {
let voided: BTreeSet<EventId> = events
.iter()
.filter_map(|e| match &e.payload {
EventPayload::VoidDecisionEvent(v) => Some(v.target_event_id.clone()),
_ => None,
})
.collect();
let elections = collect_elections(events, &voided);
let wallet_of: BTreeMap<EventId, WalletId> = events
.iter()
.filter_map(|e| e.wallet.clone().map(|w| (e.id.clone(), w)))
.collect();
let mut selections: Vec<(u64, &LedgerEvent)> = events
.iter()
.filter_map(|e| match e.id {
EventId::Decision { seq } => Some((seq, e)),
_ => None,
})
.filter(|(_, e)| {
!voided.contains(&e.id) && matches!(e.payload, EventPayload::LotSelection(_))
})
.collect();
selections.sort_by_key(|(s, _)| *s);
let mut sel_made: BTreeMap<EventId, TaxDate> = BTreeMap::new();
for (_seq, e) in &selections {
if let EventPayload::LotSelection(ls) = &e.payload {
sel_made.insert(
ls.disposal_event.clone(),
tax_date(e.utc_timestamp, e.original_tz),
);
}
}
let classify = |disposal: &EventId, wallet: &WalletId, date: TaxDate| -> ComplianceStatus {
let broker = matches!(wallet, WalletId::Exchange { .. });
if broker && date.year() >= 2027 {
return ComplianceStatus::NonCompliant;
}
if let Some(made) = sel_made.get(disposal) {
if *made <= date {
return ComplianceStatus::Contemporaneous;
}
return ComplianceStatus::NonCompliant;
}
if let Some(ef) = resolve_election(date, wallet, &elections).map(|e| e.effective_from) {
return ComplianceStatus::StandingOrder { effective_from: ef };
}
ComplianceStatus::NonCompliant
};
let mut out: Vec<DisposalCompliance> = Vec::new();
for d in &state.disposals {
if d.fee_mini_disposition || d.disposed_at < TRANSITION_DATE {
continue;
}
if let Some(w) = wallet_of.get(&d.event) {
out.push(DisposalCompliance {
disposal: d.event.clone(),
wallet: w.clone(),
date: d.disposed_at,
status: classify(&d.event, w, d.disposed_at),
});
}
}
for r in &state.removals {
if r.removed_at < TRANSITION_DATE {
continue;
}
if let Some(w) = wallet_of.get(&r.event) {
out.push(DisposalCompliance {
disposal: r.event.clone(),
wallet: w.clone(),
date: r.removed_at,
status: classify(&r.event, w, r.removed_at),
});
}
}
out.sort_by(|a, b| a.disposal.cmp(&b.disposal));
out
}