1
pub mod user {
2
    use crate::db::DBError;
3
    use crate::error::ServerError;
4
    use crate::user::User;
5
    #[cfg(feature = "scripting")]
6
    use finance::commodity::CommodityBuilder;
7
    use finance::{commodity::Commodity, tag::Tag};
8
    #[cfg(feature = "scripting")]
9
    use scripting::commodity::apply_commodity_hook;
10
    use sqlx::types::Uuid;
11
    #[cfg(feature = "scripting")]
12
    use std::collections::HashMap;
13
    #[cfg(feature = "scripting")]
14
    use std::sync::{Arc, Mutex};
15

            
16
    impl User {
17
        #[cfg(feature = "scripting")]
18
1
        pub async fn add_commodity(&self, script: &[u8]) -> Result<Commodity, ServerError> {
19
1
            let c = CommodityBuilder::new().id(Uuid::new_v4()).build()?;
20

            
21
1
            let mut tags = self.get_commodity_tags(&c).await?;
22
1
            let tagdb: Arc<Mutex<HashMap<String, String>>> = Arc::new(Mutex::new(HashMap::new()));
23
            {
24
1
                let mut tagdb_lock = tagdb.lock().map_err(|err| {
25
                    log::error!("{}", t!("Mutex error: %{err}", err = err : {:?}));
26
                    ServerError::Lock
27
                })?;
28
1
                for t in &tags {
29
                    tagdb_lock.insert(t.tag_name.clone(), t.tag_value.clone());
30
                }
31
            }
32

            
33
1
            let commodity = if script.is_empty() {
34
                c
35
            } else {
36
1
                apply_commodity_hook(c, tagdb.clone(), script).await?
37
            };
38

            
39
            // Update the tags with the new values from `tagdb`
40
            {
41
1
                let tagdb_lock = tagdb.lock().map_err(|err| {
42
                    log::error!("{}", t!("Mutex error: %{err}", err = err : {:?}));
43
                    ServerError::Lock
44
                })?;
45
1
                for t in &mut tags {
46
                    if let Some(value) = tagdb_lock.get(&t.tag_name) {
47
                        t.tag_value = value.to_owned();
48
                    }
49
                }
50
            }
51

            
52
            // Call `update_commodity_tags` to apply the updates to the database
53
1
            self.update_commodity_tags(&commodity, &tags).await?;
54

            
55
1
            Ok(commodity)
56
1
        }
57

            
58
119
        pub async fn create_commodity(
59
119
            &self,
60
119
            symbol: String,
61
119
            name: String,
62
119
        ) -> Result<Commodity, ServerError> {
63
107
            let c = Commodity { id: Uuid::new_v4() };
64

            
65
107
            let mut conn = self.get_connection().await.map_err(|err| {
66
30
                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
67
30
                ServerError::DB(err)
68
30
            })?;
69

            
70
77
            c.commit(&mut *conn).await?;
71
77
            let tags: Vec<Tag> = vec![
72
77
                Tag {
73
77
                    id: Uuid::new_v4(),
74
77
                    tag_name: "symbol".to_string(),
75
77
                    tag_value: symbol,
76
77
                    description: None,
77
77
                },
78
77
                Tag {
79
77
                    id: Uuid::new_v4(),
80
77
                    tag_name: "name".to_string(),
81
77
                    tag_value: name,
82
77
                    description: None,
83
77
                },
84
            ];
85
77
            self.update_commodity_tags(&c, &tags).await?;
86

            
87
77
            Ok(c)
88
107
        }
89

            
90
79
        pub async fn update_commodity_tags(
91
79
            &self,
92
79
            c: &Commodity,
93
79
            tags: &[Tag],
94
79
        ) -> Result<(), ServerError> {
95
157
            for tag in tags {
96
157
                self.set_commodity_tag(c, tag).await?;
97
            }
98
79
            Ok(())
99
79
        }
100

            
101
162
        pub async fn set_commodity_tag(&self, c: &Commodity, t: &Tag) -> Result<(), ServerError> {
102
162
            if t.tag_name.trim().is_empty() || t.tag_value.trim().is_empty() {
103
                return Err(ServerError::Creation);
104
162
            }
105
162
            let mut conn = self.get_connection().await.map_err(|err| {
106
                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
107
                ServerError::DB(err)
108
            })?;
109

            
110
162
            sqlx::query_file!(
111
                "sql/set/commodities/tag.sql",
112
                &c.id,
113
                &t.tag_name,
114
                &t.tag_value,
115
                t.description
116
            )
117
162
            .execute(&mut *conn)
118
162
            .await
119
162
            .map_err(|err| {
120
                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
121
                ServerError::DB(DBError::Sqlx(err))
122
            })?;
123

            
124
162
            Ok(())
125
162
        }
126

            
127
6
        pub async fn get_commodity_tags(&self, c: &Commodity) -> Result<Vec<Tag>, ServerError> {
128
6
            let mut conn = self.get_connection().await.map_err(|err| {
129
                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
130
                ServerError::DB(err)
131
            })?;
132

            
133
6
            let tags = sqlx::query_file_as!(Tag, "sql/select/commodities/tags.sql", &c.id)
134
6
                .fetch_all(&mut *conn)
135
6
                .await
136
6
                .map_err(|err| {
137
                    log::error!("Database error: {err:?}");
138
                    ServerError::DB(DBError::Sqlx(err))
139
                })?;
140

            
141
6
            Ok(tags)
142
6
        }
143

            
144
2
        pub async fn get_commodity_tag(
145
2
            &self,
146
2
            c: &Commodity,
147
2
            tag: &String,
148
2
        ) -> Result<Tag, ServerError> {
149
2
            let mut conn = self.get_connection().await.map_err(|err| {
150
                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
151
                ServerError::DB(err)
152
            })?;
153

            
154
2
            let tag = sqlx::query_file_as!(Tag, "sql/select/commodities/tag.sql", &c.id, tag)
155
2
                .fetch_one(&mut *conn)
156
2
                .await
157
2
                .map_err(|err| {
158
                    log::error!("Database error: {err:?}");
159
                    ServerError::DB(DBError::Sqlx(err))
160
                })?;
161

            
162
2
            Ok(tag)
163
2
        }
164
    }
165

            
166
    #[cfg(test)]
167
    mod commodity_tests {
168
        use super::*;
169
        use crate::db::DB_POOL;
170
        #[cfg(feature = "testlog")]
171
        use env_logger;
172
        #[cfg(feature = "testlog")]
173
        use log;
174
        use sqlx::PgPool;
175
        #[cfg(feature = "scripting")]
176
        use std::collections::HashMap;
177
        #[cfg(feature = "scripting")]
178
        use std::fs;
179
        #[cfg(feature = "scripting")]
180
        use std::sync::{Arc, Mutex};
181
        use supp_macro::local_db_sqlx_test;
182
        use tokio::sync::OnceCell;
183

            
184
        /// Context for keeping environment intact
185
        static CONTEXT: OnceCell<()> = OnceCell::const_new();
186
        static USER: OnceCell<User> = OnceCell::const_new();
187

            
188
4
        async fn setup() {
189
4
            CONTEXT
190
4
                .get_or_init(|| async {
191
                    #[cfg(feature = "testlog")]
192
1
                    let _ = env_logger::builder()
193
1
                        .is_test(true)
194
1
                        .filter_level(log::LevelFilter::Trace)
195
1
                        .try_init();
196
2
                })
197
4
                .await;
198

            
199
4
            USER.get_or_init(|| async { User { id: Uuid::new_v4() } })
200
4
                .await;
201
4
        }
202

            
203
        #[cfg(feature = "scripting")]
204
        #[local_db_sqlx_test]
205
        async fn test_commodity_creation(pool: PgPool) -> Result<(), anyhow::Error> {
206
            USER.get()
207
                .unwrap()
208
                .commit()
209
                .await
210
                .expect("Failed to commit user to database");
211

            
212
            let mut conn = pool.acquire().await.unwrap();
213

            
214
            let commodity = Commodity { id: Uuid::new_v4() };
215
            let user = USER.get().unwrap();
216

            
217
            sqlx::query!("INSERT INTO commodities (id) VALUES ($1)", &commodity.id,)
218
                .execute(&mut *conn)
219
                .await
220
                .unwrap();
221

            
222
            let script = fs::read("../target/commodity.wasm")?;
223
            let comm = user.add_commodity(&script).await?;
224
            assert_eq!(comm.id, comm.id);
225
        }
226

            
227
        #[local_db_sqlx_test]
228
        async fn test_create_commodity(pool: PgPool) -> Result<(), anyhow::Error> {
229
            let user = USER.get().unwrap();
230
            user.commit()
231
                .await
232
                .expect("Failed to commit user to database");
233

            
234
            let c = user
235
                .create_commodity("JPY".to_string(), "Japanese Yen".to_string())
236
                .await?;
237
            let mut conn = pool.acquire().await.unwrap();
238
            let res = sqlx::query!("SELECT id FROM commodities WHERE id = $1", c.id)
239
                .fetch_one(&mut *conn)
240
                .await?;
241

            
242
            assert_eq!(res.id, c.id);
243

            
244
            let tag = user.get_commodity_tag(&c, &"symbol".to_string()).await?;
245

            
246
            assert_eq!(tag.tag_value, "JPY");
247

            
248
            let tag = user.get_commodity_tag(&c, &"name".to_string()).await?;
249
            assert_eq!(tag.tag_value, "Japanese Yen");
250
        }
251

            
252
        #[local_db_sqlx_test]
253
        async fn test_commodity_tag(pool: PgPool) {
254
            let user = USER.get().unwrap();
255
            user.commit()
256
                .await
257
                .expect("Failed to commit user to database");
258

            
259
            let mut conn = user.get_connection().await?;
260
            let commodity = Commodity { id: Uuid::new_v4() };
261
            commodity.commit(&mut *conn).await?;
262
            user.set_commodity_tag(
263
                &commodity,
264
                &Tag::builder()
265
                    .id(Uuid::new_v4())
266
                    .tag_name("test")
267
                    .tag_value("testval")
268
                    .build()?,
269
            )
270
            .await?;
271

            
272
            let res = sqlx::query_file!("testdata/query_tag.sql", &commodity.id)
273
                .fetch_one(&mut *conn)
274
                .await?;
275
            assert_eq!(res.tag_name_result, "test".to_string());
276
            assert_eq!(res.tag_value_result, "testval".to_string());
277
            user.set_commodity_tag(
278
                &commodity,
279
                &Tag::builder()
280
                    .id(Uuid::new_v4())
281
                    .tag_name("test")
282
                    .tag_value("testval2")
283
                    .build()?,
284
            )
285
            .await?;
286
            let res = sqlx::query_file!("testdata/query_tag.sql", &commodity.id)
287
                .fetch_one(&mut *conn)
288
                .await?;
289
            assert_eq!(res.tag_name_result, "test".to_string());
290
            assert_eq!(res.tag_value_result, "testval2".to_string());
291
        }
292

            
293
        #[cfg(feature = "scripting")]
294
        #[local_db_sqlx_test]
295
        async fn test_get_commodity_tags(pool: PgPool) {
296
            let user = USER.get().unwrap();
297
            user.commit()
298
                .await
299
                .expect("Failed to commit user to database");
300

            
301
            let commodity = Commodity { id: Uuid::new_v4() };
302
            {
303
                let mut conn = user.get_connection().await?;
304
                commodity.commit(&mut *conn).await?;
305
            }
306
            user.set_commodity_tag(
307
                &commodity,
308
                &Tag::builder()
309
                    .id(Uuid::new_v4())
310
                    .tag_name("test")
311
                    .tag_value("testval")
312
                    .build()?,
313
            )
314
            .await?;
315
            let tags = user.get_commodity_tags(&commodity).await?;
316
            assert_eq!(tags.len(), 1);
317
            assert_eq!(tags.first().unwrap().tag_name, "test".to_string());
318
            user.set_commodity_tag(
319
                &commodity,
320
                &Tag::builder()
321
                    .id(Uuid::new_v4())
322
                    .tag_name("test2")
323
                    .tag_value("testval2")
324
                    .build()?,
325
            )
326
            .await?;
327

            
328
            let tags = user.get_commodity_tags(&commodity).await?;
329
            assert_eq!(tags.len(), 2);
330
            assert_eq!(tags.last().unwrap().tag_name, "test2".to_string());
331

            
332
            user.set_commodity_tag(
333
                &commodity,
334
                &Tag::builder()
335
                    .id(Uuid::new_v4())
336
                    .tag_name("newname")
337
                    .tag_value("the new full name of the Yen")
338
                    .build()?,
339
            )
340
            .await?;
341

            
342
            let tags = user.get_commodity_tags(&commodity).await?;
343
            let script = fs::read("../target/commodity.wasm")?;
344
            let tagdb: Arc<Mutex<HashMap<String, String>>> = Arc::new(Mutex::new(HashMap::new()));
345
            {
346
                let mut tagdb_lock = tagdb
347
                    .lock()
348
                    .map_err(|e| anyhow::anyhow!("Mutex is poisoned: {e}"))?;
349
                for t in &tags {
350
                    tagdb_lock.insert(t.tag_name.clone(), t.tag_value.clone());
351
                }
352
            }
353
            let comm = apply_commodity_hook(commodity, tagdb, &script).await?;
354
            // assert_eq!(
355
            //     comm.fullname,
356
            //     Some("the new full name of the Yen".to_string())
357
            // );
358

            
359
            let mut newtags = user.get_commodity_tags(&comm).await?;
360
            let name = newtags[0].tag_name.clone();
361
            newtags[0].tag_value = "thenewval".to_string();
362
            user.update_commodity_tags(&comm, &newtags).await?;
363
            let updated_tags = user.get_commodity_tags(&comm).await?;
364

            
365
1
            if let Some(tag) = updated_tags.iter().find(|t| t.tag_name == *name) {
366
                assert_eq!(
367
                    tag.tag_value, "thenewval",
368
                    "The tag value was not updated correctly"
369
                );
370
            } else {
371
                panic!("Tag with name '{name}' not found in updated tags");
372
            }
373
        }
374
    }
375
}