1pub 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 #[derive(Debug, Clone, Copy)]
10 pub enum TagScope {
11 Transaction,
12 Account,
13 Split,
14 }
15
16 impl User {
17 pub async fn create_tag(
18 &self,
19 name: String,
20 value: String,
21 description: Option<String>,
22 ) -> Result<Uuid, ServerError> {
23 if name.trim().is_empty() || value.trim().is_empty() {
24 return Err(ServerError::Creation);
25 }
26 let mut conn = self.get_connection().await.map_err(|err| {
27 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
28 ServerError::DB(err)
29 })?;
30
31 Tag {
32 id: Uuid::new_v4(),
33 tag_name: name,
34 tag_value: value,
35 description,
36 }
37 .commit(&mut *conn)
38 .await
39 .map_err(|err| {
40 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
41 ServerError::Finance(err)
42 })
43 }
44
45 pub async fn list_tags(&self) -> Result<Vec<Tag>, ServerError> {
46 let mut conn = self.get_connection().await?;
47
48 let tags = sqlx::query_file_as!(Tag, "sql/select/tags/all.sql")
49 .fetch_all(&mut *conn)
50 .await
51 .map_err(|err| {
52 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
53 ServerError::DB(crate::db::DBError::Sqlx(err))
54 })?;
55
56 Ok(tags)
57 }
58
59 pub async fn get_tag(&self, id: Uuid) -> Result<Tag, ServerError> {
60 let mut conn = self.get_connection().await?;
61
62 let tag = sqlx::query_file_as!(Tag, "sql/select/tags/by_id.sql", &id)
63 .fetch_one(&mut *conn)
64 .await
65 .map_err(|err| {
66 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
67 ServerError::DB(crate::db::DBError::Sqlx(err))
68 })?;
69
70 Ok(tag)
71 }
72
73 pub async fn update_tag(
74 &self,
75 id: Uuid,
76 name: String,
77 value: String,
78 description: Option<String>,
79 ) -> Result<(), ServerError> {
80 let mut conn = self.get_connection().await?;
81
82 sqlx::query_file!(
83 "sql/update/tags/update.sql",
84 &id,
85 &name,
86 &value,
87 description.as_deref()
88 )
89 .execute(&mut *conn)
90 .await
91 .map_err(|err| {
92 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
93 ServerError::DB(crate::db::DBError::Sqlx(err))
94 })?;
95
96 Ok(())
97 }
98
99 pub async fn get_transaction_tags(&self, tx_id: Uuid) -> Result<Vec<Tag>, ServerError> {
100 let mut conn = self.get_connection().await?;
101
102 let tags = sqlx::query_file_as!(Tag, "sql/select/tags/by_transaction.sql", &tx_id)
103 .fetch_all(&mut *conn)
104 .await
105 .map_err(|err| {
106 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
107 ServerError::DB(crate::db::DBError::Sqlx(err))
108 })?;
109
110 Ok(tags)
111 }
112
113 pub async fn create_transaction_tag(
114 &self,
115 tx_id: Uuid,
116 name: String,
117 value: String,
118 description: Option<String>,
119 ) -> Result<Uuid, ServerError> {
120 let tag_id = self.create_tag(name, value, description).await?;
121 let mut conn = self.get_connection().await?;
122 sqlx::query_file!(
123 "sql/insert/transaction_tags/transaction_tag.sql",
124 &tx_id,
125 &tag_id
126 )
127 .execute(&mut *conn)
128 .await
129 .map_err(|err| {
130 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
131 ServerError::DB(crate::db::DBError::Sqlx(err))
132 })?;
133 Ok(tag_id)
134 }
135
136 pub async fn delete_tag(&self, id: Uuid) -> Result<(), ServerError> {
142 use sqlx::Connection;
143 let mut conn = self.get_connection().await?;
144 let mut tx = conn.begin().await.map_err(|err| {
145 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
146 ServerError::DB(crate::db::DBError::Sqlx(err))
147 })?;
148 for sql in [
149 "DELETE FROM transaction_tags WHERE tag_id = $1",
150 "DELETE FROM split_tags WHERE tag_id = $1",
151 "DELETE FROM account_tags WHERE tag_id = $1",
152 "DELETE FROM budget_tags WHERE tag_id = $1",
153 "DELETE FROM commodity_tags WHERE tag_id = $1",
154 "DELETE FROM price_tags WHERE tag_id = $1",
155 "DELETE FROM book_tags WHERE tag_id = $1",
156 "DELETE FROM artifact_tags WHERE tag_id = $1",
157 "DELETE FROM tag_tags WHERE tagged_tag_id = $1 OR tagging_tag_id = $1",
158 "DELETE FROM tags WHERE id = $1",
159 ] {
160 sqlx::query(sql)
161 .bind(id)
162 .execute(&mut *tx)
163 .await
164 .map_err(|err| {
165 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
166 ServerError::DB(crate::db::DBError::Sqlx(err))
167 })?;
168 }
169 tx.commit().await.map_err(|err| {
170 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
171 ServerError::DB(crate::db::DBError::Sqlx(err))
172 })
173 }
174
175 pub async fn detach_transaction_tag(
176 &self,
177 tx_id: Uuid,
178 tag_id: Uuid,
179 ) -> Result<(), ServerError> {
180 let mut conn = self.get_connection().await?;
181 sqlx::query_file!("sql/delete/transaction_tags/by_pair.sql", &tx_id, &tag_id)
182 .execute(&mut *conn)
183 .await
184 .map_err(|err| {
185 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
186 ServerError::DB(crate::db::DBError::Sqlx(err))
187 })?;
188 Ok(())
189 }
190
191 pub async fn detach_split_tag(
192 &self,
193 split_id: Uuid,
194 tag_id: Uuid,
195 ) -> Result<(), ServerError> {
196 let mut conn = self.get_connection().await?;
197 sqlx::query_file!("sql/delete/split_tags/by_pair.sql", &split_id, &tag_id)
198 .execute(&mut *conn)
199 .await
200 .map_err(|err| {
201 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
202 ServerError::DB(crate::db::DBError::Sqlx(err))
203 })?;
204 Ok(())
205 }
206
207 pub async fn detach_account_tag(
208 &self,
209 account_id: Uuid,
210 tag_id: Uuid,
211 ) -> Result<(), ServerError> {
212 let mut conn = self.get_connection().await?;
213 sqlx::query_file!("sql/delete/account_tags/by_pair.sql", &account_id, &tag_id)
214 .execute(&mut *conn)
215 .await
216 .map_err(|err| {
217 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
218 ServerError::DB(crate::db::DBError::Sqlx(err))
219 })?;
220 Ok(())
221 }
222
223 pub async fn detach_commodity_tag(
224 &self,
225 commodity_id: Uuid,
226 tag_id: Uuid,
227 ) -> Result<(), ServerError> {
228 let mut conn = self.get_connection().await?;
229 sqlx::query_file!(
230 "sql/delete/commodity_tags/by_pair.sql",
231 &commodity_id,
232 &tag_id
233 )
234 .execute(&mut *conn)
235 .await
236 .map_err(|err| {
237 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
238 ServerError::DB(crate::db::DBError::Sqlx(err))
239 })?;
240 Ok(())
241 }
242
243 pub async fn cleanup_orphan_tag(&self, tag_id: Uuid) -> Result<(), ServerError> {
246 let mut conn = self.get_connection().await?;
247 let row = sqlx::query_file!("sql/check/tags/is_orphaned.sql", &tag_id)
248 .fetch_one(&mut *conn)
249 .await
250 .map_err(|err| {
251 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
252 ServerError::DB(crate::db::DBError::Sqlx(err))
253 })?;
254 if row.is_orphaned.unwrap_or(false) {
255 sqlx::query_file!("sql/delete/tags/by_id.sql", &tag_id)
256 .execute(&mut *conn)
257 .await
258 .map_err(|err| {
259 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
260 ServerError::DB(crate::db::DBError::Sqlx(err))
261 })?;
262 }
263 Ok(())
264 }
265
266 pub async fn list_tag_names(&self, scope: TagScope) -> Result<Vec<String>, ServerError> {
267 let mut conn = self.get_connection().await?;
268 let result = match scope {
269 TagScope::Transaction => sqlx::query_file!("sql/select/tags/transaction/names.sql")
270 .fetch_all(&mut *conn)
271 .await
272 .map(|rows| rows.into_iter().map(|r| r.tag_name).collect::<Vec<_>>()),
273 TagScope::Account => sqlx::query_file!("sql/select/tags/account/names.sql")
274 .fetch_all(&mut *conn)
275 .await
276 .map(|rows| rows.into_iter().map(|r| r.tag_name).collect::<Vec<_>>()),
277 TagScope::Split => sqlx::query_file!("sql/select/tags/split/names.sql")
278 .fetch_all(&mut *conn)
279 .await
280 .map(|rows| rows.into_iter().map(|r| r.tag_name).collect::<Vec<_>>()),
281 };
282 result.map_err(|err| {
283 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
284 ServerError::DB(crate::db::DBError::Sqlx(err))
285 })
286 }
287
288 pub async fn list_tag_values_for(
289 &self,
290 scope: TagScope,
291 tag_name: &str,
292 ) -> Result<Vec<String>, ServerError> {
293 let mut conn = self.get_connection().await?;
294 let result = match scope {
295 TagScope::Transaction => {
296 sqlx::query_file!("sql/select/tags/transaction/values_by_name.sql", tag_name)
297 .fetch_all(&mut *conn)
298 .await
299 .map(|rows| rows.into_iter().map(|r| r.tag_value).collect::<Vec<_>>())
300 }
301 TagScope::Account => {
302 sqlx::query_file!("sql/select/tags/account/values_by_name.sql", tag_name)
303 .fetch_all(&mut *conn)
304 .await
305 .map(|rows| rows.into_iter().map(|r| r.tag_value).collect::<Vec<_>>())
306 }
307 TagScope::Split => {
308 sqlx::query_file!("sql/select/tags/split/values_by_name.sql", tag_name)
309 .fetch_all(&mut *conn)
310 .await
311 .map(|rows| rows.into_iter().map(|r| r.tag_value).collect::<Vec<_>>())
312 }
313 };
314 result.map_err(|err| {
315 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
316 ServerError::DB(crate::db::DBError::Sqlx(err))
317 })
318 }
319
320 pub async fn list_transaction_tag_names(&self) -> Result<Vec<String>, ServerError> {
321 self.list_tag_names(TagScope::Transaction).await
322 }
323
324 pub async fn list_transaction_tag_values(
325 &self,
326 tag_name: &str,
327 ) -> Result<Vec<String>, ServerError> {
328 self.list_tag_values_for(TagScope::Transaction, tag_name)
329 .await
330 }
331
332 pub async fn create_split_tag(
333 &self,
334 split_id: Uuid,
335 name: String,
336 value: String,
337 description: Option<String>,
338 ) -> Result<Uuid, ServerError> {
339 let tag_id = self.create_tag(name, value, description).await?;
340 let mut conn = self.get_connection().await?;
341 sqlx::query_file!("sql/insert/split_tags/split_tag.sql", &split_id, &tag_id)
342 .execute(&mut *conn)
343 .await
344 .map_err(|err| {
345 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
346 ServerError::DB(crate::db::DBError::Sqlx(err))
347 })?;
348 Ok(tag_id)
349 }
350
351 pub async fn set_split_tag(&self, split_id: Uuid, t: &Tag) -> Result<(), ServerError> {
355 if t.tag_name.trim().is_empty() || t.tag_value.trim().is_empty() {
356 return Err(ServerError::Creation);
357 }
358 let mut conn = self.get_connection().await.map_err(|err| {
359 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
360 ServerError::DB(err)
361 })?;
362
363 sqlx::query_file!(
364 "sql/set/splits/tag.sql",
365 &split_id,
366 &t.tag_name,
367 &t.tag_value,
368 t.description
369 )
370 .execute(&mut *conn)
371 .await
372 .map_err(|err| {
373 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
374 ServerError::DB(crate::db::DBError::Sqlx(err))
375 })?;
376
377 Ok(())
378 }
379
380 pub async fn set_transaction_tag(&self, tx_id: Uuid, t: &Tag) -> Result<(), ServerError> {
382 if t.tag_name.trim().is_empty() || t.tag_value.trim().is_empty() {
383 return Err(ServerError::Creation);
384 }
385 let mut conn = self.get_connection().await.map_err(|err| {
386 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
387 ServerError::DB(err)
388 })?;
389
390 sqlx::query_file!(
391 "sql/set/transactions/tag.sql",
392 &tx_id,
393 &t.tag_name,
394 &t.tag_value,
395 t.description
396 )
397 .execute(&mut *conn)
398 .await
399 .map_err(|err| {
400 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
401 ServerError::DB(crate::db::DBError::Sqlx(err))
402 })?;
403
404 Ok(())
405 }
406
407 pub async fn get_split_tags(&self, split_id: Uuid) -> Result<Vec<Tag>, ServerError> {
411 let mut conn = self.get_connection().await?;
412
413 let tags = sqlx::query_file_as!(Tag, "sql/select/tags/by_split.sql", &split_id)
414 .fetch_all(&mut *conn)
415 .await
416 .map_err(|err| {
417 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
418 ServerError::DB(crate::db::DBError::Sqlx(err))
419 })?;
420
421 Ok(tags)
422 }
423
424 pub async fn create_account_tag(
425 &self,
426 account_id: Uuid,
427 name: String,
428 value: String,
429 description: Option<String>,
430 ) -> Result<Uuid, ServerError> {
431 let tag_id = self.create_tag(name, value, description).await?;
432 let mut conn = self.get_connection().await?;
433 sqlx::query_file!(
434 "sql/insert/account_tags/account_tag.sql",
435 &account_id,
436 &tag_id
437 )
438 .execute(&mut *conn)
439 .await
440 .map_err(|err| {
441 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
442 ServerError::DB(crate::db::DBError::Sqlx(err))
443 })?;
444 Ok(tag_id)
445 }
446
447 pub async fn list_account_tag_names(&self) -> Result<Vec<String>, ServerError> {
448 self.list_tag_names(TagScope::Account).await
449 }
450
451 pub async fn list_account_tag_values(
452 &self,
453 tag_name: &str,
454 ) -> Result<Vec<String>, ServerError> {
455 self.list_tag_values_for(TagScope::Account, tag_name).await
456 }
457
458 pub async fn list_split_tag_names(&self) -> Result<Vec<String>, ServerError> {
459 self.list_tag_names(TagScope::Split).await
460 }
461
462 pub async fn list_split_tag_values(
463 &self,
464 tag_name: &str,
465 ) -> Result<Vec<String>, ServerError> {
466 self.list_tag_values_for(TagScope::Split, tag_name).await
467 }
468 }
469
470 #[cfg(test)]
471 mod tag_tests {
472 use super::*;
473 use crate::db::DB_POOL;
474 #[cfg(feature = "testlog")]
475 use env_logger;
476 use finance::{split::Split, transaction::Transaction};
477 #[cfg(feature = "testlog")]
478 use log;
479 use sqlx::PgPool;
480 use sqlx::types::chrono;
481
482 use supp_macro::local_db_sqlx_test;
483 use tokio::sync::OnceCell;
484
485 static CONTEXT: OnceCell<()> = OnceCell::const_new();
487 static USER: OnceCell<User> = OnceCell::const_new();
488
489 async fn setup() {
490 CONTEXT
491 .get_or_init(|| async {
492 #[cfg(feature = "testlog")]
493 let _ = env_logger::builder()
494 .is_test(true)
495 .filter_level(log::LevelFilter::Trace)
496 .try_init();
497 })
498 .await;
499 USER.get_or_init(|| async { User { id: Uuid::new_v4() } })
500 .await;
501 }
502
503 #[local_db_sqlx_test]
504 async fn test_tag_creation(pool: PgPool) -> Result<(), anyhow::Error> {
505 let user = USER.get().unwrap();
506 user.commit()
507 .await
508 .expect("Failed to commit user to database");
509
510 let id = user
511 .create_tag("testtag".to_string(), "testval".to_string(), None)
512 .await?;
513
514 let mut conn = user.get_connection().await?;
515 let res = sqlx::query_file!("testdata/query_tag_by_id.sql", &id)
516 .fetch_one(&mut *conn)
517 .await?;
518
519 assert_eq!(res.tag_name, "testtag".to_string());
520 assert_eq!(res.tag_value, "testval".to_string());
521 assert_eq!(res.description, None);
522 }
523
524 #[local_db_sqlx_test]
525 async fn test_tag_creation_with_description(pool: PgPool) -> Result<(), anyhow::Error> {
526 let user = USER.get().unwrap();
527 user.commit()
528 .await
529 .expect("Failed to commit user to database");
530
531 let id = user
532 .create_tag(
533 "categorytag".to_string(),
534 "category1".to_string(),
535 Some("Test description".to_string()),
536 )
537 .await?;
538
539 let tag = user.get_tag(id).await?;
540
541 assert_eq!(tag.tag_name, "categorytag");
542 assert_eq!(tag.tag_value, "category1");
543 assert_eq!(tag.description, Some("Test description".to_string()));
544 }
545
546 #[local_db_sqlx_test]
547 async fn test_list_tags(pool: PgPool) -> Result<(), anyhow::Error> {
548 let user = USER.get().unwrap();
549 user.commit()
550 .await
551 .expect("Failed to commit user to database");
552
553 let id1 = user
554 .create_tag("tag1".to_string(), "value1".to_string(), None)
555 .await?;
556
557 let id2 = user
558 .create_tag(
559 "tag2".to_string(),
560 "value2".to_string(),
561 Some("desc".to_string()),
562 )
563 .await?;
564
565 let tags = user.list_tags().await?;
566
567 assert!(tags.len() >= 2);
568 assert!(tags.iter().any(|t| t.id == id1));
569 assert!(tags.iter().any(|t| t.id == id2));
570
571 let tag1 = tags.iter().find(|t| t.id == id1).unwrap();
572 assert_eq!(tag1.tag_name, "tag1");
573 assert_eq!(tag1.tag_value, "value1");
574 assert_eq!(tag1.description, None);
575
576 let tag2 = tags.iter().find(|t| t.id == id2).unwrap();
577 assert_eq!(tag2.tag_name, "tag2");
578 assert_eq!(tag2.tag_value, "value2");
579 assert_eq!(tag2.description, Some("desc".to_string()));
580 }
581
582 #[local_db_sqlx_test]
583 async fn test_get_tag(pool: PgPool) -> Result<(), anyhow::Error> {
584 let user = USER.get().unwrap();
585 user.commit()
586 .await
587 .expect("Failed to commit user to database");
588
589 let id = user
590 .create_tag(
591 "gettag".to_string(),
592 "getvalue".to_string(),
593 Some("Get description".to_string()),
594 )
595 .await?;
596
597 let tag = user.get_tag(id).await?;
598
599 assert_eq!(tag.id, id);
600 assert_eq!(tag.tag_name, "gettag");
601 assert_eq!(tag.tag_value, "getvalue");
602 assert_eq!(tag.description, Some("Get description".to_string()));
603 }
604
605 #[local_db_sqlx_test]
606 async fn test_get_nonexistent_tag(pool: PgPool) -> Result<(), anyhow::Error> {
607 let user = USER.get().unwrap();
608 user.commit()
609 .await
610 .expect("Failed to commit user to database");
611
612 let nonexistent_id = Uuid::new_v4();
613 let result = user.get_tag(nonexistent_id).await;
614
615 assert!(result.is_err());
616 }
617
618 #[local_db_sqlx_test]
619 async fn test_update_tag(pool: PgPool) -> Result<(), anyhow::Error> {
620 let user = USER.get().unwrap();
621 user.commit()
622 .await
623 .expect("Failed to commit user to database");
624
625 let id = user
626 .create_tag("oldname".to_string(), "oldvalue".to_string(), None)
627 .await?;
628
629 user.update_tag(
630 id,
631 "newname".to_string(),
632 "newvalue".to_string(),
633 Some("Updated description".to_string()),
634 )
635 .await?;
636
637 let tag = user.get_tag(id).await?;
638
639 assert_eq!(tag.id, id);
640 assert_eq!(tag.tag_name, "newname");
641 assert_eq!(tag.tag_value, "newvalue");
642 assert_eq!(tag.description, Some("Updated description".to_string()));
643 }
644
645 #[local_db_sqlx_test]
646 async fn test_update_tag_remove_description(pool: PgPool) {
647 let user = USER.get().unwrap();
648 user.commit()
649 .await
650 .expect("Failed to commit user to database");
651
652 let id = user
653 .create_tag(
654 "tagname".to_string(),
655 "tagvalue".to_string(),
656 Some("Initial description".to_string()),
657 )
658 .await?;
659
660 user.update_tag(id, "tagname".to_string(), "tagvalue".to_string(), None)
661 .await?;
662
663 let tag = user.get_tag(id).await?;
664
665 assert_eq!(tag.description, None);
666 }
667
668 #[local_db_sqlx_test]
669 async fn test_delete_tag(pool: PgPool) -> Result<(), anyhow::Error> {
670 let user = USER.get().unwrap();
671 user.commit()
672 .await
673 .expect("Failed to commit user to database");
674
675 let id = user
676 .create_tag("deletetag".to_string(), "deletevalue".to_string(), None)
677 .await?;
678
679 let tag = user.get_tag(id).await;
680 assert!(tag.is_ok());
681
682 user.delete_tag(id).await?;
683
684 let result = user.get_tag(id).await;
685 assert!(result.is_err());
686 }
687
688 #[local_db_sqlx_test]
689 async fn test_delete_nonexistent_tag(pool: PgPool) -> Result<(), anyhow::Error> {
690 let user = USER.get().unwrap();
691 user.commit()
692 .await
693 .expect("Failed to commit user to database");
694
695 let nonexistent_id = Uuid::new_v4();
696 let result = user.delete_tag(nonexistent_id).await;
697
698 assert!(result.is_ok());
699 }
700
701 #[local_db_sqlx_test]
702 async fn test_list_tags_empty(pool: PgPool) -> Result<(), anyhow::Error> {
703 let user = USER.get().unwrap();
704 user.commit()
705 .await
706 .expect("Failed to commit user to database");
707
708 let tags = user.list_tags().await?;
709
710 assert!(
711 tags.is_empty()
712 || tags.iter().all(|t| t.tag_name == "name"
713 || t.tag_name == "note"
714 || t.tag_name == "symbol")
715 );
716 }
717
718 #[local_db_sqlx_test]
719 async fn test_list_transaction_tag_names_empty(pool: PgPool) -> Result<(), anyhow::Error> {
720 let user = USER.get().unwrap();
721 user.commit()
722 .await
723 .expect("Failed to commit user to database");
724
725 let names = user.list_transaction_tag_names().await?;
726
727 assert!(names.is_empty());
728 }
729
730 #[local_db_sqlx_test]
731 async fn test_list_transaction_tag_names_with_data(
732 pool: PgPool,
733 ) -> Result<(), anyhow::Error> {
734 let user = USER.get().unwrap();
735 user.commit()
736 .await
737 .expect("Failed to commit user to database");
738
739 let commodity_id = user
740 .create_commodity("USD".to_string(), "US Dollar".to_string())
741 .await?
742 .id;
743 let acc1 = user.create_account("test_acc1", None).await?.id;
744 let acc2 = user.create_account("test_acc2", None).await?.id;
745
746 let tx = Transaction {
747 id: sqlx::types::Uuid::new_v4(),
748 post_date: chrono::Utc::now(),
749 enter_date: chrono::Utc::now(),
750 };
751
752 let mut conn = user.get_connection().await?;
753 let mut ticket = tx.enter(&mut *conn).await?;
754 let split1 = Split {
755 id: sqlx::types::Uuid::new_v4(),
756 account_id: acc1,
757 tx_id: tx.id,
758 value_num: 100,
759 value_denom: 1,
760 commodity_id,
761 reconcile_state: None,
762 reconcile_date: None,
763 lot_id: None,
764 };
765 let split2 = Split {
766 id: sqlx::types::Uuid::new_v4(),
767 account_id: acc2,
768 tx_id: tx.id,
769 value_num: -100,
770 value_denom: 1,
771 commodity_id,
772 reconcile_state: None,
773 reconcile_date: None,
774 lot_id: None,
775 };
776 ticket.add_splits(&[&split1, &split2]).await?;
777 ticket.commit().await?;
778
779 user.create_transaction_tag(tx.id, "category".to_string(), "food".to_string(), None)
780 .await?;
781 user.create_transaction_tag(tx.id, "priority".to_string(), "high".to_string(), None)
782 .await?;
783
784 let names = user.list_transaction_tag_names().await?;
785
786 assert_eq!(names.len(), 2);
787 assert!(names.contains(&"category".to_string()));
788 assert!(names.contains(&"priority".to_string()));
789 }
790
791 #[local_db_sqlx_test]
792 async fn test_list_transaction_tag_values_empty(pool: PgPool) -> Result<(), anyhow::Error> {
793 let user = USER.get().unwrap();
794 user.commit()
795 .await
796 .expect("Failed to commit user to database");
797
798 let values = user.list_transaction_tag_values("category").await?;
799
800 assert!(values.is_empty());
801 }
802
803 #[local_db_sqlx_test]
804 async fn test_list_transaction_tag_values_with_data(
805 pool: PgPool,
806 ) -> Result<(), anyhow::Error> {
807 let user = USER.get().unwrap();
808 user.commit()
809 .await
810 .expect("Failed to commit user to database");
811
812 let commodity_id = user
813 .create_commodity("USD".to_string(), "US Dollar".to_string())
814 .await?
815 .id;
816 let acc1 = user.create_account("test_acc1", None).await?.id;
817 let acc2 = user.create_account("test_acc2", None).await?.id;
818
819 let tx1 = Transaction {
820 id: sqlx::types::Uuid::new_v4(),
821 post_date: chrono::Utc::now(),
822 enter_date: chrono::Utc::now(),
823 };
824 let tx2 = Transaction {
825 id: sqlx::types::Uuid::new_v4(),
826 post_date: chrono::Utc::now(),
827 enter_date: chrono::Utc::now(),
828 };
829 {
830 let mut conn = user.get_connection().await?;
831 let mut ticket1 = tx1.enter(&mut *conn).await?;
832 let split1a = Split {
833 id: sqlx::types::Uuid::new_v4(),
834 account_id: acc1,
835 tx_id: tx1.id,
836 value_num: 100,
837 value_denom: 1,
838 commodity_id,
839 reconcile_state: None,
840 reconcile_date: None,
841 lot_id: None,
842 };
843 let split1b = Split {
844 id: sqlx::types::Uuid::new_v4(),
845 account_id: acc2,
846 tx_id: tx1.id,
847 value_num: -100,
848 value_denom: 1,
849 commodity_id,
850 reconcile_state: None,
851 reconcile_date: None,
852 lot_id: None,
853 };
854 ticket1.add_splits(&[&split1a, &split1b]).await?;
855 ticket1.commit().await?;
856 }
857 {
858 let mut conn = user.get_connection().await?;
859 let mut ticket2 = tx2.enter(&mut *conn).await?;
860 let split2a = Split {
861 id: sqlx::types::Uuid::new_v4(),
862 account_id: acc1,
863 tx_id: tx2.id,
864 value_num: 200,
865 value_denom: 1,
866 commodity_id,
867 reconcile_state: None,
868 reconcile_date: None,
869 lot_id: None,
870 };
871 let split2b = Split {
872 id: sqlx::types::Uuid::new_v4(),
873 account_id: acc2,
874 tx_id: tx2.id,
875 value_num: -200,
876 value_denom: 1,
877 commodity_id,
878 reconcile_state: None,
879 reconcile_date: None,
880 lot_id: None,
881 };
882 ticket2.add_splits(&[&split2a, &split2b]).await?;
883 ticket2.commit().await?;
884 }
885 user.create_transaction_tag(tx1.id, "category".to_string(), "food".to_string(), None)
886 .await?;
887 user.create_transaction_tag(
888 tx2.id,
889 "category".to_string(),
890 "transport".to_string(),
891 None,
892 )
893 .await?;
894 user.create_transaction_tag(tx1.id, "priority".to_string(), "high".to_string(), None)
895 .await?;
896
897 let category_values = user.list_transaction_tag_values("category").await?;
898
899 assert_eq!(category_values.len(), 2);
900 assert!(category_values.contains(&"food".to_string()));
901 assert!(category_values.contains(&"transport".to_string()));
902
903 let priority_values = user.list_transaction_tag_values("priority").await?;
904
905 assert_eq!(priority_values.len(), 1);
906 assert!(priority_values.contains(&"high".to_string()));
907 }
908
909 #[local_db_sqlx_test]
910 async fn test_list_transaction_tag_values_nonexistent_name(
911 pool: PgPool,
912 ) -> Result<(), anyhow::Error> {
913 let user = USER.get().unwrap();
914 user.commit()
915 .await
916 .expect("Failed to commit user to database");
917
918 let commodity_id = user
919 .create_commodity("USD".to_string(), "US Dollar".to_string())
920 .await?
921 .id;
922 let acc1 = user.create_account("test_acc1", None).await?.id;
923 let acc2 = user.create_account("test_acc2", None).await?.id;
924
925 let tx = Transaction {
926 id: sqlx::types::Uuid::new_v4(),
927 post_date: chrono::Utc::now(),
928 enter_date: chrono::Utc::now(),
929 };
930
931 let mut conn = user.get_connection().await?;
932 let mut ticket = tx.enter(&mut *conn).await?;
933 let split1 = Split {
934 id: sqlx::types::Uuid::new_v4(),
935 account_id: acc1,
936 tx_id: tx.id,
937 value_num: 100,
938 value_denom: 1,
939 commodity_id,
940 reconcile_state: None,
941 reconcile_date: None,
942 lot_id: None,
943 };
944 let split2 = Split {
945 id: sqlx::types::Uuid::new_v4(),
946 account_id: acc2,
947 tx_id: tx.id,
948 value_num: -100,
949 value_denom: 1,
950 commodity_id,
951 reconcile_state: None,
952 reconcile_date: None,
953 lot_id: None,
954 };
955 ticket.add_splits(&[&split1, &split2]).await?;
956 ticket.commit().await?;
957
958 user.create_transaction_tag(tx.id, "category".to_string(), "food".to_string(), None)
959 .await?;
960
961 let values = user.list_transaction_tag_values("nonexistent").await?;
962
963 assert!(values.is_empty());
964 }
965
966 #[local_db_sqlx_test]
967 async fn test_create_split_tag(pool: PgPool) -> Result<(), anyhow::Error> {
968 let user = USER.get().unwrap();
969 user.commit()
970 .await
971 .expect("Failed to commit user to database");
972
973 let commodity_id = user
974 .create_commodity("USD".to_string(), "US Dollar".to_string())
975 .await?
976 .id;
977 let acc1 = user.create_account("test_acc1", None).await?.id;
978 let acc2 = user.create_account("test_acc2", None).await?.id;
979
980 let tx = Transaction {
981 id: sqlx::types::Uuid::new_v4(),
982 post_date: chrono::Utc::now(),
983 enter_date: chrono::Utc::now(),
984 };
985
986 let mut conn = user.get_connection().await?;
987 let mut ticket = tx.enter(&mut *conn).await?;
988 let split1 = Split {
989 id: sqlx::types::Uuid::new_v4(),
990 account_id: acc1,
991 tx_id: tx.id,
992 value_num: 100,
993 value_denom: 1,
994 commodity_id,
995 reconcile_state: None,
996 reconcile_date: None,
997 lot_id: None,
998 };
999 let split2 = Split {
1000 id: sqlx::types::Uuid::new_v4(),
1001 account_id: acc2,
1002 tx_id: tx.id,
1003 value_num: -100,
1004 value_denom: 1,
1005 commodity_id,
1006 reconcile_state: None,
1007 reconcile_date: None,
1008 lot_id: None,
1009 };
1010 ticket.add_splits(&[&split1, &split2]).await?;
1011 ticket.commit().await?;
1012
1013 let tag_id = user
1014 .create_split_tag(
1015 split1.id,
1016 "project".to_string(),
1017 "nomisync".to_string(),
1018 Some("Split tag for project tracking".to_string()),
1019 )
1020 .await?;
1021
1022 let mut conn = user.get_connection().await?;
1023 let res = sqlx::query!(
1024 "SELECT tag_id FROM split_tags WHERE split_id = $1",
1025 &split1.id
1026 )
1027 .fetch_one(&mut *conn)
1028 .await?;
1029
1030 assert_eq!(res.tag_id, tag_id);
1031
1032 let tag = user.get_tag(tag_id).await?;
1033 assert_eq!(tag.tag_name, "project");
1034 assert_eq!(tag.tag_value, "nomisync");
1035 assert_eq!(
1036 tag.description,
1037 Some("Split tag for project tracking".to_string())
1038 );
1039 }
1040
1041 #[local_db_sqlx_test]
1042 async fn test_list_split_tag_names_empty(pool: PgPool) -> Result<(), anyhow::Error> {
1043 let user = USER.get().unwrap();
1044 user.commit()
1045 .await
1046 .expect("Failed to commit user to database");
1047
1048 let names = user.list_split_tag_names().await?;
1049
1050 assert!(names.is_empty());
1051 }
1052
1053 #[local_db_sqlx_test]
1054 async fn test_list_split_tag_names_with_data(pool: PgPool) -> Result<(), anyhow::Error> {
1055 let user = USER.get().unwrap();
1056 user.commit()
1057 .await
1058 .expect("Failed to commit user to database");
1059
1060 let commodity_id = user
1061 .create_commodity("USD".to_string(), "US Dollar".to_string())
1062 .await?
1063 .id;
1064 let acc1 = user.create_account("test_acc1", None).await?.id;
1065 let acc2 = user.create_account("test_acc2", None).await?.id;
1066
1067 let tx = Transaction {
1068 id: sqlx::types::Uuid::new_v4(),
1069 post_date: chrono::Utc::now(),
1070 enter_date: chrono::Utc::now(),
1071 };
1072
1073 let mut conn = user.get_connection().await?;
1074 let mut ticket = tx.enter(&mut *conn).await?;
1075 let split1 = Split {
1076 id: sqlx::types::Uuid::new_v4(),
1077 account_id: acc1,
1078 tx_id: tx.id,
1079 value_num: 100,
1080 value_denom: 1,
1081 commodity_id,
1082 reconcile_state: None,
1083 reconcile_date: None,
1084 lot_id: None,
1085 };
1086 let split2 = Split {
1087 id: sqlx::types::Uuid::new_v4(),
1088 account_id: acc2,
1089 tx_id: tx.id,
1090 value_num: -100,
1091 value_denom: 1,
1092 commodity_id,
1093 reconcile_state: None,
1094 reconcile_date: None,
1095 lot_id: None,
1096 };
1097 ticket.add_splits(&[&split1, &split2]).await?;
1098 ticket.commit().await?;
1099
1100 user.create_split_tag(
1101 split1.id,
1102 "project".to_string(),
1103 "nomisync".to_string(),
1104 None,
1105 )
1106 .await?;
1107 user.create_split_tag(
1108 split2.id,
1109 "department".to_string(),
1110 "engineering".to_string(),
1111 None,
1112 )
1113 .await?;
1114
1115 let names = user.list_split_tag_names().await?;
1116
1117 assert_eq!(names.len(), 2);
1118 assert!(names.contains(&"project".to_string()));
1119 assert!(names.contains(&"department".to_string()));
1120 }
1121
1122 #[local_db_sqlx_test]
1123 async fn test_list_split_tag_values_empty(pool: PgPool) -> Result<(), anyhow::Error> {
1124 let user = USER.get().unwrap();
1125 user.commit()
1126 .await
1127 .expect("Failed to commit user to database");
1128
1129 let values = user.list_split_tag_values("project").await?;
1130
1131 assert!(values.is_empty());
1132 }
1133
1134 #[local_db_sqlx_test]
1135 async fn test_list_split_tag_values_with_data(pool: PgPool) -> Result<(), anyhow::Error> {
1136 let user = USER.get().unwrap();
1137 user.commit()
1138 .await
1139 .expect("Failed to commit user to database");
1140
1141 let commodity_id = user
1142 .create_commodity("USD".to_string(), "US Dollar".to_string())
1143 .await?
1144 .id;
1145 let acc1 = user.create_account("test_acc1", None).await?.id;
1146 let acc2 = user.create_account("test_acc2", None).await?.id;
1147
1148 let tx1 = Transaction {
1149 id: sqlx::types::Uuid::new_v4(),
1150 post_date: chrono::Utc::now(),
1151 enter_date: chrono::Utc::now(),
1152 };
1153 let tx2 = Transaction {
1154 id: sqlx::types::Uuid::new_v4(),
1155 post_date: chrono::Utc::now(),
1156 enter_date: chrono::Utc::now(),
1157 };
1158
1159 let split1_id = sqlx::types::Uuid::new_v4();
1160 let split2_id = sqlx::types::Uuid::new_v4();
1161 let split3_id = sqlx::types::Uuid::new_v4();
1162 let split4_id = sqlx::types::Uuid::new_v4();
1163
1164 {
1165 let mut conn = user.get_connection().await?;
1166 let mut ticket1 = tx1.enter(&mut *conn).await?;
1167 let split1a = Split {
1168 id: split1_id,
1169 account_id: acc1,
1170 tx_id: tx1.id,
1171 value_num: 100,
1172 value_denom: 1,
1173 commodity_id,
1174 reconcile_state: None,
1175 reconcile_date: None,
1176 lot_id: None,
1177 };
1178 let split1b = Split {
1179 id: split2_id,
1180 account_id: acc2,
1181 tx_id: tx1.id,
1182 value_num: -100,
1183 value_denom: 1,
1184 commodity_id,
1185 reconcile_state: None,
1186 reconcile_date: None,
1187 lot_id: None,
1188 };
1189 ticket1.add_splits(&[&split1a, &split1b]).await?;
1190 ticket1.commit().await?;
1191 }
1192 {
1193 let mut conn = user.get_connection().await?;
1194 let mut ticket2 = tx2.enter(&mut *conn).await?;
1195 let split2a = Split {
1196 id: split3_id,
1197 account_id: acc1,
1198 tx_id: tx2.id,
1199 value_num: 200,
1200 value_denom: 1,
1201 commodity_id,
1202 reconcile_state: None,
1203 reconcile_date: None,
1204 lot_id: None,
1205 };
1206 let split2b = Split {
1207 id: split4_id,
1208 account_id: acc2,
1209 tx_id: tx2.id,
1210 value_num: -200,
1211 value_denom: 1,
1212 commodity_id,
1213 reconcile_state: None,
1214 reconcile_date: None,
1215 lot_id: None,
1216 };
1217 ticket2.add_splits(&[&split2a, &split2b]).await?;
1218 ticket2.commit().await?;
1219 }
1220
1221 user.create_split_tag(
1222 split1_id,
1223 "project".to_string(),
1224 "nomisync".to_string(),
1225 None,
1226 )
1227 .await?;
1228 user.create_split_tag(
1229 split3_id,
1230 "project".to_string(),
1231 "website".to_string(),
1232 None,
1233 )
1234 .await?;
1235 user.create_split_tag(
1236 split2_id,
1237 "department".to_string(),
1238 "engineering".to_string(),
1239 None,
1240 )
1241 .await?;
1242
1243 let project_values = user.list_split_tag_values("project").await?;
1244
1245 assert_eq!(project_values.len(), 2);
1246 assert!(project_values.contains(&"nomisync".to_string()));
1247 assert!(project_values.contains(&"website".to_string()));
1248
1249 let department_values = user.list_split_tag_values("department").await?;
1250
1251 assert_eq!(department_values.len(), 1);
1252 assert!(department_values.contains(&"engineering".to_string()));
1253 }
1254
1255 #[local_db_sqlx_test]
1256 async fn test_list_split_tag_values_nonexistent_name(
1257 pool: PgPool,
1258 ) -> Result<(), anyhow::Error> {
1259 let user = USER.get().unwrap();
1260 user.commit()
1261 .await
1262 .expect("Failed to commit user to database");
1263
1264 let commodity_id = user
1265 .create_commodity("USD".to_string(), "US Dollar".to_string())
1266 .await?
1267 .id;
1268 let acc1 = user.create_account("test_acc1", None).await?.id;
1269 let acc2 = user.create_account("test_acc2", None).await?.id;
1270
1271 let tx = Transaction {
1272 id: sqlx::types::Uuid::new_v4(),
1273 post_date: chrono::Utc::now(),
1274 enter_date: chrono::Utc::now(),
1275 };
1276
1277 let mut conn = user.get_connection().await?;
1278 let mut ticket = tx.enter(&mut *conn).await?;
1279 let split1 = Split {
1280 id: sqlx::types::Uuid::new_v4(),
1281 account_id: acc1,
1282 tx_id: tx.id,
1283 value_num: 100,
1284 value_denom: 1,
1285 commodity_id,
1286 reconcile_state: None,
1287 reconcile_date: None,
1288 lot_id: None,
1289 };
1290 let split2 = Split {
1291 id: sqlx::types::Uuid::new_v4(),
1292 account_id: acc2,
1293 tx_id: tx.id,
1294 value_num: -100,
1295 value_denom: 1,
1296 commodity_id,
1297 reconcile_state: None,
1298 reconcile_date: None,
1299 lot_id: None,
1300 };
1301 ticket.add_splits(&[&split1, &split2]).await?;
1302 ticket.commit().await?;
1303
1304 user.create_split_tag(
1305 split1.id,
1306 "project".to_string(),
1307 "nomisync".to_string(),
1308 None,
1309 )
1310 .await?;
1311
1312 let values = user.list_split_tag_values("nonexistent").await?;
1313
1314 assert!(values.is_empty());
1315 }
1316
1317 #[local_db_sqlx_test]
1318 async fn test_create_account_tag(pool: PgPool) -> Result<(), anyhow::Error> {
1319 let user = USER.get().unwrap();
1320 user.commit()
1321 .await
1322 .expect("Failed to commit user to database");
1323
1324 let acc = user.create_account("test_acc", None).await?;
1325
1326 let tag_id = user
1327 .create_account_tag(
1328 acc.id,
1329 "category".to_string(),
1330 "assets".to_string(),
1331 Some("Account category".to_string()),
1332 )
1333 .await?;
1334
1335 let mut conn = user.get_connection().await?;
1336 let res = sqlx::query!(
1337 "SELECT tag_id FROM account_tags WHERE account_id = $1 AND tag_id = $2",
1338 &acc.id,
1339 &tag_id
1340 )
1341 .fetch_one(&mut *conn)
1342 .await?;
1343
1344 assert_eq!(res.tag_id, tag_id);
1345
1346 let tag = user.get_tag(tag_id).await?;
1347 assert_eq!(tag.tag_name, "category");
1348 assert_eq!(tag.tag_value, "assets");
1349 assert_eq!(tag.description, Some("Account category".to_string()));
1350 }
1351
1352 #[local_db_sqlx_test]
1353 async fn test_list_account_tag_names_empty(pool: PgPool) -> Result<(), anyhow::Error> {
1354 let user = USER.get().unwrap();
1355 user.commit()
1356 .await
1357 .expect("Failed to commit user to database");
1358
1359 let names = user.list_account_tag_names().await?;
1360
1361 assert!(names.is_empty());
1362 }
1363
1364 #[local_db_sqlx_test]
1365 async fn test_list_account_tag_names_with_data(pool: PgPool) -> Result<(), anyhow::Error> {
1366 let user = USER.get().unwrap();
1367 user.commit()
1368 .await
1369 .expect("Failed to commit user to database");
1370
1371 let acc1 = user.create_account("test_acc1", None).await?;
1372 let acc2 = user.create_account("test_acc2", None).await?;
1373
1374 user.set_account_tag(
1375 &acc1,
1376 &Tag {
1377 id: Uuid::new_v4(),
1378 tag_name: "category".to_string(),
1379 tag_value: "assets".to_string(),
1380 description: None,
1381 },
1382 )
1383 .await?;
1384 user.set_account_tag(
1385 &acc2,
1386 &Tag {
1387 id: Uuid::new_v4(),
1388 tag_name: "priority".to_string(),
1389 tag_value: "high".to_string(),
1390 description: None,
1391 },
1392 )
1393 .await?;
1394
1395 let names = user.list_account_tag_names().await?;
1396
1397 assert!(names.contains(&"category".to_string()));
1398 assert!(names.contains(&"priority".to_string()));
1399 }
1400
1401 #[local_db_sqlx_test]
1402 async fn test_list_account_tag_values_empty(pool: PgPool) -> Result<(), anyhow::Error> {
1403 let user = USER.get().unwrap();
1404 user.commit()
1405 .await
1406 .expect("Failed to commit user to database");
1407
1408 let values = user.list_account_tag_values("category").await?;
1409
1410 assert!(values.is_empty());
1411 }
1412
1413 #[local_db_sqlx_test]
1414 async fn test_list_account_tag_values_with_data(pool: PgPool) -> Result<(), anyhow::Error> {
1415 let user = USER.get().unwrap();
1416 user.commit()
1417 .await
1418 .expect("Failed to commit user to database");
1419
1420 let acc1 = user.create_account("test_acc1", None).await?;
1421 let acc2 = user.create_account("test_acc2", None).await?;
1422
1423 user.set_account_tag(
1424 &acc1,
1425 &Tag {
1426 id: Uuid::new_v4(),
1427 tag_name: "category".to_string(),
1428 tag_value: "assets".to_string(),
1429 description: None,
1430 },
1431 )
1432 .await?;
1433 user.set_account_tag(
1434 &acc2,
1435 &Tag {
1436 id: Uuid::new_v4(),
1437 tag_name: "category".to_string(),
1438 tag_value: "liabilities".to_string(),
1439 description: None,
1440 },
1441 )
1442 .await?;
1443
1444 let values = user.list_account_tag_values("category").await?;
1445
1446 assert_eq!(values.len(), 2);
1447 assert!(values.contains(&"assets".to_string()));
1448 assert!(values.contains(&"liabilities".to_string()));
1449
1450 let nonexistent = user.list_account_tag_values("nonexistent").await?;
1451
1452 assert!(nonexistent.is_empty());
1453 }
1454 }
1455}