Skip to main content

rex_db/models/
balances.rs

1use chrono::{Datelike, Months, NaiveDate};
2use diesel::prelude::*;
3use diesel::result::Error;
4use diesel::upsert::excluded;
5use rex_shared::models::Cent;
6use std::collections::{HashMap, HashSet};
7
8use crate::ConnCache;
9use crate::models::FetchNature;
10use crate::schema::balances;
11
12#[derive(Clone, Debug, Queryable, Insertable, Selectable)]
13pub struct Balance {
14    pub method_id: i32,
15    pub year: i32,
16    pub month: i32,
17    pub balance: i64,
18    pub is_final_balance: bool,
19}
20
21impl Balance {
22    #[must_use]
23    pub fn new(
24        method_id: i32,
25        year: i32,
26        month: i32,
27        balance: i64,
28        is_final_balance: bool,
29    ) -> Self {
30        Balance {
31            method_id,
32            year,
33            month,
34            balance,
35            is_final_balance,
36        }
37    }
38
39    pub fn insert(&self, db_conn: &mut impl ConnCache) -> Result<usize, Error> {
40        use crate::schema::balances::dsl::{balance, balances, method_id, month, year};
41
42        diesel::insert_into(balances)
43            .values(self)
44            .on_conflict((method_id, year, month))
45            .do_update()
46            .set(balance.eq(excluded(balance)))
47            .execute(db_conn.conn())
48    }
49
50    pub fn insert_batch_final_balance(
51        txs: Vec<Balance>,
52        db_conn: &mut impl ConnCache,
53    ) -> Result<usize, Error> {
54        use crate::schema::balances::dsl::balances;
55
56        diesel::insert_into(balances)
57            .values(txs)
58            .execute(db_conn.conn())
59    }
60
61    pub fn update_final_balance(&self, db_conn: &mut impl ConnCache) -> Result<usize, Error> {
62        use crate::schema::balances::dsl::{balance, balances, is_final_balance, method_id};
63
64        diesel::update(
65            balances
66                .filter(method_id.eq(self.method_id))
67                .filter(is_final_balance.eq(true)),
68        )
69        .set(balance.eq(self.balance))
70        .execute(db_conn.conn())
71    }
72
73    pub fn get_balance_map(
74        date: NaiveDate,
75        db_conn: &mut impl ConnCache,
76    ) -> Result<HashMap<i32, Self>, Error> {
77        use crate::schema::balances::dsl::{balances, is_final_balance, method_id, month, year};
78
79        let date_year = date.year();
80        let date_month = date.month() as i32;
81
82        let tx_method_ids: Vec<i32> = db_conn.cache().tx_methods.keys().copied().collect();
83
84        let balance_list = balances
85            .filter(year.eq(date_year))
86            .filter(month.eq(date_month))
87            .filter(method_id.eq_any(tx_method_ids))
88            .filter(is_final_balance.eq(false))
89            .select(Self::as_select())
90            .load(db_conn.conn())?;
91
92        let mut balance_map: HashMap<i32, Self> =
93            balance_list.into_iter().map(|b| (b.method_id, b)).collect();
94
95        if balance_map.len() == db_conn.cache().tx_methods.len() {
96            return Ok(balance_map);
97        }
98
99        for bal in db_conn.cache().tx_methods.values() {
100            balance_map
101                .entry(bal.id)
102                .or_insert_with(|| Balance::new(bal.id, date_year, date_month, 0, false));
103        }
104
105        Ok(balance_map)
106    }
107
108    pub fn get_balance(
109        date: NaiveDate,
110        nature: FetchNature,
111        db_conn: &mut impl ConnCache,
112    ) -> Result<Vec<Self>, Error> {
113        use crate::schema::balances::dsl::{balances, is_final_balance, method_id, month, year};
114
115        let mut pending_balance_tx_methods = HashSet::new();
116
117        for key in db_conn.cache().tx_methods.keys().copied() {
118            pending_balance_tx_methods.insert(key);
119        }
120
121        let date_year = date.year();
122        let mut date_month = date.month() as i32;
123
124        if let FetchNature::Yearly = nature {
125            date_month = 1;
126        }
127
128        let tx_method_ids: Vec<i32> = db_conn.cache().tx_methods.keys().copied().collect();
129
130        if let FetchNature::All = nature {
131            let mut balance_list = Vec::new();
132
133            for m_id in pending_balance_tx_methods {
134                let new_bal = Balance::new(m_id, date_year, date_month, 0, false);
135                balance_list.push(new_bal);
136            }
137
138            return Ok(balance_list);
139        }
140
141        let mut balance_list = balances
142            .filter(year.eq(date_year))
143            .filter(month.eq(date_month))
144            .filter(method_id.eq_any(tx_method_ids))
145            .filter(is_final_balance.eq(false))
146            .select(Self::as_select())
147            .load(db_conn.conn())?;
148
149        if balance_list.len() == pending_balance_tx_methods.len() {
150            return Ok(balance_list);
151        }
152
153        for bal in &balance_list {
154            pending_balance_tx_methods.remove(&bal.method_id);
155        }
156
157        for m_id in pending_balance_tx_methods {
158            let new_bal = Balance::new(m_id, date_year, date_month, 0, false);
159            balance_list.push(new_bal);
160        }
161
162        Ok(balance_list)
163    }
164
165    /// Get the last non-final balance of a method. Date = Current date
166    pub fn get_last_balance(
167        date: NaiveDate,
168        nature: FetchNature,
169        db_conn: &mut impl ConnCache,
170    ) -> Result<HashMap<i32, Cent>, Error> {
171        use crate::schema::balances::dsl::{balances, is_final_balance, method_id, month, year};
172
173        let mut ongoing_date = date;
174
175        if let FetchNature::Yearly = nature {
176            ongoing_date = NaiveDate::from_ymd_opt(date.year(), 1, 1).unwrap();
177        }
178
179        let mut found_method_balances = HashMap::new();
180
181        let mut pending_balance_tx_methods = HashSet::new();
182
183        // All means all TXs were fetched. The last balance is the balance before the first TX
184        // which is 0
185        if let FetchNature::All = nature {
186            let mut to_return = HashMap::new();
187
188            for key in db_conn.cache().tx_methods.keys().copied() {
189                to_return.insert(key, Cent::new(0));
190            }
191
192            return Ok(to_return);
193        }
194
195        for key in db_conn.cache().tx_methods.keys().copied() {
196            pending_balance_tx_methods.insert(key);
197        }
198
199        for _ in 0..3 {
200            ongoing_date = ongoing_date - Months::new(1);
201
202            let date_year = ongoing_date.year();
203            let date_month = ongoing_date.month() as i32;
204
205            let Some(last_balances) = balances
206                .filter(year.eq(date_year))
207                .filter(month.eq(date_month))
208                .filter(method_id.eq_any(&pending_balance_tx_methods))
209                .filter(is_final_balance.eq(false))
210                .select(Self::as_select())
211                .load(db_conn.conn())
212                .optional()?
213            else {
214                continue;
215            };
216
217            for bal in last_balances {
218                if found_method_balances.contains_key(&bal.method_id) {
219                    continue;
220                }
221
222                found_method_balances.insert(bal.method_id, Cent::new(bal.balance));
223                pending_balance_tx_methods.remove(&bal.method_id);
224            }
225        }
226
227        // Fallback. Start from the previous month and look for the last non-final balance
228        let date = date - Months::new(1);
229
230        if !pending_balance_tx_methods.is_empty() {
231            for mid in pending_balance_tx_methods {
232                if let Some(last_balance) = balances
233                    .filter(method_id.eq(mid))
234                    .filter(is_final_balance.eq(false))
235                    .filter(
236                        year.lt(date.year())
237                            .or(year.eq(date.year()).and(month.lt(date.month() as i32))),
238                    )
239                    .order((year.desc(), month.desc()))
240                    .select(Self::as_select())
241                    .first::<Self>(db_conn.conn())
242                    .optional()?
243                {
244                    found_method_balances.insert(mid, Cent::new(last_balance.balance));
245                }
246            }
247        }
248
249        if found_method_balances.len() != db_conn.cache().tx_methods.len() {
250            for method in db_conn.cache().tx_methods.keys() {
251                if !found_method_balances.contains_key(method) {
252                    found_method_balances.insert(*method, Cent::new(0));
253                }
254            }
255        }
256
257        Ok(found_method_balances)
258    }
259
260    pub fn get_final_balance(db_conn: &mut impl ConnCache) -> Result<HashMap<i32, Self>, Error> {
261        use crate::schema::balances::dsl::{balances, is_final_balance};
262
263        let balance_list = balances
264            .filter(is_final_balance.eq(true))
265            .select(Balance::as_select())
266            .load(db_conn.conn())?;
267
268        assert!(
269            (balance_list.len() == db_conn.cache().tx_methods.len()),
270            "Final balances are not set for all transaction methods"
271        );
272
273        let balance_map = balance_list.into_iter().map(|b| (b.method_id, b)).collect();
274
275        Ok(balance_map)
276    }
277
278    pub fn get_balance_highest_date(db_conn: &mut impl ConnCache) -> Result<Vec<Self>, Error> {
279        use crate::schema::balances::dsl::{balances, is_final_balance, month, year};
280
281        let total_methods = db_conn.cache().tx_methods.len();
282
283        balances
284            .filter(is_final_balance.eq(false))
285            .order((year.desc(), month.desc()))
286            .limit(total_methods as i64)
287            .select(Self::as_select())
288            .load(db_conn.conn())
289    }
290
291    pub fn get_highest_date(db_conn: &mut impl ConnCache) -> Result<Self, Error> {
292        use crate::schema::balances::dsl::{balances, is_final_balance, month, year};
293
294        let total_methods = db_conn.cache().tx_methods.len();
295
296        balances
297            .filter(is_final_balance.eq(false))
298            .order((year.desc(), month.desc()))
299            .limit(total_methods as i64)
300            .select(Self::as_select())
301            .first(db_conn.conn())
302    }
303}