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

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

            
34
16
            Ok(id)
35
20
        }
36

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

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

            
48
4
            Ok(tags)
49
6
        }
50

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

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

            
62
12
            Ok(tag)
63
16
        }
64

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

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

            
88
4
            Ok(())
89
8
        }
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
18
        pub async fn create_transaction_tag(
106
18
            &self,
107
18
            tx_id: Uuid,
108
18
            name: String,
109
18
            value: String,
110
18
            description: Option<String>,
111
25
        ) -> Result<Uuid, ServerError> {
112
14
            let mut conn = self.get_connection().await?;
113

            
114
12
            let tag_id = Uuid::new_v4();
115
12
            let tag = Tag {
116
12
                id: tag_id,
117
12
                tag_name: name,
118
12
                tag_value: value,
119
12
                description,
120
12
            };
121

            
122
12
            tag.commit(&mut *conn).await.map_err(|err| {
123
                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
124
                ServerError::Finance(err)
125
            })?;
126

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

            
139
12
            Ok(tag_id)
140
14
        }
141

            
142
13
        pub async fn delete_tag(&self, id: Uuid) -> Result<(), ServerError> {
143
6
            let mut conn = self.get_connection().await?;
144

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

            
153
4
            Ok(())
154
6
        }
155

            
156
6
        pub async fn list_transaction_tag_names(&self) -> Result<Vec<String>, ServerError> {
157
4
            let mut conn = self.get_connection().await?;
158

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

            
167
4
            Ok(records.into_iter().map(|r| r.tag_name).collect())
168
4
        }
169

            
170
8
        pub async fn list_transaction_tag_values(
171
8
            &self,
172
8
            tag_name: &str,
173
12
        ) -> Result<Vec<String>, ServerError> {
174
8
            let mut conn = self.get_connection().await?;
175

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

            
185
8
            Ok(records.into_iter().map(|r| r.tag_value).collect())
186
8
        }
187

            
188
14
        pub async fn create_split_tag(
189
14
            &self,
190
14
            split_id: Uuid,
191
14
            name: String,
192
14
            value: String,
193
14
            description: Option<String>,
194
21
        ) -> Result<Uuid, ServerError> {
195
14
            let mut conn = self.get_connection().await?;
196

            
197
14
            let tag_id = Uuid::new_v4();
198
14
            let tag = Tag {
199
14
                id: tag_id,
200
14
                tag_name: name,
201
14
                tag_value: value,
202
14
                description,
203
14
            };
204

            
205
14
            tag.commit(&mut *conn).await.map_err(|err| {
206
                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
207
                ServerError::Finance(err)
208
            })?;
209

            
210
14
            sqlx::query_file!("sql/insert/split_tags/split_tag.sql", &split_id, &tag_id)
211
14
                .execute(&mut *conn)
212
14
                .await
213
14
                .map_err(|err| {
214
                    log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
215
                    ServerError::DB(crate::db::DBError::Sqlx(err))
216
                })?;
217

            
218
14
            Ok(tag_id)
219
14
        }
220

            
221
6
        pub async fn list_split_tag_names(&self) -> Result<Vec<String>, ServerError> {
222
4
            let mut conn = self.get_connection().await?;
223

            
224
4
            let records = sqlx::query_file!("sql/select/tags/split/names.sql")
225
4
                .fetch_all(&mut *conn)
226
4
                .await
227
4
                .map_err(|err| {
228
                    log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
229
                    ServerError::DB(crate::db::DBError::Sqlx(err))
230
                })?;
231

            
232
4
            Ok(records.into_iter().map(|r| r.tag_name).collect())
233
4
        }
234

            
235
8
        pub async fn list_split_tag_values(
236
8
            &self,
237
8
            tag_name: &str,
238
12
        ) -> Result<Vec<String>, ServerError> {
239
8
            let mut conn = self.get_connection().await?;
240

            
241
8
            let records = sqlx::query_file!("sql/select/tags/split/values_by_name.sql", tag_name)
242
8
                .fetch_all(&mut *conn)
243
8
                .await
244
8
                .map_err(|err| {
245
                    log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
246
                    ServerError::DB(crate::db::DBError::Sqlx(err))
247
                })?;
248

            
249
8
            Ok(records.into_iter().map(|r| r.tag_value).collect())
250
8
        }
251
    }
252

            
253
    #[cfg(test)]
