1
//! Shared name-resolution and entity-builder helpers for logical transaction natives.
2

            
3
use chrono::{DateTime, Utc};
4
use finance::price::Price;
5
use finance::split::Split;
6
use finance::tag::Tag;
7
use server::command::account::ListAccounts;
8
use server::command::commodity::ListCommodities;
9
use server::command::{CmdError, CmdResult, FinanceEntity};
10
use server::logical::{LogicalSplit, PhysicalSplit, PriceRow};
11
use uuid::Uuid;
12

            
13
use super::parse::LogicalSplitInput;
14

            
15
/// Resolve a logical split's named accounts and commodities to UUIDs.
16
148
pub(super) async fn resolve_logical_split(
17
148
    ctx: &str,
18
148
    user_id: Uuid,
19
148
    s: LogicalSplitInput,
20
148
) -> wasmtime::Result<LogicalSplit> {
21
148
    let from = resolve_account(ctx, user_id, &s.from).await?;
22
143
    let to = resolve_account(ctx, user_id, &s.to).await?;
23
143
    let from_commodity = resolve_commodity(ctx, user_id, &s.from_commodity).await?;
24
142
    let to_commodity = resolve_commodity(ctx, user_id, &s.to_commodity).await?;
25
142
    Ok(LogicalSplit {
26
142
        from,
27
142
        to,
28
142
        from_commodity,
29
142
        to_commodity,
30
142
        value: s.value,
31
142
        to_amount: s.to_amount,
32
142
    })
33
148
}
34

            
35
/// Resolve an account identifier (UUID string or name tag) to a UUID.
36
///
37
/// Lists all user-scoped accounts and matches against ID string or `name` tag.
38
/// Returns an error when the key is unknown or (for name keys) ambiguous.
39
291
async fn resolve_account(ctx: &str, user_id: Uuid, key: &str) -> wasmtime::Result<Uuid> {
40
291
    let result = ListAccounts::new().user_id(user_id).run().await;
41
291
    let mut matches = extract_named_account_ids(ctx, result)?
42
291
        .into_iter()
43
693
        .filter(|(id, name)| id.to_string() == key || name.as_deref() == Some(key))
44
291
        .map(|(id, _)| id);
45
291
    let first = matches.next();
46
291
    if first.is_some() && matches.next().is_some() {
47
2
        let total = 2 + matches.count();
48
2
        return Err(wasmtime::Error::msg(format!(
49
2
            "{ctx}: ambiguous account name '{key}' ({total} matches)"
50
2
        )));
51
289
    }
52
289
    first.ok_or_else(|| wasmtime::Error::msg(format!("{ctx}: unknown account '{key}'")))
53
291
}
54

            
55
291
fn extract_named_account_ids(
56
291
    ctx: &str,
57
291
    result: Result<Option<CmdResult>, CmdError>,
58
291
) -> wasmtime::Result<Vec<(Uuid, Option<String>)>> {
59
291
    match result {
60
291
        Ok(Some(CmdResult::TaggedEntities { entities, .. })) => Ok(entities
61
291
            .into_iter()
62
693
            .filter_map(|(entity, tags)| match entity {
63
693
                FinanceEntity::Account(a) => {
64
693
                    let name = tags.get("name").and_then(|t| match t {
65
693
                        FinanceEntity::Tag(Tag { tag_value, .. }) => Some(tag_value.clone()),
66
                        _ => None,
67
693
                    });
68
693
                    Some((a.id, name))
69
                }
70
                _ => None,
71
693
            })
72
291
            .collect()),
73
        Ok(Some(other)) => Err(wasmtime::Error::msg(format!(
74
            "{ctx}: unexpected account result {other:?}"
75
        ))),
76
        Ok(None) => Ok(Vec::new()),
77
        Err(err) => Err(wasmtime::Error::msg(format!(
78
            "{ctx}: account lookup failed: {err}"
79
        ))),
80
    }
81
291
}
82

            
83
/// Resolve a commodity identifier (UUID string or symbol) to a UUID.
84
///
85
/// Lists all user-scoped commodities and matches against ID string or `symbol`
86
/// tag (case-insensitive). Returns an error when unknown or ambiguous.
87
285
async fn resolve_commodity(ctx: &str, user_id: Uuid, key: &str) -> wasmtime::Result<Uuid> {
88
285
    let result = ListCommodities::new().user_id(user_id).run().await;
89
285
    let mut matches = extract_commodity_symbols(ctx, result)?
90
285
        .into_iter()
91
294
        .filter(|(id, sym)| {
92
294
            id == key || sym.as_deref().is_some_and(|s| s.eq_ignore_ascii_case(key))
93
294
        })
94
285
        .map(|(id, _)| id);
95
285
    let first = matches.next();
96
285
    if first.is_some() && matches.next().is_some() {
97
        return Err(wasmtime::Error::msg(format!(
98
            "{ctx}: symbol '{key}' is ambiguous; reference by uuid"
99
        )));
100
285
    }
101
284
    let id_str =
102
285
        first.ok_or_else(|| wasmtime::Error::msg(format!("{ctx}: unknown commodity '{key}'")))?;
103
284
    Uuid::parse_str(&id_str)
104
284
        .map_err(|e| wasmtime::Error::msg(format!("{ctx}: commodity uuid parse error: {e}")))
105
285
}
106

            
107
285
fn extract_commodity_symbols(
108
285
    ctx: &str,
109
285
    result: Result<Option<CmdResult>, CmdError>,
110
285
) -> wasmtime::Result<Vec<(String, Option<String>)>> {
111
285
    match result {
112
285
        Ok(Some(CmdResult::TaggedEntities { entities, .. })) => Ok(entities
113
285
            .into_iter()
114
294
            .filter_map(|(entity, tags)| match entity {
115
294
                FinanceEntity::Commodity(c) => {
116
294
                    let sym = tags.get("symbol").and_then(|t| match t {
117
294
                        FinanceEntity::Tag(Tag { tag_value, .. }) => Some(tag_value.clone()),
118
                        _ => None,
119
294
                    });
120
294
                    Some((c.id.to_string(), sym))
121
                }
122
                _ => None,
123
294
            })
124
285
            .collect()),
125
        Ok(Some(other)) => Err(wasmtime::Error::msg(format!(
126
            "{ctx}: unexpected commodity result {other:?}"
127
        ))),
128
        Ok(None) => Ok(Vec::new()),
129
        Err(err) => Err(wasmtime::Error::msg(format!(
130
            "{ctx}: commodity lookup failed: {err}"
131
        ))),
132
    }
133
285
}
134

            
135
/// Convert physical splits to `FinanceEntity::Split` with the given transaction ID.
136
141
pub(super) fn physical_splits_to_entities(
137
141
    splits: Vec<PhysicalSplit>,
138
141
    tx_id: Uuid,
139
141
) -> Vec<FinanceEntity> {
140
141
    splits
141
141
        .into_iter()
142
282
        .map(|ps| {
143
282
            FinanceEntity::Split(Split {
144
282
                id: ps.id,
145
282
                tx_id,
146
282
                account_id: ps.account_id,
147
282
                commodity_id: ps.commodity_id,
148
282
                value_num: *ps.value.numer(),
149
282
                value_denom: *ps.value.denom(),
150
282
                reconcile_state: None,
151
282
                reconcile_date: None,
152
282
                lot_id: None,
153
282
            })
154
282
        })
155
141
        .collect()
156
141
}
157

            
158
/// Convert price rows to `FinanceEntity::Price`, using `post_date` as the
159
/// fallback date when the row carries none.
160
4
pub(super) fn price_rows_to_entities(
161
4
    price_rows: Vec<PriceRow>,
162
4
    post_date: DateTime<Utc>,
163
4
) -> Vec<FinanceEntity> {
164
4
    price_rows
165
4
        .into_iter()
166
4
        .map(|pr| {
167
4
            FinanceEntity::Price(Price {
168
4
                id: Uuid::new_v4(),
169
4
                date: pr.date.unwrap_or(post_date),
170
4
                commodity_id: pr.commodity_id,
171
4
                currency_id: pr.currency_id,
172
4
                commodity_split: Some(pr.commodity_split),
173
4
                currency_split: Some(pr.currency_split),
174
4
                value_num: pr.value_num,
175
4
                value_denom: pr.value_denom,
176
4
            })
177
4
        })
178
4
        .collect()
179
4
}