Lines
46.43 %
Functions
19.61 %
Branches
100 %
use finance::{split::Split, tag::Tag};
use sqlx::types::Uuid;
use std::{collections::HashMap, fmt::Debug};
use supp_macro::command;
use super::{CmdError, CmdResult};
use crate::{command::FinanceEntity, config::ConfigError, user::User};
command! {
ListSplits {
#[required]
user_id: Uuid,
#[optional]
account: Uuid,
transaction: Uuid,
} => {
let user = User { id: user_id };
let mut conn = user.get_connection().await.map_err(|err| {
log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
ConfigError::DB
})?;
// Ensure at least one filter is provided
if account.is_none() && transaction.is_none() {
return Err(CmdError::Args(
"At least one filter (account or transaction) is required".to_string(),
));
}
// Execute the appropriate query based on provided filters
let splits = match (account, transaction) {
(Some(account_id), Some(tx_id)) => {
// Filter by both account and transaction
sqlx::query_file_as!(
Split,
"sql/select/splits/by_account_and_transaction.sql",
account_id,
tx_id
)
.fetch_all(&mut *conn)
.await?
(Some(account_id), None) => {
// Filter by account only
"sql/select/splits/by_account.sql",
account_id
(None, Some(tx_id)) => {
// Filter by transaction only
"sql/select/splits/by_transaction.sql",
_ => unreachable!(), // We already checked this case above
};
// Convert Split objects to tagged entities
let mut split_entities = Vec::new();
for split in splits {
// Get tags for this split
let tags: HashMap<String, FinanceEntity> = sqlx::query_file!(
"sql/select/tags/by_split.sql",
split.id
.into_iter()
.map(|row| {
(
row.tag_name.clone(),
FinanceEntity::Tag(Tag {
id: row.id,
tag_name: row.tag_name,
tag_value: row.tag_value,
description: row.description,
}),
})
.collect();
split_entities.push((FinanceEntity::Split(split), tags));
Ok(Some(CmdResult::TaggedEntities(split_entities)))
#[cfg(test)]
mod command_tests {
use super::*;
use crate::{
command::{
account::CreateAccount, commodity::CreateCommodity, transaction::CreateTransaction,
},
db::DB_POOL,
use sqlx::{PgPool, types::chrono::Utc};
use supp_macro::local_db_sqlx_test;
use tokio::sync::OnceCell;
/// Context for keeping environment intact
static CONTEXT: OnceCell<()> = OnceCell::const_new();
static USER: OnceCell<User> = OnceCell::const_new();
async fn setup() {
CONTEXT
.get_or_init(|| async {
#[cfg(feature = "testlog")]
let _ = env_logger::builder()
.is_test(true)
.filter_level(log::LevelFilter::Trace)
.try_init();
.await;
USER.get_or_init(|| async { User { id: Uuid::new_v4() } })
#[local_db_sqlx_test]
async fn test_list_splits(pool: PgPool) -> anyhow::Result<()> {
let user = USER.get().unwrap();
user.commit()
.await
.expect("Failed to commit user to database");
// First create a commodity
let commodity_result = CreateCommodity::new()
.fraction(1.into())
.symbol("TST".to_string())
.name("Test Commodity".to_string())
.user_id(user.id)
.run()
.await?;
// Get the commodity ID
let commodity_id = if let Some(CmdResult::String(id)) = commodity_result {
uuid::Uuid::parse_str(&id)?
} else {
panic!("Expected commodity ID string result");
// Create two accounts
let account1 = if let Some(CmdResult::Entity(FinanceEntity::Account(account))) =
CreateAccount::new()
.name("Account 1".to_string())
{
account
panic!("Expected account entity result");
let account2 = if let Some(CmdResult::Entity(FinanceEntity::Account(account))) =
.name("Account 2".to_string())
// Create a transaction between the accounts
let tx_id = Uuid::new_v4();
let now = Utc::now();
let split1_id = Uuid::new_v4();
let split1 = Split {
id: split1_id,
tx_id,
account_id: account1.id,
commodity_id,
value_num: -100,
value_denom: 1,
reconcile_state: None,
reconcile_date: None,
lot_id: None,
let split2_id = Uuid::new_v4();
let split2 = Split {
id: split2_id,
account_id: account2.id,
value_num: 100,
let splits = vec![FinanceEntity::Split(split1), FinanceEntity::Split(split2)];
CreateTransaction::new()
.splits(splits)
.id(tx_id)
.post_date(now)
.enter_date(now)
.note("Test transaction".to_string())
// List splits for account1
if let Some(CmdResult::TaggedEntities(entities)) = ListSplits::new()
.account(account1.id)
assert_eq!(entities.len(), 1, "Expected one split for account1");
let (entity, _tags) = &entities[0];
if let FinanceEntity::Split(split) = entity {
assert_eq!(split.id, split1_id);
assert_eq!(split.account_id, account1.id);
assert_eq!(split.value_num, -100);
assert_eq!(split.value_denom, 1);
panic!("Expected Split entity");
panic!("Expected TaggedEntities result");
// List splits for account2
.account(account2.id)
assert_eq!(entities.len(), 1, "Expected one split for account2");
assert_eq!(split.id, split2_id);
assert_eq!(split.account_id, account2.id);
assert_eq!(split.value_num, 100);
// List splits for non-existent account
.account(Uuid::new_v4())
assert_eq!(
entities.len(),
0,
"Expected no splits for non-existent account"
);