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;
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,
}
struct Election {
effective_from: TaxDate,
decision_seq: u64,
}
fn collect_elections(events: &[LedgerEvent], voided: &BTreeSet<EventId>) -> Vec<Election> {
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(Election {
effective_from: me.effective_from,
decision_seq: seq,
});
}
}
}
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, date: TaxDate| -> ComplianceStatus {
let broker = matches!(wallet_of.get(disposal), Some(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) = elections
.iter()
.filter(|e| e.effective_from <= date)
.max_by(|a, b| {
a.effective_from
.cmp(&b.effective_from)
.then(a.decision_seq.cmp(&b.decision_seq))
})
.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, 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, r.removed_at),
});
}
}
out.sort_by(|a, b| a.disposal.cmp(&b.disposal));
out
}