1use crate::error::AppError;
2use crate::presentation::transaction::StoreTransaction;
3use crate::storage::config::DatabaseConfig;
4use serde::Serialize;
5use serde::de::DeserializeOwned;
6use serde_json;
7use sqlx::{Executor, PgPool};
8use tracing::info;
9
10pub async fn store_transactions(
19 pool: &sqlx::PgPool,
20 txs: &[StoreTransaction],
21) -> Result<usize, AppError> {
22 let mut tx = pool.begin().await?;
23 let mut inserted = 0;
24
25 for t in txs {
26 let result = tx
27 .execute(
28 sqlx::query(
29 r#"
30 INSERT INTO ig_options (
31 reference, deal_date, underlying, strike,
32 option_type, expiry, transaction_type, pnl_eur, is_fee, raw
33 )
34 VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)
35 ON CONFLICT (raw_hash) DO NOTHING
36 "#,
37 )
38 .bind(&t.reference)
39 .bind(t.deal_date)
40 .bind(&t.underlying)
41 .bind(t.strike)
42 .bind(&t.option_type)
43 .bind(t.expiry)
44 .bind(&t.transaction_type)
45 .bind(t.pnl_eur)
46 .bind(t.is_fee)
47 .bind(&t.raw_json),
48 )
49 .await?;
50
51 inserted += result.rows_affected() as usize;
52 }
53
54 tx.commit().await?;
55 Ok(inserted)
56}
57
58pub fn serialize_to_json<T: Serialize>(value: &T) -> Result<String, serde_json::Error> {
60 serde_json::to_string(value)
61}
62
63pub fn deserialize_from_json<T: DeserializeOwned>(s: &str) -> Result<T, serde_json::Error> {
65 serde_json::from_str(s)
66}
67
68pub async fn create_connection_pool(config: &DatabaseConfig) -> Result<PgPool, AppError> {
76 info!(
77 "Creating PostgreSQL connection pool with max {} connections",
78 config.max_connections
79 );
80
81 let pool = sqlx::postgres::PgPoolOptions::new()
82 .max_connections(config.max_connections)
83 .connect(&config.url)
84 .await
85 .map_err(AppError::Db)?;
86
87 info!("PostgreSQL connection pool created successfully");
88 Ok(pool)
89}
90
91pub fn create_database_config_from_env() -> Result<DatabaseConfig, AppError> {
96 dotenv::dotenv().ok();
97 let url = std::env::var("DATABASE_URL").map_err(|_| {
98 AppError::InvalidInput("DATABASE_URL environment variable is required".to_string())
99 })?;
100
101 let max_connections = std::env::var("DATABASE_MAX_CONNECTIONS")
102 .unwrap_or_else(|_| "10".to_string())
103 .parse::<u32>()
104 .map_err(|e| {
105 AppError::InvalidInput(format!("Invalid DATABASE_MAX_CONNECTIONS value: {e}"))
106 })?;
107
108 Ok(DatabaseConfig {
109 url,
110 max_connections,
111 })
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use serde::{Deserialize, Serialize};
118
119 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
120 struct TestStruct {
121 name: String,
122 value: i32,
123 optional: Option<f64>,
124 }
125
126 #[test]
127 fn test_serialize_to_json_simple_struct() {
128 let test_data = TestStruct {
129 name: "test".to_string(),
130 value: 42,
131 optional: Some(3.15),
132 };
133
134 let result = serialize_to_json(&test_data);
135 assert!(result.is_ok());
136
137 let json = result.expect("should serialize");
138 assert!(json.contains("\"name\":\"test\""));
139 assert!(json.contains("\"value\":42"));
140 assert!(json.contains("\"optional\":3.15"));
141 }
142
143 #[test]
144 fn test_serialize_to_json_with_none() {
145 let test_data = TestStruct {
146 name: "none_test".to_string(),
147 value: 0,
148 optional: None,
149 };
150
151 let result = serialize_to_json(&test_data);
152 assert!(result.is_ok());
153
154 let json = result.expect("should serialize");
155 assert!(json.contains("\"optional\":null"));
156 }
157
158 #[test]
159 fn test_deserialize_from_json_valid() {
160 let json = r#"{"name":"deserialized","value":100,"optional":2.5}"#;
161
162 let result: Result<TestStruct, _> = deserialize_from_json(json);
163 assert!(result.is_ok());
164
165 let data = result.expect("should deserialize");
166 assert_eq!(data.name, "deserialized");
167 assert_eq!(data.value, 100);
168 assert_eq!(data.optional, Some(2.5));
169 }
170
171 #[test]
172 fn test_deserialize_from_json_with_null() {
173 let json = r#"{"name":"null_test","value":50,"optional":null}"#;
174
175 let result: Result<TestStruct, _> = deserialize_from_json(json);
176 assert!(result.is_ok());
177
178 let data = result.expect("should deserialize");
179 assert_eq!(data.name, "null_test");
180 assert_eq!(data.value, 50);
181 assert!(data.optional.is_none());
182 }
183
184 #[test]
185 fn test_deserialize_from_json_invalid() {
186 let json = r#"{"invalid": "json for TestStruct"}"#;
187
188 let result: Result<TestStruct, _> = deserialize_from_json(json);
189 assert!(result.is_err());
190 }
191
192 #[test]
193 fn test_deserialize_from_json_malformed() {
194 let json = r#"{"name": "incomplete"#;
195
196 let result: Result<TestStruct, _> = deserialize_from_json(json);
197 assert!(result.is_err());
198 }
199
200 #[test]
201 fn test_serialize_deserialize_roundtrip() {
202 let original = TestStruct {
203 name: "roundtrip".to_string(),
204 value: 999,
205 optional: Some(1.234),
206 };
207
208 let json = serialize_to_json(&original).expect("should serialize");
209 let deserialized: TestStruct = deserialize_from_json(&json).expect("should deserialize");
210
211 assert_eq!(original, deserialized);
212 }
213
214 #[test]
215 fn test_serialize_vec() {
216 let vec = vec![
217 TestStruct {
218 name: "first".to_string(),
219 value: 1,
220 optional: None,
221 },
222 TestStruct {
223 name: "second".to_string(),
224 value: 2,
225 optional: Some(2.0),
226 },
227 ];
228
229 let result = serialize_to_json(&vec);
230 assert!(result.is_ok());
231
232 let json = result.expect("should serialize");
233 assert!(json.starts_with('['));
234 assert!(json.ends_with(']'));
235 assert!(json.contains("\"first\""));
236 assert!(json.contains("\"second\""));
237 }
238
239 #[test]
240 fn test_deserialize_vec() {
241 let json =
242 r#"[{"name":"a","value":1,"optional":null},{"name":"b","value":2,"optional":3.0}]"#;
243
244 let result: Result<Vec<TestStruct>, _> = deserialize_from_json(json);
245 assert!(result.is_ok());
246
247 let vec = result.expect("should deserialize");
248 assert_eq!(vec.len(), 2);
249 assert_eq!(vec[0].name, "a");
250 assert_eq!(vec[1].name, "b");
251 }
252
253 #[test]
254 fn test_serialize_empty_string() {
255 let test_data = TestStruct {
256 name: String::new(),
257 value: 0,
258 optional: None,
259 };
260
261 let result = serialize_to_json(&test_data);
262 assert!(result.is_ok());
263
264 let json = result.expect("should serialize");
265 assert!(json.contains("\"name\":\"\""));
266 }
267
268 #[test]
269 fn test_serialize_special_characters() {
270 let test_data = TestStruct {
271 name: "test\"with\\special\nchars".to_string(),
272 value: 0,
273 optional: None,
274 };
275
276 let result = serialize_to_json(&test_data);
277 assert!(result.is_ok());
278
279 let json = result.expect("should serialize");
280 assert!(json.contains("\\\""));
282 assert!(json.contains("\\\\"));
283 assert!(json.contains("\\n"));
284 }
285
286 #[test]
287 fn test_database_config_creation() {
288 let config = DatabaseConfig {
289 url: "postgres://localhost/test".to_string(),
290 max_connections: 5,
291 };
292 assert_eq!(config.url, "postgres://localhost/test");
293 assert_eq!(config.max_connections, 5);
294 }
295}