1pub mod user {
2 use crate::db::DBError;
3 use crate::error::ServerError;
4 use crate::user::User;
5 use finance::commodity::CommodityBuilder;
6 use finance::{commodity::Commodity, tag::Tag};
7 use scripting::commodity::apply_commodity_hook;
8 use sqlx::types::Uuid;
9 use std::collections::HashMap;
10 use std::sync::{Arc, Mutex};
11
12 impl User {
13 pub async fn add_commodity(&self, script: &[u8]) -> Result<Commodity, ServerError> {
14 let c = CommodityBuilder::new().id(Uuid::new_v4()).build()?;
15
16 let mut tags = self.get_commodity_tags(&c).await?;
17 let tagdb: Arc<Mutex<HashMap<String, String>>> = Arc::new(Mutex::new(HashMap::new()));
18 {
19 let mut tagdb_lock = tagdb.lock().map_err(|err| {
20 log::error!("{}", t!("Mutex error: %{err}", err = err : {:?}));
21 ServerError::Lock
22 })?;
23 for t in &tags {
24 tagdb_lock.insert(t.tag_name.clone(), t.tag_value.clone());
25 }
26 }
27
28 let commodity = if script.is_empty() {
29 c
30 } else {
31 apply_commodity_hook(c, tagdb.clone(), script).await?
32 };
33
34 {
36 let tagdb_lock = tagdb.lock().map_err(|err| {
37 log::error!("{}", t!("Mutex error: %{err}", err = err : {:?}));
38 ServerError::Lock
39 })?;
40 for t in &mut tags {
41 if let Some(value) = tagdb_lock.get(&t.tag_name) {
42 t.tag_value = value.to_owned();
43 }
44 }
45 }
46
47 self.update_commodity_tags(&commodity, &tags).await?;
49
50 Ok(commodity)
51 }
52
53 pub async fn create_commodity(
54 &self,
55 symbol: String,
56 name: String,
57 ) -> Result<Commodity, ServerError> {
58 let c = Commodity { id: Uuid::new_v4() };
59
60 let mut conn = self.get_connection().await.map_err(|err| {
61 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
62 ServerError::DB(err)
63 })?;
64
65 c.commit(&mut *conn).await?;
66 let tags: Vec<Tag> = vec![
67 Tag {
68 id: Uuid::new_v4(),
69 tag_name: "symbol".to_string(),
70 tag_value: symbol,
71 description: None,
72 },
73 Tag {
74 id: Uuid::new_v4(),
75 tag_name: "name".to_string(),
76 tag_value: name,
77 description: None,
78 },
79 ];
80 self.update_commodity_tags(&c, &tags).await?;
81
82 Ok(c)
83 }
84
85 pub async fn update_commodity_tags(
86 &self,
87 c: &Commodity,
88 tags: &[Tag],
89 ) -> Result<(), ServerError> {
90 for tag in tags {
91 self.set_commodity_tag(c, tag).await?;
92 }
93 Ok(())
94 }
95
96 pub async fn set_commodity_tag(&self, c: &Commodity, t: &Tag) -> Result<(), ServerError> {
97 if t.tag_name.trim().is_empty() || t.tag_value.trim().is_empty() {
98 return Err(ServerError::Creation);
99 }
100 let mut conn = self.get_connection().await.map_err(|err| {
101 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
102 ServerError::DB(err)
103 })?;
104
105 sqlx::query_file!(
106 "sql/set/commodities/tag.sql",
107 &c.id,
108 &t.tag_name,
109 &t.tag_value,
110 t.description
111 )
112 .execute(&mut *conn)
113 .await
114 .map_err(|err| {
115 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
116 ServerError::DB(DBError::Sqlx(err))
117 })?;
118
119 Ok(())
120 }
121
122 pub async fn get_commodity_tags(&self, c: &Commodity) -> Result<Vec<Tag>, ServerError> {
123 let mut conn = self.get_connection().await.map_err(|err| {
124 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
125 ServerError::DB(err)
126 })?;
127
128 let tags = sqlx::query_file_as!(Tag, "sql/select/commodities/tags.sql", &c.id)
129 .fetch_all(&mut *conn)
130 .await
131 .map_err(|err| {
132 log::error!("Database error: {err:?}");
133 ServerError::DB(DBError::Sqlx(err))
134 })?;
135
136 Ok(tags)
137 }
138
139 pub async fn get_commodity_tag(
140 &self,
141 c: &Commodity,
142 tag: &String,
143 ) -> Result<Tag, ServerError> {
144 let mut conn = self.get_connection().await.map_err(|err| {
145 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
146 ServerError::DB(err)
147 })?;
148
149 let tag = sqlx::query_file_as!(Tag, "sql/select/commodities/tag.sql", &c.id, tag)
150 .fetch_one(&mut *conn)
151 .await
152 .map_err(|err| {
153 log::error!("Database error: {err:?}");
154 ServerError::DB(DBError::Sqlx(err))
155 })?;
156
157 Ok(tag)
158 }
159 }
160
161 #[cfg(test)]
162 mod commodity_tests {
163 use super::*;
164 use crate::db::DB_POOL;
165 #[cfg(feature = "testlog")]
166 use env_logger;
167 #[cfg(feature = "testlog")]
168 use log;
169 use sqlx::PgPool;
170 use std::collections::HashMap;
171 use std::fs;
172 use std::sync::{Arc, Mutex};
173 use supp_macro::local_db_sqlx_test;
174 use tokio::sync::OnceCell;
175
176 static CONTEXT: OnceCell<()> = OnceCell::const_new();
178 static USER: OnceCell<User> = OnceCell::const_new();
179
180 async fn setup() {
181 CONTEXT
182 .get_or_init(|| async {
183 #[cfg(feature = "testlog")]
184 let _ = env_logger::builder()
185 .is_test(true)
186 .filter_level(log::LevelFilter::Trace)
187 .try_init();
188 })
189 .await;
190
191 USER.get_or_init(|| async { User { id: Uuid::new_v4() } })
192 .await;
193 }
194
195 #[local_db_sqlx_test]
196 async fn test_commodity_creation(pool: PgPool) -> Result<(), anyhow::Error> {
197 USER.get()
198 .unwrap()
199 .commit()
200 .await
201 .expect("Failed to commit user to database");
202
203 let mut conn = pool.acquire().await.unwrap();
204
205 let commodity = Commodity { id: Uuid::new_v4() };
206 let user = USER.get().unwrap();
207
208 sqlx::query!("INSERT INTO commodities (id) VALUES ($1)", &commodity.id,)
209 .execute(&mut *conn)
210 .await
211 .unwrap();
212
213 let script = fs::read(env!("WASM_COMMODITY"))?;
214 let comm = user.add_commodity(&script).await?;
215 assert_eq!(comm.id, comm.id);
216 }
217
218 #[local_db_sqlx_test]
219 async fn test_create_commodity(pool: PgPool) -> Result<(), anyhow::Error> {
220 let user = USER.get().unwrap();
221 user.commit()
222 .await
223 .expect("Failed to commit user to database");
224
225 let c = user
226 .create_commodity("JPY".to_string(), "Japanese Yen".to_string())
227 .await?;
228 let mut conn = pool.acquire().await.unwrap();
229 let res = sqlx::query!("SELECT id FROM commodities WHERE id = $1", c.id)
230 .fetch_one(&mut *conn)
231 .await?;
232
233 assert_eq!(res.id, c.id);
234
235 let tag = user.get_commodity_tag(&c, &"symbol".to_string()).await?;
236
237 assert_eq!(tag.tag_value, "JPY");
238
239 let tag = user.get_commodity_tag(&c, &"name".to_string()).await?;
240 assert_eq!(tag.tag_value, "Japanese Yen");
241 }
242
243 #[local_db_sqlx_test]
244 async fn test_commodity_tag(pool: PgPool) {
245 let user = USER.get().unwrap();
246 user.commit()
247 .await
248 .expect("Failed to commit user to database");
249
250 let mut conn = user.get_connection().await?;
251 let commodity = Commodity { id: Uuid::new_v4() };
252 commodity.commit(&mut *conn).await?;
253 user.set_commodity_tag(
254 &commodity,
255 &Tag::builder()
256 .id(Uuid::new_v4())
257 .tag_name("test")
258 .tag_value("testval")
259 .build()?,
260 )
261 .await?;
262
263 let res = sqlx::query_file!("testdata/query_tag.sql", &commodity.id)
264 .fetch_one(&mut *conn)
265 .await?;
266 assert_eq!(res.tag_name_result, "test".to_string());
267 assert_eq!(res.tag_value_result, "testval".to_string());
268 user.set_commodity_tag(
269 &commodity,
270 &Tag::builder()
271 .id(Uuid::new_v4())
272 .tag_name("test")
273 .tag_value("testval2")
274 .build()?,
275 )
276 .await?;
277 let res = sqlx::query_file!("testdata/query_tag.sql", &commodity.id)
278 .fetch_one(&mut *conn)
279 .await?;
280 assert_eq!(res.tag_name_result, "test".to_string());
281 assert_eq!(res.tag_value_result, "testval2".to_string());
282 }
283
284 #[local_db_sqlx_test]
285 async fn test_get_commodity_tags(pool: PgPool) {
286 let user = USER.get().unwrap();
287 user.commit()
288 .await
289 .expect("Failed to commit user to database");
290
291 let commodity = Commodity { id: Uuid::new_v4() };
292 {
293 let mut conn = user.get_connection().await?;
294 commodity.commit(&mut *conn).await?;
295 }
296 user.set_commodity_tag(
297 &commodity,
298 &Tag::builder()
299 .id(Uuid::new_v4())
300 .tag_name("test")
301 .tag_value("testval")
302 .build()?,
303 )
304 .await?;
305 let tags = user.get_commodity_tags(&commodity).await?;
306 assert_eq!(tags.len(), 1);
307 assert_eq!(tags.first().unwrap().tag_name, "test".to_string());
308 user.set_commodity_tag(
309 &commodity,
310 &Tag::builder()
311 .id(Uuid::new_v4())
312 .tag_name("test2")
313 .tag_value("testval2")
314 .build()?,
315 )
316 .await?;
317
318 let tags = user.get_commodity_tags(&commodity).await?;
319 assert_eq!(tags.len(), 2);
320 assert_eq!(tags.last().unwrap().tag_name, "test2".to_string());
321
322 user.set_commodity_tag(
323 &commodity,
324 &Tag::builder()
325 .id(Uuid::new_v4())
326 .tag_name("newname")
327 .tag_value("the new full name of the Yen")
328 .build()?,
329 )
330 .await?;
331
332 let tags = user.get_commodity_tags(&commodity).await?;
333 let script = fs::read(env!("WASM_COMMODITY"))?;
334 let tagdb: Arc<Mutex<HashMap<String, String>>> = Arc::new(Mutex::new(HashMap::new()));
335 {
336 let mut tagdb_lock = tagdb
337 .lock()
338 .map_err(|e| anyhow::anyhow!("Mutex is poisoned: {e}"))?;
339 for t in &tags {
340 tagdb_lock.insert(t.tag_name.clone(), t.tag_value.clone());
341 }
342 }
343 let comm = apply_commodity_hook(commodity, tagdb, &script).await?;
344 let mut newtags = user.get_commodity_tags(&comm).await?;
350 let name = newtags[0].tag_name.clone();
351 newtags[0].tag_value = "thenewval".to_string();
352 user.update_commodity_tags(&comm, &newtags).await?;
353 let updated_tags = user.get_commodity_tags(&comm).await?;
354
355 if let Some(tag) = updated_tags.iter().find(|t| t.tag_name == *name) {
356 assert_eq!(
357 tag.tag_value, "thenewval",
358 "The tag value was not updated correctly"
359 );
360 } else {
361 panic!("Tag with name '{name}' not found in updated tags");
362 }
363 }
364 }
365}