1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(rename_all = "camelCase")]
12pub struct FilingCompany {
13 pub cik: u64,
15 pub name: String,
17 pub state_of_incorporation: Option<String>,
19 pub state_of_incorporation_description: Option<String>,
21 pub fiscal_year_end: Option<String>,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(rename_all = "lowercase")]
28pub enum EntityClass {
29 Company,
31 Person,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
39#[serde(rename_all = "camelCase")]
40pub struct Filing {
41 pub accession_number: String,
43 pub accession_no_dashes: Option<String>,
45 pub cik: u64,
47 pub company_name: Option<String>,
49 pub form_type: String,
51 pub filed_at: DateTime<Utc>,
53 pub accept_ts: Option<DateTime<Utc>>,
55 pub provisional: bool,
57 pub feed_day: Option<String>,
59 pub size_bytes: u64,
61 pub url: String,
63 pub title: String,
65 pub status: String,
67 pub updated_at: DateTime<Utc>,
69 pub primary_ticker: Option<String>,
71 pub primary_exchange: Option<String>,
73 pub company: Option<FilingCompany>,
75 pub sorted_at: DateTime<Utc>,
77 pub logo_url: Option<String>,
79 pub entity_class: Option<EntityClass>,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
85#[serde(rename_all = "camelCase")]
86pub struct FilingDocument {
87 pub seq: u32,
89 pub filename: String,
91 pub doc_type: String,
93 pub description: Option<String>,
95 pub is_primary: bool,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101#[serde(rename_all = "camelCase")]
102pub struct FilingRole {
103 pub cik: u64,
105 pub role: String,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
114#[serde(rename_all = "camelCase")]
115pub struct FilingDetail {
116 pub accession_number: String,
118 pub accession_no_dashes: Option<String>,
120 pub cik: u64,
122 pub form_type: String,
124 pub filed_at: DateTime<Utc>,
126 pub accept_ts: Option<DateTime<Utc>>,
128 pub provisional: bool,
130 pub feed_day: Option<String>,
132 pub title: String,
134 pub url: String,
136 pub size_bytes: u64,
138 pub sec_relative_dir: Option<String>,
140 pub company_name: Option<String>,
142 pub primary_ticker: Option<String>,
144 pub company: Option<FilingCompany>,
146 pub documents: Vec<FilingDocument>,
148 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}