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)] 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 = serde_json::from_value(raw).map_err(|e| {
84 crate::error::FinanceError::ResponseStructureError {
85 field: "timeseries".to_string(),
86 context: format!("Failed to parse financials response: {}", e),
87 }
88 })?;
89
90 if raw_response.timeseries.result.is_empty() {
91 return Err(crate::error::FinanceError::SymbolNotFound {
92 symbol: Some(symbol.to_string()),
93 context: format!(
94 "No {} {} data found",
95 frequency.as_str(),
96 statement_type.as_str()
97 ),
98 });
99 }
100
101 let mut statement: HashMap<String, HashMap<String, f64>> = HashMap::new();
102
103 for result in raw_response.timeseries.result {
104 let metric_name_with_prefix =
106 result.meta.data_type.first().cloned().unwrap_or_default();
107
108 if metric_name_with_prefix.is_empty() {
109 continue;
110 }
111
112 let metric_name = strip_frequency_prefix(&metric_name_with_prefix);
114
115 let data_points = match result.data.get(&metric_name_with_prefix) {
117 Some(serde_json::Value::Array(arr)) => arr,
118 _ => continue,
119 };
120
121 let mut date_values: HashMap<String, f64> = HashMap::new();
122
123 for point in data_points {
124 if point.is_null() {
125 continue;
126 }
127
128 let as_of_date = point
129 .get("asOfDate")
130 .and_then(|v| v.as_str())
131 .unwrap_or_default();
132
133 if as_of_date.is_empty() {
134 continue;
135 }
136
137 let value = extract_value(point.get("reportedValue"));
139
140 if let Some(v) = value {
141 date_values.insert(as_of_date.to_string(), v);
142 }
143 }
144
145 if !date_values.is_empty() {
146 statement.insert(metric_name, date_values);
147 }
148 }
149
150 if statement.is_empty() {
151 return Err(crate::error::FinanceError::SymbolNotFound {
152 symbol: Some(symbol.to_string()),
153 context: format!(
154 "No {} {} data found",
155 frequency.as_str(),
156 statement_type.as_str()
157 ),
158 });
159 }
160
161 Ok(Self {
162 symbol: symbol.to_uppercase(),
163 statement_type: statement_type.as_str().to_string(),
164 frequency: frequency.as_str().to_string(),
165 statement,
166 provider_id: None,
167 })
168 }
169}
170
171fn strip_frequency_prefix(name: &str) -> String {
175 for prefix in &["annual", "quarterly", "trailing"] {
176 if let Some(stripped) = name.strip_prefix(prefix) {
177 return stripped.to_string();
178 }
179 }
180 name.to_string()
181}
182
183fn extract_value(reported_value: Option<&serde_json::Value>) -> Option<f64> {
187 let rv = reported_value?;
188
189 if let Some(raw) = rv.get("raw") {
191 if let Some(n) = raw.as_f64() {
193 return Some(n);
194 }
195 if let Some(parsed) = raw.get("parsedValue") {
197 return parsed
198 .as_f64()
199 .or_else(|| parsed.as_i64().map(|i| i as f64));
200 }
201 }
202
203 None
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn test_strip_frequency_prefix() {
212 assert_eq!(strip_frequency_prefix("annualTotalRevenue"), "TotalRevenue");
213 assert_eq!(strip_frequency_prefix("quarterlyNetIncome"), "NetIncome");
214 assert_eq!(strip_frequency_prefix("trailingMarketCap"), "MarketCap");
215 assert_eq!(strip_frequency_prefix("SomeOther"), "SomeOther");
216 }
217
218 #[test]
219 fn test_extract_value_simple() {
220 let json: serde_json::Value = serde_json::json!({
221 "raw": 123.45,
222 "fmt": "123.45"
223 });
224 assert_eq!(extract_value(Some(&json)), Some(123.45));
225 }
226
227 #[test]
228 fn test_extract_value_nested() {
229 let json: serde_json::Value = serde_json::json!({
230 "raw": {
231 "source": "1.23E12",
232 "parsedValue": 1230000000000_i64
233 },
234 "fmt": "1.23T"
235 });
236 assert_eq!(extract_value(Some(&json)), Some(1230000000000.0));
237 }
238
239 #[test]
240 fn test_from_response() {
241 let json: serde_json::Value = serde_json::json!({
242 "timeseries": {
243 "result": [
244 {
245 "meta": {
246 "symbol": ["AAPL"],
247 "type": ["annualTotalRevenue"]
248 },
249 "annualTotalRevenue": [
250 {
251 "asOfDate": "2024-09-30",
252 "periodType": "12M",
253 "currencyCode": "USD",
254 "reportedValue": {
255 "raw": 391035000000.0,
256 "fmt": "391.04B"
257 }
258 },
259 {
260 "asOfDate": "2023-09-30",
261 "periodType": "12M",
262 "currencyCode": "USD",
263 "reportedValue": {
264 "raw": 383285000000.0,
265 "fmt": "383.29B"
266 }
267 }
268 ]
269 },
270 {
271 "meta": {
272 "symbol": ["AAPL"],
273 "type": ["annualNetIncome"]
274 },
275 "annualNetIncome": [
276 {
277 "asOfDate": "2024-09-30",
278 "periodType": "12M",
279 "currencyCode": "USD",
280 "reportedValue": {
281 "raw": 100913000000.0,
282 "fmt": "100.91B"
283 }
284 }
285 ]
286 }
287 ],
288 "error": null
289 }
290 });
291
292 let result = FinancialStatement::from_response(
293 json,
294 "AAPL",
295 StatementType::Income,
296 Frequency::Annual,
297 );
298 assert!(result.is_ok());
299
300 let statement = result.unwrap();
301 assert_eq!(statement.symbol, "AAPL");
302 assert_eq!(statement.statement_type, "income");
303 assert_eq!(statement.frequency, "annual");
304 assert!(statement.statement.contains_key("TotalRevenue"));
305 assert!(statement.statement.contains_key("NetIncome"));
306
307 let revenue = statement.statement.get("TotalRevenue").unwrap();
308 assert_eq!(revenue.get("2024-09-30"), Some(&391035000000.0));
309 assert_eq!(revenue.get("2023-09-30"), Some(&383285000000.0));
310 }
311
312 #[test]
313 fn test_from_response_empty() {
314 let json: serde_json::Value = serde_json::json!({
315 "timeseries": {
316 "result": [],
317 "error": null
318 }
319 });
320
321 let result = FinancialStatement::from_response(
322 json,
323 "INVALID",
324 StatementType::Income,
325 Frequency::Annual,
326 );
327 assert!(result.is_err());
328 }
329}