254
    mod tag_tests {
255
        use super::*;
256
        use crate::db::DB_POOL;
257
        #[cfg(feature = "testlog")]
258
        use env_logger;
259
        use finance::{split::Split, transaction::Transaction};
260
        #[cfg(feature = "testlog")]
261
        use log;
262
        use sqlx::PgPool;
263
        use sqlx::types::chrono;
264

            
265
        use supp_macro::local_db_sqlx_test;
266
        use tokio::sync::OnceCell;
267

            
268
        /// Context for keeping environment intact
269
        static CONTEXT: OnceCell<()> = OnceCell::const_new();
270
        static USER: OnceCell<User> = OnceCell::const_new();
271

            
272
63
        async fn setup() {
273
42
            CONTEXT
274
42
                .get_or_init(|| async {
275
                    #[cfg(feature = "testlog")]
276
2
                    let _ = env_logger::builder()
277
2
                        .is_test(true)
278
2
                        .filter_level(log::LevelFilter::Trace)
279
2
                        .try_init();
280
4
                })
281
42
                .await;
282
42
            USER.get_or_init(|| async { User { id: Uuid::new_v4() } })
283
42
                .await;
284
42
        }
285

            
286
        #[local_db_sqlx_test]
287
        async fn test_tag_creation(pool: PgPool) -> Result<(), anyhow::Error> {
288
            let user = USER.get().unwrap();
289
            user.commit()
290
                .await
291
                .expect("Failed to commit user to database");
292

            
293
            let id = user
294
                .create_tag("testtag".to_string(), "testval".to_string(), None)
295
                .await?;
296

            
297
            let mut conn = user.get_connection().await?;
298
            let res = sqlx::query_file!("testdata/query_tag_by_id.sql", &id)
299
                .fetch_one(&mut *conn)
300
                .await?;
301

            
302
            assert_eq!(res.tag_name, "testtag".to_string());
303
            assert_eq!(res.tag_value, "testval".to_string());
304
            assert_eq!(res.description, None);
305
        }
306

            
307
        #[local_db_sqlx_test]
308
        async fn test_tag_creation_with_description(pool: PgPool) -> Result<(), anyhow::Error> {
309
            let user = USER.get().unwrap();
310
            user.commit()
311
                .await
312
                .expect("Failed to commit user to database");
313

            
314
            let id = user
315
                .create_tag(
316
                    "categorytag".to_string(),
317
                    "category1".to_string(),
318
                    Some("Test description".to_string()),
319
                )
320
                .await?;
321

            
322
            let tag = user.get_tag(id).await?;
323

            
324
            assert_eq!(tag.tag_name, "categorytag");
325
            assert_eq!(tag.tag_value, "category1");
326
            assert_eq!(tag.description, Some("Test description".to_string()));
327
        }
328

            
329
        #[local_db_sqlx_test]
330
        async fn test_list_tags(pool: PgPool) -> Result<(), anyhow::Error> {
331
            let user = USER.get().unwrap();
332
            user.commit()
333
                .await
334
                .expect("Failed to commit user to database");
335

            
336
            let id1 = user
337
                .create_tag("tag1".to_string(), "value1".to_string(), None)
338
                .await?;
339

            
340
            let id2 = user
341
                .create_tag(
342
                    "tag2".to_string(),
343
                    "value2".to_string(),
344
                    Some("desc".to_string()),
345
                )
346
                .await?;
347

            
348
            let tags = user.list_tags().await?;
349

            
350
            assert!(tags.len() >= 2);
351
2
            assert!(tags.iter().any(|t| t.id == id1));
352
4
            assert!(tags.iter().any(|t| t.id == id2));
353

            
354
2
            let tag1 = tags.iter().find(|t| t.id == id1).unwrap();
355
            assert_eq!(tag1.tag_name, "tag1");
356
            assert_eq!(tag1.tag_value, "value1");
357
            assert_eq!(tag1.description, None);
358

            
359
4
            let tag2 = tags.iter().find(|t| t.id == id2).unwrap();
360
            assert_eq!(tag2.tag_name, "tag2");
361
            assert_eq!(tag2.tag_value, "value2");
362
            assert_eq!(tag2.description, Some("desc".to_string()));
363
        }
364

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

            
372
            let id = user
373
                .create_tag(
374
                    "gettag".to_string(),
375
                    "getvalue".to_string(),
376
                    Some("Get description".to_string()),
377
                )
378
                .await?;
379

            
380
            let tag = user.get_tag(id).await?;
381

            
382
            assert_eq!(tag.id, id);
383
            assert_eq!(tag.tag_name, "gettag");
384
            assert_eq!(tag.tag_value, "getvalue");
385
            assert_eq!(tag.description, Some("Get description".to_string()));
386
        }
