use core::fmt;
use std::collections::{BTreeMap, BTreeSet};
use time::Date;
use super::query::Selection;
use crate::error::{IntegrityError, IntegrityKind};
use crate::model::{Amount, ClearingAction, ClearingEvent, EntryId, PostingRef, Side, pad};
use crate::store::PostingLine;
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct OpenItem<const P: u8 = 2> {
pub position: u64,
pub entry: EntryId,
pub posting: u16,
pub booking_date: Date,
pub value_date: Date,
pub side: Side,
pub original: Amount<P>,
pub applied: Amount<P>,
pub residual: Amount<P>,
}
impl<const P: u8> OpenItem<P> {
#[must_use]
pub const fn posting_ref(&self) -> PostingRef {
PostingRef::at(self.entry, self.posting)
}
}
impl<const P: u8> fmt::Display for OpenItem<P> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
pad(
f,
&format!(
"{} {}/{} {} {}, applied {}, open {}",
self.booking_date,
self.position,
self.posting,
self.side,
self.original,
self.applied,
self.residual
),
)
}
}
#[derive(Debug)]
struct Applied<const P: u8> {
postings: [PostingRef; 2],
amount: Amount<P>,
}
#[derive(Debug)]
pub(crate) struct OpenItemsFold<'a, const P: u8> {
selection: &'a Selection,
lines: Vec<PostingLine<P>>,
applies: BTreeMap<u64, Applied<P>>,
resets: BTreeSet<u64>,
}
impl<'a, const P: u8> OpenItemsFold<'a, P> {
pub(crate) const fn new(selection: &'a Selection) -> Self {
Self {
selection,
lines: Vec::new(),
applies: BTreeMap::new(),
resets: BTreeSet::new(),
}
}
pub(crate) fn feed(&mut self, line: PostingLine<P>) {
if self.selection.selects(&line) {
self.lines.push(line);
}
}
pub(crate) fn clearing(&mut self, position: u64, event: &ClearingEvent<P>) {
let in_force = position < self.selection.bound()
&& self.selection.end().is_none_or(|end| event.date <= end);
if !in_force {
return;
}
match &event.action {
ClearingAction::Apply {
debit,
credit,
amount,
..
} => {
self.applies.insert(
position,
Applied {
postings: [*debit, *credit],
amount: *amount,
},
);
}
ClearingAction::Reset { clearing, .. } => {
self.resets.insert(*clearing);
}
}
}
pub(crate) fn finish(self) -> Result<Vec<OpenItem<P>>, IntegrityError> {
let mut applied: BTreeMap<PostingRef, Amount<P>> = BTreeMap::new();
for (position, apply) in &self.applies {
if self.resets.contains(position) {
continue;
}
for posting in apply.postings {
let total = applied.entry(posting).or_insert(Amount::ZERO);
*total = total
.checked_add(apply.amount)
.map_err(|_| overcleared(*position))?;
}
}
let mut items = Vec::new();
for line in self.lines {
let cleared = applied
.get(&line.posting_ref())
.copied()
.unwrap_or(Amount::ZERO);
let residual = line
.amount
.checked_sub(cleared)
.map_err(|_| overcleared(line.position))?;
if residual.is_negative() {
return Err(overcleared(line.position));
}
if residual.is_positive() {
items.push(OpenItem {
position: line.position,
entry: line.entry,
posting: line.index,
booking_date: line.booking,
value_date: line.value,
side: line.side,
original: line.amount,
applied: cleared,
residual,
});
}
}
items.sort_by_key(|item| (item.position, item.posting));
Ok(items)
}
}
const fn overcleared(position: u64) -> IntegrityError {
IntegrityError::new(IntegrityKind::ClearingMismatch, Some(position))
}