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