use crate::{CliError, Session};
use btctax_core::conventions::TRANSITION_DATE;
use btctax_core::event::DeclareTranche;
use btctax_core::persistence::{append_decision, load_all};
use btctax_core::{EventId, EventPayload, LedgerEvent, Sat, TaxDate, WalletId};
use btctax_store::Passphrase;
use std::collections::BTreeSet;
use std::path::Path;
use time::{OffsetDateTime, UtcOffset};
const ALLOCATION_IS_FINAL_HINT: &str = "revisit the in-app safe-harbor allocation; if your filed \
allocation is already final, unallocated pre-2025 units are a facts-and-circumstances matter for a \
professional";
const TRANCHE_IS_FINAL_HINT: &str = "Void the tranche first (`reconcile void <decision-ref>`); if you \
have already filed the tranche's basis ($0 or a promoted floor), unallocated pre-2025 units are a \
facts-and-circumstances matter for a professional";
fn void_targets(events: &[LedgerEvent]) -> BTreeSet<EventId> {
events
.iter()
.filter_map(|e| match &e.payload {
EventPayload::VoidDecisionEvent(v) => Some(v.target_event_id.clone()),
_ => None,
})
.collect()
}
pub fn in_force_allocation_exists(events: &[LedgerEvent]) -> bool {
let voided = void_targets(events);
events.iter().any(|e| {
matches!(e.payload, EventPayload::SafeHarborAllocation(_)) && !voided.contains(&e.id)
})
}
pub fn pre2025_tranche_exists(events: &[LedgerEvent]) -> bool {
let voided = void_targets(events);
events.iter().any(|e| {
matches!(&e.payload, EventPayload::DeclareTranche(t) if t.window_end < TRANSITION_DATE)
&& !voided.contains(&e.id)
})
}
pub fn wallet_is_known(events: &[LedgerEvent], wallet: &WalletId) -> bool {
events.iter().any(|e| {
e.wallet.as_ref() == Some(wallet)
|| matches!(&e.payload, EventPayload::DeclareTranche(t) if &t.wallet == wallet)
})
}
pub fn guard_allocation_vs_tranche(events: &[LedgerEvent]) -> Result<(), CliError> {
if pre2025_tranche_exists(events) {
return Err(CliError::Usage(format!(
"refusing to record a safe-harbor allocation while a pre-2025 conservative-filing tranche \
($0 or a promoted floor, EstimatedConservative) is on file — v1 makes the two mutually \
exclusive. {TRANCHE_IS_FINAL_HINT}."
)));
}
Ok(())
}
fn guard_tranche_vs_allocation(
events: &[LedgerEvent],
window_end: TaxDate,
) -> Result<(), CliError> {
if window_end < TRANSITION_DATE && in_force_allocation_exists(events) {
return Err(CliError::Usage(format!(
"refusing to record a pre-2025 conservative-filing tranche while a safe-harbor allocation \
is on file — v1 makes the two mutually exclusive; {ALLOCATION_IS_FINAL_HINT}."
)));
}
Ok(())
}
pub fn declare_tranche(
vault_path: &Path,
pp: &Passphrase,
sat: Sat,
wallet: WalletId,
window_start: TaxDate,
window_end: TaxDate,
now: OffsetDateTime,
) -> Result<EventId, CliError> {
if sat <= 0 {
return Err(CliError::Usage(format!(
"tranche amount must be > 0 sat (got {sat})"
)));
}
if window_start > window_end {
return Err(CliError::Usage(format!(
"tranche window_start ({window_start}) must be <= window_end ({window_end})"
)));
}
let mut session = Session::open(vault_path, pp)?;
let events = load_all(session.conn())?;
guard_tranche_vs_allocation(&events, window_end)?;
if !wallet_is_known(&events, &wallet) {
eprintln!(
"warning: --wallet {} has no prior events in this vault; if this is a typo the conservative \
tranche lot is stranded in a phantom wallet (it still files at its conservative basis — $0, \
or a promoted floor). Re-run with the intended --wallet if this was unintended.",
crate::render::wallet_label(&wallet)
);
}
let payload = EventPayload::DeclareTranche(DeclareTranche {
sat,
wallet,
window_start,
window_end,
});
let id = append_decision(session.conn(), payload, now, UtcOffset::UTC, None)?;
session.save()?;
Ok(id)
}