Skip to main content

server/command/report/
activity.rs

1use chrono::{DateTime, Utc};
2use sqlx::types::Uuid;
3use supp_macro::command;
4
5use crate::{config::ConfigError, user::User};
6
7use super::super::{
8    ActivityData, ActivityGroup, ActivityGroupResult, ActivityPeriod, CmdError, CmdResult,
9    FilterEntity, PeriodGrouping, ReportFilter, ReportMeta,
10};
11use super::fetch::{
12    fetch_accounts, fetch_date_range_splits_filtered_no_conversion,
13    fetch_date_range_splits_filtered_with_conversion, fetch_target_symbol,
14};
15use super::period::{
16    generate_month_boundaries, generate_quarter_boundaries, generate_year_boundaries,
17};
18use 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.
23fn default_activity_groups() -> Vec<ActivityGroup> {
24    vec![
25        ActivityGroup {
26            label: "Income".to_owned(),
27            filter: ReportFilter::Tag {
28                entity: FilterEntity::Account,
29                name: "type".to_owned(),
30                value: "income".to_owned(),
31            },
32            flip_sign: true,
33        },
34        ActivityGroup {
35            label: "Expense".to_owned(),
36            filter: ReportFilter::Tag {
37                entity: FilterEntity::Account,
38                name: "type".to_owned(),
39                value: "expense".to_owned(),
40            },
41            flip_sign: false,
42        },
43    ]
44}
45
46fn compose_group_filter(group: &ActivityGroup, user_filter: Option<&ReportFilter>) -> ReportFilter {
47    match user_filter {
48        Some(f) => ReportFilter::And(vec![group.filter.clone(), f.clone()]),
49        None => group.filter.clone(),
50    }
51}
52
53fn boundaries_for(
54    grouping: PeriodGrouping,
55    from: DateTime<Utc>,
56    to: DateTime<Utc>,
57) -> Vec<(String, DateTime<Utc>, DateTime<Utc>)> {
58    match grouping {
59        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}
64
65async fn fetch_range(
66    conn: &mut sqlx::PgConnection,
67    target_commodity_id: Option<Uuid>,
68    target_symbol: Option<&str>,
69    from: DateTime<Utc>,
70    to: DateTime<Utc>,
71    filter: &ReportFilter,
72) -> Result<super::tree::AccountAmounts, CmdError> {
73    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        _ => fetch_date_range_splits_filtered_no_conversion(conn, from, to, filter).await,
78    }
79}
80
81struct 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
88async fn run_group_for_period(
89    conn: &mut sqlx::PgConnection,
90    ctx: &RunCtx<'_>,
91    group: &ActivityGroup,
92    from: DateTime<Utc>,
93    to: DateTime<Utc>,
94) -> Result<ActivityGroupResult, CmdError> {
95    let filter = compose_group_filter(group, ctx.user_filter);
96    let amounts = fetch_range(
97        conn,
98        ctx.target_commodity_id,
99        ctx.target_symbol,
100        from,
101        to,
102        &filter,
103    )
104    .await?;
105    let roots = build_tree(ctx.accounts, &amounts);
106    Ok(ActivityGroupResult {
107        label: group.label.clone(),
108        flip_sign: group.flip_sign,
109        roots,
110    })
111}
112
113command! {
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            .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}