1
use finance::commodity::Commodity;
2
use num_rational::Rational64;
3
use scripting::runtime::{alloc_pair_chain, alloc_string_ref};
4
use server::command::account::{GetAccountCommodities, GetBalance, ListAccounts};
5
use server::command::{CmdResult, CommodityInfo};
6
use uuid::Uuid;
7
use wasmtime::{AnyRef, Caller, Rooted, StructRef};
8

            
9
use crate::session::SessionData;
10

            
11
260
pub(super) async fn resolve_balance(
12
260
    user_id: Uuid,
13
260
    id_arg: Option<String>,
14
260
) -> wasmtime::Result<(i64, i64, Uuid)> {
15
260
    let raw = id_arg
16
260
        .filter(|s| !s.is_empty())
17
260
        .ok_or_else(|| wasmtime::Error::msg("account-balance: missing or empty :account-id arg"))?;
18
260
    let account_id = Uuid::parse_str(&raw).map_err(|err| {
19
        wasmtime::Error::msg(format!("account-balance: invalid uuid '{raw}': {err}"))
20
    })?;
21
260
    let commodity_id = single_commodity_for(user_id, account_id).await?;
22
234
    let (numer, denom) = single_rational_for(user_id, account_id).await?;
23
234
    Ok((numer, denom, commodity_id))
24
260
}
25

            
26
260
async fn single_commodity_for(user_id: Uuid, account_id: Uuid) -> wasmtime::Result<Uuid> {
27
260
    match GetAccountCommodities::new()
28
260
        .user_id(user_id)
29
260
        .account_id(account_id)
30
260
        .run()
31
260
        .await
32
    {
33
260
        Ok(Some(CmdResult::CommodityInfoList(items))) => match items.as_slice() {
34
234
            [info] => Ok(info.commodity_id),
35
26
            [] => Err(wasmtime::Error::msg(
36
26
                "account-balance: account has no commodity yet (no splits); cannot produce \
37
26
                 Commodity-typed value",
38
26
            )),
39
            _ => Err(wasmtime::Error::msg(
40
                "account-balance: account holds multiple commodities; use get-balance instead",
41
            )),
42
        },
43
        Ok(Some(other)) => Err(wasmtime::Error::msg(format!(
44
            "account-balance: expected CommodityInfoList, got {other:?}"
45
        ))),
46
        Ok(None) => Err(wasmtime::Error::msg(
47
            "account-balance: account has no commodity yet (no splits); cannot produce \
48
             Commodity-typed value",
49
        )),
50
        Err(err) => Err(wasmtime::Error::msg(format!("account-balance: {err}"))),
51
    }
52
260
}
53

            
54
234
async fn single_rational_for(user_id: Uuid, account_id: Uuid) -> wasmtime::Result<(i64, i64)> {
55
234
    match GetBalance::new()
56
234
        .user_id(user_id)
57
234
        .account_id(account_id)
58
234
        .run()
59
234
        .await
60
    {
61
234
        Ok(Some(CmdResult::Rational(r))) => Ok((*r.numer(), *r.denom())),
62
        Ok(None) => Ok((0, 1)),
63
        Ok(Some(CmdResult::MultiCurrencyBalance(_))) => Err(wasmtime::Error::msg(
64
            "account-balance: account holds multiple commodities; use get-balance instead",
65
        )),
66
        Ok(Some(other)) => Err(wasmtime::Error::msg(format!(
67
            "account-balance: unexpected variant {other:?}"
68
        ))),
69
        Err(err) => Err(wasmtime::Error::msg(format!("account-balance: {err}"))),
70
    }
71
234
}
72

            
73
pub(super) async fn count_accounts(user_id: Uuid) -> i32 {
74
    match ListAccounts::new().user_id(user_id).run().await {
75
        Ok(Some(CmdResult::TaggedEntities { entities, .. })) => entities.len() as i32,
76
        _ => 0,
77
    }
78
}
79

            
80
28
pub(super) async fn run_get_balance_single(
81
28
    user_id: Uuid,
82
28
    id_arg: Option<String>,
83
28
) -> wasmtime::Result<(i64, i64)> {
84
28
    let raw = id_arg
85
28
        .filter(|s| !s.is_empty())
86
28
        .ok_or_else(|| wasmtime::Error::msg("get-balance: missing or empty :account-id arg"))?;
87
27
    let account_id = Uuid::parse_str(&raw)
88
27
        .map_err(|err| wasmtime::Error::msg(format!("get-balance: invalid uuid '{raw}': {err}")))?;
89
26
    match GetBalance::new()
90
26
        .user_id(user_id)
91
26
        .account_id(account_id)
92
26
        .run()
93
26
        .await
94
    {
95
26
        Ok(Some(CmdResult::Rational(r))) => Ok((*r.numer(), *r.denom())),
96
        Ok(Some(CmdResult::MultiCurrencyBalance(_))) => Err(wasmtime::Error::msg(
97
            "get-balance: multi-currency account — use get-balances for the typed pair return",
98
        )),
99
        Ok(Some(other)) => Err(wasmtime::Error::msg(format!(
100
            "get-balance: expected Rational, got {other:?}"
101
        ))),
102
        Ok(None) => Ok((0, 1)),
103
        Err(err) => Err(wasmtime::Error::msg(format!("get-balance: {err}"))),
104
    }
105
28
}
106

            
107
288
pub(super) async fn run_get_balances(
108
288
    user_id: Uuid,
109
288
    id_arg: Option<String>,
110
288
) -> wasmtime::Result<Vec<String>> {
111
288
    let raw = id_arg
112
288
        .filter(|s| !s.is_empty())
113
288
        .ok_or_else(|| wasmtime::Error::msg("get-balances: missing or empty :account-id arg"))?;
114
261
    let account_id = Uuid::parse_str(&raw).map_err(|err| {
115
1
        wasmtime::Error::msg(format!("get-balances: invalid uuid '{raw}': {err}"))
116
1
    })?;
117
260
    let infos = fetch_commodity_infos(user_id, account_id).await?;
118
260
    if infos.is_empty() {
119
78
        return Ok(Vec::new());
120
182
    }
121
182
    let balance = GetBalance::new()
122
182
        .user_id(user_id)
123
182
        .account_id(account_id)
124
182
        .run()
125
182
        .await
126
182
        .map_err(|err| wasmtime::Error::msg(format!("get-balances: {err}")))?;
127
182
    match balance {
128
        None => Ok(Vec::new()),
129
156
        Some(CmdResult::Rational(r)) => {
130
156
            let info = infos.into_iter().next().ok_or_else(|| {
131
                wasmtime::Error::msg("get-balances: rational balance but no commodity info")
132
            })?;
133
156
            Ok(vec![format_balance_plist(
134
156
                &info.commodity_id.to_string(),
135
156
                &info.symbol,
136
156
                &info.name,
137
156
                *r.numer(),
138
156
                *r.denom(),
139
156
            )])
140
        }
141
26
        Some(CmdResult::MultiCurrencyBalance(pairs)) => {
142
26
            let result: Vec<String> = pairs
143
26
                .iter()
144
52
                .filter_map(|(commodity, r)| balance_plist_for(commodity, r, &infos))
145
26
                .collect();
146
26
            Ok(result)
147
        }
148
        Some(other) => Err(wasmtime::Error::msg(format!(
149
            "get-balances: unexpected variant {other:?}"
150
        ))),
151
    }
152
288
}
153

            
154
52
fn balance_plist_for(
155
52
    commodity: &Commodity,
156
52
    r: &Rational64,
157
52
    infos: &[CommodityInfo],
158
52
) -> Option<String> {
159
78
    let info = infos.iter().find(|i| i.commodity_id == commodity.id)?;
160
52
    Some(format_balance_plist(
161
52
        &commodity.id.to_string(),
162
52
        &info.symbol,
163
52
        &info.name,
164
52
        *r.numer(),
165
52
        *r.denom(),
166
52
    ))
167
52
}
168

            
169
260
async fn fetch_commodity_infos(
170
260
    user_id: Uuid,
171
260
    account_id: Uuid,
172
260
) -> wasmtime::Result<Vec<CommodityInfo>> {
173
260
    match GetAccountCommodities::new()
174
260
        .user_id(user_id)
175
260
        .account_id(account_id)
176
260
        .run()
177
260
        .await
178
    {
179
260
        Ok(Some(CmdResult::CommodityInfoList(items))) => Ok(items),
180
        Ok(None) => Ok(Vec::new()),
181
        Ok(Some(other)) => Err(wasmtime::Error::msg(format!(
182
            "get-balances: expected CommodityInfoList, got {other:?}"
183
        ))),
184
        Err(err) => Err(wasmtime::Error::msg(format!("get-balances: {err}"))),
185
    }
186
260
}
187

            
188
208
fn format_balance_plist(id: &str, symbol: &str, name: &str, numer: i64, denom: i64) -> String {
189
208
    format!(
190
        "(:commodity-id {} :symbol {} :name {} :value-num {} :value-denom {})",
191
208
        super::quote_string(id),
192
208
        super::quote_string(symbol),
193
208
        super::quote_string(name),
194
        numer,
195
        denom,
196
    )
197
208
}
198

            
199
260
pub(super) async fn alloc_string_pair_chain(
200
260
    caller: &mut Caller<'_, SessionData>,
201
260
    lines: Vec<String>,
202
260
) -> wasmtime::Result<Option<Rooted<StructRef>>> {
203
260
    let mut anyrefs: Vec<Rooted<AnyRef>> = Vec::with_capacity(lines.len());
204
260
    for line in lines {
205
208
        let s = alloc_string_ref(caller, line.as_bytes())?;
206
208
        anyrefs.push(s.to_anyref());
207
    }
208
260
    alloc_pair_chain(caller, anyrefs).await
209
260
}
210

            
211
#[cfg(test)]
212
5
pub(super) fn format_rational(r: &Rational64) -> String {
213
5
    if *r.denom() == 1 {
214
3
        r.numer().to_string()
215
    } else {
216
2
        format!("{}/{}", r.numer(), r.denom())
217
    }
218
5
}