Skip to main content

server/
commodity.rs

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            // Update the tags with the new values from `tagdb`
35            {
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            // Call `update_commodity_tags` to apply the updates to the database
48            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        /// Context for keeping environment intact
177        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            // assert_eq!(
345            //     comm.fullname,
346            //     Some("the new full name of the Yen".to_string())
347            // );
348
349            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}