Skip to main content

rex_app/views/
summary_view.rs

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