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

            
26
250
async fn single_commodity_for(user_id: Uuid, account_id: Uuid) -> wasmtime::Result<Uuid> {
27
250
    match GetAccountCommodities::new()
28
250
        .user_id(user_id)
29
250
        .account_id(account_id)
30
250
        .run()
31
250
        .await
32
    {
33
250
        Ok(Some(CmdResult::CommodityInfoList(items))) => match items.as_slice() {
34
225
            [info] => Ok(info.commodity_id),
35
25
            [] => Err(wasmtime::Error::msg(
36
25
                "account-balance: account has no commodity yet (no splits); cannot produce \
37
25
                 Commodity-typed value",
38
25
            )),
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
250
}
53

            
54
225
async fn single_rational_for(user_id: Uuid, account_id: Uuid) -> wasmtime::Result<(i64, i64)> {
55
225
    match GetBalance::new()
56
225
        .user_id(user_id)
57
225
        .account_id(account_id)
58
225
        .run()
59
225
        .await
60
    {
61
225
        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
225
}
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
27
pub(super) async fn run_get_balance_single(
81
27
    user_id: Uuid,
82
27
    id_arg: Option<String>,
83
27
) -> wasmtime::Result<(i64, i64)> {
84
27
    let raw = id_arg
85
27
        .filter(|s| !s.is_empty())
86
27
        .ok_or_else(|| wasmtime::Error::msg("get-balance: missing or empty :account-id arg"))?;
87
26
    let account_id = Uuid::parse_str(&raw)
88
26
        .map_err(|err| wasmtime::Error::msg(format!("get-balance: invalid uuid '{raw}': {err}")))?;
89
25
    match GetBalance::new()
90
25
        .user_id(user_id)
91
25
        .account_id(account_id)
92
25
        .run()
93
25
        .await
94
    {
95
25
        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
27
}
106

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

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

            
169
250
async fn fetch_commodity_infos(
170
250
    user_id: Uuid,
171
250
    account_id: Uuid,
172
250
) -> wasmtime::Result<Vec<CommodityInfo>> {
173
250
    match GetAccountCommodities::new()
174
250
        .user_id(user_id)
175
250
        .account_id(account_id)
176
250
        .run()
177
250
        .await
178
    {
179
250
        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
250
}
187

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

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