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::boundaries;
16
17pub(super) const MAX_QUERIES: usize = 4096;
23use super::tree::build_tree;
24
25fn default_activity_groups() -> Vec<ActivityGroup> {
29 vec![
30 ActivityGroup {
31 label: "Income".to_owned(),
32 filter: ReportFilter::Tag {
33 entity: FilterEntity::Account,
34 name: "type".to_owned(),
35 value: "income".to_owned(),
36 },
37 flip_sign: true,
38 },
39 ActivityGroup {
40 label: "Expense".to_owned(),
41 filter: ReportFilter::Tag {
42 entity: FilterEntity::Account,
43 name: "type".to_owned(),
44 value: "expense".to_owned(),
45 },
46 flip_sign: false,
47 },
48 ]
49}
50
51pub(super) fn widest_query_leaves(
58 report_filter: Option<&ReportFilter>,
59 groups: &[ActivityGroup],
60) -> usize {
61 report_filter.map_or(0, ReportFilter::leaves)
62 + groups.iter().map(|g| g.filter.leaves()).max().unwrap_or(0)
63}
64
65fn compose_group_filter(group: &ActivityGroup, user_filter: Option<&ReportFilter>) -> ReportFilter {
66 match user_filter {
67 Some(f) => ReportFilter::And(vec![group.filter.clone(), f.clone()]),
68 None => group.filter.clone(),
69 }
70}
71
72async fn fetch_range(
73 conn: &mut sqlx::PgConnection,
74 target_commodity_id: Option<Uuid>,
75 target_symbol: Option<&str>,
76 from: DateTime<Utc>,
77 to: DateTime<Utc>,
78 filter: &ReportFilter,
79) -> Result<super::tree::AccountAmounts, CmdError> {
80 match (target_commodity_id, target_symbol) {
81 (Some(tid), Some(sym)) => {
82 fetch_date_range_splits_filtered_with_conversion(conn, tid, sym, from, to, filter).await
83 }
84 _ => fetch_date_range_splits_filtered_no_conversion(conn, from, to, filter).await,
85 }
86}
87
88struct RunCtx<'a> {
89 accounts: &'a [super::tree::AccountRow],
90 user_filter: Option<&'a ReportFilter>,
91 target_commodity_id: Option<Uuid>,
92 target_symbol: Option<&'a str>,
93}
94
95async fn run_group_for_period(
96 conn: &mut sqlx::PgConnection,
97 ctx: &RunCtx<'_>,
98 group: &ActivityGroup,
99 from: DateTime<Utc>,
100 to: DateTime<Utc>,
101) -> Result<ActivityGroupResult, CmdError> {
102 let filter = compose_group_filter(group, ctx.user_filter);
103 let amounts = fetch_range(
104 conn,
105 ctx.target_commodity_id,
106 ctx.target_symbol,
107 from,
108 to,
109 &filter,
110 )
111 .await?;
112 let roots = build_tree(ctx.accounts, &amounts);
113 Ok(ActivityGroupResult {
114 label: group.label.clone(),
115 flip_sign: group.flip_sign,
116 roots,
117 })
118}
119
120command! {
121 ActivityReport {
122 #[required]
123 user_id: Uuid,
124 #[required]
125 date_from: DateTime<Utc>,
126 #[required]
127 date_to: DateTime<Utc>,
128 #[optional]
129 target_commodity_id: Uuid,
130 #[optional]
131 period_grouping: PeriodGrouping,
132 #[optional]
133 groups: Vec<ActivityGroup>,
134 #[optional]
135 report_filter: ReportFilter,
136 } => {
137 let user = User { id: user_id };
138 let mut conn = user.get_connection().await.map_err(|err| {
139 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
140 ConfigError::DB
141 })?;
142
143 let accounts = fetch_accounts(&mut conn).await?;
144 let groups = groups.unwrap_or_else(default_activity_groups);
145
146 let target_symbol = match target_commodity_id {
147 Some(tid) => Some(fetch_target_symbol(&mut conn, tid).await?),
148 None => None,
149 };
150
151 super::period::check_range(date_from, date_to)?;
152 let period_boundaries = period_grouping
153 .map(|g| boundaries(g, date_from, date_to))
154 .transpose()?
155 .unwrap_or_default();
156
157 super::period::query_budget(period_boundaries.len(), groups.len(), MAX_QUERIES)?;
162
163 super::filter::check_filter_size(widest_query_leaves(report_filter.as_ref(), &groups))?;
164
165 let ctx = RunCtx {
166 accounts: &accounts,
167 user_filter: report_filter.as_ref(),
168 target_commodity_id,
169 target_symbol: target_symbol.as_deref(),
170 };
171
172 let periods = if period_boundaries.is_empty() {
173 let mut group_results = Vec::with_capacity(groups.len());
174 for group in &groups {
175 group_results.push(
176 run_group_for_period(&mut conn, &ctx, group, date_from, date_to).await?,
177 );
178 }
179 vec![ActivityPeriod {
180 label: None,
181 groups: group_results,
182 }]
183 } else {
184 let mut out = Vec::with_capacity(period_boundaries.len());
185 for (label, pfrom, pto) in &period_boundaries {
186 let mut group_results = Vec::with_capacity(groups.len());
187 for group in &groups {
188 group_results
189 .push(run_group_for_period(&mut conn, &ctx, group, *pfrom, *pto).await?);
190 }
191 out.push(ActivityPeriod {
192 label: Some(label.clone()),
193 groups: group_results,
194 });
195 }
196 out
197 };
198
199 Ok(Some(CmdResult::Activity(ActivityData {
200 meta: ReportMeta {
201 date_from: Some(date_from),
202 date_to: Some(date_to),
203 target_commodity_id,
204 },
205 periods,
206 })))
207 }
208}