cala-ledger 0.15.3

An embeddable double sided accounting ledger built on PG/SQLx
Documentation
mod helpers;

use chrono::{TimeZone, Utc};
use es_entity::clock::ClockHandle;
use rand::distr::{Alphanumeric, SampleString};
use rust_decimal::Decimal;

use cala_ledger::{tx_template::*, *};

#[tokio::test]
async fn transaction_effective_date_uses_clock() -> anyhow::Result<()> {
    let fixed_time = Utc.with_ymd_and_hms(2025, 6, 15, 10, 30, 0).unwrap();
    let (clock_handle, _clock_ctrl) = ClockHandle::manual_at(fixed_time);

    let pool = helpers::init_pool().await?;
    let cala = CalaLedger::init(
        CalaLedgerConfig::builder()
            .pool(pool)
            .exec_migrations(false)
            .clock(clock_handle)
            .build()?,
    )
    .await?;

    let journal = cala.journals().create(helpers::test_journal()).await?;
    let (sender, recipient) = helpers::test_accounts();
    let sender = cala.accounts().create(sender).await?;
    let recipient = cala.accounts().create(recipient).await?;

    let tx_code = Alphanumeric.sample_string(&mut rand::rng(), 32);
    let template = helpers::simple_template_with_date_default(&tx_code);
    cala.tx_templates().create(template).await?;

    let mut params = Params::new();
    params.insert("journal_id", journal.id().to_string());
    params.insert("sender", sender.id());
    params.insert("recipient", recipient.id());
    params.insert("amount", Decimal::from(100));

    let tx = cala
        .post_transaction(TransactionId::new(), &tx_code, params)
        .await?;

    assert_eq!(tx.values().effective, fixed_time.date_naive());

    Ok(())
}

#[tokio::test]
async fn clock_advancement_changes_effective_date() -> anyhow::Result<()> {
    let time_1 = Utc.with_ymd_and_hms(2025, 1, 1, 12, 0, 0).unwrap();
    let (clock_handle, clock_ctrl) = ClockHandle::manual_at(time_1);

    let pool = helpers::init_pool().await?;
    let cala = CalaLedger::init(
        CalaLedgerConfig::builder()
            .pool(pool)
            .exec_migrations(false)
            .clock(clock_handle)
            .build()?,
    )
    .await?;

    let journal = cala.journals().create(helpers::test_journal()).await?;
    let (sender, recipient) = helpers::test_accounts();
    let sender = cala.accounts().create(sender).await?;
    let recipient = cala.accounts().create(recipient).await?;

    let tx_code = Alphanumeric.sample_string(&mut rand::rng(), 32);
    let template = helpers::simple_template_with_date_default(&tx_code);
    cala.tx_templates().create(template).await?;

    let mut params = Params::new();
    params.insert("journal_id", journal.id().to_string());
    params.insert("sender", sender.id());
    params.insert("recipient", recipient.id());
    params.insert("amount", Decimal::from(50));

    let tx1 = cala
        .post_transaction(TransactionId::new(), &tx_code, params)
        .await?;

    let time_2 = Utc.with_ymd_and_hms(2025, 12, 25, 14, 0, 0).unwrap();
    let advance_duration = (time_2 - time_1).to_std().expect("positive duration");
    clock_ctrl.advance(advance_duration).await;

    let mut params = Params::new();
    params.insert("journal_id", journal.id().to_string());
    params.insert("sender", sender.id());
    params.insert("recipient", recipient.id());
    params.insert("amount", Decimal::from(75));

    let tx2 = cala
        .post_transaction(TransactionId::new(), &tx_code, params)
        .await?;

    assert_eq!(tx1.values().effective, time_1.date_naive());
    assert_eq!(tx2.values().effective, time_2.date_naive());
    assert_ne!(tx1.values().effective, tx2.values().effective);

    Ok(())
}

