Lines
76.7 %
Functions
47.5 %
Branches
100 %
use server::command::transaction::{
CreateTransaction, DeleteTransaction, GetTransaction, GetTransactionDetail, GetTransactionTag,
ListTransactions, SetTransactionTag, UpdateTransaction,
};
use server::command::{CmdResult, FinanceEntity};
use uuid::Uuid;
use wasmtime::{Caller, Rooted, StructRef};
use super::parse::{
parse_account_filter_arg, parse_create_transaction_payload, parse_transaction_id_arg,
parse_update_transaction_payload,
use super::render::{
TransactionEntry, alloc_transaction_entity, list_transaction_entries, render_full_transaction,
use crate::session::SessionData;
/// `set-transaction-tag` upsert (idempotent). Mirrors `set-split-tag`
/// and `set-account-tag` — i32 return = 1 on success.
pub(super) async fn run_set_transaction_tag(
user_id: Uuid,
id_arg: Option<String>,
name_arg: Option<String>,
value_arg: Option<String>,
) -> wasmtime::Result<i32> {
let raw = id_arg.filter(|s| !s.is_empty()).ok_or_else(|| {
wasmtime::Error::msg("set-transaction-tag: missing or empty :transaction-id arg")
})?;
let transaction_id = Uuid::parse_str(&raw).map_err(|err| {
wasmtime::Error::msg(format!("set-transaction-tag: invalid uuid '{raw}': {err}"))
let tag_name = name_arg.filter(|s| !s.is_empty()).ok_or_else(|| {
wasmtime::Error::msg("set-transaction-tag: missing or empty :tag-name arg")
let tag_value = value_arg
.ok_or_else(|| wasmtime::Error::msg("set-transaction-tag: missing :tag-value arg"))?;
SetTransactionTag::new()
.user_id(user_id)
.transaction_id(transaction_id)
.tag_name(tag_name)
.tag_value(tag_value)
.run()
.await
.map(|_| 1)
.map_err(|err| wasmtime::Error::msg(format!("set-transaction-tag: {err}")))
}
/// `get-transaction-tag` lookup. Empty-string return on absence — same
/// shape as `get-split-tag`.
pub(super) async fn run_get_transaction_tag(
) -> wasmtime::Result<String> {
wasmtime::Error::msg("get-transaction-tag: missing or empty :transaction-id arg")
wasmtime::Error::msg(format!("get-transaction-tag: invalid uuid '{raw}': {err}"))
wasmtime::Error::msg("get-transaction-tag: missing or empty :tag-name arg")
match GetTransactionTag::new()
{
Ok(Some(CmdResult::String(s))) => Ok(s),
Ok(Some(other)) => Err(wasmtime::Error::msg(format!(
"get-transaction-tag: expected String, got {other:?}"
))),
Ok(None) => Ok(String::new()),
Err(err) => Err(wasmtime::Error::msg(format!("get-transaction-tag: {err}"))),
/// Companion to create-transaction. Payload mirrors the create shape but
/// `:transaction-id` is required (the row being updated) and every other
/// field is optional — caller only supplies what they want changed.
/// Splits when present REPLACE the existing set; partial split updates
/// aren't representable in this slice (the server's UpdateTransaction
/// runner treats `splits` as a full-list replace).
pub(super) async fn run_update_transaction(
payload_arg: Option<String>,
let payload = payload_arg
.filter(|s| !s.is_empty())
.ok_or_else(|| wasmtime::Error::msg("update-transaction: missing or empty :payload arg"))?;
let input = parse_update_transaction_payload(&payload)
.map_err(|err| wasmtime::Error::msg(format!("update-transaction: {err}")))?;
let mut runner = UpdateTransaction::new()
.transaction_id(input.transaction_id);
if let Some(post_date) = input.post_date {
runner = runner.post_date(post_date);
if let Some(enter_date) = input.enter_date {
runner = runner.enter_date(enter_date);
if let Some(note) = input.note {
runner = runner.note(note);
if let Some(splits) = input.splits {
let entities: Vec<FinanceEntity> = splits
.into_iter()
.map(|mut s| {
s.tx_id = input.transaction_id;
FinanceEntity::Split(s)
})
.collect();
runner = runner.splits(entities);
if !input.prices.is_empty() {
let price_entities: Vec<FinanceEntity> =
input.prices.into_iter().map(FinanceEntity::Price).collect();
runner = runner.prices(price_entities);
match runner.run().await {
Ok(Some(CmdResult::Entity(FinanceEntity::Transaction(tx)))) => Ok(tx.id.to_string()),
"update-transaction: expected Transaction entity, got {other:?}"
Ok(None) => Err(wasmtime::Error::msg(
"update-transaction: command returned no entity",
)),
Err(err) => Err(wasmtime::Error::msg(format!("update-transaction: {err}"))),
pub(super) async fn run_create_transaction(
.ok_or_else(|| wasmtime::Error::msg("create-transaction: missing or empty :payload arg"))?;
let input = parse_create_transaction_payload(&payload)
.map_err(|err| wasmtime::Error::msg(format!("create-transaction: {err}")))?;
let splits: Vec<FinanceEntity> = input
.splits
s.tx_id = input.id;
let mut runner = CreateTransaction::new()
.id(input.id)
.post_date(input.post_date)
.enter_date(input.enter_date)
.splits(splits);
"create-transaction: expected Transaction entity, got {other:?}"
"create-transaction: command returned no entity",
Err(err) => Err(wasmtime::Error::msg(format!("create-transaction: {err}"))),
/// Removes the transaction by UUID along with its splits, prices, and
/// owned tags. Not idempotent — server returns an error when the row
/// isn't there. Returns `t` on success.
pub(super) async fn run_delete_transaction(
wasmtime::Error::msg("delete-transaction: missing or empty :transaction-id arg")
wasmtime::Error::msg(format!("delete-transaction: invalid uuid '{raw}': {err}"))
DeleteTransaction::new()
.map_err(|err| wasmtime::Error::msg(format!("delete-transaction: {err}")))
pub(super) async fn run_get_transaction(
caller: &mut Caller<'_, SessionData>,
) -> wasmtime::Result<Option<Rooted<StructRef>>> {
let transaction_id = parse_transaction_id_arg(id_arg)?;
let result = GetTransaction::new()
.await;
let entries = list_transaction_entries("get-transaction", result)?;
match entries.into_iter().next() {
Some((id, note, amount, post_date)) => Ok(Some(
alloc_transaction_entity(
caller,
&id,
note.as_deref(),
amount.as_deref(),
Some(&post_date),
)
.await?,
None => Ok(None),
/// `get-transaction-detail` — returns the full transaction as a string
/// plist (`:id :post-date :enter-date :note :splits :prices`) so a client
/// can reconstruct + edit it. Empty `:prices ()` when single-currency.
pub(super) async fn run_get_transaction_detail(
) -> wasmtime::Result<Option<String>> {
let result = GetTransactionDetail::new()
render_full_transaction("get-transaction-detail", result)
pub(super) async fn run_list_transactions(
account_arg: Option<String>,
) -> wasmtime::Result<Vec<TransactionEntry>> {
let mut cmd = ListTransactions::new().user_id(user_id);
if let Some(account_id) = parse_account_filter_arg(account_arg)? {
cmd = cmd.account(account_id);
let result = cmd.run().await;
list_transaction_entries("list-transactions", result)