Skip to main content

server/
account.rs

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                // TODO: Implement account hook similar to commodity
33                a
34            };
35
36            // Update the tags with the new values from `tagdb`
37            {
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            // Call `update_account_tags` to apply the updates to the database
50            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            // Every account must carry a `name` tag: the UI relies on it.
63            // Reject a blank name before anything is written.
64            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            // The account row and its name tag are written in one transaction,
79            // so a failure while tagging cannot leave a nameless account behind.
80            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        /// Context for keeping environment intact
215        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]
239        // async fn test_add_account(pool: PgPool) -> Result<(), anyhow::Error> {
240        //     let mut conn = pool.acquire().await.unwrap();
241        //     USER.get()
242        //         .unwrap()
243        //         .commit()
244        //         .await
245        //         .expect("Failed to commit user to database");
246
247        //     let mut conn = pool.acquire().await.unwrap();
248        //     COMMODITY.get().unwrap().commit(&mut *conn).await?;
249
250        //     let account = Account {
251        //         id: Uuid::new_v4(),
252        //         commodity_id: COMMODITY.get().unwrap().id,
253        //         parent: None,
254        //     };
255
256        //     account.commit(&mut *conn).await?;
257
258        //     let script = vec![];
259        //     let user = USER.get().unwrap();
260        //     let acc = user.add_account(&script).await?;
261        //     assert!(!acc.commodity_id.is_nil());
262        // }
263
264        #[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            // A rejected creation must not leave a nameless account behind.
302            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            // Re-setting the current value (e.g. saving a rename unchanged)
321            // must keep the tag rather than detach it.
322            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}