mod entity;
mod repo;
pub mod error;
use chrono::NaiveDate;
use es_entity::clock::ClockHandle;
use rust_decimal::Decimal;
use sqlx::PgPool;
use std::collections::HashMap;
use tracing::instrument;
use uuid::Uuid;
use crate::outbox::*;
pub use crate::param::*;
use crate::{entry::NewEntry, primitives::*, transaction::NewTransaction};
pub use entity::*;
use error::*;
pub use repo::tx_template_cursor::TxTemplateByCodeCursor;
use repo::*;
pub(crate) struct PreparedTransaction {
pub(crate) tx_id: TransactionId,
pub(crate) journal_id: JournalId,
pub(crate) tx_template_id: TxTemplateId,
pub(crate) effective: NaiveDate,
pub(crate) correlation_id: Option<String>,
pub(crate) external_id: Option<String>,
pub(crate) description: Option<String>,
pub(crate) metadata: Option<serde_json::Value>,
pub(crate) entries: Vec<NewEntry>,
}
impl PreparedTransaction {
pub(crate) fn into_new_transaction(
self,
created_at: chrono::DateTime<chrono::Utc>,
) -> (NewTransaction, Vec<NewEntry>) {
let mut builder = NewTransaction::builder();
builder
.id(self.tx_id)
.created_at(created_at)
.journal_id(self.journal_id)
.tx_template_id(self.tx_template_id)
.effective(self.effective)
.entry_ids(self.entries.iter().map(|e| e.id).collect());
if let Some(correlation_id) = self.correlation_id {
builder.correlation_id(correlation_id);
}
if let Some(external_id) = self.external_id {
builder.external_id(external_id);
}
if let Some(description) = self.description {
builder.description(description);
}
if let Some(metadata) = self.metadata {
builder.metadata(Some(metadata));
}
(
builder.build().expect("tx_build should succeed"),
self.entries,
)
}
}
#[derive(Clone)]
pub struct TxTemplates {
repo: TxTemplateRepo,
clock: ClockHandle,
}
impl TxTemplates {
pub(crate) fn new(pool: &PgPool, publisher: &OutboxPublisher, clock: &ClockHandle) -> Self {
Self {
repo: TxTemplateRepo::new(pool, publisher),
clock: clock.clone(),
}
}
#[instrument(name = "cala_ledger.tx_template.create", skip(self))]
pub async fn create(
&self,
new_tx_template: NewTxTemplate,
) -> Result<TxTemplate, TxTemplateError> {
let mut op = self.repo.begin_op_with_clock(&self.clock).await?;
let tx_template = self.create_in_op(&mut op, new_tx_template).await?;
op.commit().await?;
Ok(tx_template)
}
#[instrument(name = "cala_ledger.tx_template.create_in_op", skip(self, db))]
pub async fn create_in_op(
&self,
db: &mut impl es_entity::AtomicOperation,
new_tx_template: NewTxTemplate,
) -> Result<TxTemplate, TxTemplateError> {
let tx_template = self.repo.create_in_op(db, new_tx_template).await?;
Ok(tx_template)
}
#[instrument(level = "debug", name = "cala_ledger.tx_templates.find_all", skip(self, tx_template_ids), fields(tx_template_ids_count = tx_template_ids.len()))]
pub async fn find_all<T: From<TxTemplate>>(
&self,
tx_template_ids: &[TxTemplateId],
) -> Result<HashMap<TxTemplateId, T>, TxTemplateError> {
Ok(self.repo.find_all(tx_template_ids).await?)
}
#[instrument(level = "debug", name = "cala_ledger.tx_templates.list", skip(self))]
pub async fn list(
&self,
cursor: es_entity::PaginatedQueryArgs<TxTemplateByCodeCursor>,
direction: es_entity::ListDirection,
) -> Result<es_entity::PaginatedQueryRet<TxTemplate, TxTemplateByCodeCursor>, TxTemplateError>
{
Ok(self.repo.list_by_code(cursor, direction).await?)
}
#[instrument(level = "debug", name = "cala_ledger.tx_templates.find_by_code", skip(self), fields(code = %code.as_ref()), err(level = tracing::Level::WARN))]
pub async fn find_by_code(&self, code: impl AsRef<str>) -> Result<TxTemplate, TxTemplateError> {
Ok(self.repo.find_by_code(code.as_ref().to_string()).await?)
}
#[instrument(
level = "debug",
name = "cala_ledger.tx_template.prepare_transaction",
skip(self, tmpl, params)
)]
pub(crate) fn prepare_transaction(
&self,
tx_id: TransactionId,
tmpl: &TxTemplateValues,
params: Params,
) -> Result<PreparedTransaction, TxTemplateError> {
let ctx = params.into_context(&self.clock, tmpl.params.as_ref())?;
let journal_id: Uuid = tmpl.transaction.journal_id.try_evaluate(&ctx)?;
let journal_id = JournalId::from(journal_id);
let entries = self.prep_entries(tmpl, tx_id, journal_id, &ctx)?;
let effective: NaiveDate = tmpl.transaction.effective.try_evaluate(&ctx)?;
let correlation_id = tmpl
.transaction
.correlation_id
.as_ref()
.map(|e| e.try_evaluate(&ctx))
.transpose()?;
let external_id = tmpl
.transaction
.external_id
.as_ref()
.map(|e| e.try_evaluate(&ctx))
.transpose()?;
let description = tmpl
.transaction
.description
.as_ref()
.map(|e| e.try_evaluate(&ctx))
.transpose()?;
let metadata = tmpl
.transaction
.metadata
.as_ref()
.map(|e| e.try_evaluate(&ctx))
.transpose()?;
Ok(PreparedTransaction {
tx_id,
journal_id,
tx_template_id: tmpl.id,
effective,
correlation_id,
external_id,
description,
metadata,
entries,
})
}
#[instrument(
level = "debug",
name = "tx_template.prep_entries",
skip(self, tmpl, ctx),
fields(
template_id = %tmpl.id,
template_code = %tmpl.code,
transaction_id = %transaction_id,
journal_id = %journal_id,
entries_count = tmpl.entries.len()
),
)]
fn prep_entries(
&self,
tmpl: &TxTemplateValues,
transaction_id: TransactionId,
journal_id: JournalId,
ctx: &cel_interpreter::CelContext,
) -> Result<Vec<NewEntry>, TxTemplateError> {
let mut new_entries = Vec::with_capacity(tmpl.entries.len());
let mut totals = HashMap::new();
for (zero_based_sequence, entry) in tmpl.entries.iter().enumerate() {
let mut builder = NewEntry::builder();
builder
.id(EntryId::new())
.transaction_id(transaction_id)
.journal_id(journal_id)
.sequence(zero_based_sequence as u32 + 1);
let account_id: Uuid = entry.account_id.try_evaluate(ctx)?;
builder.account_id(account_id);
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, layer)).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);
}
if let Some(metadata) = entry.metadata.as_ref() {
let metadata: serde_json::Value = metadata.try_evaluate(ctx)?;
builder.metadata(metadata);
}
new_entries.push(builder.build().expect("Couldn't build entry"));
}
for ((c, l), v) in totals {
if v != Decimal::ZERO {
return Err(TxTemplateError::UnbalancedTransaction(c, l, v));
}
}
Ok(new_entries)
}
}
impl From<&TxTemplateEvent> for OutboxEventPayload {
fn from(event: &TxTemplateEvent) -> Self {
match event {
TxTemplateEvent::Initialized {
values: tx_template,
} => OutboxEventPayload::TxTemplateCreated {
tx_template: tx_template.clone(),
},
}
}
}