1use anyhow::Result;
2use chrono::{Datelike, NaiveDate};
3use rex_db::ConnCache;
4use rex_db::models::{FetchNature, FullTx, TxType};
5use rex_shared::models::{Cent, Dollar};
6use std::collections::HashMap;
7
8use crate::utils::{get_percentages, month_year_to_unique};
9use crate::views::{
10 LargestMomvement, LargestType, PeakMonthlyMovement, PeakType, SummaryLargest, SummaryMethods,
11 SummaryNet, SummaryPeak,
12};
13
14pub struct SummaryView {
17 txs: Vec<FullTx>,
18 nature: FetchNature,
19}
20
21pub struct FullSummary {
22 methods: Vec<SummaryMethods>,
23 largest: Vec<SummaryLargest>,
24 peak: Vec<SummaryPeak>,
25 net: SummaryNet,
26}
27
28impl FullSummary {
29 #[must_use]
30 pub fn net_array(&self) -> Vec<Vec<String>> {
31 self.net.array()
32 }
33
34 pub fn peak_array(&self) -> Vec<Vec<String>> {
35 self.peak.iter().map(SummaryPeak::array).collect()
36 }
37
38 pub fn method_array(&self) -> Vec<Vec<String>> {
39 self.methods.iter().map(SummaryMethods::array).collect()
40 }
41
42 pub fn largest_array(&self) -> Vec<Vec<String>> {
43 self.largest.iter().map(SummaryLargest::array).collect()
44 }
45}
46
47type CacheTxs = HashMap<i32, Vec<FullTx>>;
48
49pub(crate) fn get_summary(
50 date: NaiveDate,
51 nature: FetchNature,
52 conn: &mut impl ConnCache,
53) -> Result<(SummaryView, Option<CacheTxs>)> {
54 let txs = FullTx::get_txs(date, nature, conn)?;
55
56 let mut create_map = false;
57 if let FetchNature::All = nature {
58 create_map = true;
59 }
60
61 if create_map {
62 let mut map = HashMap::with_capacity(txs.len());
63
64 for tx in &txs {
65 let unique_value = month_year_to_unique(date.month() as i32, date.year());
66
67 map.entry(unique_value)
68 .or_insert_with(Vec::new)
69 .push(tx.clone());
70 }
71
72 let summary_view = SummaryView { txs, nature };
73
74 return Ok((summary_view, Some(map)));
75 }
76
77 let summary_view = SummaryView { txs, nature };
78
79 Ok((summary_view, None))
80}
81
82impl SummaryView {
83 pub fn tags_array(
84 &self,
85 compare: Option<&SummaryView>,
86 conn: &impl ConnCache,
87 ) -> Vec<Vec<String>> {
88 let mut income_tags = HashMap::new();
89 let mut expense_tags = HashMap::new();
90
91 let mut total_income = Cent::new(0);
92 let mut total_expense = Cent::new(0);
93
94 let mut no_mom_yoy = false;
95
96 if let FetchNature::All = self.nature {
97 no_mom_yoy = true;
98 }
99
100 let mut compare_income_tags = HashMap::new();
101 let mut compare_expense_tags = HashMap::new();
102
103 if !no_mom_yoy && let Some(compare) = compare {
104 let (income_map, expense_map) = compare.get_tags_movement_map();
105 compare_income_tags = income_map;
106 compare_expense_tags = expense_map;
107 }
108
109 for tx in &self.txs {
110 for tag in &tx.tags {
111 match tx.tx_type {
112 TxType::Income => {
113 total_income += tx.amount;
114
115 let value = income_tags.entry(tag.name.clone()).or_insert(Cent::new(0));
116 *value += tx.amount;
117 }
118 TxType::Expense => {
119 total_expense += tx.amount;
120
121 let value = expense_tags.entry(tag.name.clone()).or_insert(Cent::new(0));
122 *value += tx.amount;
123 }
124 TxType::Transfer => {}
125 }
126 }
127 }
128
129 let mut to_return = Vec::new();
130
131 for tag in conn.cache().tags.values() {
132 let mut no_push = true;
133
134 let mut to_push = vec![tag.name.clone()];
135
136 let mut income_percentage = 0.0;
137 let mut expense_percentage = 0.0;
138
139 let mut income_amount = Dollar::new(0.0);
140 let mut expense_amount = Dollar::new(0.0);
141
142 if let Some(income) = income_tags.get(&tag.name) {
143 income_percentage = (income.value() as f64 / total_income.value() as f64) * 100.0;
144 income_amount = income.dollar();
145
146 no_push = false;
147 }
148
149 if let Some(expense) = expense_tags.get(&tag.name) {
150 expense_percentage =
151 (expense.value() as f64 / total_expense.value() as f64) * 100.0;
152 expense_amount = expense.dollar();
153
154 no_push = false;
155 }
156
157 if no_push {
158 continue;
159 }
160
161 to_push.push(format!("{income_amount:.2}"));
162 to_push.push(format!("{expense_amount:.2}"));
163
164 to_push.push(format!("{income_percentage:.2}"));
165 to_push.push(format!("{expense_percentage:.2}"));
166
167 if !no_mom_yoy && compare.is_some() {
168 let compare_income = compare_income_tags.get(&tag.name);
169
170 let compare_expense = compare_expense_tags.get(&tag.name);
171
172 if let Some(last_income) = compare_income {
173 let last_earning = last_income.dollar();
174
175 let earning_increased_percentage =
176 ((income_amount - last_earning) / last_earning) * 100.0;
177
178 if last_earning == 0.0 {
179 to_push.push("∞".to_string());
180 } else if earning_increased_percentage < 0.0 {
181 to_push.push(format!("↓{:.2}", earning_increased_percentage.abs()));
182 } else {
183 to_push.push(format!("↑{earning_increased_percentage:.2}"));
184 }
185 } else {
186 to_push.push("∞".to_string());
187 }
188
189 if let Some(last_expense) = compare_expense {
190 let last_expense = last_expense.dollar();
191 let expense_increased_percentage =
192 ((expense_amount - last_expense) / last_expense) * 100.0;
193
194 if last_expense == 0.0 {
195 to_push.push("∞".to_string());
196 } else if expense_increased_percentage < 0.0 {
197 to_push.push(format!("↓{:.2}", expense_increased_percentage.abs()));
198 } else {
199 to_push.push(format!("↑{expense_increased_percentage:.2}"));
200 }
201 } else {
202 to_push.push("∞".to_string());
203 }
204 }
205
206 to_return.push(to_push);
207 }
208
209 to_return
210 }
211
212 fn get_tags_movement_map(&self) -> (HashMap<String, Cent>, HashMap<String, Cent>) {
213 let mut income_tags = HashMap::new();
214 let mut expense_tags = HashMap::new();
215
216 for tx in &self.txs {
217 for tag in &tx.tags {
218 match tx.tx_type {
219 TxType::Income => {
220 let value = income_tags.entry(tag.name.clone()).or_insert(Cent::new(0));
221 *value += tx.amount;
222 }
223 TxType::Expense => {
224 let value = expense_tags.entry(tag.name.clone()).or_insert(Cent::new(0));
225 *value += tx.amount;
226 }
227 TxType::Transfer => {}
228 }
229 }
230 }
231
232 (income_tags, expense_tags)
233 }
234
235 #[allow(private_interfaces)]
236 pub fn generate_summary(
237 &self,
238 last_summary: Option<&FullSummary>,
239 conn: &impl ConnCache,
240 ) -> FullSummary {
241 let mut no_mom_yoy = false;
242
243 if let FetchNature::All = self.nature {
244 no_mom_yoy = true;
245 }
246
247 let mut total_income = Cent::new(0);
248 let mut total_expense = Cent::new(0);
249
250 let mut total_month_checked = 0;
251
252 let mut biggest_earning = LargestMomvement::default();
253 let mut biggest_expense = LargestMomvement::default();
254
255 let mut method_earning = HashMap::new();
256 let mut method_expense = HashMap::new();
257
258 for method in conn.cache().get_methods() {
259 method_earning.insert(method.name.to_string(), Cent::new(0));
260 method_expense.insert(method.name.to_string(), Cent::new(0));
261 }
262
263 let mut ongoing_month = 0;
264
265 let mut ongoing_date = NaiveDate::default();
266
267 let mut peak_earning = PeakMonthlyMovement::default();
268 let mut peak_expense = PeakMonthlyMovement::default();
269
270 let mut last_peak_earning = PeakMonthlyMovement::default();
271 let mut last_peak_expense = PeakMonthlyMovement::default();
272
273 for tx in &self.txs {
274 ongoing_date = tx.date.date();
275
276 let time_unique = month_year_to_unique(tx.date.month() as i32, tx.date.year());
277
278 if ongoing_month == 0 {
279 ongoing_month = time_unique;
280 }
281
282 if time_unique != ongoing_month {
283 ongoing_month = time_unique;
284 total_month_checked += 1;
285
286 if last_peak_earning.amount > peak_earning.amount {
287 peak_earning = last_peak_earning;
288 last_peak_earning = PeakMonthlyMovement::new(tx.date.date());
289 }
290
291 if last_peak_expense.amount > peak_expense.amount {
292 peak_expense = last_peak_expense;
293 last_peak_expense = PeakMonthlyMovement::new(tx.date.date());
294 }
295 }
296
297 match tx.tx_type {
298 TxType::Income => {
299 total_income += tx.amount;
300 let amount = method_earning
301 .entry(tx.from_method.name.clone())
302 .or_insert(Cent::new(0));
303
304 *amount += tx.amount;
305
306 if biggest_earning.amount < tx.amount {
307 biggest_earning.amount = tx.amount;
308 biggest_earning.date = tx.date.date();
309 biggest_earning.method.clone_from(&tx.from_method.name);
310 }
311
312 last_peak_earning.amount += tx.amount;
313 }
314 TxType::Expense => {
315 total_expense += tx.amount;
316 let amount = method_expense
317 .entry(tx.from_method.name.clone())
318 .or_insert(Cent::new(0));
319
320 *amount += tx.amount;
321
322 if biggest_expense.amount < tx.amount {
323 biggest_expense.amount = tx.amount;
324 biggest_expense.date = tx.date.date();
325 biggest_expense.method.clone_from(&tx.from_method.name);
326 }
327
328 last_peak_expense.amount += tx.amount;
329 }
330 TxType::Transfer => {}
331 }
332 }
333
334 total_month_checked += 1;
335
336 if last_peak_earning.amount > peak_earning.amount {
337 peak_earning = last_peak_earning;
338 peak_earning.date = ongoing_date;
339 }
340
341 if last_peak_expense.amount > peak_expense.amount {
342 peak_expense = last_peak_expense;
343 peak_expense.date = ongoing_date;
344 }
345
346 let (income_percentage, expense_percentage) =
347 get_percentages(total_income.value() as f64, total_expense.value() as f64);
348
349 let mut average_income = if total_income == 0 {
350 Some(Dollar::new(0.0))
351 } else {
352 Some(total_income.dollar() / f64::from(total_month_checked))
353 };
354
355 let mut average_expense = if total_income == 0 {
356 Some(Dollar::new(0.0))
357 } else {
358 Some(total_expense.dollar() / f64::from(total_month_checked))
359 };
360
361 let mut method_data = Vec::new();
362
363 for (index, method) in conn.cache().get_methods().iter().enumerate() {
364 let earning_percentage = if method_earning[&method.name].value() == 0 {
367 0.0
368 } else {
369 (method_earning[&method.name].value() as f64 / total_income.value() as f64) * 100.0
370 };
371
372 let expense_percentage = if method_expense[&method.name] == 0 {
373 0.0
374 } else {
375 (method_expense[&method.name].value() as f64 / total_expense.value() as f64) * 100.0
376 };
377
378 let mut average_earning = if method_earning[&method.name] == 0 {
379 Some(Dollar::new(0.0))
380 } else {
381 Some(method_earning[&method.name].dollar() / f64::from(total_month_checked))
382 };
383
384 let mut average_expense = if method_expense[&method.name] == 0 {
385 Some(Dollar::new(0.0))
386 } else {
387 Some(method_expense[&method.name].dollar() / f64::from(total_month_checked))
388 };
389
390 let mut mom_yoy_earning = None;
391 let mut mom_yoy_expense = None;
392
393 if let FetchNature::Monthly = self.nature {
394 average_earning = None;
395 average_expense = None;
396 }
397
398 if let Some(last_summary) = last_summary
399 && !no_mom_yoy
400 {
401 let comparison = &last_summary.methods;
402
403 let last_earning = comparison[index].total_earning;
404 let last_expense = comparison[index].total_expense;
405
406 let current_earning = method_earning[&method.name].dollar();
407 let current_expense = method_expense[&method.name].dollar();
408
409 let earning_increased_percentage =
410 ((current_earning - last_earning) / last_earning) * 100.0;
411
412 if last_earning == 0.0 {
413 mom_yoy_earning = Some("∞".to_string());
414 } else if earning_increased_percentage < 0.0 {
415 mom_yoy_earning = Some(format!("↓{:.2}", earning_increased_percentage.abs()));
416 } else {
417 mom_yoy_earning = Some(format!("↑{earning_increased_percentage:.2}"));
418 }
419
420 let expense_increased_percentage =
421 ((current_expense - last_expense) / last_expense) * 100.0;
422
423 if last_expense == 0.0 {
424 mom_yoy_expense = Some("∞".to_string());
425 } else if expense_increased_percentage < 0.0 {
426 mom_yoy_expense = Some(format!("↓{:.2}", expense_increased_percentage.abs()));
427 } else {
428 mom_yoy_expense = Some(format!("↑{expense_increased_percentage:.2}"));
429 }
430 }
431
432 if !no_mom_yoy && mom_yoy_expense.is_none() && mom_yoy_earning.is_none() {
433 mom_yoy_earning = Some("∞".to_string());
434 mom_yoy_expense = Some("∞".to_string());
435 }
436
437 let method_summary = SummaryMethods::new(
438 method.name.to_string(),
439 method_earning[&method.name].dollar(),
440 method_expense[&method.name].dollar(),
441 earning_percentage,
442 expense_percentage,
443 average_earning,
444 average_expense,
445 mom_yoy_earning,
446 mom_yoy_expense,
447 );
448
449 method_data.push(method_summary);
450 }
451
452 if let FetchNature::Monthly = self.nature {
453 average_income = None;
454 average_expense = None;
455 }
456
457 let mut net_mom_yoy_earning = None;
458 let mut net_mom_yoy_expense = None;
459
460 if let Some(last_summary) = last_summary
461 && !no_mom_yoy
462 {
463 let comparison = &last_summary.net;
464
465 let last_earning = comparison.total_income;
466 let last_expense = comparison.total_expense;
467
468 let earning_increased_percentage =
469 ((total_income.dollar() - last_earning) / last_earning) * 100.0;
470
471 if last_earning == 0.0 {
472 net_mom_yoy_earning = Some("∞".to_string());
473 } else if earning_increased_percentage < 0.0 {
474 net_mom_yoy_earning = Some(format!("↓{:.2}", earning_increased_percentage.abs()));
475 } else {
476 net_mom_yoy_earning = Some(format!("↑{earning_increased_percentage:.2}"));
477 }
478
479 let expense_increased_percentage =
480 ((total_expense.dollar() - last_expense) / last_expense) * 100.0;
481
482 if last_expense == 0.0 {
483 net_mom_yoy_expense = Some("∞".to_string());
484 } else if expense_increased_percentage < 0.0 {
485 net_mom_yoy_expense = Some(format!("↓{:.2}", expense_increased_percentage.abs()));
486 } else {
487 net_mom_yoy_expense = Some(format!("↑{expense_increased_percentage:.2}"));
488 }
489 }
490
491 if !no_mom_yoy && net_mom_yoy_expense.is_none() && net_mom_yoy_earning.is_none() {
492 net_mom_yoy_earning = Some("∞".to_string());
493 net_mom_yoy_expense = Some("∞".to_string());
494 }
495
496 let summary_net = SummaryNet::new(
497 total_income.dollar(),
498 total_expense.dollar(),
499 average_income,
500 average_expense,
501 income_percentage,
502 expense_percentage,
503 net_mom_yoy_earning,
504 net_mom_yoy_expense,
505 );
506
507 let summary_largest = vec![
508 SummaryLargest::new(
509 LargestType::Earning,
510 biggest_earning.method,
511 biggest_earning.amount.dollar(),
512 biggest_earning.date,
513 ),
514 SummaryLargest::new(
515 LargestType::Expense,
516 biggest_expense.method,
517 biggest_expense.amount.dollar(),
518 biggest_expense.date,
519 ),
520 ];
521
522 let summary_peak = vec![
523 SummaryPeak::new(
524 PeakType::Earning,
525 peak_earning.amount.dollar(),
526 peak_earning.date,
527 ),
528 SummaryPeak::new(
529 PeakType::Expense,
530 peak_expense.amount.dollar(),
531 peak_expense.date,
532 ),
533 ];
534
535 FullSummary {
536 methods: method_data,
537 net: summary_net,
538 largest: summary_largest,
539 peak: summary_peak,
540 }
541 }
542}