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 => {
48                let method_id = tx.from_method.id;
49                *last_balance.get_mut(&method_id).unwrap() += tx.amount;
50            }
51            TxType::Expense => {
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.to_string()));
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.to_string());
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.iter().map(|tx_view| tx_view.tx.to_array()).collect()
325    }
326
327    #[must_use]
328    pub fn get_tx(&self, index: usize) -> &FullTx {
329        &self.0[index].tx
330    }
331
332    #[must_use]
333    pub fn get_tx_by_id(&self, id: i32) -> Option<&FullTx> {
334        self.0
335            .iter()
336            .find(|tx_view| tx_view.tx.id == id)
337            .map(|tx_view| &tx_view.tx)
338    }
339
340    pub fn add_tx_balance_array(
341        &self,
342        index: Option<usize>,
343        partial_tx: Option<PartialTx>,
344        db_conn: &mut DbConn,
345    ) -> Result<Vec<Vec<String>>> {
346        let mut final_balance: Option<HashMap<i32, Balance>> = None;
347
348        if index.is_none() {
349            final_balance = Some(Balance::get_final_balance(db_conn)?);
350        }
351
352        let mut sorted_methods: Vec<&TxMethod> = db_conn.cache().tx_methods.values().collect();
353        sorted_methods.sort_by_key(|value| value.position);
354
355        let mut to_return = vec![vec![String::new()]];
356
357        to_return[0].extend(sorted_methods.iter().map(|m| m.name.to_string()));
358
359        to_return[0].push(String::from("Total"));
360
361        let changes = if let Some(partial_tx) = &partial_tx {
362            let tx_type = partial_tx.tx_type.into();
363            let amount = Dollar::new(partial_tx.amount.parse()?).cent();
364
365            let from_method = db_conn.cache().get_method_id(partial_tx.from_method)?;
366            let to_method = if partial_tx.to_method.is_empty() {
367                None
368            } else {
369                Some(db_conn.cache().get_method_id(partial_tx.to_method)?)
370            };
371
372            FullTx::get_changes_partial(from_method, to_method, tx_type, amount, db_conn)
373        } else {
374            FullTx::empty_changes(db_conn)
375        };
376
377        let mut total_balance = Cent::new(0);
378        let mut to_insert_balance = vec![String::from("Balance")];
379        let mut to_insert_changes = vec![String::from("Changes")];
380
381        for method in sorted_methods {
382            let method_id = method.id;
383
384            let mut method_balance = if let Some(mut index) = index {
385                if index == 0 {
386                    let target_tx = &self.0[index];
387
388                    let mut balance = *target_tx.balance.get(&method_id).unwrap();
389                    let amount = target_tx.tx.amount;
390
391                    match target_tx.tx.tx_type {
392                        TxType::Income => balance -= amount,
393                        TxType::Expense => balance += amount,
394                        TxType::Transfer => {
395                            if method_id == target_tx.tx.from_method.id {
396                                balance += amount;
397                            }
398                            if let Some(to_method_id) = &target_tx.tx.to_method
399                                && method_id == to_method_id.id
400                            {
401                                balance -= amount;
402                            }
403                        }
404                    }
405
406                    total_balance += balance;
407
408                    balance
409                } else {
410                    index = index.saturating_sub(1);
411
412                    let target_tx = &self.0[index];
413
414                    let balance = *target_tx.balance.get(&method_id).unwrap();
415                    total_balance += balance;
416
417                    balance
418                }
419            } else {
420                let balance = final_balance
421                    .as_ref()
422                    .unwrap()
423                    .get(&method_id)
424                    .unwrap()
425                    .balance;
426                total_balance += balance;
427
428                Cent::new(balance)
429            };
430
431            if let Some(partial_tx) = &partial_tx {
432                let tx_type = partial_tx.tx_type.into();
433                let amount = Dollar::new(partial_tx.amount.parse()?).cent();
434
435                let from_method = db_conn.cache().get_method_id(partial_tx.from_method)?;
436                let to_method = if partial_tx.to_method.is_empty() {
437                    None
438                } else {
439                    Some(db_conn.cache().get_method_id(partial_tx.to_method)?)
440                };
441
442                match tx_type {
443                    TxType::Income => {
444                        if from_method == method_id {
445                            method_balance += amount;
446                            total_balance += amount;
447                        }
448                    }
449                    TxType::Expense => {
450                        if from_method == method_id {
451                            method_balance -= amount;
452                            total_balance -= amount;
453                        }
454                    }
455
456                    TxType::Transfer => {
457                        if from_method == method_id {
458                            method_balance -= amount;
459                            total_balance -= amount;
460                        } else if let Some(to_method) = to_method
461                            && to_method == method_id
462                        {
463                            method_balance += amount;
464                            total_balance += amount;
465                        }
466                    }
467                }
468            }
469            to_insert_balance.push(format!("{:.2}", method_balance.dollar()));
470
471            let changes_value = changes.get(&method_id).unwrap();
472            to_insert_changes.push(changes_value.to_string());
473        }
474
475        to_insert_balance.push(format!("{:.2}", total_balance.dollar()));
476
477        to_return.push(to_insert_balance);
478        to_return.push(to_insert_changes);
479
480        Ok(to_return)
481    }
482
483    pub(crate) fn switch_tx_index(
484        &mut self,
485        index_1: usize,
486        index_2: usize,
487        db_conn: &mut impl ConnCache,
488    ) -> Result<bool> {
489        let tx_1 = self.0.get(index_1).unwrap();
490        let tx_2 = self.0.get(index_2).unwrap();
491
492        // Can't switch index if not in the same date
493        if tx_1.tx.date.date() != tx_2.tx.date.date() {
494            return Ok(false);
495        }
496
497        let tx_1_order = tx_1.tx.display_order;
498        let tx_2_order = tx_2.tx.display_order;
499
500        let new_tx_1_order = if tx_2_order == 0 {
501            tx_2.tx.id
502        } else {
503            tx_2_order
504        };
505        let new_tx_2_order = if tx_1_order == 0 {
506            tx_1.tx.id
507        } else {
508            tx_1_order
509        };
510
511        let tx_1 = self.0.get_mut(index_1).unwrap();
512        tx_1.tx.display_order = new_tx_1_order;
513
514        tx_1.tx.set_display_order(db_conn)?;
515
516        let tx_2 = self.0.get_mut(index_2).unwrap();
517        tx_2.tx.display_order = new_tx_2_order;
518
519        tx_2.tx.set_display_order(db_conn)?;
520
521        self.0.swap(index_1, index_2);
522
523        Ok(true)
524    }
525
526    #[must_use]
527    pub fn is_empty(&self) -> bool {
528        self.0.is_empty()
529    }
530
531    #[must_use]
532    pub fn len(&self) -> usize {
533        self.0.len()
534    }
535
536    #[must_use]
537    pub fn get_tx_balance(&self, index: usize) -> &HashMap<i32, Cent> {
538        &self.0[index].balance
539    }
540}