387

            
388
        #[local_db_sqlx_test]
389
        async fn test_get_nonexistent_tag(pool: PgPool) -> Result<(), anyhow::Error> {
390
            let user = USER.get().unwrap();
391
            user.commit()
392
                .await
393
                .expect("Failed to commit user to database");
394

            
395
            let nonexistent_id = Uuid::new_v4();
396
            let result = user.get_tag(nonexistent_id).await;
397

            
398
            assert!(result.is_err());
399
        }
400

            
401
        #[local_db_sqlx_test]
402
        async fn test_update_tag(pool: PgPool) -> Result<(), anyhow::Error> {
403
            let user = USER.get().unwrap();
404
            user.commit()
405
                .await
406
                .expect("Failed to commit user to database");
407

            
408
            let id = user
409
                .create_tag("oldname".to_string(), "oldvalue".to_string(), None)
410
                .await?;
411

            
412
            user.update_tag(
413
                id,
414
                "newname".to_string(),
415
                "newvalue".to_string(),
416
                Some("Updated description".to_string()),
417
            )
418
            .await?;
419

            
420
            let tag = user.get_tag(id).await?;
421

            
422
            assert_eq!(tag.id, id);
423
            assert_eq!(tag.tag_name, "newname");
424
            assert_eq!(tag.tag_value, "newvalue");
425
            assert_eq!(tag.description, Some("Updated description".to_string()));
426
        }
427

            
428
        #[local_db_sqlx_test]
429
        async fn test_update_tag_remove_description(pool: PgPool) {
430
            let user = USER.get().unwrap();
431
            user.commit()
432
                .await
433
                .expect("Failed to commit user to database");
434

            
435
            let id = user
436
                .create_tag(
437
                    "tagname".to_string(),
438
                    "tagvalue".to_string(),
439
                    Some("Initial description".to_string()),
440
                )
441
                .await?;
442

            
443
            user.update_tag(id, "tagname".to_string(), "tagvalue".to_string(), None)
444
                .await?;
445

            
446
            let tag = user.get_tag(id).await?;
447

            
448
            assert_eq!(tag.description, None);
449
        }
450

            
451
        #[local_db_sqlx_test]
452
        async fn test_delete_tag(pool: PgPool) -> Result<(), anyhow::Error> {
453
            let user = USER.get().unwrap();
454
            user.commit()
455
                .await
456
                .expect("Failed to commit user to database");
457

            
458
            let id = user
459
                .create_tag("deletetag".to_string(), "deletevalue".to_string(), None)
460
                .await?;
461

            
462
            let tag = user.get_tag(id).await;
463
            assert!(tag.is_ok());
464

            
465
            user.delete_tag(id).await?;
466

            
467
            let result = user.get_tag(id).await;
468
            assert!(result.is_err());
469
        }
470

            
471
        #[local_db_sqlx_test]
472
        async fn test_delete_nonexistent_tag(pool: PgPool) -> Result<(), anyhow::Error> {
473
            let user = USER.get().unwrap();
474
            user.commit()
475
                .await
476
                .expect("Failed to commit user to database");
477

            
478
            let nonexistent_id = Uuid::new_v4();
479
            let result = user.delete_tag(nonexistent_id).await;
480

            
481
            assert!(result.is_ok());
482
        }
483

            
484
        #[local_db_sqlx_test]
485
        async fn test_list_tags_empty(pool: PgPool) -> Result<(), anyhow::Error> {
486
            let user = USER.get().unwrap();
487
            user.commit()
488
                .await
489
                .expect("Failed to commit user to database");
490

            
491
            let tags = user.list_tags().await?;
492

            
493
            assert!(
494
                tags.is_empty()
495
                    || tags.iter().all(|t| t.tag_name == "name"
496
                        || t.tag_name == "note"
497
                        || t.tag_name == "symbol")
498
            );
499
        }
500

            
501
        #[local_db_sqlx_test]
