1
pub 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
1
        pub async fn add_commodity(&self, script: &[u8]) -> Result<Commodity, ServerError> {
14
1
            let c = CommodityBuilder::new().id(Uuid::new_v4()).build()?;
15

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

            
28
1
            let commodity = if script.is_empty() {
29
                c
30
            } else {
31
1
                apply_commodity_hook(c, tagdb.clone(), script).await?
32
            };
33

            
34
            // Update the tags with the new values from `tagdb`
35
            {
36
1
                let tagdb_lock = tagdb.lock().map_err(|err| {
37
                    log::error!("{}", t!("Mutex error: %{err}", err = err : {:?}));
38
                    ServerError::Lock
39
                })?;
40
1
                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
1
            self.update_commodity_tags(&commodity, &tags).await?;
49

            
50
1
            Ok(commodity)
51
1
        }
52

            
53
2328
        pub async fn create_commodity(
54
2328
            &self,
55
2328
            symbol: String,
56
2328
            name: String,
57
2328
        ) -> Result<Commodity, ServerError> {
58
1377
            let c = Commodity { id: Uuid::new_v4() };
59

            
60
1377
            let mut conn = self.get_connection().await.map_err(|err| {
61
75
                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
62
75
                ServerError::DB(err)
63
75
            })?;
64

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

            
82
1302
            Ok(c)
83
1377
        }
84

            
85
2240
        pub async fn update_commodity_tags(
86
2240
            &self,
87
2240
            c: &Commodity,
88
2240
            tags: &[Tag],
89
2240
        ) -> Result<(), ServerError> {
90
2607
            for tag in tags {
91
2607
                self.set_commodity_tag(c, tag).await?;
92
            }
93
1304
            Ok(())
94
1304
        }
95

            
96
4484
        pub async fn set_commodity_tag(&self, c: &Commodity, t: &Tag) -> Result<(), ServerError> {
97
2612
            if t.tag_name.trim().is_empty() || t.tag_value.trim().is_empty() {
98
                return Err(ServerError::Creation);
99
2612
            }
100
2612
            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
2612
            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
2612
            .execute(&mut *conn)
113
2612
            .await
114
2612
            .map_err(|err| {
115
                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
116
                ServerError::DB(DBError::Sqlx(err))
117
            })?;
118

            
119
2612
            Ok(())
120
2612
        }
121

            
122
6
        pub async fn get_commodity_tags(&self, c: &Commodity) -> Result<Vec<Tag>, ServerError> {
123
6
            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
6
            let tags = sqlx::query_file_as!(Tag, "sql/select/commodities/tags.sql", &c.id)
129
6
                .fetch_all(&mut *conn)
130
6
                .await
131
6
                .map_err(|err| {
132
                    log::error!("Database error: {err:?}");
133
                    ServerError::DB(DBError::Sqlx(err))
134
                })?;
135

            
136
6
            Ok(tags)
137
6
        }
138

            
139
2
        pub async fn get_commodity_tag(
140
2
            &self,
141
2
            c: &Commodity,
142
2
            tag: &String,
143
2
        ) -> Result<Tag, ServerError> {
144
2
            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
2
            let tag = sqlx::query_file_as!(Tag, "sql/select/commodities/tag.sql", &c.id, tag)
150
2
                .fetch_one(&mut *conn)
151
2
                .await
152
2
                .map_err(|err| {
153
                    log::error!("Database error: {err:?}");
154
                    ServerError::DB(DBError::Sqlx(err))
155
                })?;
156

            
157
2
            Ok(tag)
158
2
        }
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
4
        async fn setup() {
181
4
            CONTEXT
182
4
                .get_or_init(|| async {
183
                    #[cfg(feature = "testlog")]
184
1
                    let _ = env_logger::builder()
185
1
                        .is_test(true)
186
1
                        .filter_level(log::LevelFilter::Trace)
187
1
                        .try_init();
188
2
                })
189
4
                .await;
190

            
191
4
            USER.get_or_init(|| async { User { id: Uuid::new_v4() } })
192
4
                .await;
193
4
        }
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
1
            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
}