1
use finance::error::ReportError;
2
use num_rational::Rational64;
3
use sqlx::types::Uuid;
4
use std::collections::HashMap;
5

            
6
use super::super::{CommodityAmount, ReportNode};
7

            
8
pub(super) struct AccountRow {
9
    pub account_id: Uuid,
10
    pub parent_id: Option<Uuid>,
11
    pub account_name: String,
12
    pub account_type: Option<String>,
13
}
14

            
15
pub(super) type AmountMap = HashMap<Uuid, (Rational64, String)>;
16
pub(super) type AccountAmounts = HashMap<Uuid, AmountMap>;
17

            
18
57
pub(super) fn accumulate_split(
19
57
    amounts: &mut AccountAmounts,
20
57
    account_id: Uuid,
21
57
    commodity_id: Uuid,
22
57
    value: Rational64,
23
57
    symbol: &str,
24
57
) {
25
57
    amounts
26
57
        .entry(account_id)
27
57
        .or_default()
28
57
        .entry(commodity_id)
29
57
        .and_modify(|(sum, _)| *sum += value)
30
57
        .or_insert_with(|| (value, symbol.to_owned()));
31
57
}
32

            
33
pub(super) struct ConversionTarget<'a> {
34
    pub commodity_id: Uuid,
35
    pub symbol: &'a str,
36
}
37

            
38
28
pub(super) fn accumulate_split_converted(
39
28
    amounts: &mut AccountAmounts,
40
28
    account_id: Uuid,
41
28
    commodity_id: Uuid,
42
28
    value: Rational64,
43
28
    symbol: &str,
44
28
    target: &ConversionTarget<'_>,
45
28
    price: (Option<i64>, Option<i64>),
46
28
) -> Result<(), ReportError> {
47
28
    if commodity_id == target.commodity_id {
48
13
        accumulate_split(
49
13
            amounts,
50
13
            account_id,
51
13
            target.commodity_id,
52
13
            value,
53
13
            target.symbol,
54
        );
55
13
        Ok(())
56
13
    } else if let (Some(pn), Some(pd)) = price {
57
13
        let converted = value * Rational64::new(pn, pd);
58
13
        accumulate_split(
59
13
            amounts,
60
13
            account_id,
61
13
            target.commodity_id,
62
13
            converted,
63
13
            target.symbol,
64
        );
65
13
        Ok(())
66
    } else {
67
2
        Err(ReportError::MissingConversion {
68
2
            from_commodity: symbol.to_owned(),
69
2
            to_commodity: target.symbol.to_owned(),
70
2
        })
71
    }
72
28
}
73

            
74
29
pub(super) fn build_tree(accounts: &[AccountRow], amounts: &AccountAmounts) -> Vec<ReportNode> {
75
29
    let children_map: HashMap<Option<Uuid>, Vec<&AccountRow>> = {
76
29
        let mut m: HashMap<Option<Uuid>, Vec<&AccountRow>> = HashMap::new();
77
70
        for a in accounts {
78
70
            m.entry(a.parent_id).or_default().push(a);
79
70
        }
80
29
        m
81
    };
82

            
83
29
    let roots = children_map.get(&None).cloned().unwrap_or_default();
84
29
    roots
85
29
        .iter()
86
63
        .map(|a| build_node(a, &children_map, amounts, "", 0))
87
63
        .filter(|n| !n.amounts.is_empty())
88
29
        .collect()
89
29
}
90

            
91
70
fn build_node(
92
70
    account: &AccountRow,
93
70
    children_map: &HashMap<Option<Uuid>, Vec<&AccountRow>>,
94
70
    amounts: &AccountAmounts,
95
70
    parent_path: &str,
96
70
    depth: usize,
97
70
) -> ReportNode {
98
70
    let path = if parent_path.is_empty() {
99
63
        account.account_name.clone()
100
    } else {
101
7
        format!("{parent_path}:{}", account.account_name)
102
    };
103

            
104
70
    let children: Vec<ReportNode> = children_map
105
70
        .get(&Some(account.account_id))
106
70
        .cloned()
107
70
        .unwrap_or_default()
108
70
        .iter()
109
70
        .map(|child| build_node(child, children_map, amounts, &path, depth + 1))
110
70
        .collect();
111

            
112
70
    let mut own_amounts: AmountMap = amounts
113
70
        .get(&account.account_id)
114
70
        .cloned()
115
70
        .unwrap_or_default();
116

            
117
70
    for child in &children {
118
7
        for ca in &child.amounts {
119
6
            own_amounts
120
6
                .entry(ca.commodity_id)
121
6
                .and_modify(|(sum, _)| *sum += ca.amount)
122
6
                .or_insert_with(|| (ca.amount, ca.commodity_symbol.clone()));
123
        }
124
    }
125

            
126
70
    let children: Vec<ReportNode> = children
127
70
        .into_iter()
128
70
        .filter(|n| !n.amounts.is_empty())
129
70
        .collect();
130

            
131
70
    let mut sorted_amounts: Vec<CommodityAmount> = own_amounts
132
70
        .into_iter()
133
70
        .map(|(cid, (amount, symbol))| CommodityAmount {
134
47
            commodity_id: cid,
135
47
            commodity_symbol: symbol,
136
47
            amount,
137
47
        })
138
70
        .collect();
139
70
    sorted_amounts.sort_by_key(|ca| ca.commodity_id);
140

            
141
70
    ReportNode {
142
70
        account_id: account.account_id,
143
70
        account_name: account.account_name.clone(),
144
70
        account_path: path,
145
70
        depth,
146
70
        account_type: account.account_type.clone(),
147
70
        amounts: sorted_amounts,
148
70
        children,
149
70
    }
150
70
}