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 script_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 create_account_tag(
352 &self,
353 account_id: Uuid,
354 name: String,
355 value: String,
356 description: Option<String>,
357 ) -> Result<Uuid, ServerError> {
358 let tag_id = self.create_tag(name, value, description).await?;
359 let mut conn = self.get_connection().await?;
360 sqlx::query_file!(
361 "sql/insert/account_tags/account_tag.sql",
362 &account_id,
363 &tag_id
364 )
365 .execute(&mut *conn)
366 .await
367 .map_err(|err| {
368 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
369 ServerError::DB(crate::db::DBError::Sqlx(err))
370 })?;
371 Ok(tag_id)
372 }
373
374 pub async fn list_account_tag_names(&self) -> Result<Vec<String>, ServerError> {
375 self.list_tag_names(TagScope::Account).await
376 }
377
378 pub async fn list_account_tag_values(
379 &self,
380 tag_name: &str,
381 ) -> Result<Vec<String>, ServerError> {
382 self.list_tag_values_for(TagScope::Account, tag_name).await
383 }
384
385 pub async fn list_split_tag_names(&self) -> Result<Vec<String>, ServerError> {
386 self.list_tag_names(TagScope::Split).await
387 }
388
389 pub async fn list_split_tag_values(
390 &self,
391 tag_name: &str,
392 ) -> Result<Vec<String>, ServerError> {
393 self.list_tag_values_for(TagScope::Split, tag_name).await
394 }
395 }
396
397 #[cfg(test)]
398 mod tag_tests {
399 use super::*;
400 use crate::db::DB_POOL;
401 #[cfg(feature = "testlog")]
402 use env_logger;
403 use finance::{split::Split, transaction::Transaction};
404 #[cfg(feature = "testlog")]
405 use log;
406 use sqlx::PgPool;
407 use sqlx::types::chrono;
408
409 use supp_macro::local_db_sqlx_test;
410 use tokio::sync::OnceCell;
411
412 static CONTEXT: OnceCell<()> = OnceCell::const_new();
414 static USER: OnceCell<User> = OnceCell::const_new();
415
416 async fn setup() {
417 CONTEXT
418 .get_or_init(|| async {
419 #[cfg(feature = "testlog")]
420 let _ = env_logger::builder()
421 .is_test(true)
422 .filter_level(log::LevelFilter::Trace)
423 .try_init();
424 })
425 .await;
426 USER.get_or_init(|| async { User { id: Uuid::new_v4() } })
427 .await;
428 }
429
430 #[local_db_sqlx_test]
431 async fn test_tag_creation(pool: PgPool) -> Result<(), anyhow::Error> {
432 let user = USER.get().unwrap();
433 user.commit()
434 .await
435 .expect("Failed to commit user to database");
436
437 let id = user
438 .create_tag("testtag".to_string(), "testval".to_string(), None)
439 .await?;
440
441 let mut conn = user.get_connection().await?;
442 let res = sqlx::query_file!("testdata/query_tag_by_id.sql", &id)
443 .fetch_one(&mut *conn)
444 .await?;
445
446 assert_eq!(res.tag_name, "testtag".to_string());
447 assert_eq!(res.tag_value, "testval".to_string());
448 assert_eq!(res.description, None);
449 }
450
451 #[local_db_sqlx_test]
452 async fn test_tag_creation_with_description(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(
460 "categorytag".to_string(),
461 "category1".to_string(),
462 Some("Test description".to_string()),
463 )
464 .await?;
465
466 let tag = user.get_tag(id).await?;
467
468 assert_eq!(tag.tag_name, "categorytag");
469 assert_eq!(tag.tag_value, "category1");
470 assert_eq!(tag.description, Some("Test description".to_string()));
471 }
472
473 #[local_db_sqlx_test]
474 async fn test_list_tags(pool: PgPool) -> Result<(), anyhow::Error> {
475 let user = USER.get().unwrap();
476 user.commit()
477 .await
478 .expect("Failed to commit user to database");
479
480 let id1 = user
481 .create_tag("tag1".to_string(), "value1".to_string(), None)
482 .await?;
483
484 let id2 = user
485 .create_tag(
486 "tag2".to_string(),
487 "value2".to_string(),
488 Some("desc".to_string()),
489 )
490 .await?;
491
492 let tags = user.list_tags().await?;
493
494 assert!(tags.len() >= 2);
495 assert!(tags.iter().any(|t| t.id == id1));
496 assert!(tags.iter().any(|t| t.id == id2));
497
498 let tag1 = tags.iter().find(|t| t.id == id1).unwrap();
499 assert_eq!(tag1.tag_name, "tag1");
500 assert_eq!(tag1.tag_value, "value1");
501 assert_eq!(tag1.description, None);
502
503 let tag2 = tags.iter().find(|t| t.id == id2).unwrap();
504 assert_eq!(tag2.tag_name, "tag2");
505 assert_eq!(tag2.tag_value, "value2");
506 assert_eq!(tag2.description, Some("desc".to_string()));
507 }
508
509 #[local_db_sqlx_test]
510 async fn test_get_tag(pool: PgPool) -> Result<(), anyhow::Error> {
511 let user = USER.get().unwrap();
512 user.commit()
513 .await
514 .expect("Failed to commit user to database");
515
516 let id = user
517 .create_tag(
518 "gettag".to_string(),
519 "getvalue".to_string(),
520 Some("Get description".to_string()),
521 )
522 .await?;
523
524 let tag = user.get_tag(id).await?;
525
526 assert_eq!(tag.id, id);
527 assert_eq!(tag.tag_name, "gettag");
528 assert_eq!(tag.tag_value, "getvalue");
529 assert_eq!(tag.description, Some("Get description".to_string()));
530 }
531
532 #[local_db_sqlx_test]
533 async fn test_get_nonexistent_tag(pool: PgPool) -> Result<(), anyhow::Error> {
534 let user = USER.get().unwrap();
535 user.commit()
536 .await
537 .expect("Failed to commit user to database");
538
539 let nonexistent_id = Uuid::new_v4();
540 let result = user.get_tag(nonexistent_id).await;
541
542 assert!(result.is_err());
543 }
544
545 #[local_db_sqlx_test]
546 async fn test_update_tag(pool: PgPool) -> Result<(), anyhow::Error> {
547 let user = USER.get().unwrap();
548 user.commit()
549 .await
550 .expect("Failed to commit user to database");
551
552 let id = user
553 .create_tag("oldname".to_string(), "oldvalue".to_string(), None)
554 .await?;
555
556 user.update_tag(
557 id,
558 "newname".to_string(),
559 "newvalue".to_string(),
560 Some("Updated description".to_string()),
561 )
562 .await?;
563
564 let tag = user.get_tag(id).await?;
565
566 assert_eq!(tag.id, id);
567 assert_eq!(tag.tag_name, "newname");
568 assert_eq!(tag.tag_value, "newvalue");
569 assert_eq!(tag.description, Some("Updated description".to_string()));
570 }
571
572 #[local_db_sqlx_test]
573 async fn test_update_tag_remove_description(pool: PgPool) {
574 let user = USER.get().unwrap();
575 user.commit()
576 .await
577 .expect("Failed to commit user to database");
578
579 let id = user
580 .create_tag(
581 "tagname".to_string(),
582 "tagvalue".to_string(),
583 Some("Initial description".to_string()),
584 )
585 .await?;
586
587 user.update_tag(id, "tagname".to_string(), "tagvalue".to_string(), None)
588 .await?;
589
590 let tag = user.get_tag(id).await?;
591
592 assert_eq!(tag.description, None);
593 }
594
595 #[local_db_sqlx_test]
596 async fn test_delete_tag(pool: PgPool) -> Result<(), anyhow::Error> {
597 let user = USER.get().unwrap();
598 user.commit()
599 .await
600 .expect("Failed to commit user to database");
601
602 let id = user
603 .create_tag("deletetag".to_string(), "deletevalue".to_string(), None)
604 .await?;
605
606 let tag = user.get_tag(id).await;
607 assert!(tag.is_ok());
608
609 user.delete_tag(id).await?;
610
611 let result = user.get_tag(id).await;
612 assert!(result.is_err());
613 }
614
615 #[local_db_sqlx_test]
616 async fn test_delete_nonexistent_tag(pool: PgPool) -> Result<(), anyhow::Error> {
617 let user = USER.get().unwrap();
618 user.commit()
619 .await
620 .expect("Failed to commit user to database");
621
622 let nonexistent_id = Uuid::new_v4();
623 let result = user.delete_tag(nonexistent_id).await;
624
625 assert!(result.is_ok());
626 }
627
628 #[local_db_sqlx_test]
629 async fn test_list_tags_empty(pool: PgPool) -> Result<(), anyhow::Error> {
630 let user = USER.get().unwrap();
631 user.commit()
632 .await
633 .expect("Failed to commit user to database");
634
635 let tags = user.list_tags().await?;
636
637 assert!(
638 tags.is_empty()
639 || tags.iter().all(|t| t.tag_name == "name"
640 || t.tag_name == "note"
641 || t.tag_name == "symbol")
642 );
643 }
644
645 #[local_db_sqlx_test]
646 async fn test_list_transaction_tag_names_empty(pool: PgPool) -> Result<(), anyhow::Error> {
647 let user = USER.get().unwrap();
648 user.commit()
649 .await
650 .expect("Failed to commit user to database");
651
652 let names = user.list_transaction_tag_names().await?;
653
654 assert!(names.is_empty());
655 }
656
657 #[local_db_sqlx_test]
658 async fn test_list_transaction_tag_names_with_data(
659 pool: PgPool,
660 ) -> Result<(), anyhow::Error> {
661 let user = USER.get().unwrap();
662 user.commit()
663 .await
664 .expect("Failed to commit user to database");
665
666 let commodity_id = user
667 .create_commodity("USD".to_string(), "US Dollar".to_string())
668 .await?
669 .id;
670 let acc1 = user.create_account("test_acc1", None).await?.id;
671 let acc2 = user.create_account("test_acc2", None).await?.id;
672
673 let tx = Transaction {
674 id: sqlx::types::Uuid::new_v4(),
675 post_date: chrono::Utc::now(),
676 enter_date: chrono::Utc::now(),
677 };
678
679 let mut conn = user.get_connection().await?;
680 let mut ticket = tx.enter(&mut *conn).await?;
681 let split1 = Split {
682 id: sqlx::types::Uuid::new_v4(),
683 account_id: acc1,
684 tx_id: tx.id,
685 value_num: 100,
686 value_denom: 1,
687 commodity_id,
688 reconcile_state: None,
689 reconcile_date: None,
690 lot_id: None,
691 };
692 let split2 = Split {
693 id: sqlx::types::Uuid::new_v4(),
694 account_id: acc2,
695 tx_id: tx.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 ticket.add_splits(&[&split1, &split2]).await?;
704 ticket.commit().await?;
705
706 user.create_transaction_tag(tx.id, "category".to_string(), "food".to_string(), None)
707 .await?;
708 user.create_transaction_tag(tx.id, "priority".to_string(), "high".to_string(), None)
709 .await?;
710
711 let names = user.list_transaction_tag_names().await?;
712
713 assert_eq!(names.len(), 2);
714 assert!(names.contains(&"category".to_string()));
715 assert!(names.contains(&"priority".to_string()));
716 }
717
718 #[local_db_sqlx_test]
719 async fn test_list_transaction_tag_values_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 values = user.list_transaction_tag_values("category").await?;
726
727 assert!(values.is_empty());
728 }
729
730 #[local_db_sqlx_test]
731 async fn test_list_transaction_tag_values_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 tx1 = Transaction {
747 id: sqlx::types::Uuid::new_v4(),
748 post_date: chrono::Utc::now(),
749 enter_date: chrono::Utc::now(),
750 };
751 let tx2 = Transaction {
752 id: sqlx::types::Uuid::new_v4(),
753 post_date: chrono::Utc::now(),
754 enter_date: chrono::Utc::now(),
755 };
756 {
757 let mut conn = user.get_connection().await?;
758 let mut ticket1 = tx1.enter(&mut *conn).await?;
759 let split1a = Split {
760 id: sqlx::types::Uuid::new_v4(),
761 account_id: acc1,
762 tx_id: tx1.id,
763 value_num: 100,
764 value_denom: 1,
765 commodity_id,
766 reconcile_state: None,
767 reconcile_date: None,
768 lot_id: None,
769 };
770 let split1b = Split {
771 id: sqlx::types::Uuid::new_v4(),
772 account_id: acc2,
773 tx_id: tx1.id,
774 value_num: -100,
775 value_denom: 1,
776 commodity_id,
777 reconcile_state: None,
778 reconcile_date: None,
779 lot_id: None,
780 };
781 ticket1.add_splits(&[&split1a, &split1b]).await?;
782 ticket1.commit().await?;
783 }
784 {
785 let mut conn = user.get_connection().await?;
786 let mut ticket2 = tx2.enter(&mut *conn).await?;
787 let split2a = Split {
788 id: sqlx::types::Uuid::new_v4(),
789 account_id: acc1,
790 tx_id: tx2.id,
791 value_num: 200,
792 value_denom: 1,
793 commodity_id,
794 reconcile_state: None,
795 reconcile_date: None,
796 lot_id: None,
797 };
798 let split2b = Split {
799 id: sqlx::types::Uuid::new_v4(),
800 account_id: acc2,
801 tx_id: tx2.id,
802 value_num: -200,
803 value_denom: 1,
804 commodity_id,
805 reconcile_state: None,
806 reconcile_date: None,
807 lot_id: None,
808 };
809 ticket2.add_splits(&[&split2a, &split2b]).await?;
810 ticket2.commit().await?;
811 }
812 user.create_transaction_tag(tx1.id, "category".to_string(), "food".to_string(), None)
813 .await?;
814 user.create_transaction_tag(
815 tx2.id,
816 "category".to_string(),
817 "transport".to_string(),
818 None,
819 )
820 .await?;
821 user.create_transaction_tag(tx1.id, "priority".to_string(), "high".to_string(), None)
822 .await?;
823
824 let category_values = user.list_transaction_tag_values("category").await?;
825
826 assert_eq!(category_values.len(), 2);
827 assert!(category_values.contains(&"food".to_string()));
828 assert!(category_values.contains(&"transport".to_string()));
829
830 let priority_values = user.list_transaction_tag_values("priority").await?;
831
832 assert_eq!(priority_values.len(), 1);
833 assert!(priority_values.contains(&"high".to_string()));
834 }
835
836 #[local_db_sqlx_test]
837 async fn test_list_transaction_tag_values_nonexistent_name(
838 pool: PgPool,
839 ) -> Result<(), anyhow::Error> {
840 let user = USER.get().unwrap();
841 user.commit()
842 .await
843 .expect("Failed to commit user to database");
844
845 let commodity_id = user
846 .create_commodity("USD".to_string(), "US Dollar".to_string())
847 .await?
848 .id;
849 let acc1 = user.create_account("test_acc1", None).await?.id;
850 let acc2 = user.create_account("test_acc2", None).await?.id;
851
852 let tx = Transaction {
853 id: sqlx::types::Uuid::new_v4(),
854 post_date: chrono::Utc::now(),
855 enter_date: chrono::Utc::now(),
856 };
857
858 let mut conn = user.get_connection().await?;
859 let mut ticket = tx.enter(&mut *conn).await?;
860 let split1 = Split {
861 id: sqlx::types::Uuid::new_v4(),
862 account_id: acc1,
863 tx_id: tx.id,
864 value_num: 100,
865 value_denom: 1,
866 commodity_id,
867 reconcile_state: None,
868 reconcile_date: None,
869 lot_id: None,
870 };
871 let split2 = Split {
872 id: sqlx::types::Uuid::new_v4(),
873 account_id: acc2,
874 tx_id: tx.id,
875 value_num: -100,
876 value_denom: 1,
877 commodity_id,
878 reconcile_state: None,
879 reconcile_date: None,
880 lot_id: None,
881 };
882 ticket.add_splits(&[&split1, &split2]).await?;
883 ticket.commit().await?;
884
885 user.create_transaction_tag(tx.id, "category".to_string(), "food".to_string(), None)
886 .await?;
887
888 let values = user.list_transaction_tag_values("nonexistent").await?;
889
890 assert!(values.is_empty());
891 }
892
893 #[local_db_sqlx_test]
894 async fn test_create_split_tag(pool: PgPool) -> Result<(), anyhow::Error> {
895 let user = USER.get().unwrap();
896 user.commit()
897 .await
898 .expect("Failed to commit user to database");
899
900 let commodity_id = user
901 .create_commodity("USD".to_string(), "US Dollar".to_string())
902 .await?
903 .id;
904 let acc1 = user.create_account("test_acc1", None).await?.id;
905 let acc2 = user.create_account("test_acc2", None).await?.id;
906
907 let tx = Transaction {
908 id: sqlx::types::Uuid::new_v4(),
909 post_date: chrono::Utc::now(),
910 enter_date: chrono::Utc::now(),
911 };
912
913 let mut conn = user.get_connection().await?;
914 let mut ticket = tx.enter(&mut *conn).await?;
915 let split1 = Split {
916 id: sqlx::types::Uuid::new_v4(),
917 account_id: acc1,
918 tx_id: tx.id,
919 value_num: 100,
920 value_denom: 1,
921 commodity_id,
922 reconcile_state: None,
923 reconcile_date: None,
924 lot_id: None,
925 };
926 let split2 = Split {
927 id: sqlx::types::Uuid::new_v4(),
928 account_id: acc2,
929 tx_id: tx.id,
930 value_num: -100,
931 value_denom: 1,
932 commodity_id,
933 reconcile_state: None,
934 reconcile_date: None,
935 lot_id: None,
936 };
937 ticket.add_splits(&[&split1, &split2]).await?;
938 ticket.commit().await?;
939
940 let tag_id = user
941 .create_split_tag(
942 split1.id,
943 "project".to_string(),
944 "nomisync".to_string(),
945 Some("Split tag for project tracking".to_string()),
946 )
947 .await?;
948
949 let mut conn = user.get_connection().await?;
950 let res = sqlx::query!(
951 "SELECT tag_id FROM split_tags WHERE split_id = $1",
952 &split1.id
953 )
954 .fetch_one(&mut *conn)
955 .await?;
956
957 assert_eq!(res.tag_id, tag_id);
958
959 let tag = user.get_tag(tag_id).await?;
960 assert_eq!(tag.tag_name, "project");
961 assert_eq!(tag.tag_value, "nomisync");
962 assert_eq!(
963 tag.description,
964 Some("Split tag for project tracking".to_string())
965 );
966 }
967
968 #[local_db_sqlx_test]
969 async fn test_list_split_tag_names_empty(pool: PgPool) -> Result<(), anyhow::Error> {
970 let user = USER.get().unwrap();
971 user.commit()
972 .await
973 .expect("Failed to commit user to database");
974
975 let names = user.list_split_tag_names().await?;
976
977 assert!(names.is_empty());
978 }
979
980 #[local_db_sqlx_test]
981 async fn test_list_split_tag_names_with_data(pool: PgPool) -> Result<(), anyhow::Error> {
982 let user = USER.get().unwrap();
983 user.commit()
984 .await
985 .expect("Failed to commit user to database");
986
987 let commodity_id = user
988 .create_commodity("USD".to_string(), "US Dollar".to_string())
989 .await?
990 .id;
991 let acc1 = user.create_account("test_acc1", None).await?.id;
992 let acc2 = user.create_account("test_acc2", None).await?.id;
993
994 let tx = Transaction {
995 id: sqlx::types::Uuid::new_v4(),
996 post_date: chrono::Utc::now(),
997 enter_date: chrono::Utc::now(),
998 };
999
1000 let mut conn = user.get_connection().await?;
1001 let mut ticket = tx.enter(&mut *conn).await?;
1002 let split1 = Split {
1003 id: sqlx::types::Uuid::new_v4(),
1004 account_id: acc1,
1005 tx_id: tx.id,
1006 value_num: 100,
1007 value_denom: 1,
1008 commodity_id,
1009 reconcile_state: None,
1010 reconcile_date: None,
1011 lot_id: None,
1012 };
1013 let split2 = Split {
1014 id: sqlx::types::Uuid::new_v4(),
1015 account_id: acc2,
1016 tx_id: tx.id,
1017 value_num: -100,
1018 value_denom: 1,
1019 commodity_id,
1020 reconcile_state: None,
1021 reconcile_date: None,
1022 lot_id: None,
1023 };
1024 ticket.add_splits(&[&split1, &split2]).await?;
1025 ticket.commit().await?;
1026
1027 user.create_split_tag(
1028 split1.id,
1029 "project".to_string(),
1030 "nomisync".to_string(),
1031 None,
1032 )
1033 .await?;
1034 user.create_split_tag(
1035 split2.id,
1036 "department".to_string(),
1037 "engineering".to_string(),
1038 None,
1039 )
1040 .await?;
1041
1042 let names = user.list_split_tag_names().await?;
1043
1044 assert_eq!(names.len(), 2);
1045 assert!(names.contains(&"project".to_string()));
1046 assert!(names.contains(&"department".to_string()));
1047 }
1048
1049 #[local_db_sqlx_test]
1050 async fn test_list_split_tag_values_empty(pool: PgPool) -> Result<(), anyhow::Error> {
1051 let user = USER.get().unwrap();
1052 user.commit()
1053 .await
1054 .expect("Failed to commit user to database");
1055
1056 let values = user.list_split_tag_values("project").await?;
1057
1058 assert!(values.is_empty());
1059 }
1060
1061 #[local_db_sqlx_test]
1062 async fn test_list_split_tag_values_with_data(pool: PgPool) -> Result<(), anyhow::Error> {
1063 let user = USER.get().unwrap();
1064 user.commit()
1065 .await
1066 .expect("Failed to commit user to database");
1067
1068 let commodity_id = user
1069 .create_commodity("USD".to_string(), "US Dollar".to_string())
1070 .await?
1071 .id;
1072 let acc1 = user.create_account("test_acc1", None).await?.id;
1073 let acc2 = user.create_account("test_acc2", None).await?.id;
1074
1075 let tx1 = Transaction {
1076 id: sqlx::types::Uuid::new_v4(),
1077 post_date: chrono::Utc::now(),
1078 enter_date: chrono::Utc::now(),
1079 };
1080 let tx2 = Transaction {
1081 id: sqlx::types::Uuid::new_v4(),
1082 post_date: chrono::Utc::now(),
1083 enter_date: chrono::Utc::now(),
1084 };
1085
1086 let split1_id = sqlx::types::Uuid::new_v4();
1087 let split2_id = sqlx::types::Uuid::new_v4();
1088 let split3_id = sqlx::types::Uuid::new_v4();
1089 let split4_id = sqlx::types::Uuid::new_v4();
1090
1091 {
1092 let mut conn = user.get_connection().await?;
1093 let mut ticket1 = tx1.enter(&mut *conn).await?;
1094 let split1a = Split {
1095 id: split1_id,
1096 account_id: acc1,
1097 tx_id: tx1.id,
1098 value_num: 100,
1099 value_denom: 1,
1100 commodity_id,
1101 reconcile_state: None,
1102 reconcile_date: None,
1103 lot_id: None,
1104 };
1105 let split1b = Split {
1106 id: split2_id,
1107 account_id: acc2,
1108 tx_id: tx1.id,
1109 value_num: -100,
1110 value_denom: 1,
1111 commodity_id,
1112 reconcile_state: None,
1113 reconcile_date: None,
1114 lot_id: None,
1115 };
1116 ticket1.add_splits(&[&split1a, &split1b]).await?;
1117 ticket1.commit().await?;
1118 }
1119 {
1120 let mut conn = user.get_connection().await?;
1121 let mut ticket2 = tx2.enter(&mut *conn).await?;
1122 let split2a = Split {
1123 id: split3_id,
1124 account_id: acc1,
1125 tx_id: tx2.id,
1126 value_num: 200,
1127 value_denom: 1,
1128 commodity_id,
1129 reconcile_state: None,
1130 reconcile_date: None,
1131 lot_id: None,
1132 };
1133 let split2b = Split {
1134 id: split4_id,
1135 account_id: acc2,
1136 tx_id: tx2.id,
1137 value_num: -200,
1138 value_denom: 1,
1139 commodity_id,
1140 reconcile_state: None,
1141 reconcile_date: None,
1142 lot_id: None,
1143 };
1144 ticket2.add_splits(&[&split2a, &split2b]).await?;
1145 ticket2.commit().await?;
1146 }
1147
1148 user.create_split_tag(
1149 split1_id,
1150 "project".to_string(),
1151 "nomisync".to_string(),
1152 None,
1153 )
1154 .await?;
1155 user.create_split_tag(
1156 split3_id,
1157 "project".to_string(),
1158 "website".to_string(),
1159 None,
1160 )
1161 .await?;
1162 user.create_split_tag(
1163 split2_id,
1164 "department".to_string(),
1165 "engineering".to_string(),
1166 None,
1167 )
1168 .await?;
1169
1170 let project_values = user.list_split_tag_values("project").await?;
1171
1172 assert_eq!(project_values.len(), 2);
1173 assert!(project_values.contains(&"nomisync".to_string()));
1174 assert!(project_values.contains(&"website".to_string()));
1175
1176 let department_values = user.list_split_tag_values("department").await?;
1177
1178 assert_eq!(department_values.len(), 1);
1179 assert!(department_values.contains(&"engineering".to_string()));
1180 }
1181
1182 #[local_db_sqlx_test]
1183 async fn test_list_split_tag_values_nonexistent_name(
1184 pool: PgPool,
1185 ) -> Result<(), anyhow::Error> {
1186 let user = USER.get().unwrap();
1187 user.commit()
1188 .await
1189 .expect("Failed to commit user to database");
1190
1191 let commodity_id = user
1192 .create_commodity("USD".to_string(), "US Dollar".to_string())
1193 .await?
1194 .id;
1195 let acc1 = user.create_account("test_acc1", None).await?.id;
1196 let acc2 = user.create_account("test_acc2", None).await?.id;
1197
1198 let tx = Transaction {
1199 id: sqlx::types::Uuid::new_v4(),
1200 post_date: chrono::Utc::now(),
1201 enter_date: chrono::Utc::now(),
1202 };
1203
1204 let mut conn = user.get_connection().await?;
1205 let mut ticket = tx.enter(&mut *conn).await?;
1206 let split1 = Split {
1207 id: sqlx::types::Uuid::new_v4(),
1208 account_id: acc1,
1209 tx_id: tx.id,
1210 value_num: 100,
1211 value_denom: 1,
1212 commodity_id,
1213 reconcile_state: None,
1214 reconcile_date: None,
1215 lot_id: None,
1216 };
1217 let split2 = Split {
1218 id: sqlx::types::Uuid::new_v4(),
1219 account_id: acc2,
1220 tx_id: tx.id,
1221 value_num: -100,
1222 value_denom: 1,
1223 commodity_id,
1224 reconcile_state: None,
1225 reconcile_date: None,
1226 lot_id: None,
1227 };
1228 ticket.add_splits(&[&split1, &split2]).await?;
1229 ticket.commit().await?;
1230
1231 user.create_split_tag(
1232 split1.id,
1233 "project".to_string(),
1234 "nomisync".to_string(),
1235 None,
1236 )
1237 .await?;
1238
1239 let values = user.list_split_tag_values("nonexistent").await?;
1240
1241 assert!(values.is_empty());
1242 }
1243
1244 #[local_db_sqlx_test]
1245 async fn test_create_account_tag(pool: PgPool) -> Result<(), anyhow::Error> {
1246 let user = USER.get().unwrap();
1247 user.commit()
1248 .await
1249 .expect("Failed to commit user to database");
1250
1251 let acc = user.create_account("test_acc", None).await?;
1252
1253 let tag_id = user
1254 .create_account_tag(
1255 acc.id,
1256 "category".to_string(),
1257 "assets".to_string(),
1258 Some("Account category".to_string()),
1259 )
1260 .await?;
1261
1262 let mut conn = user.get_connection().await?;
1263 let res = sqlx::query!(
1264 "SELECT tag_id FROM account_tags WHERE account_id = $1 AND tag_id = $2",
1265 &acc.id,
1266 &tag_id
1267 )
1268 .fetch_one(&mut *conn)
1269 .await?;
1270
1271 assert_eq!(res.tag_id, tag_id);
1272
1273 let tag = user.get_tag(tag_id).await?;
1274 assert_eq!(tag.tag_name, "category");
1275 assert_eq!(tag.tag_value, "assets");
1276 assert_eq!(tag.description, Some("Account category".to_string()));
1277 }
1278
1279 #[local_db_sqlx_test]
1280 async fn test_list_account_tag_names_empty(pool: PgPool) -> Result<(), anyhow::Error> {
1281 let user = USER.get().unwrap();
1282 user.commit()
1283 .await
1284 .expect("Failed to commit user to database");
1285
1286 let names = user.list_account_tag_names().await?;
1287
1288 assert!(names.is_empty());
1289 }
1290
1291 #[local_db_sqlx_test]
1292 async fn test_list_account_tag_names_with_data(pool: PgPool) -> Result<(), anyhow::Error> {
1293 let user = USER.get().unwrap();
1294 user.commit()
1295 .await
1296 .expect("Failed to commit user to database");
1297
1298 let acc1 = user.create_account("test_acc1", None).await?;
1299 let acc2 = user.create_account("test_acc2", None).await?;
1300
1301 user.set_account_tag(
1302 &acc1,
1303 &Tag {
1304 id: Uuid::new_v4(),
1305 tag_name: "category".to_string(),
1306 tag_value: "assets".to_string(),
1307 description: None,
1308 },
1309 )
1310 .await?;
1311 user.set_account_tag(
1312 &acc2,
1313 &Tag {
1314 id: Uuid::new_v4(),
1315 tag_name: "priority".to_string(),
1316 tag_value: "high".to_string(),
1317 description: None,
1318 },
1319 )
1320 .await?;
1321
1322 let names = user.list_account_tag_names().await?;
1323
1324 assert!(names.contains(&"category".to_string()));
1325 assert!(names.contains(&"priority".to_string()));
1326 }
1327
1328 #[local_db_sqlx_test]
1329 async fn test_list_account_tag_values_empty(pool: PgPool) -> Result<(), anyhow::Error> {
1330 let user = USER.get().unwrap();
1331 user.commit()
1332 .await
1333 .expect("Failed to commit user to database");
1334
1335 let values = user.list_account_tag_values("category").await?;
1336
1337 assert!(values.is_empty());
1338 }
1339
1340 #[local_db_sqlx_test]
1341 async fn test_list_account_tag_values_with_data(pool: PgPool) -> Result<(), anyhow::Error> {
1342 let user = USER.get().unwrap();
1343 user.commit()
1344 .await
1345 .expect("Failed to commit user to database");
1346
1347 let acc1 = user.create_account("test_acc1", None).await?;
1348 let acc2 = user.create_account("test_acc2", None).await?;
1349
1350 user.set_account_tag(
1351 &acc1,
1352 &Tag {
1353 id: Uuid::new_v4(),
1354 tag_name: "category".to_string(),
1355 tag_value: "assets".to_string(),
1356 description: None,
1357 },
1358 )
1359 .await?;
1360 user.set_account_tag(
1361 &acc2,
1362 &Tag {
1363 id: Uuid::new_v4(),
1364 tag_name: "category".to_string(),
1365 tag_value: "liabilities".to_string(),
1366 description: None,
1367 },
1368 )
1369 .await?;
1370
1371 let values = user.list_account_tag_values("category").await?;
1372
1373 assert_eq!(values.len(), 2);
1374 assert!(values.contains(&"assets".to_string()));
1375 assert!(values.contains(&"liabilities".to_string()));
1376
1377 let nonexistent = user.list_account_tag_values("nonexistent").await?;
1378
1379 assert!(nonexistent.is_empty());
1380 }
1381 }
1382}