1use crate::error::{FinanceError, SplitError};
2use sqlx::types::Uuid;
3use sqlx::types::chrono::{DateTime, Utc};
4use sqlx::{Connection, query_file};
5use supp_macro::Builder;
6
7#[derive(Debug, sqlx::FromRow, Builder)]
8#[builder(error_kind = "FinanceError")]
9pub struct Split {
10 pub id: Uuid,
11 pub tx_id: Uuid,
12 pub account_id: Uuid,
13 pub commodity_id: Uuid,
14 pub reconcile_state: Option<bool>,
15 pub reconcile_date: Option<DateTime<Utc>>,
16 pub value_num: i64,
17 pub value_denom: i64,
18 pub lot_id: Option<Uuid>,
19}
20
21impl Split {
22 pub async fn commit<E>(&self, conn: &mut E) -> Result<(), FinanceError>
23 where
24 E: Connection<Database = sqlx::Postgres>,
25 {
26 let mut tr = conn.begin().await?;
27
28 query_file!(
29 "sql/split_insert.sql",
30 &self.id,
31 &self.tx_id,
32 &self.account_id,
33 &self.commodity_id,
34 self.reconcile_state,
35 self.reconcile_date,
36 &self.value_num,
37 &self.value_denom,
38 self.lot_id,
39 )
40 .execute(&mut *tr)
41 .await?;
42 tr.commit().await?;
43
44 Ok(())
45 }
46}
47
48#[cfg(test)]
49mod split_tests {
50 use super::*;
51 use crate::account::{Account, AccountBuilder};
52 use crate::commodity::{Commodity, CommodityBuilder};
53 use crate::transaction::{Transaction, TransactionBuilder};
54 #[cfg(feature = "testlog")]
55 use env_logger;
56 #[cfg(feature = "testlog")]
57 use log;
58 use sqlx::PgPool;
59 use sqlx::types::chrono::Local;
60 use tokio::sync::OnceCell;
61
62 static CONTEXT: OnceCell<()> = OnceCell::const_new();
64 static COMMODITY: OnceCell<Commodity> = OnceCell::const_new();
65 static ACCOUNT: OnceCell<Account> = OnceCell::const_new();
66 static TRANSACTION: OnceCell<Transaction> = OnceCell::const_new();
67
68 async fn setup(pool: &PgPool) {
69 CONTEXT
70 .get_or_init(|| async {
71 #[cfg(feature = "testlog")]
72 let _ = env_logger::builder()
73 .is_test(true)
74 .filter_level(log::LevelFilter::Trace)
75 .try_init();
76 })
77 .await;
78 COMMODITY
79 .get_or_init(|| async { CommodityBuilder::new().id(Uuid::new_v4()).build().unwrap() })
80 .await;
81 let mut conn = pool.acquire().await.unwrap();
82 COMMODITY.get().unwrap().commit(&mut *conn).await.unwrap();
83
84 ACCOUNT
85 .get_or_init(|| async { AccountBuilder::new().id(Uuid::new_v4()).build().unwrap() })
86 .await;
87
88 let mut conn = pool.acquire().await.unwrap();
89 ACCOUNT.get().unwrap().commit(&mut *conn).await.unwrap();
90
91 TRANSACTION
92 .get_or_init(|| async {
93 TransactionBuilder::new()
94 .id(Uuid::new_v4())
95 .post_date(Local::now().into())
96 .enter_date(Local::now().into())
97 .build()
98 .unwrap()
99 })
100 .await;
101
102 let mut conn = pool.acquire().await.unwrap();
103 sqlx::query_file!(
104 "sql/transaction_insert.sql",
105 &TRANSACTION.get().unwrap().id,
106 &TRANSACTION.get().unwrap().post_date,
107 &TRANSACTION.get().unwrap().enter_date
108 )
109 .execute(&mut *conn)
110 .await
111 .unwrap();
112 }
113
114 #[sqlx::test(migrations = "../migrations")]
115 async fn test_split_store(pool: PgPool) -> anyhow::Result<()> {
116 setup(&pool).await;
117 let split = Split {
118 id: Uuid::new_v4(),
119 tx_id: TRANSACTION.get().unwrap().id,
120 account_id: ACCOUNT.get().unwrap().id,
121 commodity_id: COMMODITY.get().unwrap().id,
122 value_num: 100,
123 value_denom: 1,
124 reconcile_state: None,
125 reconcile_date: None,
126 lot_id: None,
127 };
128
129 let mut conn = pool.acquire().await?;
130
131 sqlx::query!(
132 "SELECT post_date FROM transactions WHERE id = $1",
133 TRANSACTION.get().unwrap().id
134 )
135 .fetch_one(&mut *conn)
136 .await?;
137
138 let mut conn = pool.begin().await?;
139 sqlx::query!("INSERT INTO splits (id, tx_id, account_id, commodity_id, value_num, value_denom) VALUES ($1, $2, $3, $4, $5, $6)",
140 &split.id,
141 &split.tx_id,
142 &split.account_id,
143 &split.commodity_id,
144 &split.value_num,
145 &split.value_denom)
146 .execute(&mut *conn)
147 .await
148 ?;
149 conn.commit().await?;
150
151 let mut conn = pool.acquire().await?;
152
153 let result = sqlx::query!("SELECT id FROM splits WHERE value_num = 100")
154 .fetch_one(&mut *conn)
155 .await?;
156
157 assert_eq!(split.id, result.id);
158
159 let split2 = Split {
160 id: Uuid::new_v4(),
161 value_num: 200,
162 ..split
163 };
164
165 let mut conn = pool.acquire().await?;
166 split2.commit(&mut *conn).await?;
167
168 let result = sqlx::query!("SELECT id FROM splits WHERE value_num = 200")
169 .fetch_one(&mut *conn)
170 .await?;
171
172 assert_eq!(split2.id, result.id);
173
174 Ok(())
175 }
176
177 #[sqlx::test(migrations = "../migrations")]
178 async fn test_split_builder(pool: PgPool) -> anyhow::Result<()> {
179 setup(&pool).await;
180
181 let split = Split::builder()
182 .id(Uuid::new_v4())
183 .tx_id(TRANSACTION.get().unwrap().id)
184 .account_id(ACCOUNT.get().unwrap().id)
185 .commodity_id(COMMODITY.get().unwrap().id)
186 .value_num(100)
187 .value_denom(1)
188 .build()?;
189
190 assert_eq!(split.value_num, 100);
191 Ok(())
192 }
193}