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

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

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

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

            
39
            // Update the tags with the new values from `tagdb`
40
            {
41
2
                let tagdb_lock = tagdb.lock().map_err(|err| {
42
                    log::error!("{}", t!("Mutex error: %{err}", err = err : {:?}));
43
                    ServerError::Lock
44
                })?;
45
2
                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
2
            self.update_commodity_tags(&commodity, &tags).await?;
54

            
55
2
            Ok(commodity)
56
2
        }
57

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

            
69
78
            let mut conn = self.get_connection().await.map_err(|err| {
70
6
                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
71
6
                ServerError::DB(err)
72
6
            })?;
73

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

            
91
72
            Ok(c)
92
78
        }
93

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

            
103
76
            let mut conn = self.get_connection().await.map_err(|err| {
104
                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
105
                ServerError::DB(err)
106
            })?;
107

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

            
122
76
            Ok(())
123
76
        }
124

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

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

            
147
10
            Ok(())
148
10
        }
149

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

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

            
164
12
            Ok(tags)
165
12
        }
166

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

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

            
185
4
            Ok(tag)
186
4
        }
187
    }
188

            
189
    #[cfg(test)]
190
    mod commodity_tests {
191
        use super::*;
192
        use crate::db::DB_POOL;
193
        #[cfg(feature = "testlog")]
194
        use env_logger;
195
        #[cfg(feature = "testlog")]
196
        use log;
197
        use sqlx::PgPool;
198
        use std::fs;
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
12
        async fn setup() {
207
8
            CONTEXT
208
8
                .get_or_init(|| async {
209
                    #[cfg(feature = "testlog")]
210
2
                    let _ = env_logger::builder()
211
2
                        .is_test(true)
212
2
                        .filter_level(log::LevelFilter::Trace)
213
2
                        .try_init();
214
4
                })
215
8
                .await;
216

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

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

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

            
231
            let commodity = Commodity {
232
                id: Uuid::new_v4(),
233
                fraction: 100,
234
            };
235
            let user = USER.get().unwrap();
236

            
237
            sqlx::query!(
238
                "INSERT INTO commodities (id, fraction) \
239
		      VALUES ($1, $2)",
240
                &commodity.id,
241
                &commodity.fraction
242
            )
243
            .execute(&mut *conn)
244
            .await
245
            .unwrap();
246

            
247
            let script = fs::read("../target/commodity.wasm")?;
248
            let comm = user.add_commodity(&script).await?;
249
            assert_eq!(comm.fraction, 15);
250
        }
251

            
252
        #[local_db_sqlx_test]
253
        async fn test_create_commodity(pool: PgPool) -> Result<(), anyhow::Error> {
254
            let user = USER.get().unwrap();
255
            user.commit()
256
                .await
257
                .expect("Failed to commit user to database");
258

            
259
            let c = user
260
                .create_commodity(15, "JPY".to_string(), "Japanese Yen".to_string())
261
                .await?;
262
            let mut conn = pool.acquire().await.unwrap();
263
            let res = sqlx::query!("SELECT fraction FROM commodities WHERE id = $1", c.id)
264
                .fetch_one(&mut *conn)
265
                .await?;
266

            
267
            assert_eq!(res.fraction, 15);
268

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

            
271
            assert_eq!(tag.tag_value, "JPY");
272

            
273
            let tag = user.get_commodity_tag(&c, &"name".to_string()).await?;
274
            assert_eq!(tag.tag_value, "Japanese Yen");
275
        }
276

            
277
        #[local_db_sqlx_test]
278
        async fn test_commodity_tag(pool: PgPool) {
279
            let user = USER.get().unwrap();
280
            user.commit()
281
                .await
282
                .expect("Failed to commit user to database");
283

            
284
            let mut conn = user.get_connection().await?;
285
            let commodity = Commodity {
286
                id: Uuid::new_v4(),
287
                fraction: 100,
288
            };
289
            user.set_commodity_tag(
290
                &commodity,
291
                &Tag::builder()
292
                    .id(Uuid::new_v4())
293
                    .tag_name("test")
294
                    .tag_value("testval")
295
                    .build()?,
296
            )
297
            .await?;
298

            
299
            let res = sqlx::query_file!("testdata/query_tag.sql", &commodity.fraction)
300
                .fetch_one(&mut *conn)
301
                .await?;
302
            assert_eq!(res.tag_name_result, "test".to_string());
303
            assert_eq!(res.tag_value_result, "testval".to_string());
304
            user.set_commodity_tag(
305
                &commodity,
306
                &Tag::builder()
307
                    .id(Uuid::new_v4())
308
                    .tag_name("test")
309
                    .tag_value("testval2")
310
                    .build()?,
311
            )
312
            .await?;
313
            let res = sqlx::query_file!("testdata/query_tag.sql", &commodity.fraction)
314
                .fetch_one(&mut *conn)
315
                .await?;
316
            assert_eq!(res.tag_name_result, "test".to_string());
317
            assert_eq!(res.tag_value_result, "testval2".to_string());
318
        }
319

            
320
        #[local_db_sqlx_test]
321
        async fn test_get_commodity_tags(pool: PgPool) {
322
            let user = USER.get().unwrap();
323
            user.commit()
324
                .await
325
                .expect("Failed to commit user to database");
326

            
327
            let commodity = Commodity {
328
                id: Uuid::new_v4(),
329
                fraction: 100,
330
            };
331
            user.set_commodity_tag(
332
                &commodity,
333
                &Tag::builder()
334
                    .id(Uuid::new_v4())
335
                    .tag_name("test")
336
                    .tag_value("testval")
337
                    .build()?,
338
            )
339
            .await?;
340
            let tags = user.get_commodity_tags(&commodity).await?;
341
            assert_eq!(tags.len(), 1);
342
            assert_eq!(tags.first().unwrap().tag_name, "test".to_string());
343
            user.set_commodity_tag(
344
                &commodity,
345
                &Tag::builder()
346
                    .id(Uuid::new_v4())
347
                    .tag_name("test2")
348
                    .tag_value("testval2")
349
                    .build()?,
350
            )
351
            .await?;
352

            
353
            let tags = user.get_commodity_tags(&commodity).await?;
354
            assert_eq!(tags.len(), 2);
355
            assert_eq!(tags.last().unwrap().tag_name, "test2".to_string());
356

            
357
            user.set_commodity_tag(
358
                &commodity,
359
                &Tag::builder()
360
                    .id(Uuid::new_v4())
361
                    .tag_name("newname")
362
                    .tag_value("the new full name of the Yen")
363
                    .build()?,
364
            )
365
            .await?;
366

            
367
            let tags = user.get_commodity_tags(&commodity).await?;
368
            let script = fs::read("../target/commodity.wasm")?;
369
            let tagdb: Arc<Mutex<HashMap<String, String>>> = Arc::new(Mutex::new(HashMap::new()));
370
            {
371
                let mut tagdb_lock = tagdb
372
                    .lock()
373
                    .map_err(|e| anyhow::anyhow!("Mutex is poisoned: {}", e))?;
374
                for t in &tags {
375
                    tagdb_lock.insert(t.tag_name.clone(), t.tag_value.clone());
376
                }
377
            }
378
            let comm = apply_commodity_hook(commodity, tagdb, &script).await?;
379
            // assert_eq!(
380
            //     comm.fullname,
381
            //     Some("the new full name of the Yen".to_string())
382
            // );
383

            
384
            let mut newtags = user.get_commodity_tags(&comm).await?;
385
            let name = newtags[0].tag_name.clone();
386
            newtags[0].tag_value = "thenewval".to_string();
387
            user.update_commodity_tags(&comm, &newtags).await?;
388
            let updated_tags = user.get_commodity_tags(&comm).await?;
389

            
390
2
            if let Some(tag) = updated_tags.iter().find(|t| t.tag_name == *name) {
391
                assert_eq!(
392
                    tag.tag_value, "thenewval",
393
                    "The tag value was not updated correctly"
394
                );
395
            } else {
396
                panic!("Tag with name '{name}' not found in updated tags");
397
            }
398
        }
399
    }
400
}