502
        async fn test_list_transaction_tag_names_empty(pool: PgPool) -> Result<(), anyhow::Error> {
503
            let user = USER.get().unwrap();
504
            user.commit()
505
                .await
506
                .expect("Failed to commit user to database");
507

            
508
            let names = user.list_transaction_tag_names().await?;
509

            
510
            assert!(names.is_empty());
511
        }
512

            
513
        #[local_db_sqlx_test]
514
        async fn test_list_transaction_tag_names_with_data(
515
            pool: PgPool,
516
        ) -> Result<(), anyhow::Error> {
517
            let user = USER.get().unwrap();
518
            user.commit()
519
                .await
520
                .expect("Failed to commit user to database");
521

            
522
            let commodity_id = user
523
                .create_commodity(100, "USD".to_string(), "US Dollar".to_string())
524
                .await?
525
                .id;
526
            let acc1 = user.create_account("test_acc1", None).await?.id;
527
            let acc2 = user.create_account("test_acc2", None).await?.id;
528

            
529
            let tx = Transaction {
530
                id: sqlx::types::Uuid::new_v4(),
531
                post_date: chrono::Utc::now(),
532
                enter_date: chrono::Utc::now(),
533
            };
534

            
535
            let mut conn = user.get_connection().await?;
536
            let mut ticket = tx.enter(&mut *conn).await?;
537
            let split1 = Split {
538
                id: sqlx::types::Uuid::new_v4(),
539
                account_id: acc1,
540
                tx_id: tx.id,
541
                value_num: 100,
542
                value_denom: 1,
543
                commodity_id,
544
                reconcile_state: None,
545
                reconcile_date: None,
546
                lot_id: None,
547
            };
548
            let split2 = Split {
549
                id: sqlx::types::Uuid::new_v4(),
550
                account_id: acc2,
551
                tx_id: tx.id,
552
                value_num: -100,
553
                value_denom: 1,
554
                commodity_id,
555
                reconcile_state: None,
556
                reconcile_date: None,
557
                lot_id: None,
558
            };
559
            ticket.add_splits(&[&split1, &split2]).await?;
560
            ticket.commit().await?;
561

            
562
            user.create_transaction_tag(tx.id, "category".to_string(), "food".to_string(), None)
563
                .await?;
564
            user.create_transaction_tag(tx.id, "priority".to_string(), "high".to_string(), None)
565
                .await?;
566

            
567
            let names = user.list_transaction_tag_names().await?;
568

            
569
            assert_eq!(names.len(), 2);
570
            assert!(names.contains(&"category".to_string()));
571
            assert!(names.contains(&"priority".to_string()));
572
        }
573

            
574
        #[local_db_sqlx_test]
575
        async fn test_list_transaction_tag_values_empty(pool: PgPool) -> Result<(), anyhow::Error> {
576
            let user = USER.get().unwrap();
577
            user.commit()
578
                .await
579
                .expect("Failed to commit user to database");
580

            
581
            let values = user.list_transaction_tag_values("category").await?;
582

            
583
            assert!(values.is_empty());
584
        }
585

            
586
        #[local_db_sqlx_test]
