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()
20
1
                .id(Uuid::new_v4())
21
1
                .fraction(1)
22
1
                .build()?;
23

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

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

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

            
55
            // Call `update_commodity_tags` to apply the updates to the database
56
1
            self.update_commodity_tags(&commodity, &tags).await?;
57

            
58
1
            Ok(commodity)
59
1
        }
60

            
61
60
        pub async fn create_commodity(
62
60
            &self,
63
60
            fraction: i64,
64
60
            symbol: String,
65
60
            name: String,
66
60
        ) -> Result<Commodity, ServerError> {
67
45
            let c = Commodity {
68
45
                id: Uuid::new_v4(),
69
45
                fraction,
70
45
            };
71

            
72
45
            let mut conn = self.get_connection().await.map_err(|err| {
73
3
                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
74
3
                ServerError::DB(err)
75
3
            })?;
76

            
77
42
            c.commit(&mut *conn).await?;
78
42
            let tags: Vec<Tag> = vec![
79
42
                Tag {
80
42
                    id: Uuid::new_v4(),
81
42
                    tag_name: "symbol".to_string(),
82
42
                    tag_value: symbol,
83
42
                    description: None,
84
42
                },
85
42
                Tag {
86
42
                    id: Uuid::new_v4(),
87
42
                    tag_name: "name".to_string(),
88
42
                    tag_value: name,
89
42
                    description: None,
90
42
                },
91
            ];
92
42
            self.update_commodity_tags(&c, &tags).await?;
93

            
94
42
            Ok(c)
95
45
        }
96

            
97
44
        pub async fn update_commodity_tags(
98
44
            &self,
99
44
            c: &Commodity,
100
44
            tags: &[Tag],
101
44
        ) -> Result<(), ServerError> {
102
44
            let tag_ids: Vec<Uuid> = tags.iter().map(|t| t.id).collect();
103
87
            let names: Vec<String> = tags.iter().map(|t| t.tag_name.clone()).collect();
104
87
            let values: Vec<String> = tags.iter().map(|t| t.tag_value.clone()).collect();
105

            
106
44
            let mut conn = self.get_connection().await.map_err(|err| {
107
                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
108
                ServerError::DB(err)
109
            })?;
110

            
111
44
            let _ = sqlx::query_file!(
112
44
                "sql/update/commodities/tags.sql",
113
                &c.id,
114
                &tag_ids,
115
                &names,
116
                &values,
117
            )
118
44
            .fetch_one(&mut *conn)
119
44
            .await
120
44
            .map_err(|err| {
121
                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
122
                ServerError::DB(DBError::Sqlx(err))
123
            })?;
124

            
125
44
            Ok(())
126
44
        }
127

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

            
134
5
            let _ = sqlx::query_file!(
135
5
                "sql/set/commodities/tag.sql",
136
                &c.id,
137
                &c.fraction,
138
                &t.id,
139
                &t.tag_name,
140
                &t.tag_value,
141
                t.description
142
            )
143
5
            .fetch_one(&mut *conn)
144
5
            .await
145
5
            .map_err(|err| {
146
                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
147
                ServerError::DB(DBError::Sqlx(err))
148
            })?;
149

            
150
5
            Ok(())
151
5
        }
152

            
153
6
        pub async fn get_commodity_tags(&self, c: &Commodity) -> Result<Vec<Tag>, ServerError> {
154
6
            let mut conn = self.get_connection().await.map_err(|err| {
155
                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
156
                ServerError::DB(err)
157
            })?;
158

            
159
6
            let tags = sqlx::query_file_as!(Tag, "sql/select/commodities/tags.sql", &c.id)
160
6
                .fetch_all(&mut *conn)
161
6
                .await
162
6
                .map_err(|err| {
163
                    log::error!("Database error: {err:?}");
164
                    ServerError::DB(DBError::Sqlx(err))
165
                })?;
166

            
167
6
            Ok(tags)
168
6
        }
