Skip to main content

finance_query_core/models/
sec_filings.rs

1use chrono::{DateTime, NaiveDate, TimeZone, Utc};
2use serde::{Deserialize, Serialize};
3
4/// Represents an SEC filing
5#[derive(Debug, Clone, Serialize, Deserialize)]
6#[serde(rename_all = "camelCase")]
7pub struct SecFiling {
8    pub date: DateTime<Utc>,
9    pub filing_type: String,
10    pub title: String,
11    pub url: String,
12    pub exhibits: Vec<SecExhibit>,
13}
14
15/// Represents an exhibit within an SEC filing
16#[derive(Debug, Clone, Serialize, Deserialize)]
17#[serde(rename_all = "camelCase")]
18pub struct SecExhibit {
19    pub exhibit_type: String,
20    pub url: String,
21}
22
23/// Response containing SEC filings
24#[derive(Debug, Clone, Serialize, Deserialize)]
25#[serde(rename_all = "camelCase")]
26pub struct SecFilingsResponse {
27    pub symbol: String,
28    pub filings: Vec<SecFiling>,
29}
30
31impl SecFilingsResponse {
32    pub(crate) fn from_yahoo_response(
33        symbol: String,
34        response: YahooSecFilingsResponse,
35    ) -> Result<Self, crate::client::YahooError> {
36        let result = response.quote_summary.result.first().ok_or_else(|| {
37            crate::client::YahooError::ParseError("No SEC filings data in response".to_string())
38        })?;
39
40        let filings = result
41            .sec_filings
42            .filings
43            .iter()
44            .filter_map(|f| {
45                // Parse date - can be string "YYYY-MM-DD" or timestamp
46                let date = parse_sec_date(&f.date)?;
47
48                let exhibits = f
49                    .exhibits
50                    .as_ref()
51                    .map(|exs| {
52                        exs.iter()
53                            .map(|e| SecExhibit {
54                                exhibit_type: e.exhibit_type.clone().unwrap_or_default(),
55                                url: e.url.clone().unwrap_or_default(),
56                            })
57                            .collect()
58                    })
59                    .unwrap_or_default();
60
61                Some(SecFiling {
62                    date,
63                    filing_type: f.filing_type.clone().unwrap_or_default(),
64                    title: f.title.clone().unwrap_or_default(),
65                    url: f.edgar_url.clone().unwrap_or_default(),
66                    exhibits,
67                })
68            })
69            .collect();
70
71        Ok(Self { symbol, filings })
72    }
73}
74
75/// Parse SEC filing date which can be either a string or timestamp
76fn parse_sec_date(date_value: &SecDateValue) -> Option<DateTime<Utc>> {
77    match date_value {
78        SecDateValue::String(s) => {
79            // Parse "YYYY-MM-DD" format
80            NaiveDate::parse_from_str(s, "%Y-%m-%d")
81                .ok()
82                .and_then(|d| d.and_hms_opt(0, 0, 0))
83                .and_then(|dt| dt.and_local_timezone(Utc).single())
84        }
85        SecDateValue::Timestamp(ts) => Utc.timestamp_opt(*ts, 0).single(),
86        SecDateValue::Object { raw, .. } => raw.and_then(|ts| Utc.timestamp_opt(ts, 0).single()),
87    }
88}
89
90// Internal Yahoo response structures
91#[derive(Debug, Deserialize)]
92pub(crate) struct YahooSecFilingsResponse {
93    #[serde(rename = "quoteSummary")]
94    pub quote_summary: SecQuoteSummaryData,
95}
96
97#[derive(Debug, Deserialize)]
98pub(crate) struct SecQuoteSummaryData {
99    pub result: Vec<SecQuoteSummaryResult>,
100}
101
102#[derive(Debug, Deserialize)]
103pub(crate) struct SecQuoteSummaryResult {
104    #[serde(rename = "secFilings")]
105    pub sec_filings: SecFilingsData,
106}
107
108#[derive(Debug, Deserialize)]
109pub(crate) struct SecFilingsData {
110    pub filings: Vec<YahooSecFiling>,
111}
112
113#[derive(Debug, Deserialize)]
114pub(crate) struct YahooSecFiling {
115    pub date: SecDateValue,
116    #[serde(rename = "type")]
117    pub filing_type: Option<String>,
118    pub title: Option<String>,
119    #[serde(rename = "edgarUrl")]
120    pub edgar_url: Option<String>,
121    pub exhibits: Option<Vec<YahooExhibit>>,
122}
123
124/// SEC date can be a string, timestamp, or object with raw field
125#[derive(Debug, Deserialize)]
126#[serde(untagged)]
127pub(crate) enum SecDateValue {
128    String(String),
129    Timestamp(i64),
130    Object {
131        raw: Option<i64>,
132        #[allow(dead_code)]
133        fmt: Option<String>,
134    },
135}
136
137#[derive(Debug, Deserialize)]
138pub(crate) struct YahooExhibit {
139    #[serde(rename = "type")]
140    pub exhibit_type: Option<String>,
141    pub url: Option<String>,
142}