rex_db/models/
txs.rs

1use chrono::{Datelike, Days, Months, NaiveDate, NaiveDateTime, NaiveTime};
2use diesel::dsl::{exists, sql};
3use diesel::prelude::*;
4use diesel::result::Error;
5use diesel::sql_types::Bool;
6use shared::models::Cent;
7use std::collections::HashMap;
8
9use crate::ConnCache;
10use crate::models::{AmountNature, DateNature, FetchNature, Tag, TxMethod, TxTag, TxType};
11use crate::schema::{tx_tags, txs};
12
13pub static EMPTY: Vec<i32> = Vec::new();
14
15pub struct NewSearch<'a> {
16    pub date: Option<DateNature>,
17    pub details: Option<&'a str>,
18    pub tx_type: Option<&'a str>,
19    pub from_method: Option<i32>,
20    pub to_method: Option<i32>,
21    pub amount: Option<AmountNature>,
22    pub tags: Option<Vec<i32>>,
23}
24
25impl<'a> NewSearch<'a> {
26    #[must_use]
27    pub fn new(
28        date: Option<DateNature>,
29        details: Option<&'a str>,
30        tx_type: Option<&'a str>,
31        from_method: Option<i32>,
32        to_method: Option<i32>,
33        amount: Option<AmountNature>,
34        tags: Option<Vec<i32>>,
35    ) -> Self {
36        Self {
37            date,
38            details,
39            tx_type,
40            from_method,
41            to_method,
42            amount,
43            tags,
44        }
45    }
46
47    pub fn search_txs(&self, db_conn: &mut impl ConnCache) -> Result<Vec<FullTx>, Error> {
48        use crate::schema::txs::dsl::{
49            amount, date, details, from_method, id, to_method, tx_type, txs,
50        };
51
52        let mut query = txs.into_boxed();
53
54        if let Some(d) = self.date.as_ref() {
55            match d {
56                DateNature::Exact(d) => {
57                    query = query.filter(date.eq(d));
58                }
59                DateNature::ByMonth {
60                    start_date,
61                    end_date,
62                }
63                | DateNature::ByYear {
64                    start_date,
65                    end_date,
66                } => {
67                    query = query.filter(date.between(start_date, end_date));
68                }
69            }
70        }
71
72        if let Some(d) = self.details {
73            query = query.filter(details.like(format!("%{d}%")));
74        }
75
76        if let Some(t) = self.tx_type {
77            query = query.filter(tx_type.eq(t));
78        }
79
80        if let Some(m) = self.from_method {
81            query = query.filter(from_method.eq(m));
82        }
83
84        if let Some(m) = self.to_method {
85            query = query.filter(to_method.eq(m));
86        }
87
88        if let Some(a) = self.amount.as_ref() {
89            match a {
90                AmountNature::Exact(a) => {
91                    query = query.filter(amount.eq(a.value()));
92                }
93                AmountNature::MoreThan(a) => {
94                    query = query.filter(amount.gt(a.value()));
95                }
96                AmountNature::MoreThanEqual(a) => {
97                    query = query.filter(amount.ge(a.value()));
98                }
99                AmountNature::LessThan(a) => {
100                    query = query.filter(amount.lt(a.value()));
101                }
102                AmountNature::LessThanEqual(a) => {
103                    query = query.filter(amount.le(a.value()));
104                }
105            }
106        }
107
108        if let Some(tag_ids) = self.tags.as_ref() {
109            query = query.filter(exists(
110                tx_tags::table
111                    .filter(tx_tags::tx_id.eq(id))
112                    .filter(tx_tags::tag_id.eq_any(tag_ids)),
113            ));
114        }
115
116        let result = query.select(Tx::as_select()).load(db_conn.conn())?;
117
118        FullTx::convert_to_full_tx(result, db_conn)
119    }
120}
121
122#[derive(Clone, Debug)]
123pub struct FullTx {
124    pub id: i32,
125    pub date: NaiveDateTime,
126    pub details: Option<String>,
127    pub from_method: TxMethod,
128    pub to_method: Option<TxMethod>,
129    pub amount: Cent,
130    pub tx_type: TxType,
131    pub tags: Vec<Tag>,
132    pub display_order: i32,
133}
134
135#[derive(Clone, Queryable, Selectable, Insertable)]
136pub struct Tx {
137    pub id: i32,
138    date: NaiveDateTime,
139    details: Option<String>,
140    pub from_method: i32,
141    pub to_method: Option<i32>,
142    pub amount: i64,
143    pub tx_type: String,
144    display_order: i32,
145}
146
147#[derive(Clone, Insertable)]
148#[diesel(table_name = txs)]
149pub struct NewTx<'a> {
150    pub date: NaiveDateTime,
151    pub details: Option<&'a str>,
152    pub from_method: i32,
153    pub to_method: Option<i32>,
154    pub amount: i64,
155    pub tx_type: &'a str,
156}
157
158impl<'a> NewTx<'a> {
159    #[must_use]
160    pub fn new(
161        date: NaiveDateTime,
162        details: Option<&'a str>,
163        from_method: i32,
164        to_method: Option<i32>,
165        amount: i64,
166        tx_type: &'a str,
167    ) -> Self {
168        NewTx {
169            date,
170            details,
171            from_method,
172            to_method,
173            amount,
174            tx_type,
175        }
176    }
177
178    pub fn insert(self, db_conn: &mut impl ConnCache) -> Result<Tx, Error> {
179        use crate::schema::txs::dsl::txs;
180
181        diesel::insert_into(txs)
182            .values(self)
183            .returning(Tx::as_returning())
184            .get_result(db_conn.conn())
185    }
186}
187
188impl FullTx {
189    pub fn get_txs(
190        d: NaiveDate,
191        nature: FetchNature,
192        db_conn: &mut impl ConnCache,
193    ) -> Result<Vec<Self>, Error> {
194        let all_txs = Tx::get_txs(d, nature, db_conn)?;
195
196        FullTx::convert_to_full_tx(all_txs, db_conn)
197    }
198
199    pub fn get_tx_by_id(id_num: i32, db_conn: &mut impl ConnCache) -> Result<Self, Error> {
200        let tx = Tx::get_tx_by_id(id_num, db_conn)?;
201
202        Ok(FullTx::convert_to_full_tx(vec![tx], db_conn)?
203            .pop()
204            .unwrap())
205    }
206
207    pub fn convert_to_full_tx(
208        txs: Vec<Tx>,
209        db_conn: &mut impl ConnCache,
210    ) -> Result<Vec<FullTx>, Error> {
211        let tx_ids = txs.iter().map(|t| t.id).collect::<Vec<i32>>();
212
213        let tx_tags = TxTag::get_by_tx_ids(tx_ids, db_conn)?;
214
215        let mut tx_tags_map = HashMap::new();
216
217        for tag in tx_tags {
218            tx_tags_map
219                .entry(tag.tx_id)
220                .or_insert(Vec::new())
221                .push(tag.tag_id);
222        }
223
224        let mut to_return = Vec::new();
225
226        for tx in txs {
227            let tags: Vec<Tag> = {
228                let tag_ids = tx_tags_map.get(&tx.id).unwrap_or(&EMPTY);
229                let mut v = Vec::with_capacity(tag_ids.len());
230                for tag_id in tag_ids {
231                    v.push(db_conn.cache().tags.get(tag_id).unwrap().clone());
232                }
233                v
234            };
235
236            let full_tx = FullTx {
237                id: tx.id,
238                date: tx.date,
239                details: tx.details,
240                from_method: db_conn
241                    .cache()
242                    .tx_methods
243                    .get(&tx.from_method)
244                    .unwrap()
245                    .clone(),
246                to_method: tx
247                    .to_method
248                    .map(|method_id| db_conn.cache().tx_methods.get(&method_id).unwrap().clone()),
249                amount: Cent::new(tx.amount),
250                tx_type: tx.tx_type.as_str().into(),
251                tags,
252                display_order: tx.display_order,
253            };
254
255            to_return.push(full_tx);
256        }
257
258        Ok(to_return)
259    }
260
261    pub fn get_changes(&self, db_conn: &impl ConnCache) -> HashMap<i32, String> {
262        let mut map = HashMap::new();
263
264        for method_id in db_conn.cache().tx_methods.keys() {
265            let mut no_impact = true;
266
267            if self.from_method.id == *method_id {
268                no_impact = false;
269            }
270
271            if let Some(to_method) = &self.to_method
272                && to_method.id == *method_id
273            {
274                no_impact = false;
275            }
276
277            if no_impact {
278                map.insert(*method_id, "0.00".to_string());
279                continue;
280            }
281
282            match self.tx_type {
283                TxType::Income => {
284                    map.insert(*method_id, format!("↑{:.2}", self.amount.dollar()));
285                }
286                TxType::Expense => {
287                    map.insert(*method_id, format!("↓{:.2}", self.amount.dollar()));
288                }
289                TxType::Transfer => {
290                    if self.from_method.id == *method_id {
291                        map.insert(*method_id, format!("↓{:.2}", self.amount.dollar()));
292                    } else {
293                        map.insert(*method_id, format!("↑{:.2}", self.amount.dollar()));
294                    }
295                }
296            }
297        }
298
299        map
300    }
301
302    pub fn empty_changes(db_conn: &impl ConnCache) -> HashMap<i32, String> {
303        let mut map = HashMap::new();
304
305        for method_id in db_conn.cache().tx_methods.keys() {
306            map.insert(*method_id, "0.00".to_string());
307        }
308
309        map
310    }
311
312    pub fn get_changes_partial(
313        from_method: i32,
314        to_method: Option<i32>,
315        tx_type: TxType,
316        amount: Cent,
317        db_conn: &impl ConnCache,
318    ) -> HashMap<i32, String> {
319        let mut map = HashMap::new();
320
321        for method_id in db_conn.cache().tx_methods.keys() {
322            let mut no_impact = true;
323
324            if from_method == *method_id {
325                no_impact = false;
326            }
327
328            if let Some(to_method) = &to_method
329                && to_method == method_id
330            {
331                no_impact = false;
332            }
333
334            if no_impact {
335                map.insert(*method_id, "0.00".to_string());
336                continue;
337            }
338
339            match tx_type {
340                TxType::Income => {
341                    map.insert(*method_id, format!("↑{:.2}", amount.dollar()));
342                }
343                TxType::Expense => {
344                    map.insert(*method_id, format!("↓{:.2}", amount.dollar()));
345                }
346                TxType::Transfer => {
347                    if from_method == *method_id {
348                        map.insert(*method_id, format!("↓{:.2}", amount.dollar()));
349                    } else {
350                        map.insert(*method_id, format!("↑{:.2}", amount.dollar()));
351                    }
352                }
353            }
354        }
355
356        map
357    }
358
359    #[must_use]
360    pub fn to_array(&self) -> Vec<String> {
361        let mut method = self.from_method.name.clone();
362
363        if let Some(to_method) = &self.to_method {
364            method = format!("{} → {}", self.from_method.name, to_method.name);
365        }
366
367        vec![
368            self.date.format("%a %d %I:%M %p").to_string(),
369            self.details.clone().unwrap_or_default(),
370            method,
371            format!("{:.2}", self.amount.dollar()),
372            self.tx_type.to_string(),
373            self.tags
374                .iter()
375                .map(|t| t.name.clone())
376                .collect::<Vec<String>>()
377                .join(", "),
378        ]
379    }
380
381    pub fn set_display_order(&self, db_conn: &mut impl ConnCache) -> Result<usize, Error> {
382        use crate::schema::txs::dsl::{display_order, id, txs};
383
384        diesel::update(txs.filter(id.eq(self.id)))
385            .set(display_order.eq(self.display_order))
386            .execute(db_conn.conn())
387    }
388}
389
390impl Tx {
391    pub fn insert(self, db_conn: &mut impl ConnCache) -> Result<Self, Error> {
392        use crate::schema::txs::dsl::txs;
393
394        diesel::insert_into(txs)
395            .values(self)
396            .returning(Tx::as_returning())
397            .get_result(db_conn.conn())
398    }
399
400    pub fn get_tx_by_id(id_num: i32, db_conn: &mut impl ConnCache) -> Result<Self, Error> {
401        use crate::schema::txs::dsl::{id, txs};
402
403        txs.filter(id.eq(id_num))
404            .select(Self::as_select())
405            .first(db_conn.conn())
406    }
407
408    pub fn get_txs(
409        d: NaiveDate,
410        nature: FetchNature,
411        db_conn: &mut impl ConnCache,
412    ) -> Result<Vec<Self>, Error> {
413        let d = d.and_time(NaiveTime::MIN);
414
415        use crate::schema::txs::dsl::{date, display_order, id, txs};
416
417        let dates = match nature {
418            FetchNature::Monthly => {
419                let start_date = NaiveDate::from_ymd_opt(d.year(), d.month(), 1)
420                    .unwrap()
421                    .and_time(NaiveTime::MIN);
422
423                let end_date = start_date + Months::new(1) - Days::new(1);
424                Some((start_date, end_date))
425            }
426            FetchNature::Yearly => {
427                let start_date = NaiveDate::from_ymd_opt(d.year(), 1, 1)
428                    .unwrap()
429                    .and_time(NaiveTime::MIN);
430
431                let end_date = start_date + Months::new(12) - Days::new(1);
432                Some((start_date, end_date))
433            }
434            FetchNature::All => None,
435        };
436
437        let mut query = txs.into_boxed();
438
439        if let Some((start_date, end_date)) = dates {
440            query = query.filter(date.ge(start_date)).filter(date.le(end_date));
441        }
442
443        query
444            .order((
445                date.asc(),
446                sql::<Bool>("display_order = 0"),
447                display_order.asc(),
448                id.asc(),
449            ))
450            .select(Tx::as_select())
451            .load(db_conn.conn())
452    }
453
454    pub fn delete_tx(id: i32, db_conn: &mut impl ConnCache) -> Result<usize, Error> {
455        use crate::schema::txs::dsl::txs;
456
457        diesel::delete(txs.find(id)).execute(db_conn.conn())
458    }
459
460    #[must_use]
461    pub fn from_new_tx(new_tx: NewTx, id: i32) -> Self {
462        Self {
463            id,
464            date: new_tx.date,
465            details: new_tx.details.map(std::string::ToString::to_string),
466            from_method: new_tx.from_method,
467            to_method: new_tx.to_method,
468            amount: new_tx.amount,
469            tx_type: new_tx.tx_type.to_string(),
470            display_order: 0,
471        }
472    }
473
474    pub fn get_all_details(db_conn: &mut impl ConnCache) -> Result<Vec<String>, Error> {
475        use crate::schema::txs::dsl::{details, txs};
476
477        let result: Vec<Option<String>> = txs
478            .select(details)
479            .filter(details.is_not_null())
480            .load(db_conn.conn())?;
481
482        Ok(result.into_iter().flatten().collect())
483    }
484}