Skip to main content

finance_query_core/models/
earnings_transcripts.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
6#[serde(rename_all = "UPPERCASE")]
7#[allow(dead_code)]
8pub enum Quarter {
9    Q1,
10    Q2,
11    Q3,
12    Q4,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16#[serde(rename_all = "snake_case")]
17pub struct EarningsCallListing {
18    pub event_id: String,
19    pub quarter: Option<String>,
20    pub year: Option<i32>,
21    pub title: String,
22    pub url: String,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26#[serde(rename_all = "snake_case")]
27pub struct EarningsCallsList {
28    pub symbol: String,
29    pub earnings_calls: Vec<EarningsCallListing>,
30    pub total: usize,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
34#[serde(rename_all = "snake_case")]
35pub struct TranscriptSpeaker {
36    pub name: String,
37    pub role: Option<String>,
38    pub company: Option<String>,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
42#[serde(rename_all = "snake_case")]
43pub struct TranscriptParagraph {
44    pub speaker: String,
45    pub text: String,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
49#[serde(rename_all = "snake_case")]
50pub struct EarningsTranscript {
51    pub symbol: String,
52    pub quarter: String,
53    pub year: i32,
54    pub date: DateTime<Utc>,
55    pub title: String,
56    pub speakers: Vec<TranscriptSpeaker>,
57    pub paragraphs: Vec<TranscriptParagraph>,
58    pub metadata: HashMap<String, serde_json::Value>,
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64    use proptest::prelude::*;
65
66    fn optional_string() -> impl Strategy<Value = Option<String>> {
67        proptest::option::of("[A-Za-z ]{1,30}")
68    }
69
70    // **Feature: crate-extraction, Property 1: Model Serialization Round-Trip**
71    // **Validates: Requirements 2.2**
72    proptest! {
73        #![proptest_config(ProptestConfig::with_cases(100))]
74
75        #[test]
76        fn transcript_speaker_roundtrip(
77            name in "[A-Za-z ]{1,30}",
78            role in optional_string(),
79            company in optional_string(),
80        ) {
81            let speaker = TranscriptSpeaker {
82                name: name.clone(),
83                role: role.clone(),
84                company: company.clone(),
85            };
86
87            let json = serde_json::to_string(&speaker).unwrap();
88            let parsed: TranscriptSpeaker = serde_json::from_str(&json).unwrap();
89
90            prop_assert_eq!(speaker.name, parsed.name);
91            prop_assert_eq!(speaker.role, parsed.role);
92            prop_assert_eq!(speaker.company, parsed.company);
93        }
94
95        #[test]
96        fn transcript_paragraph_roundtrip(
97            speaker in "[A-Za-z ]{1,30}",
98            text in "[A-Za-z0-9 .,!?]{1,100}",
99        ) {
100            let para = TranscriptParagraph {
101                speaker: speaker.clone(),
102                text: text.clone(),
103            };
104
105            let json = serde_json::to_string(&para).unwrap();
106            let parsed: TranscriptParagraph = serde_json::from_str(&json).unwrap();
107
108            prop_assert_eq!(para.speaker, parsed.speaker);
109            prop_assert_eq!(para.text, parsed.text);
110        }
111
112        #[test]
113        fn earnings_call_listing_roundtrip(
114            event_id in "[a-z0-9]{8}",
115            quarter in optional_string(),
116            year in proptest::option::of(2000i32..2030i32),
117            title in "[A-Za-z0-9 ]{1,50}",
118            url in "https://[a-z]{5,10}\\.com/[a-z]{5,10}",
119        ) {
120            let listing = EarningsCallListing {
121                event_id: event_id.clone(),
122                quarter: quarter.clone(),
123                year,
124                title: title.clone(),
125                url: url.clone(),
126            };
127
128            let json = serde_json::to_string(&listing).unwrap();
129            let parsed: EarningsCallListing = serde_json::from_str(&json).unwrap();
130
131            prop_assert_eq!(listing.event_id, parsed.event_id);
132            prop_assert_eq!(listing.quarter, parsed.quarter);
133            prop_assert_eq!(listing.year, parsed.year);
134            prop_assert_eq!(listing.title, parsed.title);
135            prop_assert_eq!(listing.url, parsed.url);
136        }
137    }
138}