1use chrono::Datelike;
2use diesel::prelude::*;
3use diesel::result::Error;
4use rex_shared::models::Cent;
5use std::collections::HashMap;
6
7use crate::ConnCache;
8use crate::models::{
9 ActivityTxTag, AmountNature, AmountType, DateNature, EMPTY, FullTx, NewSearch, NewTx, Tag,
10 TxMethod, TxType,
11};
12use crate::schema::activity_txs;
13
14#[derive(Clone, Queryable, Selectable, Insertable)]
15pub struct ActivityTx {
16 pub id: i32,
17 date: Option<String>,
18 details: Option<String>,
19 from_method: Option<i32>,
20 to_method: Option<i32>,
21 amount: Option<i64>,
22 amount_type: Option<String>,
23 tx_type: Option<String>,
24 display_order: Option<i32>,
25 activity_num: i32,
26}
27
28pub struct FullActivityTx {
29 pub id: i32,
30 date: Option<String>,
31 pub details: Option<String>,
32 from_method: Option<TxMethod>,
33 to_method: Option<TxMethod>,
34 amount: Option<AmountNature>,
35 tx_type: Option<TxType>,
36 pub display_order: Option<i32>,
37 tags: Vec<Tag>,
38}
39
40#[derive(Insertable)]
41#[diesel(table_name = activity_txs)]
42pub struct NewActivityTx {
43 date: Option<String>,
44 details: Option<String>,
45 from_method: Option<i32>,
46 to_method: Option<i32>,
47 amount: Option<i64>,
48 amount_type: Option<String>,
49 tx_type: Option<String>,
50 display_order: Option<i32>,
51 activity_num: i32,
52}
53
54impl NewActivityTx {
55 #[allow(clippy::too_many_arguments)]
56 #[must_use]
57 pub fn new(
58 date: Option<String>,
59 details: Option<String>,
60 from_method: Option<i32>,
61 to_method: Option<i32>,
62 amount: Option<i64>,
63 amount_type: Option<String>,
64 tx_type: Option<String>,
65 display_order: Option<i32>,
66 activity_num: i32,
67 ) -> Self {
68 Self {
69 date,
70 details,
71 from_method,
72 to_method,
73 amount,
74 amount_type,
75 tx_type,
76 display_order,
77 activity_num,
78 }
79 }
80
81 #[must_use]
82 pub fn new_from_new_tx(tx: &NewTx, activity_num: i32) -> Self {
83 let date = Some(tx.date.to_string());
84
85 let details = Some(tx.details.unwrap_or_default().to_string());
86
87 let from_method = Some(tx.from_method);
88 let amount = Some(tx.amount);
89 let amount_type = Some(AmountType::Exact.into());
90 let tx_type = Some(tx.tx_type.to_string());
91
92 Self {
93 date,
94 details,
95 from_method,
96 to_method: tx.to_method,
97
98 amount,
99 amount_type,
100 tx_type,
101 display_order: None,
102 activity_num,
103 }
104 }
105
106 #[must_use]
107 pub fn new_from_full_tx(tx: &FullTx, set_display_order: bool, activity_num: i32) -> Self {
108 let date = Some(tx.date.to_string());
109
110 let details = tx.details.as_ref().map(std::string::ToString::to_string);
111
112 let from_method = Some(tx.from_method.id);
113 let to_method = tx.to_method.as_ref().map(|to_method| to_method.id);
114
115 let amount = Some(tx.amount.value());
116 let amount_type = Some(AmountType::Exact.into());
117 let tx_type = Some(tx.tx_type.to_string());
118
119 let display_order = if set_display_order {
120 Some(tx.display_order)
121 } else {
122 None
123 };
124
125 Self {
126 date,
127 details,
128 from_method,
129 to_method,
130 amount,
131 amount_type,
132 tx_type,
133 display_order,
134 activity_num,
135 }
136 }
137
138 #[must_use]
139 pub fn new_from_search_tx(tx: &NewSearch, activity_num: i32) -> Self {
140 let date = if let Some(date) = tx.date.as_ref() {
141 match date {
142 DateNature::Exact(d) => Some(d.to_string()),
143 DateNature::ByMonth {
144 start_date,
145 end_date: _,
146 } => Some(format!("{}-{}", start_date.year(), start_date.month())),
147 DateNature::ByYear {
148 start_date,
149 end_date: _,
150 } => Some(format!("{}", start_date.year())),
151 }
152 } else {
153 None
154 };
155
156 Self {
157 date,
158 details: tx.details.map(std::string::ToString::to_string),
159 from_method: tx.from_method,
160 to_method: tx.to_method,
161 amount: tx.amount.map(|a| a.extract().value()),
162 amount_type: tx.amount.map(|a| a.to_type().into()),
163 tx_type: tx.tx_type.map(std::string::ToString::to_string),
164 display_order: None,
165 activity_num,
166 }
167 }
168
169 pub fn insert(self, db_conn: &mut impl ConnCache) -> Result<ActivityTx, Error> {
170 use crate::schema::activity_txs::dsl::activity_txs;
171
172 diesel::insert_into(activity_txs)
173 .values(self)
174 .returning(ActivityTx::as_select())
175 .get_result(db_conn.conn())
176 }
177}
178
179impl ActivityTx {
180 #[allow(clippy::too_many_arguments)]
181 #[must_use]
182 pub fn new(
183 date: Option<String>,
184 details: Option<String>,
185 from_method: Option<i32>,
186 to_method: Option<i32>,
187 amount: Option<i64>,
188 amount_type: Option<String>,
189 tx_type: Option<String>,
190 display_order: Option<i32>,
191 activity_num: i32,
192 id: i32,
193 ) -> Self {
194 Self {
195 id,
196 date,
197 details,
198 from_method,
199 to_method,
200 amount,
201 amount_type,
202 tx_type,
203 display_order,
204 activity_num,
205 }
206 }
207
208 pub fn insert(self, db_conn: &mut impl ConnCache) -> Result<usize, Error> {
209 use crate::schema::activity_txs::dsl::activity_txs;
210
211 diesel::insert_into(activity_txs)
212 .values(self)
213 .execute(db_conn.conn())
214 }
215
216 pub fn convert_to_full_tx(
217 txs: Vec<&Self>,
218 db_conn: &mut impl ConnCache,
219 ) -> Result<Vec<FullActivityTx>, Error> {
220 let tx_ids = txs.iter().map(|t| t.id).collect::<Vec<i32>>();
221
222 let tx_tags = ActivityTxTag::get_by_tx_ids(tx_ids, db_conn)?;
223
224 let mut tx_tags_map = HashMap::new();
225
226 for tag in tx_tags {
227 tx_tags_map
228 .entry(tag.tx_id)
229 .or_insert(Vec::new())
230 .push(tag.tag_id);
231 }
232
233 let mut to_return = Vec::new();
234
235 for tx in txs {
236 let tags: Vec<Tag> = {
237 let tag_ids = tx_tags_map.get(&tx.id).unwrap_or(&EMPTY);
238 let mut v = Vec::with_capacity(tag_ids.len());
239 for tag_id in tag_ids {
240 v.push(db_conn.cache().tags.get(tag_id).unwrap().clone());
241 }
242 v
243 };
244
245 let from_method = tx
246 .from_method
247 .as_ref()
248 .map(|method_id| db_conn.cache().tx_methods.get(method_id).unwrap().clone());
249
250 let to_method = tx
251 .to_method
252 .as_ref()
253 .map(|method_id| db_conn.cache().tx_methods.get(method_id).unwrap().clone());
254
255 let tx_type = tx.tx_type.as_ref().map(|tx_type| tx_type.as_str().into());
256
257 let mut amount = None;
258
259 if let Some(a) = tx.amount.as_ref() {
260 let amount_type: AmountType = tx.amount_type.as_ref().unwrap().as_str().into();
261 amount = Some(AmountNature::from_type(amount_type, Cent::new(*a)));
262 }
263
264 let full_tx = FullActivityTx {
265 id: tx.id,
266 date: tx.date.clone(),
267 details: tx.details.clone(),
268 from_method,
269 to_method,
270 amount,
271 tx_type,
272 tags,
273 display_order: tx.display_order,
274 };
275
276 to_return.push(full_tx);
277 }
278
279 Ok(to_return)
280 }
281}
282
283impl FullActivityTx {
284 #[must_use]
285 pub fn to_array(&self) -> Vec<String> {
286 let amount = if let Some(amount) = self.amount.as_ref() {
287 amount.to_string()
288 } else {
289 String::new()
290 };
291
292 let method_name = if let Some(tx_type) = self.tx_type.as_ref() {
293 match tx_type {
294 TxType::Transfer => {
295 let to_method = self
296 .to_method
297 .as_ref()
298 .map_or("?".to_string(), |m| m.name.clone());
299
300 let from_method = self
301 .from_method
302 .as_ref()
303 .map_or("?".to_string(), |m| m.name.clone());
304
305 format!("{to_method} → {from_method}")
306 }
307 TxType::Income
308 | TxType::Expense
309 | TxType::Borrow
310 | TxType::Lend
311 | TxType::BorrowRepay
312 | TxType::LendRepay => self
313 .from_method
314 .as_ref()
315 .map(|m| m.name.clone())
316 .unwrap_or_default(),
317 }
318 } else {
319 self.from_method
320 .as_ref()
321 .map(|m| m.name.clone())
322 .unwrap_or_default()
323 };
324
325 vec![
326 self.date.clone().unwrap_or_default(),
327 self.details.clone().unwrap_or_default(),
328 method_name,
329 amount,
330 self.tx_type
331 .as_ref()
332 .map(std::string::ToString::to_string)
333 .unwrap_or_default(),
334 self.tags
335 .iter()
336 .map(|t| t.name.clone())
337 .collect::<Vec<String>>()
338 .join(", "),
339 ]
340 }
341}