587
        async fn test_list_transaction_tag_values_with_data(
588
            pool: PgPool,
589
        ) -> Result<(), anyhow::Error> {
590
            let user = USER.get().unwrap();
591
            user.commit()
592
                .await
593
                .expect("Failed to commit user to database");
594

            
595
            let commodity_id = user
596
                .create_commodity(100, "USD".to_string(), "US Dollar".to_string())
597
                .await?
598
                .id;
599
            let acc1 = user.create_account("test_acc1", None).await?.id;
600
            let acc2 = user.create_account("test_acc2", None).await?.id;
601

            
602
            let tx1 = Transaction {
603
                id: sqlx::types::Uuid::new_v4(),
604
                post_date: chrono::Utc::now(),
605
                enter_date: chrono::Utc::now(),
606
            };
607
            let tx2 = Transaction {
608
                id: sqlx::types::Uuid::new_v4(),
609
                post_date: chrono::Utc::now(),
610
                enter_date: chrono::Utc::now(),
611
            };
612
            {
613
                let mut conn = user.get_connection().await?;
614
                let mut ticket1 = tx1.enter(&mut *conn).await?;
615
                let split1a = Split {
616
                    id: sqlx::types::Uuid::new_v4(),
617
                    account_id: acc1,
618
                    tx_id: tx1.id,
619
                    value_num: 100,
620
                    value_denom: 1,
621
                    commodity_id,
622
                    reconcile_state: None,
623
                    reconcile_date: None,
624
                    lot_id: None,
625
                };
626
                let split1b = Split {
627
                    id: sqlx::types::Uuid::new_v4(),
628
                    account_id: acc2,
629
                    tx_id: tx1.id,
630
                    value_num: -100,
631
                    value_denom: 1,
632
                    commodity_id,
633
                    reconcile_state: None,
634
                    reconcile_date: None,
635
                    lot_id: None,
636
                };
637
                ticket1.add_splits(&[&split1a, &split1b]).await?;
638
                ticket1.commit().await?;
639
            }
640
            {
641
                let mut conn = user.get_connection().await?;
642
                let mut ticket2 = tx2.enter(&mut *conn).await?;
643
                let split2a = Split {
644
                    id: sqlx::types::Uuid::new_v4(),
645
                    account_id: acc1,
646
                    tx_id: tx2.id,
647
                    value_num: 200,
648
                    value_denom: 1,
649
                    commodity_id,
650
                    reconcile_state: None,
651
                    reconcile_date: None,
652
                    lot_id: None,
653
                };
654
                let split2b = Split {
655
                    id: sqlx::types::Uuid::new_v4(),
656
                    account_id: acc2,
657
                    tx_id: tx2.id,
658
                    value_num: -200,
659
                    value_denom: 1,
660
                    commodity_id,
661
                    reconcile_state: None,
662
                    reconcile_date: None,
663
                    lot_id: None,
664
                };
665
                ticket2.add_splits(&[&split2a, &split2b]).await?;
666
                ticket2.commit().await?;
667
            }
668
            user.create_transaction_tag(tx1.id, "category".to_string(), "food".to_string(), None)
669
                .await?;
670
            user.create_transaction_tag(
671
                tx2.id,
672
                "category".to_string(),
673
                "transport".to_string(),
674
                None,
675
            )
676
            .await?;
677
            user.create_transaction_tag(tx1.id, "priority".to_string(), "high".to_string(), None)
678
                .await?;
679

            
680
            let category_values = user.list_transaction_tag_values("category").await?;
681

            
682
            assert_eq!(category_values.len(), 2);
683
            assert!(category_values.contains(&"food".to_string()));
684
            assert!(category_values.contains(&"transport".to_string()));
685

            
686
            let priority_values = user.list_transaction_tag_values("priority").await?;
687

            
688
            assert_eq!(priority_values.len(), 1);
689
            assert!(priority_values.contains(&"high".to_string()));
690
        }
691

            
692
        #[local_db_sqlx_test]
693
        async fn test_list_transaction_tag_values_nonexistent_name(
694
            pool: PgPool,
695
        ) -> Result<(), anyhow::Error> {
696
            let user = USER.get().unwrap();
697
            user.commit()
698
                .await
699
                .expect("Failed to commit user to database");
700

            
701
            let commodity_id = user
702
                .create_commodity(100, "USD".to_string(), "US Dollar".to_string())
703
                .await?
704
                .id;
705
            let acc1 = user.create_account("test_acc1", None).await?.id;
706
            let acc2 = user.create_account("test_acc2", None).await?.id;
707

            
708
            let tx = Transaction {
709
                id: sqlx::types::Uuid::new_v4(),
710
                post_date: chrono::Utc::now(),
711
                enter_date: chrono::Utc::now(),
712
            };
713

            
714
            let mut conn = user.get_connection().await?;
715
            let mut ticket = tx.enter(&mut *conn).await?;
716
            let split1 = Split {
717
                id: sqlx::types::Uuid::new_v4(),
718
                account_id: acc1,
719
                tx_id: tx.id,
720
                value_num: 100,
721
                value_denom: 1,
722
                commodity_id,
723
                reconcile_state: None,
724
                reconcile_date: None,
725
                lot_id: None,
726
            };
727
            let split2 = Split {
728
                id: sqlx::types::Uuid::new_v4(),
729
                account_id: acc2,
730
                tx_id: tx.id,
731
                value_num: -100,
732
                value_denom: 1,
733
                commodity_id,
734
                reconcile_state: None,
735
                reconcile_date: None,
736
                lot_id: None,
737
            };
738
            ticket.add_splits(&[&split1, &split2]).await?;
739
            ticket.commit().await?;
740

            
741
            user.create_transaction_tag(tx.id, "category".to_string(), "food".to_string(), None)
742
                .await?;
743

            
744
            let values = user.list_transaction_tag_values("nonexistent").await?;
745

            
746
            assert!(values.is_empty());
747
        }