#[tokio::test]
async fn void_transaction_uses_clock_time() -> anyhow::Result<()> {
    let original_time = Utc.with_ymd_and_hms(2025, 3, 1, 9, 0, 0).unwrap();
    let (clock_handle, clock_ctrl) = ClockHandle::manual_at(original_time);

    let pool = helpers::init_pool().await?;
    let cala = CalaLedger::init(
        CalaLedgerConfig::builder()
            .pool(pool)
            .exec_migrations(false)
            .clock(clock_handle)
            .build()?,
    )
    .await?;

    let journal = cala.journals().create(helpers::test_journal()).await?;
    let (sender, recipient) = helpers::test_accounts();
    let sender = cala.accounts().create(sender).await?;
    let recipient = cala.accounts().create(recipient).await?;

    let tx_code = Alphanumeric.sample_string(&mut rand::rng(), 32);
    let template = helpers::simple_template_with_date_default(&tx_code);
    cala.tx_templates().create(template).await?;

    let mut params = Params::new();
    params.insert("journal_id", journal.id().to_string());
    params.insert("sender", sender.id());
    params.insert("recipient", recipient.id());
    params.insert("amount", Decimal::from(100));

    let original_tx_id = TransactionId::new();
    let original_tx = cala
        .post_transaction(original_tx_id, &tx_code, params)
        .await?;

    let void_time = Utc.with_ymd_and_hms(2025, 3, 15, 16, 30, 0).unwrap();
    let advance_duration = (void_time - original_time)
        .to_std()
        .expect("positive duration");
    clock_ctrl.advance(advance_duration).await;

    let voiding_tx_id = TransactionId::new();
    let voided_tx = cala.void_transaction(voiding_tx_id, original_tx_id).await?;

    assert_eq!(original_tx.created_at(), original_time);
    assert_eq!(voided_tx.created_at(), void_time);

    Ok(())
}

#[tokio::test]
async fn begin_operation_attaches_clock_time() -> anyhow::Result<()> {
    let fixed_time = Utc.with_ymd_and_hms(2025, 7, 4, 12, 0, 0).unwrap();
    let (clock_handle, _clock_ctrl) = ClockHandle::manual_at(fixed_time);

    let pool = helpers::init_pool().await?;
    let cala = CalaLedger::init(
        CalaLedgerConfig::builder()
            .pool(pool)
            .exec_migrations(false)
            .clock(clock_handle)
            .build()?,
    )
    .await?;

    let op = cala.begin_operation().await?;
    let op_time = op.now();

    assert_eq!(op_time, fixed_time);

    Ok(())
}

#[tokio::test]
async fn clock_propagates_through_atomic_operations() -> anyhow::Result<()> {
    let fixed_time = Utc.with_ymd_and_hms(2025, 9, 21, 8, 15, 0).unwrap();
    let (clock_handle, _clock_ctrl) = ClockHandle::manual_at(fixed_time);

    let pool = helpers::init_pool().await?;
    let cala = CalaLedger::init(
        CalaLedgerConfig::builder()
            .pool(pool)
            .exec_migrations(false)
            .clock(clock_handle)
            .build()?,
    )
    .await?;

    let journal = cala.journals().create(helpers::test_journal()).await?;
    let (sender, recipient) = helpers::test_accounts();
    let sender = cala.accounts().create(sender).await?;
    let recipient = cala.accounts().create(recipient).await?;

    let tx_code = Alphanumeric.sample_string(&mut rand::rng(), 32);
    let template = helpers::simple_template_with_date_default(&tx_code);
    cala.tx_templates().create(template).await?;

    let mut op = cala.begin_operation().await?;

    let mut params = Params::new();
    params.insert("journal_id", journal.id().to_string());
    params.insert("sender", sender.id());
    params.insert("recipient", recipient.id());
    params.insert("amount", Decimal::from(200));

    let tx = cala
        .post_transaction_in_op(&mut op, TransactionId::new(), &tx_code, params)
        .await?;

    op.commit().await?;

    assert_eq!(tx.created_at(), fixed_time);
    assert_eq!(tx.values().effective, fixed_time.date_naive());

    Ok(())
}