1
use chrono::{DateTime, Utc};
2
use sqlx::types::Uuid;
3
use supp_macro::command;
4

            
5
use crate::{config::ConfigError, user::User};
6

            
7
use super::super::{
8
    ActivityData, ActivityGroup, ActivityGroupResult, ActivityPeriod, CmdError, CmdResult,
9
    FilterEntity, PeriodGrouping, ReportFilter, ReportMeta,
10
};
11
use super::fetch::{
12
    fetch_accounts, fetch_date_range_splits_filtered_no_conversion,
13
    fetch_date_range_splits_filtered_with_conversion, fetch_target_symbol,
14
};
15
use super::period::{
16
    generate_month_boundaries, generate_quarter_boundaries, generate_year_boundaries,
17
};
18
use super::tree::build_tree;
19

            
20
// TODO(script-classifier): swap this helper for one that evaluates a
21
// user-supplied nomiscript expression returning a Vec<ActivityGroup>. See
22
// doc/reporting.org "Future work" for the intent.
23
3
fn default_activity_groups() -> Vec<ActivityGroup> {
24
3
    vec![
25
3
        ActivityGroup {
26
3
            label: "Income".to_owned(),
27
3
            filter: ReportFilter::Tag {
28
3
                entity: FilterEntity::Account,
29
3
                name: "type".to_owned(),
30
3
                value: "income".to_owned(),
31
3
            },
32
3
            flip_sign: true,
33
3
        },
34
3
        ActivityGroup {
35
3
            label: "Expense".to_owned(),
36
3
            filter: ReportFilter::Tag {
37
3
                entity: FilterEntity::Account,
38
3
                name: "type".to_owned(),
39
3
                value: "expense".to_owned(),
40
3
            },
41
3
            flip_sign: false,
42
3
        },
43
    ]
44
3
}
45

            
46
13
fn compose_group_filter(group: &ActivityGroup, user_filter: Option<&ReportFilter>) -> ReportFilter {
47
13
    match user_filter {
48
2
        Some(f) => ReportFilter::And(vec![group.filter.clone(), f.clone()]),
49
11
        None => group.filter.clone(),
50
    }
51
13
}
52

            
53
1
fn boundaries_for(
54
1
    grouping: PeriodGrouping,
55
1
    from: DateTime<Utc>,
56
1
    to: DateTime<Utc>,
57
1
) -> Vec<(String, DateTime<Utc>, DateTime<Utc>)> {
58
1
    match grouping {
59
1
        PeriodGrouping::Month => generate_month_boundaries(from, to),
60
        PeriodGrouping::Quarter => generate_quarter_boundaries(from, to),
61
        PeriodGrouping::Year => generate_year_boundaries(from, to),
62
    }
63
1
}
64

            
65
13
async fn fetch_range(
66
13
    conn: &mut sqlx::PgConnection,
67
13
    target_commodity_id: Option<Uuid>,
68
13
    target_symbol: Option<&str>,
69
13
    from: DateTime<Utc>,
70
13
    to: DateTime<Utc>,
71
13
    filter: &ReportFilter,
72
13
) -> Result<super::tree::AccountAmounts, CmdError> {
73
13
    match (target_commodity_id, target_symbol) {
74
        (Some(tid), Some(sym)) => {
75
            fetch_date_range_splits_filtered_with_conversion(conn, tid, sym, from, to, filter).await
76
        }
77
13
        _ => fetch_date_range_splits_filtered_no_conversion(conn, from, to, filter).await,
78
    }
79
13
}
80

            
81
struct RunCtx<'a> {
82
    accounts: &'a [super::tree::AccountRow],
83
    user_filter: Option<&'a ReportFilter>,
84
    target_commodity_id: Option<Uuid>,
85
    target_symbol: Option<&'a str>,
86
}
87

            
88
13
async fn run_group_for_period(
89
13
    conn: &mut sqlx::PgConnection,
90
13
    ctx: &RunCtx<'_>,
91
13
    group: &ActivityGroup,
92
13
    from: DateTime<Utc>,
93
13
    to: DateTime<Utc>,
94
13
) -> Result<ActivityGroupResult, CmdError> {
95
13
    let filter = compose_group_filter(group, ctx.user_filter);
96
13
    let amounts = fetch_range(
97
13
        conn,
98
13
        ctx.target_commodity_id,
99
13
        ctx.target_symbol,
100
13
        from,
101
13
        to,
102
13
        &filter,
103
13
    )
104
13
    .await?;
105
13
    let roots = build_tree(ctx.accounts, &amounts);
106
13
    Ok(ActivityGroupResult {
107
13
        label: group.label.clone(),
108
13
        flip_sign: group.flip_sign,
109
13
        roots,
110
13
    })
111
13
}
112

            
113
command! {
114
    ActivityReport {
115
        #[required]
116
        user_id: Uuid,
117
        #[required]
118
        date_from: DateTime<Utc>,
119
        #[required]
120
        date_to: DateTime<Utc>,
121
        #[optional]
122
        target_commodity_id: Uuid,
123
        #[optional]
124
        period_grouping: PeriodGrouping,
125
        #[optional]
126
        groups: Vec<ActivityGroup>,
127
        #[optional]
128
        report_filter: ReportFilter,
129
    } => {
130
        let user = User { id: user_id };
131
        let mut conn = user.get_connection().await.map_err(|err| {
132
            log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
133
            ConfigError::DB
134
        })?;
135

            
136
        let accounts = fetch_accounts(&mut conn).await?;
137
        let groups = groups.unwrap_or_else(default_activity_groups);
138

            
139
        let target_symbol = match target_commodity_id {
140
            Some(tid) => Some(fetch_target_symbol(&mut conn, tid).await?),
141
            None => None,
142
        };
143

            
144
        let period_boundaries = period_grouping
145
1
            .map(|g| boundaries_for(g, date_from, date_to))
146
            .unwrap_or_default();
147

            
148
        let ctx = RunCtx {
149
            accounts: &accounts,
150
            user_filter: report_filter.as_ref(),
151
            target_commodity_id,
152
            target_symbol: target_symbol.as_deref(),
153
        };
154

            
155
        let periods = if period_boundaries.is_empty() {
156
            let mut group_results = Vec::with_capacity(groups.len());
157
            for group in &groups {
158
                group_results.push(
159
                    run_group_for_period(&mut conn, &ctx, group, date_from, date_to).await?,
160
                );
161
            }
162
            vec![ActivityPeriod {
163
                label: None,
164
                groups: group_results,
165
            }]
166
        } else {
167
            let mut out = Vec::with_capacity(period_boundaries.len());
168
            for (label, pfrom, pto) in &period_boundaries {
169
                let mut group_results = Vec::with_capacity(groups.len());
170
                for group in &groups {
171
                    group_results
172
                        .push(run_group_for_period(&mut conn, &ctx, group, *pfrom, *pto).await?);
173
                }
174
                out.push(ActivityPeriod {
175
                    label: Some(label.clone()),
176
                    groups: group_results,
177
                });
178
            }
179
            out
180
        };
181

            
182
        Ok(Some(CmdResult::Activity(ActivityData {
183
            meta: ReportMeta {
184
                date_from: Some(date_from),
185
                date_to: Some(date_to),
186
                target_commodity_id,
187
            },
188
            periods,
189
        })))
190
    }
191
29
}