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