use std::collections::BTreeMap;
use super::context::EntryFacts;
use super::holds;
use super::state::{BalanceKey, StateView};
use super::writeset::{BalanceDelta, Outstanding, PostingRow};
use crate::error::{Rejection, ScaledAmount};
use crate::model::{Amount, Balance, BalanceLimit, EntryRecord, Layer};
use super::structural::index;
pub(crate) struct Booked<const P: u8> {
pub(crate) rows: Vec<PostingRow<P>>,
pub(crate) deltas: Vec<BalanceDelta<P>>,
}
#[derive(Clone, Copy)]
struct Moved<const P: u8> {
before: Balance<P>,
after: Balance<P>,
delta: Balance<P>,
}
pub(crate) fn check<const P: u8>(
view: &impl StateView<P>,
facts: &EntryFacts<P>,
entry: &EntryRecord<P>,
outstanding: &[Outstanding<P>],
) -> Result<Booked<P>, Rejection> {
let parts = entry.parts();
let mut moved: BTreeMap<BalanceKey, Moved<P>> = BTreeMap::new();
let mut rows = Vec::with_capacity(parts.postings.len());
for (posting, leg) in parts.postings.iter().enumerate() {
let key = BalanceKey {
handle: leg.handle,
currency: leg.currency,
layer: leg.layer,
};
let balance = facts.balances.get(&key).copied().unwrap_or(Balance::ZERO);
let slot = moved.entry(key).or_insert(Moved {
before: balance,
after: balance,
delta: Balance::ZERO,
});
let overflow = || overflow(facts, key);
slot.after = slot
.after
.post(leg.side, leg.amount)
.map_err(|_| overflow())?;
slot.delta = slot
.delta
.post(leg.side, leg.amount)
.map_err(|_| overflow())?;
rows.push(PostingRow {
index: index(posting),
key,
side: leg.side,
amount: leg.amount,
booking: parts.booking_date,
value: parts.value_date,
dimensions: leg.dimensions.clone(),
balance_after: slot.after,
});
}
if !holds::is_release(parts.hold.as_ref()) {
for (&(handle, currency), &before_holds) in &facts.outstanding {
let Some(account) = facts.account(handle) else {
continue;
};
let limit = account.latest.limit.value;
let key = BalanceKey {
handle,
currency,
layer: Layer::Settled,
};
let settled = moved.get(&key).copied().unwrap_or_else(|| {
let balance = facts.balances.get(&key).copied().unwrap_or(Balance::ZERO);
Moved {
before: balance,
after: balance,
delta: Balance::ZERO,
}
});
let after_holds = holds::outstanding_after(view, outstanding, handle, currency);
let overflow = || overflow(facts, key);
let before = exposure(limit, settled.before, before_holds).ok_or_else(overflow)?;
let after = exposure(limit, settled.after, after_holds).ok_or_else(overflow)?;
if after.is_positive() && after.exceeds(&before) {
return Err(Rejection::LimitExceeded {
account: account.path().clone(),
currency,
limit,
shortfall: after.shortfall::<P>(),
});
}
}
}
let deltas = moved
.into_iter()
.map(|(key, moved)| BalanceDelta {
key,
delta: moved.delta,
})
.collect();
Ok(Booked { rows, deltas })
}
#[derive(Debug, Clone, Copy)]
struct Exposure {
net: i128,
holds: i128,
}
impl Exposure {
const fn is_positive(&self) -> bool {
self.net > 0 || self.net.saturating_add(self.holds) > 0
}
fn exceeds(&self, other: &Self) -> bool {
let holds = other.holds.saturating_sub(self.holds);
match self.net.checked_sub(other.net) {
Some(net) => net > holds,
None => self.net > other.net,
}
}
fn shortfall<const P: u8>(&self) -> ScaledAmount {
ScaledAmount {
minor: self.net.saturating_add(self.holds),
scale: P,
}
}
}
fn exposure<const P: u8>(
limit: BalanceLimit,
settled: Balance<P>,
(hold_debits, hold_credits): (Amount<P>, Amount<P>),
) -> Option<Exposure> {
let net = settled.debit().checked_sub(settled.credit()).ok()?;
Some(match limit {
BalanceLimit::Unlimited => Exposure { net: 0, holds: 0 },
BalanceLimit::NoDebitBalance => Exposure {
net: net.to_minor(),
holds: hold_debits.to_minor(),
},
BalanceLimit::NoCreditBalance => Exposure {
net: net.negate().to_minor(),
holds: hold_credits.to_minor(),
},
})
}
fn overflow<const P: u8>(facts: &EntryFacts<P>, key: BalanceKey) -> Rejection {
Rejection::BalanceOverflow {
account: facts
.account(key.handle)
.map(|account| account.path().clone()),
currency: key.currency,
layer: key.layer,
}
}