Lines
85.31 %
Functions
84.09 %
Branches
100 %
//! Shared name-resolution and entity-builder helpers for logical transaction natives.
use chrono::{DateTime, Utc};
use finance::price::Price;
use finance::split::Split;
use finance::tag::Tag;
use server::command::account::ListAccounts;
use server::command::commodity::ListCommodities;
use server::command::{CmdError, CmdResult, FinanceEntity};
use server::logical::{LogicalSplit, PhysicalSplit, PriceRow};
use uuid::Uuid;
use super::parse::LogicalSplitInput;
/// Resolve a logical split's named accounts and commodities to UUIDs.
pub(super) async fn resolve_logical_split(
ctx: &str,
user_id: Uuid,
s: LogicalSplitInput,
) -> wasmtime::Result<LogicalSplit> {
let from = resolve_account(ctx, user_id, &s.from).await?;
let to = resolve_account(ctx, user_id, &s.to).await?;
let from_commodity = resolve_commodity(ctx, user_id, &s.from_commodity).await?;
let to_commodity = resolve_commodity(ctx, user_id, &s.to_commodity).await?;
Ok(LogicalSplit {
from,
to,
from_commodity,
to_commodity,
value: s.value,
to_amount: s.to_amount,
})
}
/// Resolve an account identifier (UUID string or name tag) to a UUID.
///
/// Lists all user-scoped accounts and matches against ID string or `name` tag.
/// Returns an error when the key is unknown or (for name keys) ambiguous.
async fn resolve_account(ctx: &str, user_id: Uuid, key: &str) -> wasmtime::Result<Uuid> {
let result = ListAccounts::new().user_id(user_id).run().await;
let mut matches = extract_named_account_ids(ctx, result)?
.into_iter()
.filter(|(id, name)| id.to_string() == key || name.as_deref() == Some(key))
.map(|(id, _)| id);
let first = matches.next();
if first.is_some() && matches.next().is_some() {
let total = 2 + matches.count();
return Err(wasmtime::Error::msg(format!(
"{ctx}: ambiguous account name '{key}' ({total} matches)"
)));
first.ok_or_else(|| wasmtime::Error::msg(format!("{ctx}: unknown account '{key}'")))
fn extract_named_account_ids(
result: Result<Option<CmdResult>, CmdError>,
) -> wasmtime::Result<Vec<(Uuid, Option<String>)>> {
match result {
Ok(Some(CmdResult::TaggedEntities { entities, .. })) => Ok(entities
.filter_map(|(entity, tags)| match entity {
FinanceEntity::Account(a) => {
let name = tags.get("name").and_then(|t| match t {
FinanceEntity::Tag(Tag { tag_value, .. }) => Some(tag_value.clone()),
_ => None,
});
Some((a.id, name))
.collect()),
Ok(Some(other)) => Err(wasmtime::Error::msg(format!(
"{ctx}: unexpected account result {other:?}"
))),
Ok(None) => Ok(Vec::new()),
Err(err) => Err(wasmtime::Error::msg(format!(
"{ctx}: account lookup failed: {err}"
/// Resolve a commodity identifier (UUID string or symbol) to a UUID.
/// Lists all user-scoped commodities and matches against ID string or `symbol`
/// tag (case-insensitive). Returns an error when unknown or ambiguous.
async fn resolve_commodity(ctx: &str, user_id: Uuid, key: &str) -> wasmtime::Result<Uuid> {
let result = ListCommodities::new().user_id(user_id).run().await;
let mut matches = extract_commodity_symbols(ctx, result)?
.filter(|(id, sym)| {
id == key || sym.as_deref().is_some_and(|s| s.eq_ignore_ascii_case(key))
"{ctx}: symbol '{key}' is ambiguous; reference by uuid"
let id_str =
first.ok_or_else(|| wasmtime::Error::msg(format!("{ctx}: unknown commodity '{key}'")))?;
Uuid::parse_str(&id_str)
.map_err(|e| wasmtime::Error::msg(format!("{ctx}: commodity uuid parse error: {e}")))
fn extract_commodity_symbols(
) -> wasmtime::Result<Vec<(String, Option<String>)>> {
FinanceEntity::Commodity(c) => {
let sym = tags.get("symbol").and_then(|t| match t {
Some((c.id.to_string(), sym))
"{ctx}: unexpected commodity result {other:?}"
"{ctx}: commodity lookup failed: {err}"
/// Convert physical splits to `FinanceEntity::Split` with the given transaction ID.
pub(super) fn physical_splits_to_entities(
splits: Vec<PhysicalSplit>,
tx_id: Uuid,
) -> Vec<FinanceEntity> {
splits
.map(|ps| {
FinanceEntity::Split(Split {
id: ps.id,
tx_id,
account_id: ps.account_id,
commodity_id: ps.commodity_id,
value_num: *ps.value.numer(),
value_denom: *ps.value.denom(),
reconcile_state: None,
reconcile_date: None,
lot_id: None,
.collect()
/// Convert price rows to `FinanceEntity::Price`, using `post_date` as the
/// fallback date when the row carries none.
pub(super) fn price_rows_to_entities(
price_rows: Vec<PriceRow>,
post_date: DateTime<Utc>,
price_rows
.map(|pr| {
FinanceEntity::Price(Price {
id: Uuid::new_v4(),
date: pr.date.unwrap_or(post_date),
commodity_id: pr.commodity_id,
currency_id: pr.currency_id,
commodity_split: Some(pr.commodity_split),
currency_split: Some(pr.currency_split),
value_num: pr.value_num,
value_denom: pr.value_denom,