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
20fn 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}