fmp_rs/endpoints/
financials.rs

1//! Financial statements endpoints
2
3use crate::client::FmpClient;
4use crate::error::Result;
5use crate::models::common::Period;
6use crate::models::financials::{
7    BalanceSheet, CashFlowStatement, FinancialAsReported, FinancialGrowth, FinancialRatios,
8    IncomeStatement, KeyMetrics, RevenueGeographicSegmentation, RevenueProductSegmentation,
9};
10use serde::Serialize;
11
12/// Financial statements API endpoints
13pub struct Financials {
14    client: FmpClient,
15}
16
17impl Financials {
18    pub(crate) fn new(client: FmpClient) -> Self {
19        Self { client }
20    }
21
22    /// Get income statements
23    pub async fn get_income_statement(
24        &self,
25        symbol: &str,
26        period: Period,
27        limit: Option<u32>,
28    ) -> Result<Vec<IncomeStatement>> {
29        #[derive(Serialize)]
30        struct Query<'a> {
31            symbol: &'a str,
32            period: &'a str,
33            #[serde(skip_serializing_if = "Option::is_none")]
34            limit: Option<u32>,
35            apikey: &'a str,
36        }
37
38        let url = self.client.build_url("/income-statement");
39        self.client
40            .get_with_query(
41                &url,
42                &Query {
43                    symbol,
44                    period: &period.to_string(),
45                    limit,
46                    apikey: self.client.api_key(),
47                },
48            )
49            .await
50    }
51
52    /// Get balance sheet
53    pub async fn get_balance_sheet(
54        &self,
55        symbol: &str,
56        period: Period,
57        limit: Option<u32>,
58    ) -> Result<Vec<BalanceSheet>> {
59        #[derive(Serialize)]
60        struct Query<'a> {
61            symbol: &'a str,
62            period: &'a str,
63            #[serde(skip_serializing_if = "Option::is_none")]
64            limit: Option<u32>,
65            apikey: &'a str,
66        }
67
68        let url = self.client.build_url("/balance-sheet-statement");
69        self.client
70            .get_with_query(
71                &url,
72                &Query {
73                    symbol,
74                    period: &period.to_string(),
75                    limit,
76                    apikey: self.client.api_key(),
77                },
78            )
79            .await
80    }
81
82    /// Get cash flow statement
83    pub async fn get_cash_flow_statement(
84        &self,
85        symbol: &str,
86        period: Period,
87        limit: Option<u32>,
88    ) -> Result<Vec<CashFlowStatement>> {
89        #[derive(Serialize)]
90        struct Query<'a> {
91            symbol: &'a str,
92            period: &'a str,
93            #[serde(skip_serializing_if = "Option::is_none")]
94            limit: Option<u32>,
95            apikey: &'a str,
96        }
97
98        let url = self.client.build_url("/cash-flow-statement");
99        self.client
100            .get_with_query(
101                &url,
102                &Query {
103                    symbol,
104                    period: &period.to_string(),
105                    limit,
106                    apikey: self.client.api_key(),
107                },
108            )
109            .await
110    }
111
112    /// Get financial ratios
113    pub async fn get_ratios(
114        &self,
115        symbol: &str,
116        period: Period,
117        limit: Option<u32>,
118    ) -> Result<Vec<FinancialRatios>> {
119        #[derive(Serialize)]
120        struct Query<'a> {
121            symbol: &'a str,
122            period: &'a str,
123            #[serde(skip_serializing_if = "Option::is_none")]
124            limit: Option<u32>,
125            apikey: &'a str,
126        }
127
128        let url = self.client.build_url("/ratios");
129        self.client
130            .get_with_query(
131                &url,
132                &Query {
133                    symbol,
134                    period: &period.to_string(),
135                    limit,
136                    apikey: self.client.api_key(),
137                },
138            )
139            .await
140    }
141
142    /// Get key metrics
143    pub async fn get_key_metrics(
144        &self,
145        symbol: &str,
146        period: Period,
147        limit: Option<u32>,
148    ) -> Result<Vec<KeyMetrics>> {
149        #[derive(Serialize)]
150        struct Query<'a> {
151            symbol: &'a str,
152            period: &'a str,
153            #[serde(skip_serializing_if = "Option::is_none")]
154            limit: Option<u32>,
155            apikey: &'a str,
156        }
157
158        let url = self.client.build_url("/key-metrics");
159        self.client
160            .get_with_query(
161                &url,
162                &Query {
163                    symbol,
164                    period: &period.to_string(),
165                    limit,
166                    apikey: self.client.api_key(),
167                },
168            )
169            .await
170    }
171
172    /// Get key metrics TTM (Trailing Twelve Months)
173    pub async fn get_key_metrics_ttm(&self, symbol: &str) -> Result<Vec<KeyMetrics>> {
174        #[derive(Serialize)]
175        struct Query<'a> {
176            symbol: &'a str,
177            apikey: &'a str,
178        }
179
180        let url = self.client.build_url("/key-metrics-ttm");
181        self.client
182            .get_with_query(
183                &url,
184                &Query {
185                    symbol,
186                    apikey: self.client.api_key(),
187                },
188            )
189            .await
190    }
191
192    /// Get financial ratios TTM (Trailing Twelve Months)
193    pub async fn get_ratios_ttm(&self, symbol: &str) -> Result<Vec<FinancialRatios>> {
194        #[derive(Serialize)]
195        struct Query<'a> {
196            symbol: &'a str,
197            apikey: &'a str,
198        }
199
200        let url = self.client.build_url("/ratios-ttm");
201        self.client
202            .get_with_query(
203                &url,
204                &Query {
205                    symbol,
206                    apikey: self.client.api_key(),
207                },
208            )
209            .await
210    }
211
212    /// Get financial growth metrics
213    ///
214    /// Returns year-over-year and multi-year growth rates for key financial metrics.
215    ///
216    /// # Arguments
217    /// * `symbol` - Stock symbol (e.g., "AAPL")
218    /// * `period` - Period (Annual or Quarter)
219    /// * `limit` - Number of results (optional)
220    ///
221    /// # Example
222    /// ```no_run
223    /// # use fmp_rs::{FmpClient, models::common::Period};
224    /// # #[tokio::main]
225    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
226    /// let client = FmpClient::new()?;
227    /// let growth = client.financials().get_financial_growth("AAPL", Period::Annual, Some(5)).await?;
228    /// for g in growth {
229    ///     println!("{}: Revenue growth: {:.2}%, Net income growth: {:.2}%",
230    ///         g.date, g.revenue_growth * 100.0, g.net_income_growth * 100.0);
231    /// }
232    /// # Ok(())
233    /// # }
234    /// ```
235    pub async fn get_financial_growth(
236        &self,
237        symbol: &str,
238        period: Period,
239        limit: Option<u32>,
240    ) -> Result<Vec<FinancialGrowth>> {
241        #[derive(Serialize)]
242        struct Query<'a> {
243            symbol: &'a str,
244            period: &'a str,
245            #[serde(skip_serializing_if = "Option::is_none")]
246            limit: Option<u32>,
247            apikey: &'a str,
248        }
249
250        let url = self.client.build_url("/financial-growth");
251        self.client
252            .get_with_query(
253                &url,
254                &Query {
255                    symbol,
256                    period: &period.to_string(),
257                    limit,
258                    apikey: self.client.api_key(),
259                },
260            )
261            .await
262    }
263
264    /// Get financial statement as reported (XBRL data)
265    ///
266    /// Returns financial statements as reported to the SEC with XBRL tags.
267    ///
268    /// # Arguments
269    /// * `symbol` - Stock symbol (e.g., "AAPL")
270    /// * `period` - Period (Annual or Quarter)
271    /// * `limit` - Number of results (optional)
272    ///
273    /// # Example
274    /// ```no_run
275    /// # use fmp_rs::{FmpClient, models::common::Period};
276    /// # #[tokio::main]
277    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
278    /// let client = FmpClient::new()?;
279    /// let reported = client.financials().get_financial_as_reported("AAPL", Period::Annual, Some(1)).await?;
280    /// for statement in reported {
281    ///     println!("{}: {} fields reported", statement.date, statement.data.len());
282    /// }
283    /// # Ok(())
284    /// # }
285    /// ```
286    pub async fn get_financial_as_reported(
287        &self,
288        symbol: &str,
289        period: Period,
290        limit: Option<u32>,
291    ) -> Result<Vec<FinancialAsReported>> {
292        #[derive(Serialize)]
293        struct Query<'a> {
294            symbol: &'a str,
295            period: &'a str,
296            #[serde(skip_serializing_if = "Option::is_none")]
297            limit: Option<u32>,
298            apikey: &'a str,
299        }
300
301        let url = self
302            .client
303            .build_url("/financial-statement-full-as-reported");
304        self.client
305            .get_with_query(
306                &url,
307                &Query {
308                    symbol,
309                    period: &period.to_string(),
310                    limit,
311                    apikey: self.client.api_key(),
312                },
313            )
314            .await
315    }
316
317    /// Get revenue product segmentation
318    ///
319    /// Returns revenue breakdown by product or service line.
320    ///
321    /// # Arguments
322    /// * `symbol` - Stock symbol (e.g., "AAPL")
323    /// * `period` - Period (Annual or Quarter)
324    /// * `structure` - "flat" for simple structure (optional)
325    ///
326    /// # Example
327    /// ```no_run
328    /// # use fmp_rs::{FmpClient, models::common::Period};
329    /// # #[tokio::main]
330    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
331    /// let client = FmpClient::new()?;
332    /// let segments = client.financials().get_revenue_product_segmentation("AAPL", Period::Annual, None).await?;
333    /// for seg in segments {
334    ///     println!("{}: {} product segments", seg.date, seg.segments.len());
335    /// }
336    /// # Ok(())
337    /// # }
338    /// ```
339    pub async fn get_revenue_product_segmentation(
340        &self,
341        symbol: &str,
342        period: Period,
343        structure: Option<&str>,
344    ) -> Result<Vec<RevenueProductSegmentation>> {
345        #[derive(Serialize)]
346        struct Query<'a> {
347            symbol: &'a str,
348            period: &'a str,
349            #[serde(skip_serializing_if = "Option::is_none")]
350            structure: Option<&'a str>,
351            apikey: &'a str,
352        }
353
354        let url = self.client.build_url("/revenue-product-segmentation");
355        self.client
356            .get_with_query(
357                &url,
358                &Query {
359                    symbol,
360                    period: &period.to_string(),
361                    structure,
362                    apikey: self.client.api_key(),
363                },
364            )
365            .await
366    }
367
368    /// Get revenue geographic segmentation
369    ///
370    /// Returns revenue breakdown by geographic region.
371    ///
372    /// # Arguments
373    /// * `symbol` - Stock symbol (e.g., "AAPL")
374    /// * `period` - Period (Annual or Quarter)
375    /// * `structure` - "flat" for simple structure (optional)
376    ///
377    /// # Example
378    /// ```no_run
379    /// # use fmp_rs::{FmpClient, models::common::Period};
380    /// # #[tokio::main]
381    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
382    /// let client = FmpClient::new()?;
383    /// let segments = client.financials().get_revenue_geographic_segmentation("AAPL", Period::Annual, None).await?;
384    /// for seg in segments {
385    ///     println!("{}: {} geographic segments", seg.date, seg.segments.len());
386    /// }
387    /// # Ok(())
388    /// # }
389    /// ```
390    pub async fn get_revenue_geographic_segmentation(
391        &self,
392        symbol: &str,
393        period: Period,
394        structure: Option<&str>,
395    ) -> Result<Vec<RevenueGeographicSegmentation>> {
396        #[derive(Serialize)]
397        struct Query<'a> {
398            symbol: &'a str,
399            period: &'a str,
400            #[serde(skip_serializing_if = "Option::is_none")]
401            structure: Option<&'a str>,
402            apikey: &'a str,
403        }
404
405        let url = self.client.build_url("/revenue-geographic-segmentation");
406        self.client
407            .get_with_query(
408                &url,
409                &Query {
410                    symbol,
411                    period: &period.to_string(),
412                    structure,
413                    apikey: self.client.api_key(),
414                },
415            )
416            .await
417    }
418
419    /// Get full financial statement as reported (comprehensive XBRL data)
420    ///
421    /// Returns the complete set of as-reported financial data from SEC filings.
422    /// This is more comprehensive than `get_financial_as_reported()`.
423    ///
424    /// # Arguments
425    /// * `symbol` - Stock symbol (e.g., "AAPL")
426    /// * `period` - Period (Annual or Quarter)
427    ///
428    /// # Example
429    /// ```no_run
430    /// # use fmp_rs::{FmpClient, models::common::Period};
431    /// # #[tokio::main]
432    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
433    /// let client = FmpClient::new()?;
434    /// let statements = client.financials().get_financial_full_as_reported("AAPL", Period::Annual).await?;
435    /// for stmt in statements.iter().take(1) {
436    ///     println!("Date: {}, Period: {}", stmt.date, stmt.period);
437    ///     println!("XBRL fields: {}", stmt.data.len());
438    /// }
439    /// # Ok(())
440    /// # }
441    /// ```
442    pub async fn get_financial_full_as_reported(
443        &self,
444        symbol: &str,
445        period: Period,
446    ) -> Result<Vec<FinancialAsReported>> {
447        #[derive(Serialize)]
448        struct Query<'a> {
449            symbol: &'a str,
450            period: &'a str,
451            apikey: &'a str,
452        }
453
454        let url = self
455            .client
456            .build_url("/financial-statement-full-as-reported");
457        self.client
458            .get_with_query(
459                &url,
460                &Query {
461                    symbol,
462                    period: &period.to_string(),
463                    apikey: self.client.api_key(),
464                },
465            )
466            .await
467    }
468}
469
470#[cfg(test)]
471mod tests {
472    use super::*;
473
474    #[test]
475    fn test_new() {
476        let client = FmpClient::builder().api_key("test_key").build().unwrap();
477        let _ = Financials::new(client);
478    }
479
480    // Golden path tests
481    #[tokio::test]
482    #[ignore = "requires FMP API key"]
483    async fn test_get_income_statement() {
484        let client = FmpClient::new().unwrap();
485        let result = client
486            .financials()
487            .get_income_statement("AAPL", Period::Annual, Some(5))
488            .await;
489        assert!(result.is_ok());
490        let statements = result.unwrap();
491        assert!(!statements.is_empty());
492        assert!(statements.len() <= 5);
493    }
494
495    #[tokio::test]
496    #[ignore = "requires FMP API key"]
497    async fn test_get_balance_sheet() {
498        let client = FmpClient::new().unwrap();
499        let result = client
500            .financials()
501            .get_balance_sheet("AAPL", Period::Annual, Some(5))
502            .await;
503        assert!(result.is_ok());
504        let statements = result.unwrap();
505        assert!(!statements.is_empty());
506    }
507
508    #[tokio::test]
509    #[ignore = "requires FMP API key"]
510    async fn test_get_cash_flow_statement() {
511        let client = FmpClient::new().unwrap();
512        let result = client
513            .financials()
514            .get_cash_flow_statement("AAPL", Period::Annual, Some(5))
515            .await;
516        assert!(result.is_ok());
517        let statements = result.unwrap();
518        assert!(!statements.is_empty());
519    }
520
521    #[tokio::test]
522    #[ignore = "requires FMP API key"]
523    async fn test_get_ratios() {
524        let client = FmpClient::new().unwrap();
525        let result = client
526            .financials()
527            .get_ratios("AAPL", Period::Annual, Some(5))
528            .await;
529        assert!(result.is_ok());
530        let ratios = result.unwrap();
531        assert!(!ratios.is_empty());
532    }
533
534    #[tokio::test]
535    #[ignore = "requires FMP API key"]
536    async fn test_get_key_metrics() {
537        let client = FmpClient::new().unwrap();
538        let result = client
539            .financials()
540            .get_key_metrics("AAPL", Period::Annual, Some(5))
541            .await;
542        assert!(result.is_ok());
543        let metrics = result.unwrap();
544        assert!(!metrics.is_empty());
545    }
546
547    #[tokio::test]
548    #[ignore = "requires FMP API key"]
549    async fn test_get_key_metrics_ttm() {
550        let client = FmpClient::new().unwrap();
551        let result = client.financials().get_key_metrics_ttm("AAPL").await;
552        assert!(result.is_ok());
553        let metrics = result.unwrap();
554        assert!(!metrics.is_empty());
555    }
556
557    #[tokio::test]
558    #[ignore = "requires FMP API key"]
559    async fn test_get_ratios_ttm() {
560        let client = FmpClient::new().unwrap();
561        let result = client.financials().get_ratios_ttm("AAPL").await;
562        assert!(result.is_ok());
563        let ratios = result.unwrap();
564        assert!(!ratios.is_empty());
565    }
566
567    #[tokio::test]
568    #[ignore = "requires FMP API key"]
569    async fn test_get_financial_growth() {
570        let client = FmpClient::new().unwrap();
571        let result = client
572            .financials()
573            .get_financial_growth("AAPL", Period::Annual, Some(5))
574            .await;
575        assert!(result.is_ok());
576        let growth = result.unwrap();
577        assert!(!growth.is_empty());
578        assert!(growth.len() <= 5);
579    }
580
581    #[tokio::test]
582    #[ignore = "requires FMP API key"]
583    async fn test_get_financial_as_reported() {
584        let client = FmpClient::new().unwrap();
585        let result = client
586            .financials()
587            .get_financial_as_reported("AAPL", Period::Annual, Some(1))
588            .await;
589        assert!(result.is_ok());
590        let statements = result.unwrap();
591        assert!(!statements.is_empty());
592    }
593
594    #[tokio::test]
595    #[ignore = "requires FMP API key"]
596    async fn test_get_revenue_product_segmentation() {
597        let client = FmpClient::new().unwrap();
598        let result = client
599            .financials()
600            .get_revenue_product_segmentation("AAPL", Period::Annual, None)
601            .await;
602        assert!(result.is_ok());
603        let segments = result.unwrap();
604        assert!(!segments.is_empty());
605    }
606
607    #[tokio::test]
608    #[ignore = "requires FMP API key"]
609    async fn test_get_revenue_geographic_segmentation() {
610        let client = FmpClient::new().unwrap();
611        let result = client
612            .financials()
613            .get_revenue_geographic_segmentation("AAPL", Period::Annual, None)
614            .await;
615        assert!(result.is_ok());
616        let segments = result.unwrap();
617        assert!(!segments.is_empty());
618    }
619
620    #[tokio::test]
621    #[ignore = "requires FMP API key"]
622    async fn test_get_financial_full_as_reported() {
623        let client = FmpClient::new().unwrap();
624        let result = client
625            .financials()
626            .get_financial_full_as_reported("AAPL", Period::Annual)
627            .await;
628        assert!(result.is_ok());
629        let statements = result.unwrap();
630        assert!(!statements.is_empty());
631        // Full as reported should have comprehensive XBRL data
632        assert!(!statements[0].data.is_empty());
633    }
634
635    // Test quarterly period
636    #[tokio::test]
637    #[ignore = "requires FMP API key"]
638    async fn test_get_income_statement_quarterly() {
639        let client = FmpClient::new().unwrap();
640        let result = client
641            .financials()
642            .get_income_statement("AAPL", Period::Quarter, Some(4))
643            .await;
644        assert!(result.is_ok());
645        let statements = result.unwrap();
646        assert!(!statements.is_empty());
647        assert!(statements.len() <= 4);
648    }
649
650    #[tokio::test]
651    #[ignore = "requires FMP API key"]
652    async fn test_get_financial_growth_quarterly() {
653        let client = FmpClient::new().unwrap();
654        let result = client
655            .financials()
656            .get_financial_growth("AAPL", Period::Quarter, Some(4))
657            .await;
658        assert!(result.is_ok());
659    }
660
661    #[tokio::test]
662    #[ignore = "requires FMP API key"]
663    async fn test_get_financial_full_as_reported_quarterly() {
664        let client = FmpClient::new().unwrap();
665        let result = client
666            .financials()
667            .get_financial_full_as_reported("AAPL", Period::Quarter)
668            .await;
669        assert!(result.is_ok());
670        let statements = result.unwrap();
671        assert!(!statements.is_empty());
672    }
673
674    // Edge case tests
675    #[tokio::test]
676    #[ignore = "requires FMP API key"]
677    async fn test_get_income_statement_invalid_symbol() {
678        let client = FmpClient::new().unwrap();
679        let result = client
680            .financials()
681            .get_income_statement("INVALID_SYMBOL_XYZ123", Period::Annual, Some(5))
682            .await;
683        // Should either return empty vec or error
684        if let Ok(statements) = result {
685            assert!(statements.is_empty());
686        }
687    }
688
689    #[tokio::test]
690    #[ignore = "requires FMP API key"]
691    async fn test_get_financial_growth_zero_limit() {
692        let client = FmpClient::new().unwrap();
693        let result = client
694            .financials()
695            .get_financial_growth("AAPL", Period::Annual, Some(0))
696            .await;
697        // Should handle gracefully
698        assert!(result.is_ok());
699    }
700
701    #[tokio::test]
702    #[ignore = "requires FMP API key"]
703    async fn test_get_revenue_segmentation_with_structure() {
704        let client = FmpClient::new().unwrap();
705        let result = client
706            .financials()
707            .get_revenue_product_segmentation("AAPL", Period::Annual, Some("flat"))
708            .await;
709        assert!(result.is_ok());
710    }
711
712    #[tokio::test]
713    #[ignore = "requires FMP API key"]
714    async fn test_get_revenue_geographic_with_structure() {
715        let client = FmpClient::new().unwrap();
716        let result = client
717            .financials()
718            .get_revenue_geographic_segmentation("AAPL", Period::Annual, Some("flat"))
719            .await;
720        assert!(result.is_ok());
721    }
722
723    #[tokio::test]
724    #[ignore = "requires FMP API key"]
725    async fn test_get_balance_sheet_no_limit() {
726        let client = FmpClient::new().unwrap();
727        let result = client
728            .financials()
729            .get_balance_sheet("AAPL", Period::Annual, None)
730            .await;
731        assert!(result.is_ok());
732        let statements = result.unwrap();
733        assert!(!statements.is_empty());
734    }
735
736    #[tokio::test]
737    #[ignore = "requires FMP API key"]
738    async fn test_get_cash_flow_quarterly() {
739        let client = FmpClient::new().unwrap();
740        let result = client
741            .financials()
742            .get_cash_flow_statement("AAPL", Period::Quarter, Some(8))
743            .await;
744        assert!(result.is_ok());
745        let statements = result.unwrap();
746        assert!(!statements.is_empty());
747        assert!(statements.len() <= 8);
748    }
749
750    // Error handling tests
751    #[tokio::test]
752    async fn test_invalid_api_key() {
753        let client = FmpClient::builder()
754            .api_key("invalid_key_12345")
755            .build()
756            .unwrap();
757        let result = client
758            .financials()
759            .get_income_statement("AAPL", Period::Annual, Some(5))
760            .await;
761        assert!(result.is_err());
762    }
763
764    #[tokio::test]
765    async fn test_empty_symbol() {
766        let client = FmpClient::builder().api_key("test_key").build().unwrap();
767        let result = client
768            .financials()
769            .get_income_statement("", Period::Annual, Some(5))
770            .await;
771        // Should handle gracefully
772        assert!(result.is_err() || result.unwrap().is_empty());
773    }
774
775    #[tokio::test]
776    #[ignore = "requires FMP API key"]
777    async fn test_company_without_segmentation() {
778        let client = FmpClient::new().unwrap();
779        // Some companies may not have segmentation data
780        let result = client
781            .financials()
782            .get_revenue_product_segmentation("TSLA", Period::Annual, None)
783            .await;
784        // Should succeed but may be empty
785        if let Ok(_segments) = result {
786            // No assertion on emptiness as it depends on company
787        }
788    }
789
790    #[tokio::test]
791    #[ignore = "requires FMP API key"]
792    async fn test_get_financial_full_as_reported_invalid_symbol() {
793        let client = FmpClient::new().unwrap();
794        let result = client
795            .financials()
796            .get_financial_full_as_reported("INVALID_SYMBOL_XYZ123", Period::Annual)
797            .await;
798        // Should succeed but return empty for invalid symbol
799        assert!(result.is_ok());
800        if let Ok(statements) = result {
801            assert!(statements.is_empty());
802        }
803    }
804}