Skip to main content

rex_app/views/
tx_view.rs

1use anyhow::Result;
2use chrono::NaiveDate;
3use rex_db::ConnCache;
4pub use rex_db::models::FullTx;
5use rex_db::models::{Balance, FetchNature, TxMethod, TxType};
6use rex_shared::models::{Cent, Dollar};
7use std::collections::HashMap;
8
9use crate::conn::DbConn;
10
11pub struct PartialTx<'a> {
12    pub from_method: &'a str,
13    pub to_method: &'a str,
14    pub tx_type: &'a str,
15    pub amount: &'a str,
16}
17
18#[derive(Debug)]
19pub(crate) struct TxView {
20    pub(crate) tx: FullTx,
21    /// Tx Method ID -> Balance after this tx was committed
22    balance: HashMap<i32, Cent>,
23}
24
25pub struct TxViewGroup(pub(crate) Vec<TxView>);
26
27pub(crate) fn get_txs(
28    date: NaiveDate,
29    nature: FetchNature,
30    db_conn: &mut impl ConnCache,
31) -> Result<TxViewGroup> {
32    let txs = FullTx::get_txs(date, nature, db_conn)?;
33
34    let current_balance = Balance::get_balance(date, nature, db_conn)?;
35
36    let last_balance = Balance::get_last_balance(date, nature, db_conn)?;
37
38    let mut last_balance = last_balance
39        .into_iter()
40        .map(|b| (b.0, b.1))
41        .collect::<HashMap<i32, Cent>>();
42
43    let mut all_tx_views = Vec::with_capacity(txs.len());
44
45    for tx in txs {
46        match &tx.tx_type {
47            TxType::Income | TxType::Borrow | TxType::LendRepay => {
48                let method_id = tx.from_method.id;
49                *last_balance.get_mut(&method_id).unwrap() += tx.amount;
50            }
51            TxType::Expense | TxType::Lend | TxType::BorrowRepay => {
52                let method_id = tx.from_method.id;
53                *last_balance.get_mut(&method_id).unwrap() -= tx.amount;
54            }
55
56            TxType::Transfer => {
57                let from_method_id = tx.from_method.id;
58                let to_method_id = tx.to_method.as_ref().unwrap().id;
59
60                *last_balance.get_mut(&from_method_id).unwrap() -= tx.amount;
61                *last_balance.get_mut(&to_method_id).unwrap() += tx.amount;
62            }
63        }
64
65        let tx_view = TxView::new(tx, last_balance.clone());
66        all_tx_views.push(tx_view);
67    }
68
69    // If not calculating on monthly bases, no attempt to tidy up balances
70    if nature != FetchNature::Monthly {
71        return Ok(TxViewGroup(all_tx_views));
72    }
73
74    let mut to_insert_balance = Vec::new();
75
76    for mut balance in current_balance {
77        let method_id = balance.method_id;
78        let last_balance = *last_balance.get(&method_id).unwrap();
79
80        if last_balance != balance.balance {
81            balance.balance = last_balance.value();
82            to_insert_balance.push(balance);
83        }
84    }
85
86    for to_insert in to_insert_balance {
87        to_insert.insert(db_conn)?;
88    }
89
90    Ok(TxViewGroup(all_tx_views))
91}
92
93impl TxView {
94    fn new(tx: FullTx, balance: HashMap<i32, Cent>) -> Self {
95        Self { tx, balance }
96    }
97}
98
99impl TxViewGroup {
100    pub fn balance_array(
101        &self,
102        index: Option<usize>,
103        db_conn: &mut DbConn,
104    ) -> Result<Vec<Vec<String>>> {
105        let mut final_balance: Option<HashMap<i32, Balance>> = None;
106
107        if index.is_none() {
108            final_balance = Some(Balance::get_final_balance(db_conn)?);
109        }
110
111        let mut sorted_methods: Vec<&TxMethod> = db_conn.cache().tx_methods.values().collect();
112        sorted_methods.sort_by_key(|value| value.position);
113
114        let mut to_return = vec![vec![String::new()]];
115
116        to_return[0].extend(sorted_methods.iter().map(|m| m.name.clone()));
117
118        to_return[0].push(String::from("Total"));
119
120        let changes = if let Some(index) = index {
121            let target_tx = &self.0[index];
122
123            target_tx.tx.get_changes(db_conn)
124        } else {
125            FullTx::empty_changes(db_conn)
126        };
127
128        let income = self.get_income(index, db_conn);
129        let expense = self.get_expense(index, db_conn);
130
131        let daily_income = self.get_daily_income(index, db_conn);
132        let daily_expense = self.get_daily_expense(index, db_conn);
133
134        let mut to_insert_balance = vec![String::from("Balance")];
135        let mut to_insert_changes = vec![String::from("Changes")];
136
137        let mut to_insert_income = vec![String::from("Income")];
138        let mut to_insert_expense = vec![String::from("Expense")];
139
140        let mut to_insert_daily_income = vec![String::from("Daily Income")];
141        let mut to_insert_daily_expense = vec![String::from("Daily Expense")];
142
143        let mut total_balance = Cent::new(0);
144        let mut total_income = Cent::new(0);
145        let mut total_expense = Cent::new(0);
146        let mut total_daily_income = Cent::new(0);
147        let mut total_daily_expense = Cent::new(0);
148
149        for method in sorted_methods {
150            let method_id = method.id;
151
152            if let Some(index) = index {
153                let target_tx = &self.0[index];
154
155                let balance = *target_tx.balance.get(&method_id).unwrap();
156                total_balance += balance;
157
158                let method_balance = balance.dollar();
159                to_insert_balance.push(format!("{method_balance:.2}"));
160            } else {
161                let balance = final_balance
162                    .as_ref()
163                    .unwrap()
164                    .get(&method_id)
165                    .unwrap()
166                    .balance;
167                total_balance += balance;
168
169                let method_balance = Cent::new(balance).dollar();
170                to_insert_balance.push(format!("{method_balance:.2}"));
171            }
172
173            let changes_value = changes.get(&method_id).unwrap();
174            to_insert_changes.push(changes_value.clone());
175
176            let method_income = *income.get(&method_id).unwrap();
177            total_income += method_income;
178
179            to_insert_income.push(format!("{:.2}", method_income.dollar()));
180
181            let method_expense = *expense.get(&method_id).unwrap();
182            total_expense += method_expense;
183            to_insert_expense.push(format!("{:.2}", method_expense.dollar()));
184
185            let method_daily_income = *daily_income.get(&method_id).unwrap();
186            total_daily_income += method_daily_income;
187            to_insert_daily_income.push(format!("{:.2}", method_daily_income.dollar()));
188
189            let method_daily_expense = *daily_expense.get(&method_id).unwrap();
190            total_daily_expense += method_daily_expense;
191            to_insert_daily_expense.push(format!("{:.2}", method_daily_expense.dollar()));
192        }
193
194        to_insert_balance.push(format!("{:.2}", total_balance.dollar()));
195
196        to_insert_income.push(format!("{:.2}", total_income.dollar()));
197        to_insert_expense.push(format!("{:.2}", total_expense.dollar()));
198
199        to_insert_daily_income.push(format!("{:.2}", total_daily_income.dollar()));
200        to_insert_daily_expense.push(format!("{:.2}", total_daily_expense.dollar()));
201
202        to_return.push(to_insert_balance);
203        to_return.push(to_insert_changes);
204
205        to_return.push(to_insert_income);
206        to_return.push(to_insert_expense);
207
208        to_return.push(to_insert_daily_income);
209        to_return.push(to_insert_daily_expense);
210
211        Ok(to_return)
212    }
213
214    fn get_daily_income(&self, index: Option<usize>, db_conn: &DbConn) -> HashMap<i32, Cent> {
215        let mut to_return = HashMap::new();
216
217        for method in db_conn.cache().tx_methods.keys() {
218            to_return.insert(*method, Cent::new(0));
219        }
220
221        let Some(index) = index else {
222            return to_return;
223        };
224
225        let target_tx = &self.0[index];
226        let ongoing_date = target_tx.tx.date;
227
228        for tx in self.0.iter().take(index + 1).rev() {
229            if tx.tx.date != ongoing_date {
230                break;
231            }
232
233            if let TxType::Income = tx.tx.tx_type {
234                let method_id = tx.tx.from_method.id;
235                *to_return.get_mut(&method_id).unwrap() += tx.tx.amount;
236            }
237        }
238
239        to_return
240    }
241
242    fn get_daily_expense(&self, index: Option<usize>, db_conn: &DbConn) -> HashMap<i32, Cent> {
243        let mut to_return = HashMap::new();
244
245        for method in db_conn.cache().tx_methods.keys() {
246            to_return.insert(*method, Cent::new(0));
247        }
248
249        let Some(index) = index else {
250            return to_return;
251        };
252
253        let target_tx = &self.0[index];
254        let ongoing_date = target_tx.tx.date;
255
256        for tx in self.0.iter().take(index + 1).rev() {
257            if tx.tx.date != ongoing_date {
258                break;
259            }
260
261            if let TxType::Expense = tx.tx.tx_type {
262                let method_id = tx.tx.from_method.id;
263                *to_return.get_mut(&method_id).unwrap() += tx.tx.amount;
264            }
265        }
266
267        to_return
268    }
269
270    fn get_income(&self, index: Option<usize>, db_conn: &DbConn) -> HashMap<i32, Cent> {
271        let mut to_return = HashMap::new();
272
273        for method in db_conn.cache().tx_methods.keys() {
274            to_return.insert(*method, Cent::new(0));
275        }
276
277        if let Some(index) = index {
278            for tx in self.0.iter().take(index + 1).rev() {
279                if let TxType::Income = tx.tx.tx_type {
280                    let method_id = tx.tx.from_method.id;
281                    *to_return.get_mut(&method_id).unwrap() += tx.tx.amount;
282                }
283            }
284        } else {
285            for tx in &self.0 {
286                if let TxType::Income = tx.tx.tx_type {
287                    let method_id = tx.tx.from_method.id;
288                    *to_return.get_mut(&method_id).unwrap() += tx.tx.amount;
289                }
290            }
291        }
292
293        to_return
294    }
295
296    fn get_expense(&self, index: Option<usize>, db_conn: &DbConn) -> HashMap<i32, Cent> {
297        let mut to_return = HashMap::new();
298
299        for method in db_conn.cache().tx_methods.keys() {
300            to_return.insert(*method, Cent::new(0));
301        }
302
303        if let Some(index) = index {
304            for tx in self.0.iter().take(index + 1).rev() {
305                if let TxType::Expense = tx.tx.tx_type {
306                    let method_id = tx.tx.from_method.id;
307                    *to_return.get_mut(&method_id).unwrap() += tx.tx.amount;
308                }
309            }
310        } else {
311            for tx in &self.0 {
312                if let TxType::Expense = tx.tx.tx_type {
313                    let method_id = tx.tx.from_method.id;
314                    *to_return.get_mut(&method_id).unwrap() += tx.tx.amount;
315                }
316            }
317        }
318
319        to_return
320    }
321
322    #[must_use]
323    pub fn tx_array(&self) -> Vec<Vec<String>> {
324        self.0
325            .iter()
326            .map(|tx_view| tx_view.tx.to_array(false))
327            .collect()
328    }
329
330    #[must_use]
331    pub fn get_tx(&self, index: usize) -> &FullTx {
332        &self.0[index].tx
333    }
334
335    #[must_use]
336    pub fn get_tx_by_id(&self, id: i32) -> Option<&FullTx> {
337        self.0
338            .iter()
339            .find(|tx_view| tx_view.tx.id == id)
340            .map(|tx_view| &tx_view.tx)
341    }
342
343    pub fn add_tx_balance_array(
344        &self,
345        index: Option<usize>,
346        partial_tx: Option<PartialTx>,
347        db_conn: &mut DbConn,
348    ) -> Result<Vec<Vec<String>>> {
349        let mut final_balance: Option<HashMap<i32, Balance>> = None;
350
351        if index.is_none() {
352            final_balance = Some(Balance::get_final_balance(db_conn)?);
353        }
354
355        let mut sorted_methods: Vec<&TxMethod> = db_conn.cache().tx_methods.values().collect();
356        sorted_methods.sort_by_key(|value| value.position);
357
358        let mut to_return = vec![vec![String::new()]];
359
360        to_return[0].extend(sorted_methods.iter().map(|m| m.name.clone()));
361
362        to_return[0].push(String::from("Total"));
363
364        let changes = if let Some(partial_tx) = &partial_tx {
365            let tx_type = partial_tx.tx_type.into();
366            let amount = Dollar::new(partial_tx.amount.parse()?).cent();
367
368            let from_method = db_conn.cache().get_method_id(partial_tx.from_method)?;
369            let to_method = if partial_tx.to_method.is_empty() {
370                None
371            } else {
372                Some(db_conn.cache().get_method_id(partial_tx.to_method)?)
373            };
374
375            FullTx::get_changes_partial(from_method, to_method, tx_type, amount, db_conn)
376        } else {
377            FullTx::empty_changes(db_conn)
378        };
379
380        let mut total_balance = Cent::new(0);
381        let mut to_insert_balance = vec![String::from("Balance")];
382        let mut to_insert_changes = vec![String::from("Changes")];
383
384        for method in sorted_methods {
385            let method_id = method.id;
386
387            let mut method_balance = if let Some(mut index) = index {
388                if index == 0 {
389                    let target_tx = &self.0[index];
390
391                    let mut balance = *target_tx.balance.get(&method_id).unwrap();
392                    let amount = target_tx.tx.amount;
393
394                    match target_tx.tx.tx_type {
395                        TxType::Income | TxType::Borrow | TxType::LendRepay => balance -= amount,
396                        TxType::Expense | TxType::Lend | TxType::BorrowRepay => balance += amount,
397                        TxType::Transfer => {
398                            if method_id == target_tx.tx.from_method.id {
399                                balance += amount;
400                            }
401                            if let Some(to_method_id) = &target_tx.tx.to_method
402                                && method_id == to_method_id.id
403                            {
404                                balance -= amount;
405                            }
406                        }
407                    }
408
409                    total_balance += balance;
410
411                    balance
412                } else {
413                    index = index.saturating_sub(1);
414
415                    let target_tx = &self.0[index];
416
417                    let balance = *target_tx.balance.get(&method_id).unwrap();
418                    total_balance += balance;
419
420                    balance
421                }
422            } else {
423                let balance = final_balance
424                    .as_ref()
425                    .unwrap()
426                    .get(&method_id)
427                    .unwrap()
428                    .balance;
429                total_balance += balance;
430
431                Cent::new(balance)
432            };
433
434            if let Some(partial_tx) = &partial_tx {
435                let tx_type = partial_tx.tx_type.into();
436                let amount = Dollar::new(partial_tx.amount.parse()?).cent();
437
438                let from_method = db_conn.cache().get_method_id(partial_tx.from_method)?;
439                let to_method = if partial_tx.to_method.is_empty() {
440                    None
441                } else {
442                    Some(db_conn.cache().get_method_id(partial_tx.to_method)?)
443                };
444
445                match tx_type {
446                    TxType::Income | TxType::Borrow | TxType::LendRepay => {
447                        if from_method == method_id {
448                            method_balance += amount;
449                            total_balance += amount;
450                        }
451                    }
452                    TxType::Expense | TxType::Lend | TxType::BorrowRepay => {
453                        if from_method == method_id {
454                            method_balance -= amount;
455                            total_balance -= amount;
456                        }
457                    }
458
459                    TxType::Transfer => {
460                        if from_method == method_id {
461                            method_balance -= amount;
462                            total_balance -= amount;
463                        } else if let Some(to_method) = to_method
464                            && to_method == method_id
465                        {
466                            method_balance += amount;
467                            total_balance += amount;
468                        }
469                    }
470                }
471            }
472            to_insert_balance.push(format!("{:.2}", method_balance.dollar()));
473
474            let changes_value = changes.get(&method_id).unwrap();
475            to_insert_changes.push(changes_value.clone());
476        }
477
478        to_insert_balance.push(format!("{:.2}", total_balance.dollar()));
479
480        to_return.push(to_insert_balance);
481        to_return.push(to_insert_changes);
482
483        Ok(to_return)
484    }
485
486    pub(crate) fn switch_tx_index(
487        &mut self,
488        index_1: usize,
489        index_2: usize,
490        db_conn: &mut impl ConnCache,
491    ) -> Result<bool> {
492        let tx_1 = self.0.get(index_1).unwrap();
493        let tx_2 = self.0.get(index_2).unwrap();
494
495        // Can't switch index if not in the same date
496        if tx_1.tx.date.date() != tx_2.tx.date.date() {
497            return Ok(false);
498        }
499
500        let tx_1_order = tx_1.tx.display_order;
501        let tx_2_order = tx_2.tx.display_order;
502
503        let new_tx_1_order = if tx_2_order == 0 {
504            tx_2.tx.id
505        } else {
506            tx_2_order
507        };
508        let new_tx_2_order = if tx_1_order == 0 {
509            tx_1.tx.id
510        } else {
511            tx_1_order
512        };
513
514        let tx_1 = self.0.get_mut(index_1).unwrap();
515        tx_1.tx.display_order = new_tx_1_order;
516
517        tx_1.tx.set_display_order(db_conn)?;
518
519        let tx_2 = self.0.get_mut(index_2).unwrap();
520        tx_2.tx.display_order = new_tx_2_order;
521
522        tx_2.tx.set_display_order(db_conn)?;
523
524        Ok(true)
525    }
526
527    #[must_use]
528    pub fn is_empty(&self) -> bool {
529        self.0.is_empty()
530    }
531
532    #[must_use]
533    pub fn len(&self) -> usize {
534        self.0.len()
535    }
536
537    #[must_use]
538    pub fn get_tx_balance(&self, index: usize) -> &HashMap<i32, Cent> {
539        &self.0[index].balance
540    }
541}