1pub mod user {
2 use crate::db::DBError;
3 use crate::error::ServerError;
4 use crate::user::User;
5 use finance::{
6 account::{Account, AccountBuilder},
7 tag::Tag,
8 };
9 use sqlx::types::Uuid;
10 use std::collections::HashMap;
11 use std::sync::{Arc, Mutex};
12
13 impl User {
14 pub async fn add_account(&self, script: &[u8]) -> Result<Account, ServerError> {
15 let a = AccountBuilder::new().id(Uuid::new_v4()).build()?;
16
17 let mut tags = self.get_account_tags(&a).await?;
18 let tagdb: Arc<Mutex<HashMap<String, String>>> = Arc::new(Mutex::new(HashMap::new()));
19 {
20 let mut tagdb_lock = tagdb.lock().map_err(|err| {
21 log::error!("{}", t!("Mutex error: %{err}", err = err : {:?}));
22 ServerError::Lock
23 })?;
24 for t in &tags {
25 tagdb_lock.insert(t.tag_name.clone(), t.tag_value.clone());
26 }
27 }
28
29 let account = if script.is_empty() {
30 a
31 } else {
32 a
34 };
35
36 {
38 let tagdb_lock = tagdb.lock().map_err(|err| {
39 log::error!("{}", t!("Mutex error: %{err}", err = err : {:?}));
40 ServerError::Lock
41 })?;
42 for t in &mut tags {
43 if let Some(value) = tagdb_lock.get(&t.tag_name) {
44 t.tag_value = value.to_owned();
45 }
46 }
47 }
48
49 self.update_account_tags(&account, &tags).await?;
51
52 Ok(account)
53 }
54
55 pub async fn create_account(
56 &self,
57 name: &str,
58 parent: Option<Uuid>,
59 ) -> Result<Account, ServerError> {
60 use sqlx::Connection;
61
62 if name.trim().is_empty() {
65 return Err(ServerError::Creation);
66 }
67
68 let a = Account {
69 id: Uuid::new_v4(),
70 parent,
71 };
72
73 let mut conn = self.get_connection().await.map_err(|err| {
74 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
75 ServerError::DB(err)
76 })?;
77
78 let mut tx = conn.begin().await.map_err(|err| {
81 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
82 ServerError::DB(DBError::Sqlx(err))
83 })?;
84
85 a.commit(&mut *tx).await?;
86 sqlx::query_file!(
87 "sql/set/accounts/tag.sql",
88 &a.id,
89 "name",
90 name,
91 None::<String>
92 )
93 .execute(&mut *tx)
94 .await
95 .map_err(|err| {
96 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
97 ServerError::DB(DBError::Sqlx(err))
98 })?;
99
100 tx.commit().await.map_err(|err| {
101 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
102 ServerError::DB(DBError::Sqlx(err))
103 })?;
104
105 Ok(a)
106 }
107
108 pub async fn update_account_tags(
109 &self,
110 a: &Account,
111 tags: &[Tag],
112 ) -> Result<(), ServerError> {
113 for tag in tags {
114 self.set_account_tag(a, tag).await?;
115 }
116 Ok(())
117 }
118
119 pub async fn set_account_tag(&self, a: &Account, t: &Tag) -> Result<(), ServerError> {
120 if t.tag_name.trim().is_empty() || t.tag_value.trim().is_empty() {
121 return Err(ServerError::Creation);
122 }
123 let mut conn = self.get_connection().await.map_err(|err| {
124 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
125 ServerError::DB(err)
126 })?;
127
128 sqlx::query_file!(
129 "sql/set/accounts/tag.sql",
130 &a.id,
131 &t.tag_name,
132 &t.tag_value,
133 t.description
134 )
135 .execute(&mut *conn)
136 .await
137 .map_err(|err| {
138 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
139 ServerError::DB(DBError::Sqlx(err))
140 })?;
141
142 Ok(())
143 }
144
145 pub async fn get_account_tags(&self, a: &Account) -> Result<Vec<Tag>, ServerError> {
146 let mut conn = self.get_connection().await.map_err(|err| {
147 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
148 ServerError::DB(err)
149 })?;
150
151 let tags = sqlx::query_file_as!(Tag, "sql/select/accounts/tags.sql", &a.id)
152 .fetch_all(&mut *conn)
153 .await
154 .map_err(|err| {
155 log::error!("Database error: {err:?}");
156 ServerError::DB(DBError::Sqlx(err))
157 })?;
158
159 Ok(tags)
160 }
161
162 pub async fn list_transaction_ids_by_account(
163 &self,
164 account_id: Uuid,
165 ) -> Result<Vec<Uuid>, ServerError> {
166 let mut conn = self.get_connection().await.map_err(|err| {
167 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
168 ServerError::DB(err)
169 })?;
170
171 let rows = sqlx::query_file!("sql/select/transactions/by_account_all.sql", &account_id)
172 .fetch_all(&mut *conn)
173 .await
174 .map_err(|err| {
175 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
176 ServerError::DB(DBError::Sqlx(err))
177 })?;
178
179 Ok(rows.into_iter().map(|r| r.id).collect())
180 }
181
182 pub async fn get_account_tag(&self, a: &Account, tag: &String) -> Result<Tag, ServerError> {
183 let mut conn = self.get_connection().await.map_err(|err| {
184 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
185 ServerError::DB(err)
186 })?;
187
188 let tag = sqlx::query_file_as!(Tag, "sql/select/accounts/tag.sql", &a.id, tag)
189 .fetch_one(&mut *conn)
190 .await
191 .map_err(|err| {
192 log::error!("Database error: {err:?}");
193 ServerError::DB(DBError::Sqlx(err))
194 })?;
195
196 Ok(tag)
197 }
198 }
199
200 #[cfg(test)]
201 mod account_tests {
202 use super::*;
203 use crate::db::DB_POOL;
204 #[cfg(feature = "testlog")]
205 use env_logger;
206 use finance::commodity::Commodity;
207 #[cfg(feature = "testlog")]
208 use log;
209 use sqlx::PgPool;
210
211 use supp_macro::local_db_sqlx_test;
212 use tokio::sync::OnceCell;
213
214 static CONTEXT: OnceCell<()> = OnceCell::const_new();
216 static COMMODITY: OnceCell<Commodity> = OnceCell::const_new();
217 static USER: OnceCell<User> = OnceCell::const_new();
218
219 async fn setup() {
220 CONTEXT
221 .get_or_init(|| async {
222 #[cfg(feature = "testlog")]
223 let _ = env_logger::builder()
224 .is_test(true)
225 .filter_level(log::LevelFilter::Trace)
226 .try_init();
227 })
228 .await;
229
230 COMMODITY
231 .get_or_init(|| async { Commodity { id: Uuid::new_v4() } })
232 .await;
233
234 USER.get_or_init(|| async { User { id: Uuid::new_v4() } })
235 .await;
236 }
237
238 #[local_db_sqlx_test]
265 async fn test_create_account(pool: PgPool) -> Result<(), anyhow::Error> {
266 USER.get()
267 .unwrap()
268 .commit()
269 .await
270 .expect("Failed to commit user to database");
271 let mut conn = pool.acquire().await.unwrap();
272
273 COMMODITY.get().unwrap().commit(&mut *conn).await?;
274
275 let user = USER.get().unwrap();
276 let a = user.create_account("Test Account", None).await?;
277
278 let tag = user.get_account_tag(&a, &"name".to_string()).await?;
279 assert_eq!(tag.tag_value, "Test Account");
280 }
281
282 #[local_db_sqlx_test]
283 async fn test_create_account_blank_name(pool: PgPool) -> Result<(), anyhow::Error> {
284 USER.get()
285 .unwrap()
286 .commit()
287 .await
288 .expect("Failed to commit user to database");
289
290 let user = USER.get().unwrap();
291 for name in ["", " "] {
292 assert!(
293 matches!(
294 user.create_account(name, None).await,
295 Err(ServerError::Creation)
296 ),
297 "blank name {name:?} must be rejected"
298 );
299 }
300
301 let mut conn = user.get_connection().await?;
303 let count: i64 = sqlx::query_scalar("SELECT count(*) FROM accounts")
304 .fetch_one(&mut *conn)
305 .await?;
306 assert_eq!(count, 0);
307 }
308
309 #[local_db_sqlx_test]
310 async fn test_set_account_name_to_same_value(pool: PgPool) -> Result<(), anyhow::Error> {
311 USER.get()
312 .unwrap()
313 .commit()
314 .await
315 .expect("Failed to commit user to database");
316
317 let user = USER.get().unwrap();
318 let a = user.create_account("Same Name", None).await?;
319
320 user.set_account_tag(
323 &a,
324 &Tag::builder()
325 .id(Uuid::new_v4())
326 .tag_name("name")
327 .tag_value("Same Name")
328 .build()?,
329 )
330 .await?;
331
332 let tags = user.get_account_tags(&a).await?;
333 assert_eq!(tags.len(), 1);
334 assert_eq!(tags[0].tag_name, "name");
335 assert_eq!(tags[0].tag_value, "Same Name");
336 }
337
338 #[local_db_sqlx_test]
339 async fn test_account_tag(pool: PgPool) -> Result<(), anyhow::Error> {
340 USER.get()
341 .unwrap()
342 .commit()
343 .await
344 .expect("Failed to commit user to database");
345 let mut conn = pool.acquire().await.unwrap();
346 COMMODITY.get().unwrap().commit(&mut *conn).await?;
347
348 let account = Account {
349 id: Uuid::new_v4(),
350 parent: None,
351 };
352 account.commit(&mut *conn).await?;
353
354 let user = USER.get().unwrap();
355 user.set_account_tag(
356 &account,
357 &Tag::builder()
358 .id(Uuid::new_v4())
359 .tag_name("test")
360 .tag_value("testval")
361 .build()?,
362 )
363 .await?;
364
365 let tags = user.get_account_tags(&account).await?;
366 assert_eq!(tags.len(), 1);
367 assert_eq!(tags[0].tag_name, "test");
368 assert_eq!(tags[0].tag_value, "testval");
369
370 user.set_account_tag(
371 &account,
372 &Tag::builder()
373 .id(Uuid::new_v4())
374 .tag_name("test")
375 .tag_value("testval2")
376 .build()?,
377 )
378 .await?;
379
380 let tags = user.get_account_tags(&account).await?;
381 assert_eq!(tags.len(), 1);
382 assert_eq!(tags[0].tag_name, "test");
383 assert_eq!(tags[0].tag_value, "testval2");
384 }
385
386 #[local_db_sqlx_test]
387 async fn test_get_account_tags(pool: PgPool) -> Result<(), anyhow::Error> {
388 USER.get()
389 .unwrap()
390 .commit()
391 .await
392 .expect("Failed to commit user to database");
393 let mut conn = pool.acquire().await.unwrap();
394 COMMODITY.get().unwrap().commit(&mut *conn).await?;
395
396 let account = Account {
397 id: Uuid::new_v4(),
398 parent: None,
399 };
400 account.commit(&mut *conn).await?;
401
402 let user = USER.get().unwrap();
403 user.set_account_tag(
404 &account,
405 &Tag::builder()
406 .id(Uuid::new_v4())
407 .tag_name("test1")
408 .tag_value("value1")
409 .build()?,
410 )
411 .await?;
412
413 let tags = user.get_account_tags(&account).await?;
414 assert_eq!(tags.len(), 1);
415
416 user.set_account_tag(
417 &account,
418 &Tag::builder()
419 .id(Uuid::new_v4())
420 .tag_name("test2")
421 .tag_value("value2")
422 .build()?,
423 )
424 .await?;
425
426 let mut newtags = user.get_account_tags(&account).await?;
427 assert_eq!(newtags.len(), 2);
428
429 newtags[0].tag_value = "newvalue1".to_string();
430 user.update_account_tags(&account, &newtags).await?;
431
432 let updated_tags = user.get_account_tags(&account).await?;
433 assert_eq!(updated_tags[0].tag_value, "newvalue1");
434 }
435 }
436}