cala-ledger 0.15.7

An embeddable double sided accounting ledger built on PG/SQLx
Documentation
use std::collections::HashMap;
use tracing::instrument;

use cala_types::{
    entry::EntryValues, transaction::TransactionValues, velocity::VelocityContextAccountValues,
};
use cel_interpreter::{CelMap, CelValue};
use es_entity::clock::ClockHandle;

use crate::{
    cel_context::*,
    primitives::{AccountId, EntryId},
};

pub struct EvalContext {
    clock: ClockHandle,
    transaction: CelValue,
    entry_values: HashMap<EntryId, CelValue>,
    account_values: HashMap<AccountId, CelValue>,
}

impl EvalContext {
    pub fn new<'a>(
        clock: ClockHandle,
        transaction: &TransactionValues,
        accounts: impl Iterator<Item = &'a VelocityContextAccountValues>,
    ) -> Self {
        let account_values = accounts.map(|a| (a.id, a.into())).collect();
        Self {
            clock,
            transaction: transaction.into(),
            entry_values: HashMap::new(),
            account_values,
        }
    }

    #[instrument(name = "velocity.context_for_entry", skip(self, entry), fields(account_id = %account_id, entry_id = %entry.id), level = "debug")]
    pub fn context_for_entry(&mut self, account_id: AccountId, entry: &EntryValues) -> CelContext {
        let cel_entry = self
            .entry_values
            .entry(entry.id)
            .or_insert_with(|| entry.into());

        let mut vars = CelMap::new();
        vars.insert("transaction", self.transaction.clone());
        vars.insert("entry", cel_entry.clone());
        vars.insert(
            "account",
            self.account_values
                .get(&account_id)
                .expect("account values not set for context")
                .clone(),
        );

        let mut context = CelMap::new();
        context.insert("vars", vars);

        let mut ctx = initialize(self.clock.clone());
        ctx.add_variable("context", context);

        ctx
    }
}

#[cfg(test)]
mod tests {
    use es_entity::clock::Clock;
    use rust_decimal::Decimal;
    use serde_json::json;

    use cel_interpreter::CelExpression;

    use crate::{primitives::*, velocity::context::EvalContext};

    use super::*;

    fn transaction() -> TransactionValues {
        TransactionValues {
            id: TransactionId::new(),
            version: 1,
            created_at: chrono::Utc::now(),
            modified_at: chrono::Utc::now(),
            journal_id: JournalId::new(),
            tx_template_id: TxTemplateId::new(),
            entry_ids: vec![],
            effective: chrono::Utc::now().date_naive(),
            correlation_id: "correlation_id".to_string(),
            external_id: Some("external_id".to_string()),
            description: None,
            voided_by: None,
            void_of: None,
            metadata: Some(serde_json::json!({
                "tx": "metadata",
                "test": true,
            })),
        }
    }

    fn account_values() -> VelocityContextAccountValues {
        let id = AccountId::new();
        VelocityContextAccountValues {
            id,
            name: "name".to_string(),
            normal_balance_type: DebitOrCredit::Credit,
            external_id: None,
            metadata: Some(json!({
                "account": "metadata",
                "test": true,
            })),
        }
    }

    fn entry(account_id: AccountId, tx: &TransactionValues) -> EntryValues {
        EntryValues {
            id: EntryId::new(),
            version: 1,
            transaction_id: tx.id,
            journal_id: tx.journal_id,
            account_id,
            entry_type: "TEST".to_string(),
            sequence: 1,
            layer: Layer::Settled,
            currency: "USD".parse().unwrap(),
            direction: DebitOrCredit::Credit,
            units: Decimal::from(100),
            description: None,
            metadata: None,
        }
    }

    #[test]
    fn context_for_entry() {
        let account = account_values();
        let tx = transaction();
        let entry = entry(account.id, &tx);
        let mut context = EvalContext::new(Clock::handle().clone(), &tx, std::iter::once(&account));
        let ctx = context.context_for_entry(entry.account_id, &entry);

        let expr: CelExpression = "context.vars.transaction.id".parse().unwrap();
        let result: uuid::Uuid = expr.try_evaluate(&ctx).unwrap();
        assert!(result == uuid::Uuid::from(tx.id));

        let expr: CelExpression = "context.vars.account.metadata.test".parse().unwrap();
        let result: bool = expr.try_evaluate(&ctx).unwrap();
        assert!(result);

        let expr: CelExpression = "context.vars.entry.units == decimal('100')"
            .parse()
            .unwrap();
        let result: bool = expr.try_evaluate(&ctx).unwrap();
        assert!(result);
    }
}