use chrono::NaiveDate;
use rust_decimal::Decimal;
use serde::Deserialize;
use tracing::instrument;
use uuid::Uuid;
use std::collections::HashMap;
use crate::{entry::*, error::*, primitives::*, transaction::NewTransaction};
use cel_interpreter::{CelContext, CelExpression};
use super::{param_definition::ParamDefinition, tx_params::TxParams};
#[derive(Debug, Clone, Deserialize)]
pub(crate) struct TxInputCel {
effective: CelExpression,
journal_id: CelExpression,
correlation_id: Option<CelExpression>,
external_id: Option<CelExpression>,
description: Option<CelExpression>,
metadata: Option<CelExpression>,
}
#[derive(Debug, Clone, Deserialize)]
pub(crate) struct EntryCel {
entry_type: CelExpression,
account_id: CelExpression,
layer: CelExpression,
direction: CelExpression,
units: CelExpression,
currency: CelExpression,
description: Option<CelExpression>,
}
#[derive(Debug, Clone)]
pub(crate) struct TxTemplateCore {
pub(super) id: TxTemplateId,
pub(super) _code: String,
pub(super) params: Option<Vec<ParamDefinition>>,
pub(super) tx_input: TxInputCel,
pub(super) entries: Vec<EntryCel>,
}
impl TxTemplateCore {
#[instrument(level = "trace", name = "sqlx_ledger.tx_template_core.prep_tx")]
pub(crate) fn prep_tx(
&self,
params: TxParams,
) -> Result<(NewTransaction, Vec<NewEntry>), SqlxLedgerError> {
let mut tx_builder = NewTransaction::builder();
tx_builder.tx_template_id(self.id);
let ctx = params.to_context(self.params.as_ref())?;
let journal_id: Uuid = self.tx_input.journal_id.try_evaluate(&ctx)?;
tx_builder.journal_id(journal_id);
let effective: NaiveDate = self.tx_input.effective.try_evaluate(&ctx)?;
tx_builder.effective(effective);
if let Some(correlation_id) = self.tx_input.correlation_id.as_ref() {
let correlation_id: Uuid = correlation_id.try_evaluate(&ctx)?;
tx_builder.correlation_id(correlation_id.into());
}
if let Some(external_id) = self.tx_input.external_id.as_ref() {
let external_id: String = external_id.try_evaluate(&ctx)?;
tx_builder.external_id(external_id);
}
if let Some(description) = self.tx_input.description.as_ref() {
let description: String = description.try_evaluate(&ctx)?;
tx_builder.description(description);
}
if let Some(metadata) = self.tx_input.metadata.as_ref() {
let metadata: serde_json::Value = metadata.try_evaluate(&ctx)?;
tx_builder.metadata(metadata);
}
let tx = tx_builder.build().expect("tx_build should succeed");
let entries = self.prep_entries(ctx)?;
Ok((tx, entries))
}
fn prep_entries(&self, ctx: CelContext) -> Result<Vec<NewEntry>, SqlxLedgerError> {
let mut new_entries = Vec::new();
let mut totals = HashMap::new();
for entry in self.entries.iter() {
let mut builder = NewEntry::builder();
let account_id: Uuid = entry.account_id.try_evaluate(&ctx)?;
builder.account_id(account_id.into());
let entry_type: String = entry.entry_type.try_evaluate(&ctx)?;
builder.entry_type(entry_type);
let layer: Layer = entry.layer.try_evaluate(&ctx)?;
builder.layer(layer);
let units: Decimal = entry.units.try_evaluate(&ctx)?;
let currency: Currency = entry.currency.try_evaluate(&ctx)?;
let direction: DebitOrCredit = entry.direction.try_evaluate(&ctx)?;
let total = totals.entry(currency).or_insert(Decimal::ZERO);
match direction {
DebitOrCredit::Debit => *total -= units,
DebitOrCredit::Credit => *total += units,
};
builder.units(units);
builder.currency(currency);
builder.direction(direction);
if let Some(description) = entry.description.as_ref() {
let description: String = description.try_evaluate(&ctx)?;
builder.description(description);
}
new_entries.push(builder.build().expect("Couldn't build entry"));
}
for (k, v) in totals {
if v != Decimal::ZERO {
return Err(SqlxLedgerError::UnbalancedTransaction(k, v));
}
}
Ok(new_entries)
}
}