use time::Date;
use super::context::{self, Leg};
use super::master::AccountSpec;
use super::state::StateView;
use super::writeset::{Done, IndexRow, MemoPlain, Outcome, Recorded, WriteSet};
use super::{
batch, calendar, clearing, closing, holds, limits, master, references, seal, structural,
};
use crate::error::Rejection;
use crate::model::{
AccountRef, Amount, BalanceLimit, Currency, Entry, EntryParts, EntryRecord, Fingerprint,
Genesis, LedgerPolicy, MemoCommitment, PostingRecord, PostingRef, Record, ValueError,
record_fingerprint,
};
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Op<const P: u8> {
Genesis,
Record(Box<Entry<P>>),
Batch(Vec<Entry<P>>),
Register {
spec: AccountSpec,
actor: String,
},
Change {
account: AccountRef,
change: ChangeInput,
effective: Date,
actor: String,
},
Currency {
code: Currency,
exponent: u8,
effective: Date,
actor: String,
},
Policy {
policy: LedgerPolicy,
effective: Date,
actor: String,
},
Period {
id: String,
start: Date,
end: Date,
},
Clear {
key: String,
debit: PostingRef,
credit: PostingRef,
amount: Amount<P>,
date: Date,
},
ResetClearing {
key: String,
clearing: u64,
date: Date,
},
Seal {
period: String,
},
CloseYear {
key: String,
on: Date,
equity: AccountRef,
},
}
impl<const P: u8> Op<P> {
#[must_use]
pub fn max_rounds(&self) -> usize {
const ROUNDS: usize = 8;
match self {
Self::Batch(drafts) => ROUNDS.saturating_mul(drafts.len().saturating_add(1)),
_ => ROUNDS,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ChangeInput {
OpenTo(Option<Date>),
Limit(BalanceLimit),
Class(Option<String>),
}
pub fn execute<const P: u8>(view: &impl StateView<P>, op: &Op<P>) -> Result<Outcome<P>, Rejection> {
match op {
Op::Genesis => Ok(genesis(view)),
Op::Record(draft) => check(view, draft),
Op::Batch(drafts) => Ok(batch::batch(view, drafts)),
Op::Register { spec, actor } => master::register(view, spec, actor),
Op::Change {
account,
change,
effective,
actor,
} => master::change(view, account, change, *effective, actor),
Op::Currency {
code,
exponent,
effective,
actor,
} => master::currency(view, *code, *exponent, *effective, actor),
Op::Policy {
policy,
effective,
actor,
} => master::policy(view, policy, *effective, actor),
Op::Period { id, start, end } => calendar::define(view, id, *start, *end),
Op::Clear {
key,
debit,
credit,
amount,
date,
} => clearing::apply(view, key, *debit, *credit, *amount, *date),
Op::ResetClearing {
key,
clearing,
date,
} => clearing::reset(view, key, *clearing, *date),
Op::Seal { period } => seal::seal(view, period),
Op::CloseYear { key, on, equity } => closing::close_year(view, key, *on, equity)
.map(|drafts| Outcome::Unchanged(Done::Closing(drafts))),
}
}
#[must_use]
pub fn validate<const P: u8>(draft: &Entry<P>) -> Vec<Rejection> {
let mut found = structural::entry(draft);
found.retain(|rejection| !matches!(rejection, Rejection::MemoSaltMissing));
found
}
pub(crate) fn account_ref<A>(account: A) -> Result<AccountRef, Rejection>
where
A: TryInto<AccountRef>,
A::Error: Into<ValueError>,
{
account.try_into().map_err(|reason| Rejection::InvalidPath {
posting: None,
reason: reason.into(),
})
}
fn genesis<const P: u8>(view: &impl StateView<P>) -> Outcome<P> {
let header = view.header();
if header.size > 0 {
return Outcome::Unchanged(Done::Genesis);
}
Outcome::from(WriteSet::of(
0,
Record::Genesis(Genesis {
ledger: header.ledger,
}),
Done::Genesis,
))
}
pub(crate) fn check<const P: u8>(
view: &impl StateView<P>,
draft: &Entry<P>,
) -> Result<Outcome<P>, Rejection> {
if let Some(first) = structural::entry(draft).into_iter().next() {
return Err(first);
}
let key = draft.key().ok_or(Rejection::InvalidKey {
reason: ValueError::Empty,
})?;
if let Some(meta) = view.entry_by_key(key) {
let handles: Option<Vec<_>> = draft
.postings()
.iter()
.map(|posting| match posting.account()? {
AccountRef::Path(path) => view.account_by_path(path).map(|a| a.handle()),
AccountRef::Handle(handle) => Some(*handle),
})
.collect();
let fingerprint = handles.and_then(|handles| draft.resolved_fingerprint(&handles).ok());
return if fingerprint.is_some() && meta.fingerprint == fingerprint {
Ok(Outcome::Unchanged(Done::Entry(Recorded::new(
meta.id,
meta.position,
true,
))))
} else {
Err(Rejection::KeyConflict {
position: meta.position,
})
};
}
let legs = draft
.postings()
.iter()
.enumerate()
.map(|(posting, p)| {
Ok(Leg {
account: p.account().cloned().ok_or(Rejection::InvalidPath {
posting: Some(structural::index(posting)),
reason: ValueError::Empty,
})?,
amount: p.amount(),
currency: p.currency(),
layer: p.get_layer(),
dimensions: p.get_dimensions(),
})
})
.collect::<Result<Vec<_>, Rejection>>()?;
let booking = draft.booking_date();
let facts = context::gather(view, booking, &legs, draft.get_reverses());
context::check(&facts, booking, draft.get_value_date(), &legs)?;
let memo = match (draft.memo_text(), draft.memo_salt()) {
(Some(text), Some(salt)) => Some(MemoPlain {
salt: *salt,
text: text.to_owned(),
}),
(Some(_), None) => return Err(Rejection::MemoSaltMissing),
(None, _) => None,
};
let postings = legs
.iter()
.zip(&facts.accounts)
.zip(draft.postings())
.map(|((leg, account), posting)| {
let account = account.as_ref().ok_or_else(|| Rejection::UnknownAccount {
account: leg.account.clone(),
})?;
Ok(PostingRecord {
handle: account.handle(),
side: posting.side(),
amount: leg.amount,
currency: leg.currency,
layer: leg.layer,
dimensions: leg.dimensions.clone(),
})
})
.collect::<Result<Vec<_>, Rejection>>()?;
let record = EntryRecord::new(EntryParts {
key: key.clone(),
booking_date: booking,
value_date: draft.get_value_date(),
captured_at: draft.get_captured_at(),
kind: draft.get_kind().cloned(),
memo: memo
.as_ref()
.map(|memo| MemoCommitment::compute(&memo.salt, &memo.text)),
documents: draft.documents().to_vec(),
postings,
reverses: draft.get_reverses(),
hold: draft.get_hold().copied(),
rates: draft.rates().to_vec(),
})
.map_err(|error| structural::record_error(error, draft.encoded_len()))?;
let parts = record.parts();
let fingerprint = record_fingerprint(
parts,
&parts.postings,
memo.as_ref().map(|memo| memo.text.as_str()),
);
finish(view, &facts, record, memo, Some(fingerprint))
}
pub(crate) fn stored_fingerprint<const P: u8>(
record: &EntryRecord<P>,
memo: Option<&MemoPlain>,
) -> Option<Fingerprint> {
let parts = record.parts();
if parts.memo.is_some() && memo.is_none() {
return None;
}
let postings: &[PostingRecord<P>] = if holds::is_resolution(parts.hold.as_ref()) {
&[]
} else {
&parts.postings
};
Some(record_fingerprint(
parts,
postings,
memo.map(|memo| memo.text.as_str()),
))
}
pub(crate) fn replay<const P: u8>(
view: &impl StateView<P>,
record: EntryRecord<P>,
memo: Option<MemoPlain>,
) -> Result<Outcome<P>, Rejection> {
let fingerprint = stored_fingerprint(&record, memo.as_ref());
if let Some(first) = structural::record(&record).into_iter().next() {
return Err(first);
}
let parts = record.parts();
if let Some(meta) = view.entry_by_key(&parts.key) {
return Ok(Outcome::Unchanged(Done::Entry(Recorded::new(
meta.id,
meta.position,
true,
))));
}
let legs: Vec<Leg<'_, P>> = if holds::is_resolution(parts.hold.as_ref()) {
Vec::new()
} else {
context::legs(&parts.postings)
};
let facts = context::gather(view, parts.booking_date, &legs, parts.reverses);
context::check(&facts, parts.booking_date, parts.value_date, &legs)?;
finish(view, &facts, record, memo, fingerprint)
}
fn finish<const P: u8>(
view: &impl StateView<P>,
facts: &context::EntryFacts<P>,
record: EntryRecord<P>,
memo: Option<MemoPlain>,
fingerprint: Option<Fingerprint>,
) -> Result<Outcome<P>, Rejection> {
references::check(facts, &record)?;
let holds::Checked {
record,
effect,
derived,
outstanding,
} = holds::check(view, record)?;
let facts = derived.as_ref().unwrap_or(facts);
let booked = limits::check(view, facts, &record, &outstanding)?;
let header = view.header();
let position = header.size;
let id = record.id(&header.ledger);
let parts = record.parts();
let mut index = vec![IndexRow::EntryKey {
key: parts.key.clone(),
id,
position,
fingerprint,
}];
if let Some(target) = parts.reverses {
index.push(IndexRow::Reversal { target, by: id });
}
let record = Record::Entry(record);
Ok(Outcome::from(WriteSet {
position,
leaf: record.to_leaf_bytes(),
record,
memo,
done: Done::Entry(Recorded::new(id, position, false)),
postings: booked.rows,
balances: booked.deltas,
index,
hold: effect,
outstanding,
}))
}