1use crate::{client::FmpClient, error::Result, models::transcripts::*};
4use serde::Serialize;
5
6pub struct Transcripts {
8 client: FmpClient,
9}
10
11impl Transcripts {
12 pub(crate) fn new(client: FmpClient) -> Self {
13 Self { client }
14 }
15
16 pub async fn get_transcript_list(
43 &self,
44 symbol: &str,
45 year: Option<i32>,
46 ) -> Result<Vec<TranscriptSummary>> {
47 #[derive(Serialize)]
48 struct Query<'a> {
49 #[serde(skip_serializing_if = "Option::is_none")]
50 year: Option<i32>,
51 apikey: &'a str,
52 }
53
54 let url = self
55 .client
56 .build_url(&format!("/earning_call_transcript/{}", symbol));
57 self.client
58 .get_with_query(
59 &url,
60 &Query {
61 year,
62 apikey: self.client.api_key(),
63 },
64 )
65 .await
66 }
67
68 pub async fn get_earnings_transcript(
98 &self,
99 symbol: &str,
100 year: i32,
101 quarter: i32,
102 ) -> Result<Vec<EarningsTranscript>> {
103 #[derive(Serialize)]
104 struct Query<'a> {
105 year: i32,
106 quarter: i32,
107 apikey: &'a str,
108 }
109
110 let url = self
111 .client
112 .build_url(&format!("/earning_call_transcript/{}", symbol));
113 self.client
114 .get_with_query(
115 &url,
116 &Query {
117 year,
118 quarter,
119 apikey: self.client.api_key(),
120 },
121 )
122 .await
123 }
124
125 pub async fn get_press_releases(
153 &self,
154 symbol: &str,
155 limit: Option<i32>,
156 ) -> Result<Vec<PressRelease>> {
157 #[derive(Serialize)]
158 struct Query<'a> {
159 #[serde(skip_serializing_if = "Option::is_none")]
160 limit: Option<i32>,
161 apikey: &'a str,
162 }
163
164 let url = self
165 .client
166 .build_url(&format!("/press-releases/{}", symbol));
167 self.client
168 .get_with_query(
169 &url,
170 &Query {
171 limit,
172 apikey: self.client.api_key(),
173 },
174 )
175 .await
176 }
177
178 pub async fn get_conference_schedule(
205 &self,
206 from_date: Option<&str>,
207 to_date: Option<&str>,
208 ) -> Result<Vec<ConferenceCall>> {
209 #[derive(Serialize)]
210 struct Query<'a> {
211 #[serde(skip_serializing_if = "Option::is_none")]
212 from: Option<&'a str>,
213 #[serde(skip_serializing_if = "Option::is_none")]
214 to: Option<&'a str>,
215 apikey: &'a str,
216 }
217
218 let url = self.client.build_url("/earning_calendar");
219 self.client
220 .get_with_query(
221 &url,
222 &Query {
223 from: from_date,
224 to: to_date,
225 apikey: self.client.api_key(),
226 },
227 )
228 .await
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 fn create_test_client() -> FmpClient {
237 FmpClient::builder().api_key("test_key").build().unwrap()
238 }
239
240 #[test]
241 fn test_new() {
242 let client = create_test_client();
243 let transcripts = Transcripts::new(client);
244 }
246
247 #[tokio::test]
248 #[ignore] async fn test_get_transcript_list() {
250 let client = FmpClient::new().unwrap();
251 let result = client
252 .transcripts()
253 .get_transcript_list("AAPL", Some(2024))
254 .await;
255 assert!(result.is_ok());
256
257 let transcripts = result.unwrap();
258 if !transcripts.is_empty() {
259 let first_transcript = &transcripts[0];
260 assert!(first_transcript.symbol.is_some());
261 assert!(first_transcript.quarter.is_some() || first_transcript.year.is_some());
262 println!("Found {} transcript summaries for AAPL", transcripts.len());
263 }
264 }
265
266 #[tokio::test]
267 #[ignore] async fn test_get_transcript_list_no_year() {
269 let client = FmpClient::new().unwrap();
270 let result = client.transcripts().get_transcript_list("AAPL", None).await;
271 assert!(result.is_ok());
272
273 let transcripts = result.unwrap();
274 println!(
275 "Found {} total transcript summaries for AAPL",
276 transcripts.len()
277 );
278 }
279
280 #[tokio::test]
281 #[ignore] async fn test_get_earnings_transcript() {
283 let client = FmpClient::new().unwrap();
284 let result = client
285 .transcripts()
286 .get_earnings_transcript("AAPL", 2023, 4)
287 .await;
288 assert!(result.is_ok());
289
290 let transcripts = result.unwrap();
291 if let Some(transcript) = transcripts.first() {
292 assert_eq!(transcript.symbol.as_deref(), Some("AAPL"));
293 assert_eq!(transcript.year, Some(2023));
294
295 if let Some(content) = &transcript.content {
296 assert!(!content.is_empty());
297 println!("Transcript content length: {} characters", content.len());
298 }
299 }
300 }
301
302 #[tokio::test]
303 #[ignore] async fn test_get_press_releases() {
305 let client = FmpClient::new().unwrap();
306 let result = client
307 .transcripts()
308 .get_press_releases("AAPL", Some(10))
309 .await;
310 assert!(result.is_ok());
311
312 let releases = result.unwrap();
313 if !releases.is_empty() {
314 let first_release = &releases[0];
315 assert!(first_release.symbol.is_some() || first_release.company_name.is_some());
316 assert!(first_release.title.is_some());
317 assert!(first_release.date.is_some());
318 println!("Found {} press releases for AAPL", releases.len());
319 }
320 }
321
322 #[tokio::test]
323 #[ignore] async fn test_get_press_releases_no_limit() {
325 let client = FmpClient::new().unwrap();
326 let result = client.transcripts().get_press_releases("MSFT", None).await;
327 assert!(result.is_ok());
328
329 let releases = result.unwrap();
330 println!("Found {} total press releases for MSFT", releases.len());
331 }
332
333 #[tokio::test]
334 #[ignore] async fn test_get_conference_schedule() {
336 let client = FmpClient::new().unwrap();
337 let result = client
338 .transcripts()
339 .get_conference_schedule(Some("2024-01-01"), Some("2024-01-31"))
340 .await;
341 assert!(result.is_ok());
342
343 let calls = result.unwrap();
344 if !calls.is_empty() {
345 let first_call = &calls[0];
346 assert!(first_call.symbol.is_some());
347 assert!(first_call.date_time.is_some() || first_call.date_time.is_some());
348 println!("Found {} conference calls in January 2024", calls.len());
349 }
350 }
351
352 #[tokio::test]
353 #[ignore] async fn test_get_conference_schedule_no_dates() {
355 let client = FmpClient::new().unwrap();
356 let result = client
357 .transcripts()
358 .get_conference_schedule(None, None)
359 .await;
360 assert!(result.is_ok());
361
362 let calls = result.unwrap();
363 println!("Found {} total upcoming conference calls", calls.len());
364 }
365
366 #[test]
367 fn test_transcript_models_serialization() {
368 use serde_json;
369
370 let transcript = EarningsTranscript {
372 symbol: Some("AAPL".to_string()),
373 quarter: Some("Q1".to_string()),
374 year: Some(2024),
375 date: Some("2024-02-01".to_string()),
376 content: Some("Thank you for joining Apple's Q1 2024 earnings call...".to_string()),
377 company_name: Some("Apple Inc.".to_string()),
378 fiscal_quarter: Some("Q1".to_string()),
379 fiscal_year: Some(2024),
380 transcript_id: Some("AAPL-2024-Q1".to_string()),
381 language: Some("English".to_string()),
382 duration: Some(60),
383 analyst_count: Some(15),
384 call_type: Some("Earnings".to_string()),
385 };
386
387 let json = serde_json::to_string(&transcript).unwrap();
388 let deserialized: EarningsTranscript = serde_json::from_str(&json).unwrap();
389 assert_eq!(deserialized.symbol, Some("AAPL".to_string()));
390 assert_eq!(deserialized.year, Some(2024));
391
392 let release = PressRelease {
394 symbol: Some("AAPL".to_string()),
395 company_name: Some("Apple Inc.".to_string()),
396 title: Some("Apple Reports Record Q1 Results".to_string()),
397 date: Some("2024-02-01".to_string()),
398 content: Some("Apple today announced financial results for Q1...".to_string()),
399 release_type: Some("Earnings".to_string()),
400 source: Some("Business Wire".to_string()),
401 url: Some("https://investor.apple.com/news/press-release-details/2024/Apple-Reports-Record-Q1-Results/default.aspx".to_string()),
402 language: Some("English".to_string()),
403 word_count: Some(1250),
404 summary: Some("Apple reported record Q1 revenue of $119.6 billion...".to_string()),
405 related_symbols: Some(vec!["AAPL".to_string(), "NASDAQ".to_string()]),
406 tags: Some(vec!["Earnings".to_string(), "Technology".to_string()]),
407 };
408
409 let json = serde_json::to_string(&release).unwrap();
410 let deserialized: PressRelease = serde_json::from_str(&json).unwrap();
411 assert_eq!(deserialized.symbol, Some("AAPL".to_string()));
412 assert_eq!(
413 deserialized.title,
414 Some("Apple Reports Record Q1 Results".to_string())
415 );
416 }
417
418 #[test]
419 fn test_conference_call_model() {
420 let call = ConferenceCall {
421 symbol: Some("AAPL".to_string()),
422 company_name: Some("Apple Inc.".to_string()),
423 title: Some("Q1 2024 Earnings Call".to_string()),
424 date_time: Some("2024-02-01 17:00:00".to_string()),
425 call_type: Some("Earnings".to_string()),
426 quarter: Some("Q1".to_string()),
427 fiscal_year: Some(2024),
428 year: Some(2024),
429 timezone: Some("EST".to_string()),
430 dial_in_info: Some("1-800-123-4567, Conference ID: 12345".to_string()),
431 webcast_url: Some("https://investor.apple.com/webcast".to_string()),
432 estimated_duration: Some(60),
433 participants: Some(vec![
434 "Tim Cook - CEO".to_string(),
435 "Luca Maestri - CFO".to_string(),
436 ]),
437 status: Some("Scheduled".to_string()),
438 industry: Some("Technology".to_string()),
439 market_cap_category: Some("Large Cap".to_string()),
440 };
441
442 assert_eq!(call.symbol, Some("AAPL".to_string()));
444 assert_eq!(call.call_type, Some("Earnings".to_string()));
445 assert_eq!(call.estimated_duration, Some(60));
446
447 let json = serde_json::to_string(&call).unwrap();
448 let deserialized: ConferenceCall = serde_json::from_str(&json).unwrap();
449 assert_eq!(deserialized.symbol, Some("AAPL".to_string()));
450 }
451
452 #[test]
453 fn test_date_parameter_validation() {
454 let start_date = "2024-01-01";
456 let end_date = "2024-12-31";
457
458 assert!(chrono::NaiveDate::parse_from_str(start_date, "%Y-%m-%d").is_ok());
460 assert!(chrono::NaiveDate::parse_from_str(end_date, "%Y-%m-%d").is_ok());
461 }
462
463 #[test]
464 fn test_quarter_validation() {
465 let valid_quarters = [1, 2, 3, 4];
466 for quarter in valid_quarters {
467 assert!(
468 quarter >= 1 && quarter <= 4,
469 "Quarter {} should be valid",
470 quarter
471 );
472 }
473
474 let invalid_quarters = [0, 5, -1, 13];
475 for quarter in invalid_quarters {
476 assert!(
477 !(quarter >= 1 && quarter <= 4),
478 "Quarter {} should be invalid",
479 quarter
480 );
481 }
482 }
483}