Skip to main content

rex_app/modifier/
shared.rs

1use anyhow::{Result, anyhow};
2use chrono::{Days, Local, Months, NaiveDate, NaiveTime};
3use rex_db::ConnCache;
4use rex_db::models::{Balance, DateNature, FetchNature, NewSearch, NewTx, Tx, TxType};
5use rex_shared::models::{Dollar, LAST_POSSIBLE_TIME};
6
7use crate::utils::parse_amount_nature_cent;
8
9pub(crate) fn tidy_balances(date: NaiveDate, db_conn: &mut impl ConnCache) -> Result<()> {
10    let tx = Balance::get_highest_date(db_conn)?;
11
12    let max_date = NaiveDate::from_ymd_opt(tx.year, tx.month as u32, 1).unwrap();
13
14    tidy_recursive(max_date, date, db_conn)?;
15
16    // From bad delete txs, final balance got corrupted but monthly balances are fine.
17    // If does not match, trust the monthly balances
18    let mut final_balance = Balance::get_final_balance(db_conn)?;
19    let balance_highest_date = Balance::get_balance_highest_date(db_conn)?;
20
21    for balance in balance_highest_date {
22        let mut final_balance_entry = final_balance.get_mut(&balance.method_id).unwrap().clone();
23
24        if final_balance_entry.balance != balance.balance {
25            final_balance_entry.balance = balance.balance;
26            final_balance_entry.insert(db_conn)?;
27        }
28    }
29
30    Ok(())
31}
32
33/// Recursively tidy balances from the given date to the max date
34fn tidy_recursive(
35    max_date: NaiveDate,
36    date: NaiveDate,
37    db_conn: &mut impl ConnCache,
38) -> Result<()> {
39    let nature = FetchNature::Monthly;
40
41    let txs = Tx::get_txs(date, nature, db_conn)?;
42
43    let current_balance = Balance::get_balance(date, nature, db_conn)?;
44
45    let mut last_balance = Balance::get_last_balance(date, nature, db_conn)?;
46
47    for tx in txs {
48        match tx.tx_type.as_str().into() {
49            TxType::Income | TxType::Borrow | TxType::LendRepay => {
50                let method_id = tx.from_method;
51                *last_balance.get_mut(&method_id).unwrap() += tx.amount;
52            }
53            TxType::Expense | TxType::Lend | TxType::BorrowRepay => {
54                let method_id = tx.from_method;
55                *last_balance.get_mut(&method_id).unwrap() -= tx.amount;
56            }
57
58            TxType::Transfer => {
59                let from_method_id = tx.from_method;
60                let to_method_id = tx.to_method.as_ref().unwrap();
61
62                *last_balance.get_mut(&from_method_id).unwrap() -= tx.amount;
63                *last_balance.get_mut(to_method_id).unwrap() += tx.amount;
64            }
65        }
66    }
67
68    let mut to_insert_balance = Vec::new();
69
70    for mut balance in current_balance {
71        let method_id = balance.method_id;
72        let last_balance = *last_balance.get(&method_id).unwrap();
73
74        if balance.balance != last_balance {
75            balance.balance = last_balance.value();
76            to_insert_balance.push(balance);
77        }
78    }
79
80    for to_insert in to_insert_balance {
81        to_insert.insert(db_conn)?;
82    }
83
84    if date <= max_date {
85        let next_date = date + Months::new(1);
86        tidy_recursive(max_date, next_date, db_conn)?;
87    }
88
89    Ok(())
90}
91
92pub fn parse_tx_fields<'a>(
93    date: &'a str,
94    details: &'a str,
95    from_method: &'a str,
96    to_method: &'a str,
97    amount: &'a str,
98    tx_type: &'a str,
99    db_conn: &impl ConnCache,
100) -> Result<NewTx<'a>> {
101    let date = date.parse::<NaiveDate>()?;
102
103    let local_now = Local::now().naive_local();
104
105    let new_date = if date == local_now.date() {
106        local_now
107    } else {
108        date.and_time(NaiveTime::MIN)
109    };
110
111    let details = if details.is_empty() {
112        None
113    } else {
114        Some(details)
115    };
116
117    let amount = Dollar::new(amount.parse()?).cent().value();
118
119    let from_method = db_conn.cache().get_method_id(from_method)?;
120    let to_method = if to_method.is_empty() {
121        None
122    } else {
123        Some(db_conn.cache().get_method_id(to_method)?)
124    };
125
126    let new_tx = NewTx::new(new_date, details, from_method, to_method, amount, tx_type);
127    Ok(new_tx)
128}
129
130pub fn parse_search_fields<'a>(
131    date: &'a str,
132    details: &'a str,
133    from_method: &'a str,
134    to_method: &'a str,
135    amount: &'a str,
136    tx_type: &'a str,
137    tags: &'a str,
138    db_conn: &impl ConnCache,
139) -> Result<NewSearch<'a>> {
140    let date_nature = if date.is_empty() {
141        None
142    } else {
143        let split_date = date.trim().split('-').collect::<Vec<&str>>();
144
145        match split_date.len() {
146            1 => {
147                let year = split_date[0].parse::<i32>()?;
148
149                let start_date = NaiveDate::from_ymd_opt(year, 1, 1)
150                    .ok_or_else(|| anyhow!("{year} is an invalid year"))?
151                    .and_time(NaiveTime::MIN);
152
153                let end_date = NaiveDate::from_ymd_opt(year + 1, 1, 1)
154                    .ok_or_else(|| anyhow!("{year} is an invalid year"))?
155                    .and_time(LAST_POSSIBLE_TIME);
156
157                Some(DateNature::ByYear {
158                    start_date,
159                    end_date,
160                })
161            }
162            2 => {
163                let year = split_date[0].parse::<i32>()?;
164                let month = split_date[1].parse::<u32>()?;
165
166                let start_date = NaiveDate::from_ymd_opt(year, month, 1)
167                    .ok_or_else(|| anyhow!("{year} or {month} value is invalid"))?;
168
169                let end_date = start_date + Months::new(1) - Days::new(1);
170
171                let start_date = start_date.and_time(NaiveTime::MIN);
172                let end_date = end_date.and_time(LAST_POSSIBLE_TIME);
173
174                Some(DateNature::ByMonth {
175                    start_date,
176                    end_date,
177                })
178            }
179            3 => {
180                let date = date.parse::<NaiveDate>()?.and_time(NaiveTime::MIN);
181                Some(DateNature::Exact(date))
182            }
183            _ => None,
184        }
185    };
186
187    let details = if details.is_empty() {
188        None
189    } else {
190        Some(details)
191    };
192
193    let from_method = if from_method.is_empty() {
194        None
195    } else {
196        Some(db_conn.cache().get_method_id(from_method)?)
197    };
198
199    let to_method = if to_method.is_empty() {
200        None
201    } else {
202        Some(db_conn.cache().get_method_id(to_method)?)
203    };
204
205    let amount = if amount.is_empty() {
206        None
207    } else {
208        parse_amount_nature_cent(amount)?
209    };
210
211    let tx_type = if tx_type.is_empty() {
212        None
213    } else {
214        Some(tx_type)
215    };
216
217    let tags = if tags.is_empty() {
218        None
219    } else {
220        let tags = tags.split(',').map(str::trim).collect::<Vec<&str>>();
221        let tags = tags
222            .iter()
223            .map(|t| db_conn.cache().get_tag_id(t))
224            .filter_map(Result::ok)
225            .collect::<Vec<i32>>();
226
227        Some(tags)
228    };
229
230    let search_tx = NewSearch::new(
231        date_nature,
232        details,
233        tx_type,
234        from_method,
235        to_method,
236        amount,
237        tags,
238    );
239
240    Ok(search_tx)
241}