1
pub mod user {
2

            
3
    use crate::error::ServerError;
4
    use crate::user::User;
5
    use finance::tag::Tag;
6
    use sqlx::types::Uuid;
7

            
8
    impl User {
9
20
        pub async fn create_tag(
10
20
            &self,
11
20
            name: String,
12
20
            value: String,
13
20
            description: Option<String>,
14
20
        ) -> Result<Uuid, ServerError> {
15
10
            let mut conn = self.get_connection().await.map_err(|err| {
16
2
                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
17
2
                ServerError::DB(err)
18
2
            })?;
19

            
20
8
            let id = Uuid::new_v4();
21
8
            Tag {
22
8
                id,
23
8
                tag_name: name,
24
8
                tag_value: value,
25
8
                description,
26
8
            }
27
8
            .commit(&mut *conn)
28
8
            .await
29
8
            .map_err(|err| {
30
                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
31
                ServerError::Finance(err)
32
            })?;
33

            
34
8
            Ok(id)
35
10
        }
36

            
37
8
        pub async fn list_tags(&self) -> Result<Vec<Tag>, ServerError> {
38
3
            let mut conn = self.get_connection().await?;
39

            
40
2
            let tags = sqlx::query_file_as!(Tag, "sql/select/tags/all.sql")
41
2
                .fetch_all(&mut *conn)
42
2
                .await
43
2
                .map_err(|err| {
44
                    log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
45
                    ServerError::DB(crate::db::DBError::Sqlx(err))
46
                })?;
47

            
48
2
            Ok(tags)
49
3
        }
50

            
51
9
        pub async fn get_tag(&self, id: Uuid) -> Result<Tag, ServerError> {
52
9
            let mut conn = self.get_connection().await?;
53

            
54
9
            let tag = sqlx::query_file_as!(Tag, "sql/select/tags/by_id.sql", &id)
55
9
                .fetch_one(&mut *conn)
56
9
                .await
57
9
                .map_err(|err| {
58
2
                    log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
59
2
                    ServerError::DB(crate::db::DBError::Sqlx(err))
60
2
                })?;
61

            
62
7
            Ok(tag)
63
9
        }
64

            
65
14
        pub async fn update_tag(
66
14
            &self,
67
14
            id: Uuid,
68
14
            name: String,
69
14
            value: String,
70
14
            description: Option<String>,
71
14
        ) -> Result<(), ServerError> {
72
4
            let mut conn = self.get_connection().await?;
73

            
74
2
            sqlx::query_file!(
75
                "sql/update/tags/update.sql",
76
                &id,
77
                &name,
78
                &value,
79
2
                description.as_deref()
80
            )
81
2
            .execute(&mut *conn)
82
2
            .await
83
2
            .map_err(|err| {
84
                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
85
                ServerError::DB(crate::db::DBError::Sqlx(err))
86
            })?;
87

            
88
2
            Ok(())
89
4
        }
90

            
91
        pub async fn get_transaction_tags(&self, tx_id: Uuid) -> Result<Vec<Tag>, ServerError> {
92
            let mut conn = self.get_connection().await?;
93

            
94
            let tags = sqlx::query_file_as!(Tag, "sql/select/tags/by_transaction.sql", &tx_id)
95
                .fetch_all(&mut *conn)
96
                .await
97
                .map_err(|err| {
98
                    log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
99
                    ServerError::DB(crate::db::DBError::Sqlx(err))
100
                })?;
101

            
102
            Ok(tags)
103
        }
104

            
105
12
        pub async fn create_transaction_tag(
106
12
            &self,
107
12
            tx_id: Uuid,
108
12
            name: String,
109
12
            value: String,
110
12
            description: Option<String>,
111
12
        ) -> Result<Uuid, ServerError> {
112
7
            if name.is_empty() || value.is_empty() {
113
                return Err(ServerError::Creation);
114
7
            }
115
7
            let mut conn = self.get_connection().await?;
116

            
117
6
            let tag_id = Uuid::new_v4();
118
6
            let tag = Tag {
119
6
                id: tag_id,
120
6
                tag_name: name,
121
6
                tag_value: value,
122
6
                description,
123
6
            };
124

            
125
6
            tag.commit(&mut *conn).await.map_err(|err| {
126
                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
127
                ServerError::Finance(err)
128
            })?;
129

            
130
6
            sqlx::query_file!(
131
                "sql/insert/transaction_tags/transaction_tag.sql",
132
                &tx_id,
133
                &tag_id
134
            )
135
6
            .execute(&mut *conn)
136
6
            .await
137
6
            .map_err(|err| {
138
                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
139
                ServerError::DB(crate::db::DBError::Sqlx(err))
140
            })?;
141

            
142
6
            Ok(tag_id)
143
7
        }
144

            
145
8
        pub async fn delete_tag(&self, id: Uuid) -> Result<(), ServerError> {
146
3
            let mut conn = self.get_connection().await?;
147

            
148
2
            sqlx::query_file!("sql/delete/tags/by_id.sql", &id)
149
2
                .execute(&mut *conn)
150
2
                .await
151
2
                .map_err(|err| {
152
                    log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
153
                    ServerError::DB(crate::db::DBError::Sqlx(err))
154
                })?;
155

            
156
2
            Ok(())
157
3
        }
158

            
159
2
        pub async fn list_transaction_tag_names(&self) -> Result<Vec<String>, ServerError> {
160
2
            let mut conn = self.get_connection().await?;
161

            
162
2
            let records = sqlx::query_file!("sql/select/tags/transaction/names.sql")
163
2
                .fetch_all(&mut *conn)
164
2
                .await
165
2
                .map_err(|err| {
166
                    log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
167
                    ServerError::DB(crate::db::DBError::Sqlx(err))
168
                })?;
169

            
170
2
            Ok(records.into_iter().map(|r| r.tag_name).collect())
171
2
        }
172

            
173
4
        pub async fn list_transaction_tag_values(
174
4
            &self,
175
4
            tag_name: &str,
176
4
        ) -> Result<Vec<String>, ServerError> {
177
4
            let mut conn = self.get_connection().await?;
178

            
179
4
            let records =
180
4
                sqlx::query_file!("sql/select/tags/transaction/values_by_name.sql", tag_name)
181
4
                    .fetch_all(&mut *conn)
182
4
                    .await
183
4
                    .map_err(|err| {
184
                        log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
185
                        ServerError::DB(crate::db::DBError::Sqlx(err))
186
                    })?;
187

            
188
4
            Ok(records.into_iter().map(|r| r.tag_value).collect())
189
4
        }
190

            
191
7
        pub async fn create_split_tag(
192
7
            &self,
193
7
            split_id: Uuid,
194
7
            name: String,
195
7
            value: String,
196
7
            description: Option<String>,
197
7
        ) -> Result<Uuid, ServerError> {
198
7
            if name.is_empty() || value.is_empty() {
199
                return Err(ServerError::Creation);
200
7
            }
201
7
            let mut conn = self.get_connection().await?;
202

            
203
7
            let tag_id = Uuid::new_v4();
204
7
            let tag = Tag {
205
7
                id: tag_id,
206
7
                tag_name: name,
207
7
                tag_value: value,
208
7
                description,
209
7
            };
210

            
211
7
            tag.commit(&mut *conn).await.map_err(|err| {
212
                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
213
                ServerError::Finance(err)
214
            })?;
215

            
216
7
            sqlx::query_file!("sql/insert/split_tags/split_tag.sql", &split_id, &tag_id)
217
7
                .execute(&mut *conn)
218
7
                .await
219
7
                .map_err(|err| {
220
                    log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
221
                    ServerError::DB(crate::db::DBError::Sqlx(err))
222
                })?;
223

            
224
7
            Ok(tag_id)
225
7
        }
226

            
227
1
        pub async fn create_account_tag(
228
1
            &self,
229
1
            account_id: Uuid,
230
1
            name: String,
231
1
            value: String,
232
1
            description: Option<String>,
233
1
        ) -> Result<Uuid, ServerError> {
234
1
            if name.is_empty() || value.is_empty() {
235
                return Err(ServerError::Creation);
236
1
            }
237
1
            let mut conn = self.get_connection().await?;
238

            
239
1
            let tag_id = Uuid::new_v4();
240
1
            let tag = Tag {
241
1
                id: tag_id,
242
1
                tag_name: name,
243
1
                tag_value: value,
244
1
                description,
245
1
            };
246

            
247
1
            tag.commit(&mut *conn).await.map_err(|err| {
248
                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
249
                ServerError::Finance(err)
250
            })?;
251

            
252
1
            sqlx::query_file!(
253
                "sql/insert/account_tags/account_tag.sql",
254
                &account_id,
255
                &tag_id
256
            )
257
1
            .execute(&mut *conn)
258
1
            .await
259
1
            .map_err(|err| {
260
                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
261
                ServerError::DB(crate::db::DBError::Sqlx(err))
262
            })?;
263

            
264
1
            Ok(tag_id)
265
1
        }
266

            
267
2
        pub async fn list_account_tag_names(&self) -> Result<Vec<String>, ServerError> {
268
2
            let mut conn = self.get_connection().await?;
269

            
270
2
            let records = sqlx::query_file!("sql/select/tags/account/names.sql")
271
2
                .fetch_all(&mut *conn)
272
2
                .await
273
2
                .map_err(|err| {
274
                    log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
275
                    ServerError::DB(crate::db::DBError::Sqlx(err))
276
                })?;
277

            
278
2
            Ok(records.into_iter().map(|r| r.tag_name).collect())
279
2
        }
280

            
281
3
        pub async fn list_account_tag_values(
282
3
            &self,
283
3
            tag_name: &str,
284
3
        ) -> Result<Vec<String>, ServerError> {
285
3
            let mut conn = self.get_connection().await?;
286

            
287
3
            let records = sqlx::query_file!("sql/select/tags/account/values_by_name.sql", tag_name)
288
3
                .fetch_all(&mut *conn)
289
3
                .await
290
3
                .map_err(|err| {
291
                    log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
292
                    ServerError::DB(crate::db::DBError::Sqlx(err))
293
                })?;
294

            
295
3
            Ok(records.into_iter().map(|r| r.tag_value).collect())
296
3
        }
297

            
298
2
        pub async fn list_split_tag_names(&self) -> Result<Vec<String>, ServerError> {
299
2
            let mut conn = self.get_connection().await?;
300

            
301
2
            let records = sqlx::query_file!("sql/select/tags/split/names.sql")
302
2
                .fetch_all(&mut *conn)
303
2
                .await
304
2
                .map_err(|err| {
305
                    log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
306
                    ServerError::DB(crate::db::DBError::Sqlx(err))
307
                })?;
308

            
309
2
            Ok(records.into_iter().map(|r| r.tag_name).collect())
310
2
        }
311

            
312
4
        pub async fn list_split_tag_values(
313
4
            &self,
314
4
            tag_name: &str,
315
4
        ) -> Result<Vec<String>, ServerError> {
316
4
            let mut conn = self.get_connection().await?;
317

            
318
4
            let records = sqlx::query_file!("sql/select/tags/split/values_by_name.sql", tag_name)
319
4
                .fetch_all(&mut *conn)
320
4
                .await
321
4
                .map_err(|err| {
322
                    log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
323
                    ServerError::DB(crate::db::DBError::Sqlx(err))
324
                })?;
325

            
326
4
            Ok(records.into_iter().map(|r| r.tag_value).collect())
327
4
        }
328
    }
329

            
330
    #[cfg(test)]
331
    mod tag_tests {
332
        use super::*;
333
        use crate::db::DB_POOL;
334
        #[cfg(feature = "testlog")]
335
        use env_logger;
336
        use finance::{split::Split, transaction::Transaction};
337
        #[cfg(feature = "testlog")]
338
        use log;
339
        use sqlx::PgPool;
340
        use sqlx::types::chrono;
341

            
342
        use supp_macro::local_db_sqlx_test;
343
        use tokio::sync::OnceCell;
344

            
345
        /// Context for keeping environment intact
346
        static CONTEXT: OnceCell<()> = OnceCell::const_new();
347
        static USER: OnceCell<User> = OnceCell::const_new();
348

            
349
26
        async fn setup() {
350
26
            CONTEXT
351
26
                .get_or_init(|| async {
352
                    #[cfg(feature = "testlog")]
353
1
                    let _ = env_logger::builder()
354
1
                        .is_test(true)
355
1
                        .filter_level(log::LevelFilter::Trace)
356
1
                        .try_init();
357
2
                })
358
26
                .await;
359
26
            USER.get_or_init(|| async { User { id: Uuid::new_v4() } })
360
26
                .await;
361
26
        }
362

            
363
        #[local_db_sqlx_test]
364
        async fn test_tag_creation(pool: PgPool) -> Result<(), anyhow::Error> {
365
            let user = USER.get().unwrap();
366
            user.commit()
367
                .await
368
                .expect("Failed to commit user to database");
369

            
370
            let id = user
371
                .create_tag("testtag".to_string(), "testval".to_string(), None)
372
                .await?;
373

            
374
            let mut conn = user.get_connection().await?;
375
            let res = sqlx::query_file!("testdata/query_tag_by_id.sql", &id)
376
                .fetch_one(&mut *conn)
377
                .await?;
378

            
379
            assert_eq!(res.tag_name, "testtag".to_string());
380
            assert_eq!(res.tag_value, "testval".to_string());
381
            assert_eq!(res.description, None);
382
        }
383

            
384
        #[local_db_sqlx_test]
385
        async fn test_tag_creation_with_description(pool: PgPool) -> Result<(), anyhow::Error> {
386
            let user = USER.get().unwrap();
387
            user.commit()
388
                .await
389
                .expect("Failed to commit user to database");
390

            
391
            let id = user
392
                .create_tag(
393
                    "categorytag".to_string(),
394
                    "category1".to_string(),
395
                    Some("Test description".to_string()),
396
                )
397
                .await?;
398

            
399
            let tag = user.get_tag(id).await?;
400

            
401
            assert_eq!(tag.tag_name, "categorytag");
402
            assert_eq!(tag.tag_value, "category1");
403
            assert_eq!(tag.description, Some("Test description".to_string()));
404
        }
405

            
406
        #[local_db_sqlx_test]
407
        async fn test_list_tags(pool: PgPool) -> Result<(), anyhow::Error> {
408
            let user = USER.get().unwrap();
409
            user.commit()
410
                .await
411
                .expect("Failed to commit user to database");
412

            
413
            let id1 = user
414
                .create_tag("tag1".to_string(), "value1".to_string(), None)
415
                .await?;
416

            
417
            let id2 = user
418
                .create_tag(
419
                    "tag2".to_string(),
420
                    "value2".to_string(),
421
                    Some("desc".to_string()),
422
                )
423
                .await?;
424

            
425
            let tags = user.list_tags().await?;
426

            
427
            assert!(tags.len() >= 2);
428
1
            assert!(tags.iter().any(|t| t.id == id1));
429
2
            assert!(tags.iter().any(|t| t.id == id2));
430

            
431
1
            let tag1 = tags.iter().find(|t| t.id == id1).unwrap();
432
            assert_eq!(tag1.tag_name, "tag1");
433
            assert_eq!(tag1.tag_value, "value1");
434
            assert_eq!(tag1.description, None);
435

            
436
2
            let tag2 = tags.iter().find(|t| t.id == id2).unwrap();
437
            assert_eq!(tag2.tag_name, "tag2");
438
            assert_eq!(tag2.tag_value, "value2");
439
            assert_eq!(tag2.description, Some("desc".to_string()));
440
        }
441

            
442
        #[local_db_sqlx_test]
443
        async fn test_get_tag(pool: PgPool) -> Result<(), anyhow::Error> {
444
            let user = USER.get().unwrap();
445
            user.commit()
446
                .await
447
                .expect("Failed to commit user to database");
448

            
449
            let id = user
450
                .create_tag(
451
                    "gettag".to_string(),
452
                    "getvalue".to_string(),
453
                    Some("Get description".to_string()),
454
                )
455
                .await?;
456

            
457
            let tag = user.get_tag(id).await?;
458

            
459
            assert_eq!(tag.id, id);
460
            assert_eq!(tag.tag_name, "gettag");
461
            assert_eq!(tag.tag_value, "getvalue");
462
            assert_eq!(tag.description, Some("Get description".to_string()));
463
        }
464

            
465
        #[local_db_sqlx_test]
466
        async fn test_get_nonexistent_tag(pool: PgPool) -> Result<(), anyhow::Error> {
467
            let user = USER.get().unwrap();
468
            user.commit()
469
                .await
470
                .expect("Failed to commit user to database");
471

            
472
            let nonexistent_id = Uuid::new_v4();
473
            let result = user.get_tag(nonexistent_id).await;
474

            
475
            assert!(result.is_err());
476
        }
477

            
478
        #[local_db_sqlx_test]
479
        async fn test_update_tag(pool: PgPool) -> Result<(), anyhow::Error> {
480
            let user = USER.get().unwrap();
481
            user.commit()
482
                .await
483
                .expect("Failed to commit user to database");
484

            
485
            let id = user
486
                .create_tag("oldname".to_string(), "oldvalue".to_string(), None)
487
                .await?;
488

            
489
            user.update_tag(
490
                id,
491
                "newname".to_string(),
492
                "newvalue".to_string(),
493
                Some("Updated description".to_string()),
494
            )
495
            .await?;
496

            
497
            let tag = user.get_tag(id).await?;
498

            
499
            assert_eq!(tag.id, id);
500
            assert_eq!(tag.tag_name, "newname");
501
            assert_eq!(tag.tag_value, "newvalue");
502
            assert_eq!(tag.description, Some("Updated description".to_string()));
503
        }
504

            
505
        #[local_db_sqlx_test]
506
        async fn test_update_tag_remove_description(pool: PgPool) {
507
            let user = USER.get().unwrap();
508
            user.commit()
509
                .await
510
                .expect("Failed to commit user to database");
511

            
512
            let id = user
513
                .create_tag(
514
                    "tagname".to_string(),
515
                    "tagvalue".to_string(),
516
                    Some("Initial description".to_string()),
517
                )
518
                .await?;
519

            
520
            user.update_tag(id, "tagname".to_string(), "tagvalue".to_string(), None)
521
                .await?;
522

            
523
            let tag = user.get_tag(id).await?;
524

            
525
            assert_eq!(tag.description, None);
526
        }
527

            
528
        #[local_db_sqlx_test]
529
        async fn test_delete_tag(pool: PgPool) -> Result<(), anyhow::Error> {
530
            let user = USER.get().unwrap();
531
            user.commit()
532
                .await
533
                .expect("Failed to commit user to database");
534

            
535
            let id = user
536
                .create_tag("deletetag".to_string(), "deletevalue".to_string(), None)
537
                .await?;
538

            
539
            let tag = user.get_tag(id).await;
540
            assert!(tag.is_ok());
541

            
542
            user.delete_tag(id).await?;
543

            
544
            let result = user.get_tag(id).await;
545
            assert!(result.is_err());
546
        }
547

            
548
        #[local_db_sqlx_test]
549
        async fn test_delete_nonexistent_tag(pool: PgPool) -> Result<(), anyhow::Error> {
550
            let user = USER.get().unwrap();
551
            user.commit()
552
                .await
553
                .expect("Failed to commit user to database");
554

            
555
            let nonexistent_id = Uuid::new_v4();
556
            let result = user.delete_tag(nonexistent_id).await;
557

            
558
            assert!(result.is_ok());
559
        }
560

            
561
        #[local_db_sqlx_test]
562
        async fn test_list_tags_empty(pool: PgPool) -> Result<(), anyhow::Error> {
563
            let user = USER.get().unwrap();
564
            user.commit()
565
                .await
566
                .expect("Failed to commit user to database");
567

            
568
            let tags = user.list_tags().await?;
569

            
570
            assert!(
571
                tags.is_empty()
572
                    || tags.iter().all(|t| t.tag_name == "name"
573
                        || t.tag_name == "note"
574
                        || t.tag_name == "symbol")
575
            );
576
        }
577

            
578
        #[local_db_sqlx_test]
579
        async fn test_list_transaction_tag_names_empty(pool: PgPool) -> Result<(), anyhow::Error> {
580
            let user = USER.get().unwrap();
581
            user.commit()
582
                .await
583
                .expect("Failed to commit user to database");
584

            
585
            let names = user.list_transaction_tag_names().await?;
586

            
587
            assert!(names.is_empty());
588
        }
589

            
590
        #[local_db_sqlx_test]
591
        async fn test_list_transaction_tag_names_with_data(
592
            pool: PgPool,
593
        ) -> Result<(), anyhow::Error> {
594
            let user = USER.get().unwrap();
595
            user.commit()
596
                .await
597
                .expect("Failed to commit user to database");
598

            
599
            let commodity_id = user
600
                .create_commodity("USD".to_string(), "US Dollar".to_string())
601
                .await?
602
                .id;
603
            let acc1 = user.create_account("test_acc1", None).await?.id;
604
            let acc2 = user.create_account("test_acc2", None).await?.id;
605

            
606
            let tx = Transaction {
607
                id: sqlx::types::Uuid::new_v4(),
608
                post_date: chrono::Utc::now(),
609
                enter_date: chrono::Utc::now(),
610
            };
611

            
612
            let mut conn = user.get_connection().await?;
613
            let mut ticket = tx.enter(&mut *conn).await?;
614
            let split1 = Split {
615
                id: sqlx::types::Uuid::new_v4(),
616
                account_id: acc1,
617
                tx_id: tx.id,
618
                value_num: 100,
619
                value_denom: 1,
620
                commodity_id,
621
                reconcile_state: None,
622
                reconcile_date: None,
623
                lot_id: None,
624
            };
625
            let split2 = Split {
626
                id: sqlx::types::Uuid::new_v4(),
627
                account_id: acc2,
628
                tx_id: tx.id,
629
                value_num: -100,
630
                value_denom: 1,
631
                commodity_id,
632
                reconcile_state: None,
633
                reconcile_date: None,
634
                lot_id: None,
635
            };
636
            ticket.add_splits(&[&split1, &split2]).await?;
637
            ticket.commit().await?;
638

            
639
            user.create_transaction_tag(tx.id, "category".to_string(), "food".to_string(), None)
640
                .await?;
641
            user.create_transaction_tag(tx.id, "priority".to_string(), "high".to_string(), None)
642
                .await?;
643

            
644
            let names = user.list_transaction_tag_names().await?;
645

            
646
            assert_eq!(names.len(), 2);
647
            assert!(names.contains(&"category".to_string()));
648
            assert!(names.contains(&"priority".to_string()));
649
        }
650

            
651
        #[local_db_sqlx_test]
652
        async fn test_list_transaction_tag_values_empty(pool: PgPool) -> Result<(), anyhow::Error> {
653
            let user = USER.get().unwrap();
654
            user.commit()
655
                .await
656
                .expect("Failed to commit user to database");
657

            
658
            let values = user.list_transaction_tag_values("category").await?;
659

            
660
            assert!(values.is_empty());
661
        }
662

            
663
        #[local_db_sqlx_test]
664
        async fn test_list_transaction_tag_values_with_data(
665
            pool: PgPool,
666
        ) -> Result<(), anyhow::Error> {
667
            let user = USER.get().unwrap();
668
            user.commit()
669
                .await
670
                .expect("Failed to commit user to database");
671

            
672
            let commodity_id = user
673
                .create_commodity("USD".to_string(), "US Dollar".to_string())
674
                .await?
675
                .id;
676
            let acc1 = user.create_account("test_acc1", None).await?.id;
677
            let acc2 = user.create_account("test_acc2", None).await?.id;
678

            
679
            let tx1 = Transaction {
680
                id: sqlx::types::Uuid::new_v4(),
681
                post_date: chrono::Utc::now(),
682
                enter_date: chrono::Utc::now(),
683
            };
684
            let tx2 = Transaction {
685
                id: sqlx::types::Uuid::new_v4(),
686
                post_date: chrono::Utc::now(),
687
                enter_date: chrono::Utc::now(),
688
            };
689
            {
690
                let mut conn = user.get_connection().await?;
691
                let mut ticket1 = tx1.enter(&mut *conn).await?;
692
                let split1a = Split {
693
                    id: sqlx::types::Uuid::new_v4(),
694
                    account_id: acc1,
695
                    tx_id: tx1.id,
696
                    value_num: 100,
697
                    value_denom: 1,
698
                    commodity_id,
699
                    reconcile_state: None,
700
                    reconcile_date: None,
701
                    lot_id: None,
702
                };
703
                let split1b = Split {
704
                    id: sqlx::types::Uuid::new_v4(),
705
                    account_id: acc2,
706
                    tx_id: tx1.id,
707
                    value_num: -100,
708
                    value_denom: 1,
709
                    commodity_id,
710
                    reconcile_state: None,
711
                    reconcile_date: None,
712
                    lot_id: None,
713
                };
714
                ticket1.add_splits(&[&split1a, &split1b]).await?;
715
                ticket1.commit().await?;
716
            }
717
            {
718
                let mut conn = user.get_connection().await?;
719
                let mut ticket2 = tx2.enter(&mut *conn).await?;
720
                let split2a = Split {
721
                    id: sqlx::types::Uuid::new_v4(),
722
                    account_id: acc1,
723
                    tx_id: tx2.id,
724
                    value_num: 200,
725
                    value_denom: 1,
726
                    commodity_id,
727
                    reconcile_state: None,
728
                    reconcile_date: None,
729
                    lot_id: None,
730
                };
731
                let split2b = Split {
732
                    id: sqlx::types::Uuid::new_v4(),
733
                    account_id: acc2,
734
                    tx_id: tx2.id,
735
                    value_num: -200,
736
                    value_denom: 1,
737
                    commodity_id,
738
                    reconcile_state: None,
739
                    reconcile_date: None,
740
                    lot_id: None,
741
                };
742
                ticket2.add_splits(&[&split2a, &split2b]).await?;
743
                ticket2.commit().await?;
744
            }
745
            user.create_transaction_tag(tx1.id, "category".to_string(), "food".to_string(), None)
746
                .await?;
747
            user.create_transaction_tag(
748
                tx2.id,
749
                "category".to_string(),
750
                "transport".to_string(),
751
                None,
752
            )
753
            .await?;
754
            user.create_transaction_tag(tx1.id, "priority".to_string(), "high".to_string(), None)
755
                .await?;
756

            
757
            let category_values = user.list_transaction_tag_values("category").await?;
758

            
759
            assert_eq!(category_values.len(), 2);
760
            assert!(category_values.contains(&"food".to_string()));
761
            assert!(category_values.contains(&"transport".to_string()));
762

            
763
            let priority_values = user.list_transaction_tag_values("priority").await?;
764

            
765
            assert_eq!(priority_values.len(), 1);
766
            assert!(priority_values.contains(&"high".to_string()));
767
        }
768

            
769
        #[local_db_sqlx_test]
770
        async fn test_list_transaction_tag_values_nonexistent_name(
771
            pool: PgPool,
772
        ) -> Result<(), anyhow::Error> {
773
            let user = USER.get().unwrap();
774
            user.commit()
775
                .await
776
                .expect("Failed to commit user to database");
777

            
778
            let commodity_id = user
779
                .create_commodity("USD".to_string(), "US Dollar".to_string())
780
                .await?
781
                .id;
782
            let acc1 = user.create_account("test_acc1", None).await?.id;
783
            let acc2 = user.create_account("test_acc2", None).await?.id;
784

            
785
            let tx = Transaction {
786
                id: sqlx::types::Uuid::new_v4(),
787
                post_date: chrono::Utc::now(),
788
                enter_date: chrono::Utc::now(),
789
            };
790

            
791
            let mut conn = user.get_connection().await?;
792
            let mut ticket = tx.enter(&mut *conn).await?;
793
            let split1 = Split {
794
                id: sqlx::types::Uuid::new_v4(),
795
                account_id: acc1,
796
                tx_id: tx.id,
797
                value_num: 100,
798
                value_denom: 1,
799
                commodity_id,
800
                reconcile_state: None,
801
                reconcile_date: None,
802
                lot_id: None,
803
            };
804
            let split2 = Split {
805
                id: sqlx::types::Uuid::new_v4(),
806
                account_id: acc2,
807
                tx_id: tx.id,
808
                value_num: -100,
809
                value_denom: 1,
810
                commodity_id,
811
                reconcile_state: None,
812
                reconcile_date: None,
813
                lot_id: None,
814
            };
815
            ticket.add_splits(&[&split1, &split2]).await?;
816
            ticket.commit().await?;
817

            
818
            user.create_transaction_tag(tx.id, "category".to_string(), "food".to_string(), None)
819
                .await?;
820

            
821
            let values = user.list_transaction_tag_values("nonexistent").await?;
822

            
823
            assert!(values.is_empty());
824
        }
825

            
826
        #[local_db_sqlx_test]
827
        async fn test_create_split_tag(pool: PgPool) -> Result<(), anyhow::Error> {
828
            let user = USER.get().unwrap();
829
            user.commit()
830
                .await
831
                .expect("Failed to commit user to database");
832

            
833
            let commodity_id = user
834
                .create_commodity("USD".to_string(), "US Dollar".to_string())
835
                .await?
836
                .id;
837
            let acc1 = user.create_account("test_acc1", None).await?.id;
838
            let acc2 = user.create_account("test_acc2", None).await?.id;
839

            
840
            let tx = Transaction {
841
                id: sqlx::types::Uuid::new_v4(),
842
                post_date: chrono::Utc::now(),
843
                enter_date: chrono::Utc::now(),
844
            };
845

            
846
            let mut conn = user.get_connection().await?;
847
            let mut ticket = tx.enter(&mut *conn).await?;
848
            let split1 = Split {
849
                id: sqlx::types::Uuid::new_v4(),
850
                account_id: acc1,
851
                tx_id: tx.id,
852
                value_num: 100,
853
                value_denom: 1,
854
                commodity_id,
855
                reconcile_state: None,
856
                reconcile_date: None,
857
                lot_id: None,
858
            };
859
            let split2 = Split {
860
                id: sqlx::types::Uuid::new_v4(),
861
                account_id: acc2,
862
                tx_id: tx.id,
863
                value_num: -100,
864
                value_denom: 1,
865
                commodity_id,
866
                reconcile_state: None,
867
                reconcile_date: None,
868
                lot_id: None,
869
            };
870
            ticket.add_splits(&[&split1, &split2]).await?;
871
            ticket.commit().await?;
872

            
873
            let tag_id = user
874
                .create_split_tag(
875
                    split1.id,
876
                    "project".to_string(),
877
                    "nomisync".to_string(),
878
                    Some("Split tag for project tracking".to_string()),
879
                )
880
                .await?;
881

            
882
            let mut conn = user.get_connection().await?;
883
            let res = sqlx::query!(
884
                "SELECT tag_id FROM split_tags WHERE split_id = $1",
885
                &split1.id
886
            )
887
            .fetch_one(&mut *conn)
888
            .await?;
889

            
890
            assert_eq!(res.tag_id, tag_id);
891

            
892
            let tag = user.get_tag(tag_id).await?;
893
            assert_eq!(tag.tag_name, "project");
894
            assert_eq!(tag.tag_value, "nomisync");
895
            assert_eq!(
896
                tag.description,
897
                Some("Split tag for project tracking".to_string())
898
            );
899
        }
900

            
901
        #[local_db_sqlx_test]
902
        async fn test_list_split_tag_names_empty(pool: PgPool) -> Result<(), anyhow::Error> {
903
            let user = USER.get().unwrap();
904
            user.commit()
905
                .await
906
                .expect("Failed to commit user to database");
907

            
908
            let names = user.list_split_tag_names().await?;
909

            
910
            assert!(names.is_empty());
911
        }
912

            
913
        #[local_db_sqlx_test]
914
        async fn test_list_split_tag_names_with_data(pool: PgPool) -> Result<(), anyhow::Error> {
915
            let user = USER.get().unwrap();
916
            user.commit()
917
                .await
918
                .expect("Failed to commit user to database");
919

            
920
            let commodity_id = user
921
                .create_commodity("USD".to_string(), "US Dollar".to_string())
922
                .await?
923
                .id;
924
            let acc1 = user.create_account("test_acc1", None).await?.id;
925
            let acc2 = user.create_account("test_acc2", None).await?.id;
926

            
927
            let tx = Transaction {
928
                id: sqlx::types::Uuid::new_v4(),
929
                post_date: chrono::Utc::now(),
930
                enter_date: chrono::Utc::now(),
931
            };
932

            
933
            let mut conn = user.get_connection().await?;
934
            let mut ticket = tx.enter(&mut *conn).await?;
935
            let split1 = Split {
936
                id: sqlx::types::Uuid::new_v4(),
937
                account_id: acc1,
938
                tx_id: tx.id,
939
                value_num: 100,
940
                value_denom: 1,
941
                commodity_id,
942
                reconcile_state: None,
943
                reconcile_date: None,
944
                lot_id: None,
945
            };
946
            let split2 = Split {
947
                id: sqlx::types::Uuid::new_v4(),
948
                account_id: acc2,
949
                tx_id: tx.id,
950
                value_num: -100,
951
                value_denom: 1,
952
                commodity_id,
953
                reconcile_state: None,
954
                reconcile_date: None,
955
                lot_id: None,
956
            };
957
            ticket.add_splits(&[&split1, &split2]).await?;
958
            ticket.commit().await?;
959

            
960
            user.create_split_tag(
961
                split1.id,
962
                "project".to_string(),
963
                "nomisync".to_string(),
964
                None,
965
            )
966
            .await?;
967
            user.create_split_tag(
968
                split2.id,
969
                "department".to_string(),
970
                "engineering".to_string(),
971
                None,
972
            )
973
            .await?;
974

            
975
            let names = user.list_split_tag_names().await?;
976

            
977
            assert_eq!(names.len(), 2);
978
            assert!(names.contains(&"project".to_string()));
979
            assert!(names.contains(&"department".to_string()));
980
        }
981

            
982
        #[local_db_sqlx_test]
983
        async fn test_list_split_tag_values_empty(pool: PgPool) -> Result<(), anyhow::Error> {
984
            let user = USER.get().unwrap();
985
            user.commit()
986
                .await
987
                .expect("Failed to commit user to database");
988

            
989
            let values = user.list_split_tag_values("project").await?;
990

            
991
            assert!(values.is_empty());
992
        }
993

            
994
        #[local_db_sqlx_test]
995
        async fn test_list_split_tag_values_with_data(pool: PgPool) -> Result<(), anyhow::Error> {
996
            let user = USER.get().unwrap();
997
            user.commit()
998
                .await
999
                .expect("Failed to commit user to database");
            let commodity_id = user
                .create_commodity("USD".to_string(), "US Dollar".to_string())
                .await?
                .id;
            let acc1 = user.create_account("test_acc1", None).await?.id;
            let acc2 = user.create_account("test_acc2", None).await?.id;
            let tx1 = Transaction {
                id: sqlx::types::Uuid::new_v4(),
                post_date: chrono::Utc::now(),
                enter_date: chrono::Utc::now(),
            };
            let tx2 = Transaction {
                id: sqlx::types::Uuid::new_v4(),
                post_date: chrono::Utc::now(),
                enter_date: chrono::Utc::now(),
            };
            let split1_id = sqlx::types::Uuid::new_v4();
            let split2_id = sqlx::types::Uuid::new_v4();
            let split3_id = sqlx::types::Uuid::new_v4();
            let split4_id = sqlx::types::Uuid::new_v4();
            {
                let mut conn = user.get_connection().await?;
                let mut ticket1 = tx1.enter(&mut *conn).await?;
                let split1a = Split {
                    id: split1_id,
                    account_id: acc1,
                    tx_id: tx1.id,
                    value_num: 100,
                    value_denom: 1,
                    commodity_id,
                    reconcile_state: None,
                    reconcile_date: None,
                    lot_id: None,
                };
                let split1b = Split {
                    id: split2_id,
                    account_id: acc2,
                    tx_id: tx1.id,
                    value_num: -100,
                    value_denom: 1,
                    commodity_id,
                    reconcile_state: None,
                    reconcile_date: None,
                    lot_id: None,
                };
                ticket1.add_splits(&[&split1a, &split1b]).await?;
                ticket1.commit().await?;
            }
            {
                let mut conn = user.get_connection().await?;
                let mut ticket2 = tx2.enter(&mut *conn).await?;
                let split2a = Split {
                    id: split3_id,
                    account_id: acc1,
                    tx_id: tx2.id,
                    value_num: 200,
                    value_denom: 1,
                    commodity_id,
                    reconcile_state: None,
                    reconcile_date: None,
                    lot_id: None,
                };
                let split2b = Split {
                    id: split4_id,
                    account_id: acc2,
                    tx_id: tx2.id,
                    value_num: -200,
                    value_denom: 1,
                    commodity_id,
                    reconcile_state: None,
                    reconcile_date: None,
                    lot_id: None,
                };
                ticket2.add_splits(&[&split2a, &split2b]).await?;
                ticket2.commit().await?;
            }
            user.create_split_tag(
                split1_id,
                "project".to_string(),
                "nomisync".to_string(),
                None,
            )
            .await?;
            user.create_split_tag(
                split3_id,
                "project".to_string(),
                "website".to_string(),
                None,
            )
            .await?;
            user.create_split_tag(
                split2_id,
                "department".to_string(),
                "engineering".to_string(),
                None,
            )
            .await?;
            let project_values = user.list_split_tag_values("project").await?;
            assert_eq!(project_values.len(), 2);
            assert!(project_values.contains(&"nomisync".to_string()));
            assert!(project_values.contains(&"website".to_string()));
            let department_values = user.list_split_tag_values("department").await?;
            assert_eq!(department_values.len(), 1);
            assert!(department_values.contains(&"engineering".to_string()));
        }
        #[local_db_sqlx_test]
        async fn test_list_split_tag_values_nonexistent_name(
            pool: PgPool,
        ) -> Result<(), anyhow::Error> {
            let user = USER.get().unwrap();
            user.commit()
                .await
                .expect("Failed to commit user to database");
            let commodity_id = user
                .create_commodity("USD".to_string(), "US Dollar".to_string())
                .await?
                .id;
            let acc1 = user.create_account("test_acc1", None).await?.id;
            let acc2 = user.create_account("test_acc2", None).await?.id;
            let tx = Transaction {
                id: sqlx::types::Uuid::new_v4(),
                post_date: chrono::Utc::now(),
                enter_date: chrono::Utc::now(),
            };
            let mut conn = user.get_connection().await?;
            let mut ticket = tx.enter(&mut *conn).await?;
            let split1 = Split {
                id: sqlx::types::Uuid::new_v4(),
                account_id: acc1,
                tx_id: tx.id,
                value_num: 100,
                value_denom: 1,
                commodity_id,
                reconcile_state: None,
                reconcile_date: None,
                lot_id: None,
            };
            let split2 = Split {
                id: sqlx::types::Uuid::new_v4(),
                account_id: acc2,
                tx_id: tx.id,
                value_num: -100,
                value_denom: 1,
                commodity_id,
                reconcile_state: None,
                reconcile_date: None,
                lot_id: None,
            };
            ticket.add_splits(&[&split1, &split2]).await?;
            ticket.commit().await?;
            user.create_split_tag(
                split1.id,
                "project".to_string(),
                "nomisync".to_string(),
                None,
            )
            .await?;
            let values = user.list_split_tag_values("nonexistent").await?;
            assert!(values.is_empty());
        }
        #[local_db_sqlx_test]
        async fn test_create_account_tag(pool: PgPool) -> Result<(), anyhow::Error> {
            let user = USER.get().unwrap();
            user.commit()
                .await
                .expect("Failed to commit user to database");
            let acc = user.create_account("test_acc", None).await?;
            let tag_id = user
                .create_account_tag(
                    acc.id,
                    "category".to_string(),
                    "assets".to_string(),
                    Some("Account category".to_string()),
                )
                .await?;
            let mut conn = user.get_connection().await?;
            let res = sqlx::query!(
                "SELECT tag_id FROM account_tags WHERE account_id = $1 AND tag_id = $2",
                &acc.id,
                &tag_id
            )
            .fetch_one(&mut *conn)
            .await?;
            assert_eq!(res.tag_id, tag_id);
            let tag = user.get_tag(tag_id).await?;
            assert_eq!(tag.tag_name, "category");
            assert_eq!(tag.tag_value, "assets");
            assert_eq!(tag.description, Some("Account category".to_string()));
        }
        #[local_db_sqlx_test]
        async fn test_list_account_tag_names_empty(pool: PgPool) -> Result<(), anyhow::Error> {
            let user = USER.get().unwrap();
            user.commit()
                .await
                .expect("Failed to commit user to database");
            let names = user.list_account_tag_names().await?;
            assert!(names.is_empty());
        }
        #[local_db_sqlx_test]
        async fn test_list_account_tag_names_with_data(pool: PgPool) -> Result<(), anyhow::Error> {
            let user = USER.get().unwrap();
            user.commit()
                .await
                .expect("Failed to commit user to database");
            let acc1 = user.create_account("test_acc1", None).await?;
            let acc2 = user.create_account("test_acc2", None).await?;
            user.set_account_tag(
                &acc1,
                &Tag {
                    id: Uuid::new_v4(),
                    tag_name: "category".to_string(),
                    tag_value: "assets".to_string(),
                    description: None,
                },
            )
            .await?;
            user.set_account_tag(
                &acc2,
                &Tag {
                    id: Uuid::new_v4(),
                    tag_name: "priority".to_string(),
                    tag_value: "high".to_string(),
                    description: None,
                },
            )
            .await?;
            let names = user.list_account_tag_names().await?;
            assert!(names.contains(&"category".to_string()));
            assert!(names.contains(&"priority".to_string()));
        }
        #[local_db_sqlx_test]
        async fn test_list_account_tag_values_empty(pool: PgPool) -> Result<(), anyhow::Error> {
            let user = USER.get().unwrap();
            user.commit()
                .await
                .expect("Failed to commit user to database");
            let values = user.list_account_tag_values("category").await?;
            assert!(values.is_empty());
        }
        #[local_db_sqlx_test]
        async fn test_list_account_tag_values_with_data(pool: PgPool) -> Result<(), anyhow::Error> {
            let user = USER.get().unwrap();
            user.commit()
                .await
                .expect("Failed to commit user to database");
            let acc1 = user.create_account("test_acc1", None).await?;
            let acc2 = user.create_account("test_acc2", None).await?;
            user.set_account_tag(
                &acc1,
                &Tag {
                    id: Uuid::new_v4(),
                    tag_name: "category".to_string(),
                    tag_value: "assets".to_string(),
                    description: None,
                },
            )
            .await?;
            user.set_account_tag(
                &acc2,
                &Tag {
                    id: Uuid::new_v4(),
                    tag_name: "category".to_string(),
                    tag_value: "liabilities".to_string(),
                    description: None,
                },
            )
            .await?;
            let values = user.list_account_tag_values("category").await?;
            assert_eq!(values.len(), 2);
            assert!(values.contains(&"assets".to_string()));
            assert!(values.contains(&"liabilities".to_string()));
            let nonexistent = user.list_account_tag_values("nonexistent").await?;
            assert!(nonexistent.is_empty());
        }
    }
}