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