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