748

            
749
        #[local_db_sqlx_test]
750
        async fn test_create_split_tag(pool: PgPool) -> Result<(), anyhow::Error> {
751
            let user = USER.get().unwrap();
752
            user.commit()
753
                .await
754
                .expect("Failed to commit user to database");
755

            
756
            let commodity_id = user
757
                .create_commodity(100, "USD".to_string(), "US Dollar".to_string())
758
                .await?
759
                .id;
760
            let acc1 = user.create_account("test_acc1", None).await?.id;
761
            let acc2 = user.create_account("test_acc2", None).await?.id;
762

            
763
            let tx = Transaction {
764
                id: sqlx::types::Uuid::new_v4(),
765
                post_date: chrono::Utc::now(),
766
                enter_date: chrono::Utc::now(),
767
            };
768

            
769
            let mut conn = user.get_connection().await?;
770
            let mut ticket = tx.enter(&mut *conn).await?;
771
            let split1 = Split {
772
                id: sqlx::types::Uuid::new_v4(),
773
                account_id: acc1,
774
                tx_id: tx.id,
775
                value_num: 100,
776
                value_denom: 1,
777
                commodity_id,
778
                reconcile_state: None,
779
                reconcile_date: None,
780
                lot_id: None,
781
            };
782
            let split2 = Split {
783
                id: sqlx::types::Uuid::new_v4(),
784
                account_id: acc2,
785
                tx_id: tx.id,
786
                value_num: -100,
787
                value_denom: 1,
788
                commodity_id,
789
                reconcile_state: None,
790
                reconcile_date: None,
791
                lot_id: None,
792
            };
793
            ticket.add_splits(&[&split1, &split2]).await?;
794
            ticket.commit().await?;
795

            
796
            let tag_id = user
797
                .create_split_tag(
798
                    split1.id,
799
                    "project".to_string(),
800
                    "nomisync".to_string(),
801
                    Some("Split tag for project tracking".to_string()),
802
                )
803
                .await?;
804

            
805
            let mut conn = user.get_connection().await?;
806
            let res = sqlx::query!(
807
                "SELECT tag_id FROM split_tags WHERE split_id = $1",
808
                &split1.id
809
            )
810
            .fetch_one(&mut *conn)
811
            .await?;
812

            
813
            assert_eq!(res.tag_id, tag_id);
814

            
815
            let tag = user.get_tag(tag_id).await?;
816
            assert_eq!(tag.tag_name, "project");
817
            assert_eq!(tag.tag_value, "nomisync");
818
            assert_eq!(
819
                tag.description,
820
                Some("Split tag for project tracking".to_string())
821
            );
822
        }
823

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

            
831
            let names = user.list_split_tag_names().await?;
832

            
833
            assert!(names.is_empty());
834
        }
835

            
836
        #[local_db_sqlx_test]
837
        async fn test_list_split_tag_names_with_data(pool: PgPool) -> Result<(), anyhow::Error> {
838
            let user = USER.get().unwrap();
839
            user.commit()
840
                .await
841
                .expect("Failed to commit user to database");
842

            
843
            let commodity_id = user
844
                .create_commodity(100, "USD".to_string(), "US Dollar".to_string())
845
                .await?
846
                .id;
847
            let acc1 = user.create_account("test_acc1", None).await?.id;
848
            let acc2 = user.create_account("test_acc2", None).await?.id;
849

            
850
            let tx = Transaction {
851
                id: sqlx::types::Uuid::new_v4(),
852
                post_date: chrono::Utc::now(),
853
                enter_date: chrono::Utc::now(),
854
            };
855

            
856
            let mut conn = user.get_connection().await?;
857
            let mut ticket = tx.enter(&mut *conn).await?;
858
            let split1 = Split {
859
                id: sqlx::types::Uuid::new_v4(),
860
                account_id: acc1,
861
                tx_id: tx.id,
862
                value_num: 100,
863
                value_denom: 1,
864
                commodity_id,
865
                reconcile_state: None,
866
                reconcile_date: None,
867
                lot_id: None,
868
            };
869
            let split2 = Split {
870
                id: sqlx::types::Uuid::new_v4(),
871
                account_id: acc2,
872
                tx_id: tx.id,
873
                value_num: -100,
874
                value_denom: 1,
875
                commodity_id,
876
                reconcile_state: None,
877
                reconcile_date: None,
878
                lot_id: None,
879
            };
880
            ticket.add_splits(&[&split1, &split2]).await?;
881
            ticket.commit().await?;
882

            
883
            user.create_split_tag(
884
                split1.id,
885
                "project".to_string(),
886
                "nomisync".to_string(),
887
                None,
888
            )
889
            .await?;
890
            user.create_split_tag(
891
                split2.id,
892
                "department".to_string(),
893
                "engineering".to_string(),
894
                None,
895
            )
896
            .await?;
897

            
898
            let names = user.list_split_tag_names().await?;
899

            
900
            assert_eq!(names.len(), 2);
901
            assert!(names.contains(&"project".to_string()));
902
            assert!(names.contains(&"department".to_string()));
903
        }
