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
78
        pub async fn create_commodity(
59
78
            &self,
60
78
            symbol: String,
61
78
            name: String,
62
78
        ) -> Result<Commodity, ServerError> {
63
63
            let c = Commodity { id: Uuid::new_v4() };
64

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

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

            
87
60
            Ok(c)
88
63
        }
89

            
90
62
        pub async fn update_commodity_tags(
91
62
            &self,
92
62
            c: &Commodity,
93
62
            tags: &[Tag],
94
62
        ) -> Result<(), ServerError> {
95
62
            let tag_ids: Vec<Uuid> = tags.iter().map(|t| t.id).collect();
96
123
            let names: Vec<String> = tags.iter().map(|t| t.tag_name.clone()).collect();
97
123
            let values: Vec<String> = tags.iter().map(|t| t.tag_value.clone()).collect();
98

            
99
62
            let mut conn = self.get_connection().await.map_err(|err| {
100
                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
101
                ServerError::DB(err)
102
            })?;
103

            
104
62
            let _ = sqlx::query_file!(
105
62
                "sql/update/commodities/tags.sql",
106
                &c.id,
107
                &tag_ids,
108
                &names,
109
                &values,
110
            )
111
62
            .fetch_one(&mut *conn)
112
62
            .await
113
62
            .map_err(|err| {
114
                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
115
                ServerError::DB(DBError::Sqlx(err))
116
            })?;
117

            
118
62
            Ok(())
119
62
        }
120

            
121
5
        pub async fn set_commodity_tag(&self, c: &Commodity, t: &Tag) -> Result<(), ServerError> {
122
5
            let mut conn = self.get_connection().await.map_err(|err| {
123
                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
124
                ServerError::DB(err)
125
            })?;
126

            
127
5
            let _ = sqlx::query_file!(
128
5
                "sql/set/commodities/tag.sql",
129
                &c.id,
130
                &t.id,
131
                &t.tag_name,
132
                &t.tag_value,
133
                t.description
134
            )
135
5
            .fetch_one(&mut *conn)
136
5
            .await
137
5
            .map_err(|err| {
138
                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
139
                ServerError::DB(DBError::Sqlx(err))
140
            })?;
141

            
142
5
            Ok(())
143
5
        }
144

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

            
151
6
            let tags = sqlx::query_file_as!(Tag, "sql/select/commodities/tags.sql", &c.id)
152
6
                .fetch_all(&mut *conn)
153
6
                .await
154
6
                .map_err(|err| {
155
                    log::error!("Database error: {err:?}");
156
                    ServerError::DB(DBError::Sqlx(err))
157
                })?;
158

            
159
6
            Ok(tags)
160
6
        }
161

            
162
2
        pub async fn get_commodity_tag(
163
2
            &self,
164
2
            c: &Commodity,
165
2
            tag: &String,
166
2
        ) -> Result<Tag, ServerError> {
167
2
            let mut conn = self.get_connection().await.map_err(|err| {
168
                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
169
                ServerError::DB(err)
170
            })?;
171

            
172
2
            let tag = sqlx::query_file_as!(Tag, "sql/select/commodities/tag.sql", &c.id, tag)
173
2
                .fetch_one(&mut *conn)
174
2
                .await
175
2
                .map_err(|err| {
176
                    log::error!("Database error: {err:?}");
177
                    ServerError::DB(DBError::Sqlx(err))
178
                })?;
179

            
180
2
            Ok(tag)
181
2
        }
182
    }
183

            
184
    #[cfg(test)]
