server/command/
commodity.rs1use finance::{commodity::Commodity, tag::Tag};
2use sqlx::types::Uuid;
3use std::{collections::HashMap, fmt::Debug};
4use supp_macro::command;
5
6use super::{CmdError, CmdResult};
7use crate::{command::FinanceEntity, config::ConfigError, user::User};
8
9command! {
10 GetCommodity {
11 #[required]
12 user_id: Uuid,
13 #[required]
14 commodity_id: Uuid,
15 } => {
16 let user = User { id: user_id };
17
18 let mut conn = user.get_connection().await.map_err(|err| {
19 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
20 ConfigError::DB
21 })?;
22
23 let comm = sqlx::query_file_as!(Commodity, "sql/select/commodities/by_id.sql", &commodity_id)
24 .fetch_one(&mut *conn)
25 .await?;
26
27 let mut tagged_entities = Vec::new();
29 let tags: HashMap<String, FinanceEntity> =
30 sqlx::query_file!("sql/select/tags/by_commodity.sql", &commodity_id)
31 .fetch_all(&mut *conn)
32 .await?
33 .into_iter()
34 .map(|row| {
35 (
36 row.tag_name.clone(),
37 FinanceEntity::Tag(Tag {
38 id: row.id,
39 tag_name: row.tag_name,
40 tag_value: row.tag_value,
41 description: row.description,
42 }),
43 )
44 })
45 .collect();
46
47 tagged_entities.push((FinanceEntity::Commodity(comm), tags));
48 Ok(Some(CmdResult::TaggedEntities {
49 entities: tagged_entities,
50 pagination: None,
51 }))
52 }
53}
54
55command! {
56 CreateCommodity {
57 #[required]
58 symbol: String,
59 #[required]
60 name: String,
61 #[required]
62 user_id: Uuid,
63 } => {
64 let user = User { id: user_id };
65
66 Ok(Some(
67 user.create_commodity(symbol, name)
68 .await?
69 .id
70 .to_string()
71 .into(),
72 ))
73 }
74}
75
76command! {
77 ListCommodities {
78 #[required]
79 user_id: Uuid,
80 } => {
81 let user = User { id: user_id };
82 let mut conn = user.get_connection().await.map_err(|err| {
83 log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
84 ConfigError::DB
85 })?;
86
87 let commodities: Vec<Commodity> = sqlx::query_file!("sql/select/commodities/all.sql")
89 .fetch_all(&mut *conn)
90 .await?
91 .into_iter()
92 .map(|row| Commodity { id: row.id })
93 .collect();
94
95 let mut tagged_entities = Vec::new();
97 for commodity in commodities {
98 let tags: HashMap<String, FinanceEntity> =
99 sqlx::query_file!("sql/select/tags/by_commodity.sql", &commodity.id)
100 .fetch_all(&mut *conn)
101 .await?
102 .into_iter()
103 .map(|row| {
104 (
105 row.tag_name.clone(),
106 FinanceEntity::Tag(Tag {
107 id: row.id,
108 tag_name: row.tag_name,
109 tag_value: row.tag_value,
110 description: row.description,
111 }),
112 )
113 })
114 .collect();
115
116 tagged_entities.push((FinanceEntity::Commodity(commodity), tags));
117 }
118 Ok(Some(CmdResult::TaggedEntities {
119 entities: tagged_entities,
120 pagination: None,
121 }))
122 }
123}
124
125#[cfg(test)]
126mod command_tests {
127 use super::*;
128 use crate::db::DB_POOL;
129 use sqlx::PgPool;
130 use supp_macro::local_db_sqlx_test;
131 use tokio::sync::OnceCell;
132
133 static CONTEXT: OnceCell<()> = OnceCell::const_new();
135 static USER: OnceCell<User> = OnceCell::const_new();
136
137 async fn setup() {
138 CONTEXT
139 .get_or_init(|| async {
140 #[cfg(feature = "testlog")]
141 let _ = env_logger::builder()
142 .is_test(true)
143 .filter_level(log::LevelFilter::Trace)
144 .try_init();
145 })
146 .await;
147 USER.get_or_init(|| async { User { id: Uuid::new_v4() } })
148 .await;
149 }
150
151 #[local_db_sqlx_test]
152 async fn test_list_commodities_empty(pool: PgPool) -> anyhow::Result<()> {
153 let user = USER.get().unwrap();
154 user.commit()
155 .await
156 .expect("Failed to commit user to database");
157
158 if let Some(CmdResult::TaggedEntities { entities, .. }) =
159 ListCommodities::new().user_id(user.id).run().await?
160 {
161 assert!(
162 entities.is_empty(),
163 "Expected no commodities in empty database"
164 );
165 } else {
166 panic!("Expected TaggedEntities result");
167 }
168 }
169
170 #[local_db_sqlx_test]
171 async fn test_list_commodities_with_data(pool: PgPool) -> anyhow::Result<()> {
172 let user = USER.get().unwrap();
173 user.commit()
174 .await
175 .expect("Failed to commit user to database");
176
177 CreateCommodity::new()
179 .symbol("TST".to_string())
180 .name("Test Commodity".to_string())
181 .user_id(user.id)
182 .run()
183 .await?;
184
185 if let Some(CmdResult::TaggedEntities { entities, .. }) =
187 ListCommodities::new().user_id(user.id).run().await?
188 {
189 assert_eq!(entities.len(), 1, "Expected one commodity");
190
191 let (entity, tags) = &entities[0];
192 if let FinanceEntity::Commodity(_c) = entity {
193 assert_eq!(tags.len(), 2); for tag in tags.values() {
196 if let FinanceEntity::Tag(t) = tag {
197 match t.tag_name.as_str() {
198 "symbol" => assert_eq!(t.tag_value, "TST"),
199 "name" => assert_eq!(t.tag_value, "Test Commodity"),
200 _ => panic!("Unexpected tag: {}", t.tag_name),
201 }
202 }
203 }
204 } else {
205 panic!("Expected Commodity entity");
206 }
207 } else {
208 panic!("Expected TaggedEntities result");
209 }
210 }
211
212 #[local_db_sqlx_test]
213 async fn test_get_commodity(pool: PgPool) -> anyhow::Result<()> {
214 let user = USER.get().unwrap();
215 user.commit()
216 .await
217 .expect("Failed to commit user to database");
218
219 let commodity_result = CreateCommodity::new()
221 .symbol("TST".to_string())
222 .name("Test Commodity".to_string())
223 .user_id(user.id)
224 .run()
225 .await?;
226
227 let commodity_id = if let Some(CmdResult::String(id)) = commodity_result {
229 uuid::Uuid::parse_str(&id)?
230 } else {
231 panic!("Expected commodity ID string result");
232 };
233
234 if let Some(CmdResult::TaggedEntities { entities, .. }) = GetCommodity::new()
236 .user_id(user.id)
237 .commodity_id(commodity_id)
238 .run()
239 .await?
240 {
241 assert_eq!(entities.len(), 1, "Expected one commodity");
242
243 let (entity, tags) = &entities[0];
244 if let FinanceEntity::Commodity(c) = entity {
245 assert_eq!(c.id, commodity_id);
246
247 assert_eq!(tags.len(), 2); for tag in tags.values() {
250 if let FinanceEntity::Tag(t) = tag {
251 match t.tag_name.as_str() {
252 "symbol" => assert_eq!(t.tag_value, "TST"),
253 "name" => assert_eq!(t.tag_value, "Test Commodity"),
254 _ => panic!("Unexpected tag: {}", t.tag_name),
255 }
256 }
257 }
258 } else {
259 panic!("Expected Commodity entity");
260 }
261 } else {
262 panic!("Expected TaggedEntities result");
263 }
264
265 let result = GetCommodity::new()
267 .user_id(user.id)
268 .commodity_id(Uuid::new_v4())
269 .run()
270 .await;
271 assert!(result.is_err(), "Expected error for non-existent commodity");
272 }
273}