904

            
905
        #[local_db_sqlx_test]
906
        async fn test_list_split_tag_values_empty(pool: PgPool) -> Result<(), anyhow::Error> {
907
            let user = USER.get().unwrap();
908
            user.commit()
909
                .await
910
                .expect("Failed to commit user to database");
911

            
912
            let values = user.list_split_tag_values("project").await?;
913

            
914
            assert!(values.is_empty());
915
        }
916

            
917
        #[local_db_sqlx_test]
918
        async fn test_list_split_tag_values_with_data(pool: PgPool) -> Result<(), anyhow::Error> {
919
            let user = USER.get().unwrap();
920
            user.commit()
921
                .await
922
                .expect("Failed to commit user to database");
923

            
924
            let commodity_id = user
925
                .create_commodity(100, "USD".to_string(), "US Dollar".to_string())
926
                .await?
927
                .id;
928
            let acc1 = user.create_account("test_acc1", None).await?.id;
929
            let acc2 = user.create_account("test_acc2", None).await?.id;
930

            
931
            let tx1 = Transaction {
932
                id: sqlx::types::Uuid::new_v4(),
933
                post_date: chrono::Utc::now(),
934
                enter_date: chrono::Utc::now(),
935
            };
936
            let tx2 = Transaction {
937
                id: sqlx::types::Uuid::new_v4(),
938
                post_date: chrono::Utc::now(),
939
                enter_date: chrono::Utc::now(),
940
            };
941

            
942
            let split1_id = sqlx::types::Uuid::new_v4();
943
            let split2_id = sqlx::types::Uuid::new_v4();
944
            let split3_id = sqlx::types::Uuid::new_v4();
945
            let split4_id = sqlx::types::Uuid::new_v4();
946

            
947
            {
948
                let mut conn = user.get_connection().await?;
949
                let mut ticket1 = tx1.enter(&mut *conn).await?;
950
                let split1a = Split {
951
                    id: split1_id,
952
                    account_id: acc1,
953
                    tx_id: tx1.id,
954
                    value_num: 100,
955
                    value_denom: 1,
956
                    commodity_id,
957
                    reconcile_state: None,
958
                    reconcile_date: None,
959
                    lot_id: None,
960
                };
961
                let split1b = Split {
962
                    id: split2_id,
963
                    account_id: acc2,
964
                    tx_id: tx1.id,
965
                    value_num: -100,
966
                    value_denom: 1,
967
                    commodity_id,
968
                    reconcile_state: None,
969
                    reconcile_date: None,
970
                    lot_id: None,
971
                };
972
                ticket1.add_splits(&[&split1a, &split1b]).await?;
973
                ticket1.commit().await?;
974
            }
975
            {
976
                let mut conn = user.get_connection().await?;
977
                let mut ticket2 = tx2.enter(&mut *conn).await?;
978
                let split2a = Split {
979
                    id: split3_id,
980
                    account_id: acc1,
981
                    tx_id: tx2.id,
982
                    value_num: 200,
983
                    value_denom: 1,
984
                    commodity_id,
985
                    reconcile_state: None,
986
                    reconcile_date: None,
987
                    lot_id: None,
988
                };
989
                let split2b = Split {
990
                    id: split4_id,
991
                    account_id: acc2,
992
                    tx_id: tx2.id,
993
                    value_num: -200,
994
                    value_denom: 1,
995
                    commodity_id,
996
                    reconcile_state: None,
997
                    reconcile_date: None,
998
                    lot_id: None,
999
                };
                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(100, "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());
        }
    }
}