1
pub 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
4551
        pub async fn create_account(
56
4551
            &self,
57
4551
            name: &str,
58
4551
            parent: Option<Uuid>,
59
4551
        ) -> 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
2057
            if name.trim().is_empty() {
65
2
                return Err(ServerError::Creation);
66
2055
            }
67

            
68
2055
            let a = Account {
69
2055
                id: Uuid::new_v4(),
70
2055
                parent,
71
2055
            };
72

            
73
2055
            let mut conn = self.get_connection().await.map_err(|err| {
74
52
                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
75
52
                ServerError::DB(err)
76
52
            })?;
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
2003
            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
2003
            a.commit(&mut *tx).await?;
86
2003
            sqlx::query_file!(
87
                "sql/set/accounts/tag.sql",
88
                &a.id,
89
                "name",
90
                name,
91
                None::<String>
92
            )
93
2003
            .execute(&mut *tx)
94
2003
            .await
95
2003
            .map_err(|err| {
96
                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
97
                ServerError::DB(DBError::Sqlx(err))
98
            })?;
99

            
100
2003
            tx.commit().await.map_err(|err| {
101
                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
102
                ServerError::DB(DBError::Sqlx(err))
103
            })?;
104

            
105
2003
            Ok(a)
106
2057
        }
107

            
108
1
        pub async fn update_account_tags(
109
1
            &self,
110
1
            a: &Account,
111
1
            tags: &[Tag],
112
1
        ) -> Result<(), ServerError> {
113
2
            for tag in tags {
114
2
                self.set_account_tag(a, tag).await?;
115
            }
116
1
            Ok(())
117
1
        }
118

            
119
80
        pub async fn set_account_tag(&self, a: &Account, t: &Tag) -> Result<(), ServerError> {
120
67
            if t.tag_name.trim().is_empty() || t.tag_value.trim().is_empty() {
121
                return Err(ServerError::Creation);
122
67
            }
123
67
            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
67
            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
67
            .execute(&mut *conn)
136
67
            .await
137
67
            .map_err(|err| {
138
                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
139
                ServerError::DB(DBError::Sqlx(err))
140
            })?;
141

            
142
67
            Ok(())
143
67
        }
144

            
145
6
        pub async fn get_account_tags(&self, a: &Account) -> Result<Vec<Tag>, ServerError> {
146
6
            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
6
            let tags = sqlx::query_file_as!(Tag, "sql/select/accounts/tags.sql", &a.id)
152
6
                .fetch_all(&mut *conn)
153
6
                .await
154
6
                .map_err(|err| {
155
                    log::error!("Database error: {err:?}");
156
                    ServerError::DB(DBError::Sqlx(err))
157
                })?;
158

            
159
6
            Ok(tags)
160
6
        }
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
1
        pub async fn get_account_tag(&self, a: &Account, tag: &String) -> Result<Tag, ServerError> {
183
1
            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
1
            let tag = sqlx::query_file_as!(Tag, "sql/select/accounts/tag.sql", &a.id, tag)
189
1
                .fetch_one(&mut *conn)
190
1
                .await
191
1
                .map_err(|err| {
192
                    log::error!("Database error: {err:?}");
193
                    ServerError::DB(DBError::Sqlx(err))
194
                })?;
195

            
196
1
            Ok(tag)
197
1
        }
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
5
        async fn setup() {
220
5
            CONTEXT
221
5
                .get_or_init(|| async {
222
                    #[cfg(feature = "testlog")]
223
1
                    let _ = env_logger::builder()
224
1
                        .is_test(true)
225
1
                        .filter_level(log::LevelFilter::Trace)
226
1
                        .try_init();
227
2
                })
228
5
                .await;
229

            
230
5
            COMMODITY
231
5
                .get_or_init(|| async { Commodity { id: Uuid::new_v4() } })
232
5
                .await;
233

            
234
5
            USER.get_or_init(|| async { User { id: Uuid::new_v4() } })
235
5
                .await;
236
5
        }
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
}