169

            
170
2
        pub async fn get_commodity_tag(
171
2
            &self,
172
2
            c: &Commodity,
173
2
            tag: &String,
174
2
        ) -> Result<Tag, ServerError> {
175
2
            let mut conn = self.get_connection().await.map_err(|err| {
176
                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
177
                ServerError::DB(err)
178
            })?;
179

            
180
2
            let tag = sqlx::query_file_as!(Tag, "sql/select/commodities/tag.sql", &c.id, tag)
181
2
                .fetch_one(&mut *conn)
182
2
                .await
183
2
                .map_err(|err| {
184
                    log::error!("Database error: {err:?}");
185
                    ServerError::DB(DBError::Sqlx(err))
186
                })?;
187

            
188
2
            Ok(tag)
189
2
        }
190
    }
191

            
192
    #[cfg(test)]
193
    mod commodity_tests {
194
        use super::*;
195
        use crate::db::DB_POOL;
196
        #[cfg(feature = "testlog")]
197
        use env_logger;
198
        #[cfg(feature = "testlog")]
199
        use log;
200
        use sqlx::PgPool;
201
        #[cfg(feature = "scripting")]
202
        use std::collections::HashMap;
203
        #[cfg(feature = "scripting")]
204
        use std::fs;
205
        #[cfg(feature = "scripting")]
206
        use std::sync::{Arc, Mutex};
207
        use supp_macro::local_db_sqlx_test;
208
        use tokio::sync::OnceCell;
209

            
210
        /// Context for keeping environment intact
211
        static CONTEXT: OnceCell<()> = OnceCell::const_new();
212
        static USER: OnceCell<User> = OnceCell::const_new();
213

            
214
4
        async fn setup() {
215
4
            CONTEXT
216
4
                .get_or_init(|| async {
217
                    #[cfg(feature = "testlog")]
218
1
                    let _ = env_logger::builder()
219
1
                        .is_test(true)
220
1
                        .filter_level(log::LevelFilter::Trace)
221
1
                        .try_init();
222
2
                })
223
4
                .await;
224

            
225
4
            USER.get_or_init(|| async { User { id: Uuid::new_v4() } })
226
4
                .await;
227
4
        }
228

            
229
        #[cfg(feature = "scripting")]
230
        #[local_db_sqlx_test]
231
        async fn test_commodity_creation(pool: PgPool) -> Result<(), anyhow::Error> {
232
            USER.get()
233
                .unwrap()
234
                .commit()
235
                .await
236
                .expect("Failed to commit user to database");
237

            
238
            let mut conn = pool.acquire().await.unwrap();
239

            
240
            let commodity = Commodity {
241
                id: Uuid::new_v4(),
242
                fraction: 100,
243
            };
244
            let user = USER.get().unwrap();
245

            
246
            sqlx::query!(
247
                "INSERT INTO commodities (id, fraction) \
248
		      VALUES ($1, $2)",
249
                &commodity.id,
250
                &commodity.fraction
251
            )
252
            .execute(&mut *conn)
253
            .await
254
            .unwrap();
255

            
256
            let script = fs::read("../target/commodity.wasm")?;
257
            let comm = user.add_commodity(&script).await?;
258
            assert_eq!(comm.fraction, 15);
259
        }
260

            
261
        #[local_db_sqlx_test]
262
        async fn test_create_commodity(pool: PgPool) -> Result<(), anyhow::Error> {
263
            let user = USER.get().unwrap();
264
            user.commit()
265
                .await
266
                .expect("Failed to commit user to database");
267

            
268
            let c = user
269
                .create_commodity(15, "JPY".to_string(), "Japanese Yen".to_string())
270
                .await?;
271
            let mut conn = pool.acquire().await.unwrap();
272
            let res = sqlx::query!("SELECT fraction FROM commodities WHERE id = $1", c.id)
273
                .fetch_one(&mut *conn)
274
                .await?;
275

            
276
            assert_eq!(res.fraction, 15);
277

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

            
280
            assert_eq!(tag.tag_value, "JPY");
281

            
282
            let tag = user.get_commodity_tag(&c, &"name".to_string()).await?;
283
            assert_eq!(tag.tag_value, "Japanese Yen");
284
        }
285

            
286
        #[local_db_sqlx_test]
287
        async fn test_commodity_tag(pool: PgPool) {
288
            let user = USER.get().unwrap();
289
            user.commit()
290
                .await
291
                .expect("Failed to commit user to database");
292

            
293
            let mut conn = user.get_connection().await?;
294
            let commodity = Commodity {
295
                id: Uuid::new_v4(),
296
                fraction: 100,
297
            };
298
            user.set_commodity_tag(
299
                &commodity,
300
                &Tag::builder()
301
                    .id(Uuid::new_v4())
302
                    .tag_name("test")
303
                    .tag_value("testval")
304
                    .build()?,
305
            )
306
            .await?;
307

            
308
            let res = sqlx::query_file!("testdata/query_tag.sql", &commodity.fraction)
309
                .fetch_one(&mut *conn)
310
                .await?;
311
            assert_eq!(res.tag_name_result, "test".to_string());
312
            assert_eq!(res.tag_value_result, "testval".to_string());
313
            user.set_commodity_tag(
314
                &commodity,
315
                &Tag::builder()
316
                    .id(Uuid::new_v4())
317
                    .tag_name("test")
318
                    .tag_value("testval2")
319
                    .build()?,
320
            )
321
            .await?;
322
            let res = sqlx::query_file!("testdata/query_tag.sql", &commodity.fraction)
323
                .fetch_one(&mut *conn)
324
                .await?;
325
            assert_eq!(res.tag_name_result, "test".to_string());
326
            assert_eq!(res.tag_value_result, "testval2".to_string());
327
        }
328

            
329
        #[cfg(feature = "scripting")]
330
        #[local_db_sqlx_test]
331
        async fn test_get_commodity_tags(pool: PgPool) {
332
            let user = USER.get().unwrap();
333
            user.commit()
334
                .await
335
                .expect("Failed to commit user to database");
336

            
337
            let commodity = Commodity {
338
                id: Uuid::new_v4(),
339
                fraction: 100,
340
            };
341
            user.set_commodity_tag(
342
                &commodity,
343
                &Tag::builder()
344
                    .id(Uuid::new_v4())
345
                    .tag_name("test")
346
                    .tag_value("testval")
347
                    .build()?,
348
            )
349
            .await?;
350
            let tags = user.get_commodity_tags(&commodity).await?;
351
            assert_eq!(tags.len(), 1);
352
            assert_eq!(tags.first().unwrap().tag_name, "test".to_string());
353
            user.set_commodity_tag(
354
                &commodity,
355
                &Tag::builder()
356
                    .id(Uuid::new_v4())
357
                    .tag_name("test2")
358
                    .tag_value("testval2")
359
                    .build()?,
360
            )
361
            .await?;
362

            
363
            let tags = user.get_commodity_tags(&commodity).await?;
364
            assert_eq!(tags.len(), 2);
365
            assert_eq!(tags.last().unwrap().tag_name, "test2".to_string());
366

            
367
            user.set_commodity_tag(
368
                &commodity,
369
                &Tag::builder()
370
                    .id(Uuid::new_v4())
371
                    .tag_name("newname")
372
                    .tag_value("the new full name of the Yen")
373
                    .build()?,
374
            )
375
            .await?;
376

            
377
            let tags = user.get_commodity_tags(&commodity).await?;
378
            let script = fs::read("../target/commodity.wasm")?;
379
            let tagdb: Arc<Mutex<HashMap<String, String>>> = Arc::new(Mutex::new(HashMap::new()));
380
            {
381
                let mut tagdb_lock = tagdb
382
                    .lock()
383
                    .map_err(|e| anyhow::anyhow!("Mutex is poisoned: {e}"))?;
384
                for t in &tags {
385
                    tagdb_lock.insert(t.tag_name.clone(), t.tag_value.clone());
386
                }
387
            }
388
            let comm = apply_commodity_hook(commodity, tagdb, &script).await?;
389
            // assert_eq!(
390
            //     comm.fullname,
391
            //     Some("the new full name of the Yen".to_string())
392
            // );
393

            
394
            let mut newtags = user.get_commodity_tags(&comm).await?;
395
            let name = newtags[0].tag_name.clone();
396
            newtags[0].tag_value = "thenewval".to_string();
397
            user.update_commodity_tags(&comm, &newtags).await?;
398
            let updated_tags = user.get_commodity_tags(&comm).await?;
399

            
400
1
            if let Some(tag) = updated_tags.iter().find(|t| t.tag_name == *name) {
401
                assert_eq!(
402
                    tag.tag_value, "thenewval",
403
                    "The tag value was not updated correctly"
404
                );
405
            } else {
406
                panic!("Tag with name '{name}' not found in updated tags");
407
            }
408
        }
409
    }
410
}