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
}
13

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

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

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

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

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

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

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

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

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

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

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

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

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