Skip to main content

finance_query/adapters/fmp/
company.rs

1//! FMP company information endpoints.
2
3use serde::{Deserialize, Serialize};
4
5use crate::adapters::common::encode_path_segment;
6use crate::error::Result;
7
8// ============================================================================
9// Response types
10// ============================================================================
11
12/// Company profile from FMP.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14#[non_exhaustive]
15pub struct CompanyProfile {
16    /// Ticker symbol.
17    pub symbol: Option<String>,
18    /// Current price.
19    pub price: Option<f64>,
20    /// Beta.
21    pub beta: Option<f64>,
22    /// Volume average.
23    #[serde(rename = "volAvg")]
24    pub vol_avg: Option<f64>,
25    /// Market capitalization.
26    #[serde(rename = "mktCap")]
27    pub mkt_cap: Option<f64>,
28    /// Last dividend.
29    #[serde(rename = "lastDiv")]
30    pub last_div: Option<f64>,
31    /// 52-week range.
32    pub range: Option<String>,
33    /// Price changes.
34    pub changes: Option<f64>,
35    /// Company name.
36    #[serde(rename = "companyName")]
37    pub company_name: Option<String>,
38    /// Currency.
39    pub currency: Option<String>,
40    /// CIK number.
41    pub cik: Option<String>,
42    /// ISIN.
43    pub isin: Option<String>,
44    /// CUSIP.
45    pub cusip: Option<String>,
46    /// Exchange name.
47    pub exchange: Option<String>,
48    /// Exchange short name.
49    #[serde(rename = "exchangeShortName")]
50    pub exchange_short_name: Option<String>,
51    /// Industry.
52    pub industry: Option<String>,
53    /// Website.
54    pub website: Option<String>,
55    /// Company description.
56    pub description: Option<String>,
57    /// CEO.
58    pub ceo: Option<String>,
59    /// Sector.
60    pub sector: Option<String>,
61    /// Country.
62    pub country: Option<String>,
63    /// Full-time employees.
64    #[serde(rename = "fullTimeEmployees")]
65    pub full_time_employees: Option<String>,
66    /// Phone number.
67    pub phone: Option<String>,
68    /// Address.
69    pub address: Option<String>,
70    /// City.
71    pub city: Option<String>,
72    /// State.
73    pub state: Option<String>,
74    /// ZIP code.
75    pub zip: Option<String>,
76    /// DCF difference.
77    #[serde(rename = "dcfDiff")]
78    pub dcf_diff: Option<f64>,
79    /// DCF value.
80    pub dcf: Option<f64>,
81    /// Image/logo URL.
82    pub image: Option<String>,
83    /// IPO date.
84    #[serde(rename = "ipoDate")]
85    pub ipo_date: Option<String>,
86    /// Default image flag.
87    #[serde(rename = "defaultImage")]
88    pub default_image: Option<bool>,
89    /// Is ETF.
90    #[serde(rename = "isEtf")]
91    pub is_etf: Option<bool>,
92    /// Is actively trading.
93    #[serde(rename = "isActivelyTrading")]
94    pub is_actively_trading: Option<bool>,
95    /// Is ADR.
96    #[serde(rename = "isAdr")]
97    pub is_adr: Option<bool>,
98    /// Is fund.
99    #[serde(rename = "isFund")]
100    pub is_fund: Option<bool>,
101}
102
103/// Key executive from FMP.
104#[derive(Debug, Clone, Serialize, Deserialize)]
105#[non_exhaustive]
106pub struct KeyExecutive {
107    /// Executive title.
108    pub title: Option<String>,
109    /// Executive name.
110    pub name: Option<String>,
111    /// Pay.
112    pub pay: Option<f64>,
113    /// Currency of pay.
114    #[serde(rename = "currencyPay")]
115    pub currency_pay: Option<String>,
116    /// Gender.
117    pub gender: Option<String>,
118    /// Year born.
119    #[serde(rename = "yearBorn")]
120    pub year_born: Option<i32>,
121    /// Title since.
122    #[serde(rename = "titleSince")]
123    pub title_since: Option<String>,
124}
125
126/// Market capitalization from FMP.
127#[derive(Debug, Clone, Serialize, Deserialize)]
128#[non_exhaustive]
129pub struct MarketCap {
130    /// Ticker symbol.
131    pub symbol: Option<String>,
132    /// Date.
133    pub date: Option<String>,
134    /// Market capitalization.
135    #[serde(rename = "marketCap")]
136    pub market_cap: Option<f64>,
137}
138
139/// Company outlook from FMP (v4 endpoint).
140#[derive(Debug, Clone, Serialize, Deserialize)]
141#[non_exhaustive]
142pub struct CompanyOutlook {
143    /// Profile section.
144    pub profile: Option<CompanyProfile>,
145    /// Metrics section.
146    pub metrics: Option<serde_json::Value>,
147    /// Ratios section.
148    pub ratios: Option<Vec<serde_json::Value>>,
149    /// Insider trading section.
150    #[serde(rename = "insideTrades")]
151    pub inside_trades: Option<Vec<serde_json::Value>>,
152    /// Key executives.
153    #[serde(rename = "keyExecutives")]
154    pub key_executives: Option<Vec<KeyExecutive>>,
155    /// Stock news.
156    #[serde(rename = "stockNews")]
157    pub stock_news: Option<Vec<serde_json::Value>>,
158    /// Rating section.
159    pub rating: Option<Vec<serde_json::Value>>,
160}
161
162/// Stock peer from FMP (v4 endpoint).
163#[derive(Debug, Clone, Serialize, Deserialize)]
164#[non_exhaustive]
165pub struct StockPeers {
166    /// Ticker symbol.
167    pub symbol: Option<String>,
168    /// List of peer symbols.
169    #[serde(rename = "peersList")]
170    pub peers_list: Option<Vec<String>>,
171}
172
173/// Delisted company from FMP.
174#[derive(Debug, Clone, Serialize, Deserialize)]
175#[non_exhaustive]
176pub struct DelistedCompany {
177    /// Ticker symbol.
178    pub symbol: Option<String>,
179    /// Company name.
180    #[serde(rename = "companyName")]
181    pub company_name: Option<String>,
182    /// Exchange.
183    pub exchange: Option<String>,
184    /// IPO date.
185    #[serde(rename = "ipoDate")]
186    pub ipo_date: Option<String>,
187    /// Delisted date.
188    #[serde(rename = "delistedDate")]
189    pub delisted_date: Option<String>,
190}
191
192// ============================================================================
193// Query functions
194// ============================================================================
195
196/// Fetch company profile for a symbol.
197pub async fn company_profile(symbol: &str) -> Result<Vec<CompanyProfile>> {
198    let client = super::build_client()?;
199    client
200        .get(
201            &format!("/api/v3/profile/{}", encode_path_segment(symbol)),
202            &[],
203        )
204        .await
205}
206
207/// Fetch key executives for a symbol.
208pub async fn key_executives(symbol: &str) -> Result<Vec<KeyExecutive>> {
209    let client = super::build_client()?;
210    client
211        .get(
212            &format!("/api/v3/key-executives/{}", encode_path_segment(symbol)),
213            &[],
214        )
215        .await
216}
217
218/// Fetch market capitalization for a symbol.
219pub async fn market_cap(symbol: &str) -> Result<Vec<MarketCap>> {
220    let client = super::build_client()?;
221    client
222        .get(
223            &format!(
224                "/api/v3/market-capitalization/{}",
225                encode_path_segment(symbol)
226            ),
227            &[],
228        )
229        .await
230}
231
232/// Fetch historical market capitalization for a symbol.
233pub async fn historical_market_cap(symbol: &str, limit: Option<u32>) -> Result<Vec<MarketCap>> {
234    let client = super::build_client()?;
235    let limit_str = limit.unwrap_or(100).to_string();
236    client
237        .get(
238            &format!(
239                "/api/v3/historical-market-capitalization/{}",
240                encode_path_segment(symbol)
241            ),
242            &[("limit", &limit_str)],
243        )
244        .await
245}
246
247/// Fetch company outlook for a symbol (v4 endpoint).
248pub async fn company_outlook(symbol: &str) -> Result<CompanyOutlook> {
249    let client = super::build_client()?;
250    client
251        .get("/api/v4/company-outlook", &[("symbol", symbol)])
252        .await
253}
254
255/// Fetch stock peers for a symbol (v4 endpoint).
256pub async fn stock_peers(symbol: &str) -> Result<Vec<StockPeers>> {
257    let client = super::build_client()?;
258    client
259        .get("/api/v4/stock_peers", &[("symbol", symbol)])
260        .await
261}
262
263/// Fetch delisted companies.
264pub async fn delisted_companies(limit: Option<u32>) -> Result<Vec<DelistedCompany>> {
265    let client = super::build_client()?;
266    let limit_str = limit.unwrap_or(100).to_string();
267    client
268        .get("/api/v3/delisted-companies", &[("limit", &limit_str)])
269        .await
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275
276    #[tokio::test]
277    async fn test_company_profile_mock() {
278        let mut server = mockito::Server::new_async().await;
279        let _mock = server
280            .mock("GET", "/api/v3/profile/AAPL")
281            .match_query(mockito::Matcher::AllOf(vec![mockito::Matcher::UrlEncoded(
282                "apikey".into(),
283                "test-key".into(),
284            )]))
285            .with_status(200)
286            .with_body(
287                serde_json::json!([{
288                    "symbol": "AAPL",
289                    "price": 178.72,
290                    "beta": 1.286,
291                    "volAvg": 58405568,
292                    "mktCap": 2794000000000_f64,
293                    "companyName": "Apple Inc.",
294                    "currency": "USD",
295                    "exchange": "NASDAQ Global Select",
296                    "exchangeShortName": "NASDAQ",
297                    "industry": "Consumer Electronics",
298                    "sector": "Technology",
299                    "country": "US",
300                    "ceo": "Mr. Timothy D. Cook",
301                    "isEtf": false,
302                    "isActivelyTrading": true
303                }])
304                .to_string(),
305            )
306            .create_async()
307            .await;
308
309        let client = super::super::build_test_client(&server.url()).unwrap();
310        let result: Vec<CompanyProfile> = client.get("/api/v3/profile/AAPL", &[]).await.unwrap();
311
312        assert_eq!(result.len(), 1);
313        assert_eq!(result[0].symbol.as_deref(), Some("AAPL"));
314        assert_eq!(result[0].company_name.as_deref(), Some("Apple Inc."));
315        assert_eq!(result[0].sector.as_deref(), Some("Technology"));
316        assert_eq!(result[0].is_etf, Some(false));
317    }
318
319    #[tokio::test]
320    async fn test_key_executives_mock() {
321        let mut server = mockito::Server::new_async().await;
322        let _mock = server
323            .mock("GET", "/api/v3/key-executives/AAPL")
324            .match_query(mockito::Matcher::AllOf(vec![mockito::Matcher::UrlEncoded(
325                "apikey".into(),
326                "test-key".into(),
327            )]))
328            .with_status(200)
329            .with_body(
330                serde_json::json!([
331                    {
332                        "title": "Chief Executive Officer",
333                        "name": "Mr. Timothy D. Cook",
334                        "pay": 16425933,
335                        "currencyPay": "USD",
336                        "gender": "male",
337                        "yearBorn": 1960
338                    },
339                    {
340                        "title": "Chief Financial Officer",
341                        "name": "Mr. Luca Maestri",
342                        "pay": 5019783,
343                        "currencyPay": "USD",
344                        "gender": "male",
345                        "yearBorn": 1963
346                    }
347                ])
348                .to_string(),
349            )
350            .create_async()
351            .await;
352
353        let client = super::super::build_test_client(&server.url()).unwrap();
354        let result: Vec<KeyExecutive> = client
355            .get("/api/v3/key-executives/AAPL", &[])
356            .await
357            .unwrap();
358
359        assert_eq!(result.len(), 2);
360        assert_eq!(result[0].name.as_deref(), Some("Mr. Timothy D. Cook"));
361        assert_eq!(result[0].pay, Some(16425933.0));
362    }
363
364    #[tokio::test]
365    async fn test_fmp_rate_limit_returns_rate_limited_error() {
366        let mut server = mockito::Server::new_async().await;
367        let _mock = server
368            .mock("GET", mockito::Matcher::Any)
369            .with_status(429)
370            .with_body("{}")
371            .create_async()
372            .await;
373
374        let client = crate::adapters::fmp::build_test_client(&server.url()).unwrap();
375        let result = client.get_raw("/api/v3/profile/AAPL", &[]).await;
376
377        assert!(matches!(
378            result,
379            Err(crate::error::FinanceError::RateLimited { .. })
380        ));
381    }
382
383    #[tokio::test]
384    async fn test_fmp_401_returns_authentication_failed() {
385        let mut server = mockito::Server::new_async().await;
386        let _mock = server
387            .mock("GET", mockito::Matcher::Any)
388            .with_status(401)
389            .with_body("{}")
390            .create_async()
391            .await;
392
393        let client = crate::adapters::fmp::build_test_client(&server.url()).unwrap();
394        let result = client.get_raw("/api/v3/profile/AAPL", &[]).await;
395
396        assert!(matches!(
397            result,
398            Err(crate::error::FinanceError::AuthenticationFailed { .. })
399        ));
400    }
401
402    #[tokio::test]
403    async fn test_fmp_body_error_message_returns_invalid_parameter() {
404        let mut server = mockito::Server::new_async().await;
405        let _mock = server
406            .mock("GET", mockito::Matcher::Any)
407            .with_status(200)
408            .with_body(r#"{"Error Message":"Invalid API KEY."}"#)
409            .create_async()
410            .await;
411
412        let client = crate::adapters::fmp::build_test_client(&server.url()).unwrap();
413        let result = client.get_raw("/api/v3/profile/AAPL", &[]).await;
414
415        assert!(matches!(
416            result,
417            Err(crate::error::FinanceError::InvalidParameter { .. })
418        ));
419    }
420
421    #[tokio::test]
422    async fn test_fmp_500_returns_server_error() {
423        let mut server = mockito::Server::new_async().await;
424        let _mock = server
425            .mock("GET", mockito::Matcher::Any)
426            .with_status(500)
427            .with_body("{}")
428            .create_async()
429            .await;
430
431        let client = crate::adapters::fmp::build_test_client(&server.url()).unwrap();
432        let result = client.get_raw("/api/v3/profile/AAPL", &[]).await;
433
434        assert!(matches!(
435            result,
436            Err(crate::error::FinanceError::ServerError { .. })
437        ));
438    }
439}