earningsfeed/models/
filing.rs

1//! Filing-related types.
2//!
3//! This module contains types for SEC filings including 10-K, 10-Q, 8-K,
4//! and other form types.
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9/// Company details attached to a filing.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(rename_all = "camelCase")]
12pub struct FilingCompany {
13    /// SEC Central Index Key.
14    pub cik: u64,
15    /// Company name.
16    pub name: String,
17    /// State/country code.
18    pub state_of_incorporation: Option<String>,
19    /// Full state/country name.
20    pub state_of_incorporation_description: Option<String>,
21    /// Fiscal year end (MMDD format).
22    pub fiscal_year_end: Option<String>,
23}
24
25/// Entity type classification.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(rename_all = "lowercase")]
28pub enum EntityClass {
29    /// Company entity.
30    Company,
31    /// Person entity.
32    Person,
33}
34
35/// SEC filing from the filings feed.
36///
37/// Represents a filing in the list endpoint response.
38#[derive(Debug, Clone, Serialize, Deserialize)]
39#[serde(rename_all = "camelCase")]
40pub struct Filing {
41    /// SEC accession number (e.g., "0000950170-24-000001").
42    pub accession_number: String,
43    /// Accession number without dashes.
44    pub accession_no_dashes: Option<String>,
45    /// Filer CIK.
46    pub cik: u64,
47    /// Company name.
48    pub company_name: Option<String>,
49    /// SEC form type (10-K, 8-K, etc.).
50    pub form_type: String,
51    /// Filing submission time.
52    pub filed_at: DateTime<Utc>,
53    /// SEC acceptance time.
54    pub accept_ts: Option<DateTime<Utc>>,
55    /// Whether filing is provisional.
56    pub provisional: bool,
57    /// Feed day (YYYY-MM-DD).
58    pub feed_day: Option<String>,
59    /// Primary document size in bytes.
60    pub size_bytes: u64,
61    /// SEC EDGAR URL.
62    pub url: String,
63    /// Filing title.
64    pub title: String,
65    /// Filing status.
66    pub status: String,
67    /// Last updated timestamp.
68    pub updated_at: DateTime<Utc>,
69    /// Primary stock ticker.
70    pub primary_ticker: Option<String>,
71    /// Primary exchange.
72    pub primary_exchange: Option<String>,
73    /// Company details.
74    pub company: Option<FilingCompany>,
75    /// Sort timestamp.
76    pub sorted_at: DateTime<Utc>,
77    /// Company logo URL.
78    pub logo_url: Option<String>,
79    /// Entity class.
80    pub entity_class: Option<EntityClass>,
81}
82
83/// Document within a filing.
84#[derive(Debug, Clone, Serialize, Deserialize)]
85#[serde(rename_all = "camelCase")]
86pub struct FilingDocument {
87    /// Document sequence number.
88    pub seq: u32,
89    /// Filename on SEC EDGAR.
90    pub filename: String,
91    /// Document type.
92    pub doc_type: String,
93    /// Document description.
94    pub description: Option<String>,
95    /// Whether this is the primary document.
96    pub is_primary: bool,
97}
98
99/// Entity role in a filing.
100#[derive(Debug, Clone, Serialize, Deserialize)]
101#[serde(rename_all = "camelCase")]
102pub struct FilingRole {
103    /// Entity CIK.
104    pub cik: u64,
105    /// Role type (filer, issuer, reporting-owner, etc.).
106    pub role: String,
107}
108
109/// Detailed filing information.
110///
111/// Returned from the single filing endpoint with full details
112/// including documents and roles.
113#[derive(Debug, Clone, Serialize, Deserialize)]
114#[serde(rename_all = "camelCase")]
115pub struct FilingDetail {
116    /// SEC accession number.
117    pub accession_number: String,
118    /// Accession number without dashes.
119    pub accession_no_dashes: Option<String>,
120    /// Filer CIK.
121    pub cik: u64,
122    /// SEC form type.
123    pub form_type: String,
124    /// Filing submission time.
125    pub filed_at: DateTime<Utc>,
126    /// SEC acceptance time.
127    pub accept_ts: Option<DateTime<Utc>>,
128    /// Whether filing is provisional.
129    pub provisional: bool,
130    /// Feed day (YYYY-MM-DD).
131    pub feed_day: Option<String>,
132    /// Filing title.
133    pub title: String,
134    /// SEC EDGAR URL.
135    pub url: String,
136    /// Primary document size in bytes.
137    pub size_bytes: u64,
138    /// SEC relative directory.
139    pub sec_relative_dir: Option<String>,
140    /// Company name.
141    pub company_name: Option<String>,
142    /// Primary stock ticker.
143    pub primary_ticker: Option<String>,
144    /// Company details.
145    pub company: Option<FilingCompany>,
146    /// Filing documents.
147    pub documents: Vec<FilingDocument>,
148    /// Entity roles.
149    pub roles: Vec<FilingRole>,
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155    use serde_json::json;
156
157    #[test]
158    fn test_deserialize_filing_company() {
159        let json = json!({
160            "cik": 320193,
161            "name": "Apple Inc.",
162            "stateOfIncorporation": "CA",
163            "stateOfIncorporationDescription": "California",
164            "fiscalYearEnd": "0930"
165        });
166
167        let company: FilingCompany = serde_json::from_value(json).unwrap();
168        assert_eq!(company.cik, 320193);
169        assert_eq!(company.name, "Apple Inc.");
170        assert_eq!(company.state_of_incorporation, Some("CA".to_string()));
171        assert_eq!(company.fiscal_year_end, Some("0930".to_string()));
172    }
173
174    #[test]
175    fn test_deserialize_entity_class() {
176        let json = json!("company");
177        let entity_class: EntityClass = serde_json::from_value(json).unwrap();
178        assert_eq!(entity_class, EntityClass::Company);
179
180        let json = json!("person");
181        let entity_class: EntityClass = serde_json::from_value(json).unwrap();
182        assert_eq!(entity_class, EntityClass::Person);
183    }
184
185    #[test]
186    fn test_deserialize_filing() {
187        let json = json!({
188            "accessionNumber": "0000950170-24-000001",
189            "accessionNoDashes": "0000950170240000001",
190            "cik": 320193,
191            "companyName": "Apple Inc.",
192            "formType": "10-K",
193            "filedAt": "2024-01-15T16:30:00Z",
194            "acceptTs": "2024-01-15T16:25:00Z",
195            "provisional": false,
196            "feedDay": "2024-01-15",
197            "sizeBytes": 12345678,
198            "url": "https://www.sec.gov/Archives/edgar/data/320193/000095017024000001/0000950170-24-000001-index.htm",
199            "title": "Form 10-K",
200            "status": "final",
201            "updatedAt": "2024-01-15T17:00:00Z",
202            "primaryTicker": "AAPL",
203            "primaryExchange": "NASDAQ",
204            "sortedAt": "2024-01-15T16:30:00Z",
205            "entityClass": "company"
206        });
207
208        let filing: Filing = serde_json::from_value(json).unwrap();
209        assert_eq!(filing.accession_number, "0000950170-24-000001");
210        assert_eq!(filing.cik, 320193);
211        assert_eq!(filing.form_type, "10-K");
212        assert!(!filing.provisional);
213        assert_eq!(filing.primary_ticker, Some("AAPL".to_string()));
214        assert_eq!(filing.entity_class, Some(EntityClass::Company));
215    }
216
217    #[test]
218    fn test_deserialize_filing_minimal() {
219        let json = json!({
220            "accessionNumber": "0000950170-24-000001",
221            "cik": 320193,
222            "formType": "8-K",
223            "filedAt": "2024-01-15T16:30:00Z",
224            "provisional": true,
225            "sizeBytes": 1000,
226            "url": "https://www.sec.gov/...",
227            "title": "Form 8-K",
228            "status": "provisional",
229            "updatedAt": "2024-01-15T17:00:00Z",
230            "sortedAt": "2024-01-15T16:30:00Z"
231        });
232
233        let filing: Filing = serde_json::from_value(json).unwrap();
234        assert!(filing.provisional);
235        assert!(filing.company_name.is_none());
236        assert!(filing.primary_ticker.is_none());
237        assert!(filing.entity_class.is_none());
238    }
239
240    #[test]
241    fn test_deserialize_filing_document() {
242        let json = json!({
243            "seq": 1,
244            "filename": "aapl-20231230.htm",
245            "docType": "10-K",
246            "description": "10-K Annual Report",
247            "isPrimary": true
248        });
249
250        let doc: FilingDocument = serde_json::from_value(json).unwrap();
251        assert_eq!(doc.seq, 1);
252        assert_eq!(doc.filename, "aapl-20231230.htm");
253        assert!(doc.is_primary);
254    }
255
256    #[test]
257    fn test_deserialize_filing_role() {
258        let json = json!({
259            "cik": 320193,
260            "role": "filer"
261        });
262
263        let role: FilingRole = serde_json::from_value(json).unwrap();
264        assert_eq!(role.cik, 320193);
265        assert_eq!(role.role, "filer");
266    }
267
268    #[test]
269    fn test_deserialize_filing_detail() {
270        let json = json!({
271            "accessionNumber": "0000950170-24-000001",
272            "cik": 320193,
273            "formType": "10-K",
274            "filedAt": "2024-01-15T16:30:00Z",
275            "provisional": false,
276            "title": "Form 10-K",
277            "url": "https://www.sec.gov/...",
278            "sizeBytes": 12345678,
279            "documents": [
280                {
281                    "seq": 1,
282                    "filename": "aapl-20231230.htm",
283                    "docType": "10-K",
284                    "isPrimary": true
285                }
286            ],
287            "roles": [
288                {
289                    "cik": 320193,
290                    "role": "filer"
291                }
292            ]
293        });
294
295        let detail: FilingDetail = serde_json::from_value(json).unwrap();
296        assert_eq!(detail.accession_number, "0000950170-24-000001");
297        assert_eq!(detail.documents.len(), 1);
298        assert_eq!(detail.roles.len(), 1);
299        assert!(detail.documents[0].is_primary);
300    }
301
302    #[test]
303    fn test_filing_is_clone() {
304        let json = json!({
305            "accessionNumber": "0000950170-24-000001",
306            "cik": 320193,
307            "formType": "10-K",
308            "filedAt": "2024-01-15T16:30:00Z",
309            "provisional": false,
310            "sizeBytes": 1000,
311            "url": "https://www.sec.gov/...",
312            "title": "Form 10-K",
313            "status": "final",
314            "updatedAt": "2024-01-15T17:00:00Z",
315            "sortedAt": "2024-01-15T16:30:00Z"
316        });
317
318        let filing: Filing = serde_json::from_value(json).unwrap();
319        let cloned = filing.clone();
320        assert_eq!(cloned.accession_number, filing.accession_number);
321    }
322
323    #[test]
324    fn test_serialize_filing() {
325        let json = json!({
326            "accessionNumber": "0000950170-24-000001",
327            "cik": 320193,
328            "formType": "10-K",
329            "filedAt": "2024-01-15T16:30:00Z",
330            "provisional": false,
331            "sizeBytes": 1000,
332            "url": "https://www.sec.gov/...",
333            "title": "Form 10-K",
334            "status": "final",
335            "updatedAt": "2024-01-15T17:00:00Z",
336            "sortedAt": "2024-01-15T16:30:00Z"
337        });
338
339        let filing: Filing = serde_json::from_value(json.clone()).unwrap();
340        let serialized = serde_json::to_value(&filing).unwrap();
341        assert_eq!(serialized["accessionNumber"], "0000950170-24-000001");
342        assert_eq!(serialized["formType"], "10-K");
343    }
344}