From 8c14a7704efa054e0eb64f09619901338e613b78 Mon Sep 17 00:00:00 2001 From: bodymindarts Date: Wed, 2 Oct 2024 14:46:07 +0200 Subject: [PATCH] chore: check control / limit condition --- cala-cel-interpreter/src/value.rs | 16 ++++++++++ cala-ledger-core-types/src/entry.rs | 18 +++++++++++ cala-ledger-core-types/src/primitives.rs | 25 +++++++++++++++ cala-ledger-core-types/src/transaction.rs | 19 +++++++++++ cala-ledger/src/cel_context.rs | 2 +- cala-ledger/src/velocity/context.rs | 39 +++++++++++++++++++++++ cala-ledger/src/velocity/mod.rs | 38 +++++++++++++++++++--- cala-ledger/tests/velocity.rs | 4 ++- 8 files changed, 155 insertions(+), 6 deletions(-) create mode 100644 cala-ledger/src/velocity/context.rs diff --git a/cala-cel-interpreter/src/value.rs b/cala-cel-interpreter/src/value.rs index 19d15417..d96f35b6 100644 --- a/cala-cel-interpreter/src/value.rs +++ b/cala-cel-interpreter/src/value.rs @@ -269,6 +269,22 @@ impl<'a> TryFrom<&'a CelValue> for &'a Decimal { } } +impl<'a> TryFrom> for bool { + type Error = ResultCoercionError; + + fn try_from(CelResult { expr, val }: CelResult) -> Result { + if let CelValue::Bool(b) = val { + Ok(b) + } else { + Err(ResultCoercionError::BadCoreTypeCoercion( + format!("{expr:?}"), + CelType::from(&val), + CelType::Bool, + )) + } + } +} + impl<'a> TryFrom> for NaiveDate { type Error = ResultCoercionError; diff --git a/cala-ledger-core-types/src/entry.rs b/cala-ledger-core-types/src/entry.rs index cb68fa26..258c9b01 100644 --- a/cala-ledger-core-types/src/entry.rs +++ b/cala-ledger-core-types/src/entry.rs @@ -18,3 +18,21 @@ pub struct EntryValues { pub direction: DebitOrCredit, pub description: Option, } + +mod cel { + use cel_interpreter::{CelMap, CelValue}; + + impl From<&super::EntryValues> for CelValue { + fn from(entry: &super::EntryValues) -> Self { + let mut map = CelMap::new(); + map.insert("id", entry.id); + map.insert("entry_type", entry.entry_type.clone()); + map.insert("sequence", CelValue::UInt(entry.sequence as u64)); + map.insert("layer", entry.layer); + map.insert("direction", entry.direction); + map.insert("units", entry.units); + map.insert("currency", entry.currency); + map.into() + } + } +} diff --git a/cala-ledger-core-types/src/primitives.rs b/cala-ledger-core-types/src/primitives.rs index 2b7c4b3c..aa014f68 100644 --- a/cala-ledger-core-types/src/primitives.rs +++ b/cala-ledger-core-types/src/primitives.rs @@ -57,6 +57,15 @@ impl<'a> TryFrom> for DebitOrCredit { } } +impl Into for DebitOrCredit { + fn into(self) -> CelValue { + match self { + DebitOrCredit::Debit => "DEBIT".into(), + DebitOrCredit::Credit => "CREDIT".into(), + } + } +} + #[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, sqlx::Type)] #[sqlx(type_name = "Status", rename_all = "snake_case")] #[serde(rename_all = "snake_case")] @@ -108,6 +117,16 @@ impl Default for Layer { } } +impl Into for Layer { + fn into(self) -> CelValue { + match self { + Layer::Settled => "SETTLED".into(), + Layer::Pending => "PENDING".into(), + Layer::Encumbrance => "ENCUMBRANCE".into(), + } + } +} + #[derive(Debug, Clone, Copy, Eq, Serialize, Deserialize)] #[serde(try_from = "String")] #[serde(into = "&str")] @@ -131,6 +150,12 @@ impl std::fmt::Display for Currency { } } +impl Into for Currency { + fn into(self) -> CelValue { + self.code().into() + } +} + impl std::hash::Hash for Currency { fn hash(&self, state: &mut H) { self.code().hash(state); diff --git a/cala-ledger-core-types/src/transaction.rs b/cala-ledger-core-types/src/transaction.rs index 7e33e409..b69e72e6 100644 --- a/cala-ledger-core-types/src/transaction.rs +++ b/cala-ledger-core-types/src/transaction.rs @@ -15,3 +15,22 @@ pub struct TransactionValues { pub description: Option, pub metadata: Option, } + +mod cel { + use cel_interpreter::{CelMap, CelValue}; + + impl From<&super::TransactionValues> for CelValue { + fn from(tx: &super::TransactionValues) -> Self { + let mut map = CelMap::new(); + map.insert("id", tx.id); + map.insert("journal_id", tx.journal_id); + map.insert("tx_template_id", tx.tx_template_id); + map.insert("effective", tx.effective); + map.insert("correlation_id", tx.correlation_id.clone()); + if let Some(metadata) = &tx.metadata { + map.insert("metadata", metadata.clone()); + } + map.into() + } + } +} diff --git a/cala-ledger/src/cel_context.rs b/cala-ledger/src/cel_context.rs index 2ae41100..84b9aebb 100644 --- a/cala-ledger/src/cel_context.rs +++ b/cala-ledger/src/cel_context.rs @@ -1,4 +1,4 @@ -use cel_interpreter::CelContext; +pub use cel_interpreter::CelContext; pub(crate) fn initialize() -> CelContext { let mut ctx = CelContext::new(); diff --git a/cala-ledger/src/velocity/context.rs b/cala-ledger/src/velocity/context.rs new file mode 100644 index 00000000..37177b0a --- /dev/null +++ b/cala-ledger/src/velocity/context.rs @@ -0,0 +1,39 @@ +use std::collections::HashMap; + +use cala_types::{entry::EntryValues, transaction::TransactionValues}; +use cel_interpreter::{CelMap, CelValue}; + +use crate::{cel_context::*, primitives::EntryId}; + +pub struct EvalContext { + transaction: CelValue, + entry_values: HashMap, +} + +impl EvalContext { + pub fn new(transaction: &TransactionValues) -> Self { + Self { + transaction: transaction.into(), + entry_values: HashMap::new(), + } + } + + pub fn control_context(&mut self, entry: &EntryValues) -> CelContext { + let 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", entry.clone()); + + let mut context = CelMap::new(); + context.insert("vars", vars); + + let mut ctx = initialize(); + ctx.add_variable("context", context); + + ctx + } +} diff --git a/cala-ledger/src/velocity/mod.rs b/cala-ledger/src/velocity/mod.rs index 751dbbbe..a1cf807f 100644 --- a/cala-ledger/src/velocity/mod.rs +++ b/cala-ledger/src/velocity/mod.rs @@ -1,12 +1,14 @@ mod account_control; +mod context; mod control; pub mod error; mod limit; -use cala_types::{entry::EntryValues, transaction::TransactionValues}; use chrono::{DateTime, Utc}; use sqlx::PgPool; +use cala_types::{entry::EntryValues, transaction::TransactionValues}; + pub use crate::param::Params; use crate::{atomic_operation::*, outbox::*, primitives::AccountId}; @@ -127,7 +129,7 @@ impl Velocities { op: &mut AtomicOperation<'_>, created_at: DateTime, transaction: &TransactionValues, - entries: &Vec, + entries: &[EntryValues], account_ids: &[AccountId], ) -> Result<(), VelocityError> { let controls = self @@ -135,8 +137,36 @@ impl Velocities { .find_for_enforcement(op, account_ids) .await?; - for control in controls { - // + let empty = Vec::new(); + + let mut context = context::EvalContext::new(transaction); + + for entry in entries { + for control in controls.get(&entry.account_id).unwrap_or(&empty) { + let ctx = context.control_context(entry); + let control_active = if let Some(condition) = &control.condition { + let control_active: bool = condition.try_evaluate(&ctx)?; + control_active + } else { + true + }; + if control_active { + for limit in &control.velocity_limits { + if let Some(currenty) = &limit.currency { + if currenty != &entry.currency { + continue; + } + } + + let limit_active = if let Some(condition) = &limit.condition { + let limit_active: bool = condition.try_evaluate(&ctx)?; + limit_active + } else { + true + }; + } + } + } } Ok(()) } diff --git a/cala-ledger/tests/velocity.rs b/cala-ledger/tests/velocity.rs index 179da167..fc7cdae1 100644 --- a/cala-ledger/tests/velocity.rs +++ b/cala-ledger/tests/velocity.rs @@ -83,8 +83,10 @@ async fn create_control() -> anyhow::Result<()> { .add_limit_to_control(control.id(), deposit_limit.id()) .await?; - let (one, _) = helpers::test_accounts(); + let (one, two) = helpers::test_accounts(); let one = cala.accounts().create(one).await.unwrap(); + let _ = cala.accounts().create(two).await.unwrap(); + let mut params = Params::new(); params.insert("withdrawal_limit", Decimal::from(100)); params.insert("deposit_limit", Decimal::from(100));