185
    mod commodity_tests {
186
        use super::*;
187
        use crate::db::DB_POOL;
188
        #[cfg(feature = "testlog")]
189
        use env_logger;
190
        #[cfg(feature = "testlog")]
191
        use log;
192
        use sqlx::PgPool;
193
        #[cfg(feature = "scripting")]
194
        use std::collections::HashMap;
195
        #[cfg(feature = "scripting")]
196
        use std::fs;
197
        #[cfg(feature = "scripting")]
198
        use std::sync::{Arc, Mutex};
199
        use supp_macro::local_db_sqlx_test;
200
        use tokio::sync::OnceCell;
201

            
202
        /// Context for keeping environment intact
203
        static CONTEXT: OnceCell<()> = OnceCell::const_new();
204
        static USER: OnceCell<User> = OnceCell::const_new();
205

            
206
4
        async fn setup() {
207
4
            CONTEXT
208
4
                .get_or_init(|| async {
209
                    #[cfg(feature = "testlog")]
210
1
                    let _ = env_logger::builder()
211
1
                        .is_test(true)
212
1
                        .filter_level(log::LevelFilter::Trace)
213
1
                        .try_init();
214
2
                })
215
4
                .await;
216

            
217
4
            USER.get_or_init(|| async { User { id: Uuid::new_v4() } })
218
4
                .await;
219
4
        }
220

            
221
        #[cfg(feature = "scripting")]
222
        #[local_db_sqlx_test]
223
        async fn test_commodity_creation(pool: PgPool) -> Result<(), anyhow::Error> {
224
            USER.get()
225
                .unwrap()
226
                .commit()
227
                .await
228
                .expect("Failed to commit user to database");
229

            
230
            let mut conn = pool.acquire().await.unwrap();
231

            
232
            let commodity = Commodity { id: Uuid::new_v4() };
233
            let user = USER.get().unwrap();
234

            
235
            sqlx::query!("INSERT INTO commodities (id) VALUES ($1)", &commodity.id,)
236
                .execute(&mut *conn)
237
                .await
238
                .unwrap();
239

            
240
            let script = fs::read("../target/commodity.wasm")?;
241
            let comm = user.add_commodity(&script).await?;
242
            assert_eq!(comm.id, comm.id);
243
        }
244

            
245
        #[local_db_sqlx_test]
246
        async fn test_create_commodity(pool: PgPool) -> Result<(), anyhow::Error> {
247
            let user = USER.get().unwrap();
248
            user.commit()
249
                .await
250
                .expect("Failed to commit user to database");
251

            
252
            let c = user
253
                .create_commodity("JPY".to_string(), "Japanese Yen".to_string())
254
                .await?;
255
            let mut conn = pool.acquire().await.unwrap();
256
            let res = sqlx::query!("SELECT id FROM commodities WHERE id = $1", c.id)
257
                .fetch_one(&mut *conn)
258
                .await?;
259

            
260
            assert_eq!(res.id, c.id);
261

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

            
264
            assert_eq!(tag.tag_value, "JPY");
265

            
266
            let tag = user.get_commodity_tag(&c, &"name".to_string()).await?;
267
            assert_eq!(tag.tag_value, "Japanese Yen");
268
        }
269

            
270
        #[local_db_sqlx_test]
271
        async fn test_commodity_tag(pool: PgPool) {
272
            let user = USER.get().unwrap();
273
            user.commit()
274
                .await
275
                .expect("Failed to commit user to database");
276

            
277
            let mut conn = user.get_connection().await?;
278
            let commodity = Commodity { id: Uuid::new_v4() };
279
            user.set_commodity_tag(
280
                &commodity,
281
                &Tag::builder()
282
                    .id(Uuid::new_v4())
283
                    .tag_name("test")
284
                    .tag_value("testval")
285
                    .build()?,
286
            )
287
            .await?;
288

            
289
            let res = sqlx::query_file!("testdata/query_tag.sql", &commodity.id)
290
                .fetch_one(&mut *conn)
291
                .await?;
292
            assert_eq!(res.tag_name_result, "test".to_string());
293
            assert_eq!(res.tag_value_result, "testval".to_string());
294
            user.set_commodity_tag(
295
                &commodity,
296
                &Tag::builder()
297
                    .id(Uuid::new_v4())
298
                    .tag_name("test")
299
                    .tag_value("testval2")
300
                    .build()?,
301
            )
302
            .await?;
303
            let res = sqlx::query_file!("testdata/query_tag.sql", &commodity.id)
304
                .fetch_one(&mut *conn)
305
                .await?;
306
            assert_eq!(res.tag_name_result, "test".to_string());
307
            assert_eq!(res.tag_value_result, "testval2".to_string());
308
        }
309

            
310
        #[cfg(feature = "scripting")]
311
        #[local_db_sqlx_test]
312
        async fn test_get_commodity_tags(pool: PgPool) {
313
            let user = USER.get().unwrap();
314
            user.commit()
315
                .await
316
                .expect("Failed to commit user to database");
317

            
318
            let commodity = Commodity { id: Uuid::new_v4() };
319
            user.set_commodity_tag(
320
                &commodity,
321
                &Tag::builder()
322
                    .id(Uuid::new_v4())
323
                    .tag_name("test")
324
                    .tag_value("testval")
325
                    .build()?,
326
            )
327
            .await?;
328
            let tags = user.get_commodity_tags(&commodity).await?;
329
            assert_eq!(tags.len(), 1);
330
            assert_eq!(tags.first().unwrap().tag_name, "test".to_string());
331
            user.set_commodity_tag(
332
                &commodity,
333
                &Tag::builder()
334
                    .id(Uuid::new_v4())
335
                    .tag_name("test2")
336
                    .tag_value("testval2")
337
                    .build()?,
338
            )
339
            .await?;
340

            
341
            let tags = user.get_commodity_tags(&commodity).await?;
342
            assert_eq!(tags.len(), 2);
343
            assert_eq!(tags.last().unwrap().tag_name, "test2".to_string());
344

            
345
            user.set_commodity_tag(
346
                &commodity,
347
                &Tag::builder()
348
                    .id(Uuid::new_v4())
349
                    .tag_name("newname")
350
                    .tag_value("the new full name of the Yen")
351
                    .build()?,
352
            )
353
            .await?;
354

            
355
            let tags = user.get_commodity_tags(&commodity).await?;
356
            let script = fs::read("../target/commodity.wasm")?;
357
            let tagdb: Arc<Mutex<HashMap<String, String>>> = Arc::new(Mutex::new(HashMap::new()));
358
            {
359
                let mut tagdb_lock = tagdb
360
                    .lock()
361
                    .map_err(|e| anyhow::anyhow!("Mutex is poisoned: {e}"))?;
362
                for t in &tags {
363
                    tagdb_lock.insert(t.tag_name.clone(), t.tag_value.clone());
364
                }
365
            }
366
            let comm = apply_commodity_hook(commodity, tagdb, &script).await?;
367
            // assert_eq!(
368
            //     comm.fullname,
369
            //     Some("the new full name of the Yen".to_string())
370
            // );
371

            
372
            let mut newtags = user.get_commodity_tags(&comm).await?;
373
            let name = newtags[0].tag_name.clone();
374
            newtags[0].tag_value = "thenewval".to_string();
375
            user.update_commodity_tags(&comm, &newtags).await?;
376
            let updated_tags = user.get_commodity_tags(&comm).await?;
377

            
378
1
            if let Some(tag) = updated_tags.iter().find(|t| t.tag_name == *name) {
379
                assert_eq!(
380
                    tag.tag_value, "thenewval",
381
                    "The tag value was not updated correctly"
382
                );
383
            } else {
384
                panic!("Tag with name '{name}' not found in updated tags");
385
            }
386
        }
387
    }
388
}