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