finance_query/models/fundamentals/
response.rs1use crate::constants::{Frequency, StatementType};
6use crate::error::Result;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10#[derive(Debug, Clone, Deserialize)]
12struct RawTimeseriesResponse {
13 timeseries: RawTimeseries,
14}
15
16#[derive(Debug, Clone, Deserialize)]
17struct RawTimeseries {
18 result: Vec<RawTimeseriesResult>,
19 #[allow(dead_code)]
20 error: Option<serde_json::Value>,
21}
22
23#[derive(Debug, Clone, Deserialize)]
24struct RawTimeseriesResult {
25 meta: RawMeta,
26 #[serde(flatten)]
27 data: HashMap<String, serde_json::Value>,
28}
29
30#[derive(Debug, Clone, Deserialize)]
31struct RawMeta {
32 #[serde(rename = "type")]
33 data_type: Vec<String>,
34}
35
36#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
53#[serde(rename_all = "camelCase")]
54pub struct FinancialStatement {
55 pub symbol: String,
57
58 pub statement_type: String,
60
61 pub frequency: String,
63
64 pub statement: HashMap<String, HashMap<String, f64>>,
67
68 pub provider_id: Option<crate::providers::Provider>,
70}
71
72impl FinancialStatement {
73 pub(crate) fn from_response(
78 raw: &serde_json::Value,
79 symbol: &str,
80 statement_type: StatementType,
81 frequency: Frequency,
82 ) -> Result<Self> {
83 let raw_response: RawTimeseriesResponse =
84 serde_json::from_value(raw.clone()).map_err(|e| {
85 crate::error::FinanceError::ResponseStructureError {
86 field: "timeseries".to_string(),
87 context: format!("Failed to parse financials response: {}", e),
88 }
89 })?;
90
91 if raw_response.timeseries.result.is_empty() {
92 return Err(crate::error::FinanceError::SymbolNotFound {
93 symbol: Some(symbol.to_string()),
94 context: format!(
95 "No {} {} data found",
96 frequency.as_str(),
97 statement_type.as_str()
98 ),
99 });
100 }
101
102 let mut statement: HashMap<String, HashMap<String, f64>> = HashMap::new();
103
104 for result in raw_response.timeseries.result {
105 let metric_name_with_prefix =
107 result.meta.data_type.first().cloned().unwrap_or_default();
108
109 if metric_name_with_prefix.is_empty() {
110 continue;
111 }
112
113 let metric_name = strip_frequency_prefix(&metric_name_with_prefix);
115
116 let data_points = match result.data.get(&metric_name_with_prefix) {
118 Some(serde_json::Value::Array(arr)) => arr,
119 _ => continue,
120 };
121
122 let mut date_values: HashMap<String, f64> = HashMap::new();
123
124 for point in data_points {
125 if point.is_null() {
126 continue;
127 }
128
129 let as_of_date = point
130 .get("asOfDate")
131 .and_then(|v| v.as_str())
132 .unwrap_or_default();
133
134 if as_of_date.is_empty() {
135 continue;
136 }
137
138 let value = extract_value(point.get("reportedValue"));
140
141 if let Some(v) = value {
142 date_values.insert(as_of_date.to_string(), v);
143 }
144 }
145
146 if !date_values.is_empty() {
147 statement.insert(metric_name, date_values);
148 }
149 }
150
151 if statement.is_empty() {
152 return Err(crate::error::FinanceError::SymbolNotFound {
153 symbol: Some(symbol.to_string()),
154 context: format!(
155 "No {} {} data found",
156 frequency.as_str(),
157 statement_type.as_str()
158 ),
159 });
160 }
161
162 Ok(Self {
163 symbol: symbol.to_uppercase(),
164 statement_type: statement_type.as_str().to_string(),
165 frequency: frequency.as_str().to_string(),
166 statement,
167 provider_id: None,
168 })
169 }
170}
171
172fn strip_frequency_prefix(name: &str) -> String {
176 for prefix in &["annual", "quarterly", "trailing"] {
177 if let Some(stripped) = name.strip_prefix(prefix) {
178 return stripped.to_string();
179 }
180 }
181 name.to_string()
182}
183
184fn extract_value(reported_value: Option<&serde_json::Value>) -> Option<f64> {
188 let rv = reported_value?;
189
190 if let Some(raw) = rv.get("raw") {
192 if let Some(n) = raw.as_f64() {
194 return Some(n);
195 }
196 if let Some(parsed) = raw.get("parsedValue") {
198 return parsed
199 .as_f64()
200 .or_else(|| parsed.as_i64().map(|i| i as f64));
201 }
202 }
203
204 None
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 #[test]
212 fn test_strip_frequency_prefix() {
213 assert_eq!(strip_frequency_prefix("annualTotalRevenue"), "TotalRevenue");
214 assert_eq!(strip_frequency_prefix("quarterlyNetIncome"), "NetIncome");
215 assert_eq!(strip_frequency_prefix("trailingMarketCap"), "MarketCap");
216 assert_eq!(strip_frequency_prefix("SomeOther"), "SomeOther");
217 }
218
219 #[test]
220 fn test_extract_value_simple() {
221 let json: serde_json::Value = serde_json::json!({
222 "raw": 123.45,
223 "fmt": "123.45"
224 });
225 assert_eq!(extract_value(Some(&json)), Some(123.45));
226 }
227
228 #[test]
229 fn test_extract_value_nested() {
230 let json: serde_json::Value = serde_json::json!({
231 "raw": {
232 "source": "1.23E12",
233 "parsedValue": 1230000000000_i64
234 },
235 "fmt": "1.23T"
236 });
237 assert_eq!(extract_value(Some(&json)), Some(1230000000000.0));
238 }
239
240 #[test]
241 fn test_from_response() {
242 let json: serde_json::Value = serde_json::json!({
243 "timeseries": {
244 "result": [
245 {
246 "meta": {
247 "symbol": ["AAPL"],
248 "type": ["annualTotalRevenue"]
249 },
250 "annualTotalRevenue": [
251 {
252 "asOfDate": "2024-09-30",
253 "periodType": "12M",
254 "currencyCode": "USD",
255 "reportedValue": {
256 "raw": 391035000000.0,
257 "fmt": "391.04B"
258 }
259 },
260 {
261 "asOfDate": "2023-09-30",
262 "periodType": "12M",
263 "currencyCode": "USD",
264 "reportedValue": {
265 "raw": 383285000000.0,
266 "fmt": "383.29B"
267 }
268 }
269 ]
270 },
271 {
272 "meta": {
273 "symbol": ["AAPL"],
274 "type": ["annualNetIncome"]
275 },
276 "annualNetIncome": [
277 {
278 "asOfDate": "2024-09-30",
279 "periodType": "12M",
280 "currencyCode": "USD",
281 "reportedValue": {
282 "raw": 100913000000.0,
283 "fmt": "100.91B"
284 }
285 }
286 ]
287 }
288 ],
289 "error": null
290 }
291 });
292
293 let result = FinancialStatement::from_response(
294 &json,
295 "AAPL",
296 StatementType::Income,
297 Frequency::Annual,
298 );
299 assert!(result.is_ok());
300
301 let statement = result.unwrap();
302 assert_eq!(statement.symbol, "AAPL");
303 assert_eq!(statement.statement_type, "income");
304 assert_eq!(statement.frequency, "annual");
305 assert!(statement.statement.contains_key("TotalRevenue"));
306 assert!(statement.statement.contains_key("NetIncome"));
307
308 let revenue = statement.statement.get("TotalRevenue").unwrap();
309 assert_eq!(revenue.get("2024-09-30"), Some(&391035000000.0));
310 assert_eq!(revenue.get("2023-09-30"), Some(&383285000000.0));
311 }
312
313 #[test]
314 fn test_from_response_empty() {
315 let json: serde_json::Value = serde_json::json!({
316 "timeseries": {
317 "result": [],
318 "error": null
319 }
320 });
321
322 let result = FinancialStatement::from_response(
323 &json,
324 "INVALID",
325 StatementType::Income,
326 Frequency::Annual,
327 );
328 assert!(result.is_err());
329 }
330}