#会计 #sqlx #账簿 #日记账 #模板 #构建 #输入

sqlx-ledger

基于PG/SQLx构建的嵌入式双边会计账簿

43个版本

0.11.4 2024年6月21日
0.11.3 2024年3月6日
0.11.2 2023年11月10日
0.9.0 2023年7月19日
0.5.8 2023年3月31日

#1431数据库接口

Download history 94/week @ 2024-04-28 32/week @ 2024-05-05 8/week @ 2024-05-12 8/week @ 2024-05-19 23/week @ 2024-05-26 7/week @ 2024-06-02 3/week @ 2024-06-09 141/week @ 2024-06-16 62/week @ 2024-06-23 43/week @ 2024-06-30 129/week @ 2024-07-28

每月 129 次下载

MIT 许可证

125KB
3K SLoC

Sqlx-Ledger

这个crate在sqlx Postgres集成之上提供了双边会计原语。

它具有以下特性:

  • 账户可以在多个日记账上具有余额
  • 多货币/多层支持
  • 基于模板的交易输入以提高一致性
  • 基于CEL的模板插值
  • 事件流以通知更改

CEL解释器尚不完整,但提供了足够的功能来支持基本用例。随着需求的出现,将添加更多功能。

要使用它,请将迁移复制到您的项目中,并使用以下命令添加crate:cargo add sqlx-ledger.

查看文档以获取使用示例


lib.rs:

sqlx-ledger

这个crate在sqlx crate的基础上构建,提供了一套实现SQL兼容双边会计系统的原语。这个系统专门设计用于处理金钱和构建金融产品。

快速开始

在开始使用之前,请添加并执行迁移目录中的迁移。

cp ./migrations/* <path/to/your/projects/migrations>
# in your project
cargo sqlx migrate

以下是初始化账簿、创建基本模板和提交交易的方法。这是一个端到端结合所有组件的玩具示例。不推荐实际使用。

use uuid::uuid;
use rust_decimal::Decimal;
use sqlx_ledger::{*, journal::*, account::*, tx_template::*};

async fn init_ledger(journal_id: JournalId) -> SqlxLedger {
    let pg_con =
        std::env::var("PG_CON").unwrap_or(format!("postgres://user:password@localhost:5432/pg"));
    let pool = sqlx::PgPool::connect(&pg_con).await.unwrap();
    let ledger = SqlxLedger::new(&pool);

    // Initialize the journal - all entities are constructed via builders
    let new_journal = NewJournal::builder()
        .id(journal_id)
        .description("General ledger".to_string())
        .name("Ledger")
        .build()
        .expect("Couldn't build NewJournal");

    let _ = ledger.journals().create(new_journal).await;

    // Initialize an income omnibus account
    let main_account_id = uuid!("00000000-0000-0000-0000-000000000001");
    let new_account = NewAccount::builder()
        .id(main_account_id)
        .name("Income")
        .code("Income")
        .build()
        .unwrap();

    let _ = ledger.accounts().create(new_account).await;

    // Create the trivial 'income' template
    //
    // Here are the 'parameters' that the template will require as inputs.
    let params = vec![
        ParamDefinition::builder()
            .name("sender_account_id")
            .r#type(ParamDataType::UUID)
            .build()
            .unwrap(),
        ParamDefinition::builder()
            .name("units")
            .r#type(ParamDataType::DECIMAL)
            .build()
            .unwrap()
    ];

    // The templates for the Entries that will be created as part of the transaction.
    let entries = vec![
        EntryInput::builder()
            .entry_type("'INCOME_DR'")
            // Reference the input parameters via CEL syntax
            .account_id("params.sender_account_id")
            .layer("SETTLED")
            .direction("DEBIT")
            .units("params.units")
            .currency("'BTC'")
            .build()
            .unwrap(),
        EntryInput::builder()
            .entry_type("'INCOME_CR'")
            .account_id(format!("uuid('{main_account_id}')"))
            .layer("SETTLED")
            .direction("CREDIT")
            .units("params.units")
            .currency("'BTC'")
            .build()
            .unwrap(),
    ];
    let tx_code = "GENERAL_INCOME";
    let new_template = NewTxTemplate::builder()
        .id(uuid::Uuid::new_v4())
        .code(tx_code)
        .params(params)
        .tx_input(
            // Template for the Transaction metadata.
            TxInput::builder()
                .effective("date()")
                .journal_id(format!("uuid('{journal_id}')"))
                .build()
                .unwrap(),
        )
        .entries(entries)
        .build()
        .unwrap();

    let _ = ledger.tx_templates().create(new_template).await;

    ledger
}

tokio_test::block_on(async {
    let journal_id = JournalId::from(uuid!("00000000-0000-0000-0000-000000000001"));
    let ledger = init_ledger(journal_id).await;

    // The account that is sending to the general income account
    let sender_account_id = AccountId::new();
    let sender_account = NewAccount::builder()
        .id(sender_account_id)
        .name(format!("Sender-{sender_account_id}"))
        .code(format!("Sender-{sender_account_id}"))
        .build()
        .unwrap();
    ledger.accounts().create(sender_account).await.unwrap();

    // Prepare the input parameters that the template requires
    let mut params = TxParams::new();
    params.insert("sender_account_id", sender_account_id);
    params.insert("units", Decimal::ONE);

    // Create the transaction via the template
    ledger
        .post_transaction(TransactionId::new(), "GENERAL_INCOME", Some(params))
        .await
        .unwrap();

    // Check the resulting balance
    let account_balance = ledger
        .balances()
        .find(journal_id, sender_account_id, "BTC".parse().unwrap())
        .await
        .unwrap();

    assert_eq!(account_balance.unwrap().settled(), Decimal::NEGATIVE_ONE);
});

依赖关系

~41–56MB
~1M SLoC