Skip to main content

finance_query/adapters/fmp/
dividends_splits.rs

1//! Dividend and stock split history endpoints.
2
3use serde::{Deserialize, Serialize};
4
5use crate::adapters::common::encode_path_segment;
6use crate::error::Result;
7
8use super::build_client;
9
10// ============================================================================
11// Response types
12// ============================================================================
13
14/// A single historical dividend record.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16#[non_exhaustive]
17pub struct DividendHistory {
18    /// Ex-dividend date (YYYY-MM-DD).
19    pub date: Option<String>,
20    /// Human-readable label.
21    pub label: Option<String>,
22    /// Adjusted dividend amount.
23    #[serde(rename = "adjDividend")]
24    pub adj_dividend: Option<f64>,
25    /// Unadjusted dividend amount.
26    pub dividend: Option<f64>,
27    /// Record date.
28    #[serde(rename = "recordDate")]
29    pub record_date: Option<String>,
30    /// Payment date.
31    #[serde(rename = "paymentDate")]
32    pub payment_date: Option<String>,
33    /// Declaration date.
34    #[serde(rename = "declarationDate")]
35    pub declaration_date: Option<String>,
36}
37
38/// Historical dividend response wrapper.
39#[derive(Debug, Clone, Serialize, Deserialize)]
40#[non_exhaustive]
41pub struct DividendHistoryResponse {
42    /// Ticker symbol.
43    pub symbol: Option<String>,
44    /// Dividend history records.
45    pub historical: Vec<DividendHistory>,
46}
47
48/// A single historical stock split record.
49#[derive(Debug, Clone, Serialize, Deserialize)]
50#[non_exhaustive]
51pub struct SplitHistory {
52    /// Split date (YYYY-MM-DD).
53    pub date: Option<String>,
54    /// Human-readable label.
55    pub label: Option<String>,
56    /// Split numerator.
57    pub numerator: Option<f64>,
58    /// Split denominator.
59    pub denominator: Option<f64>,
60}
61
62/// Historical stock split response wrapper.
63#[derive(Debug, Clone, Serialize, Deserialize)]
64#[non_exhaustive]
65pub struct SplitHistoryResponse {
66    /// Ticker symbol.
67    pub symbol: Option<String>,
68    /// Split history records.
69    pub historical: Vec<SplitHistory>,
70}
71
72// ============================================================================
73// Public API
74// ============================================================================
75
76/// Fetch historical dividend data for a symbol.
77pub async fn historical_dividends(symbol: &str) -> Result<DividendHistoryResponse> {
78    let client = build_client()?;
79    let path = format!(
80        "/api/v3/historical-price-full/stock_dividend/{}",
81        encode_path_segment(symbol)
82    );
83    client.get(&path, &[]).await
84}
85
86/// Fetch historical stock split data for a symbol.
87pub async fn historical_splits(symbol: &str) -> Result<SplitHistoryResponse> {
88    let client = build_client()?;
89    let path = format!(
90        "/api/v3/historical-price-full/stock_split/{}",
91        encode_path_segment(symbol)
92    );
93    client.get(&path, &[]).await
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[tokio::test]
101    async fn test_historical_dividends_mock() {
102        let mut server = mockito::Server::new_async().await;
103        let _mock = server
104            .mock("GET", "/api/v3/historical-price-full/stock_dividend/AAPL")
105            .match_query(mockito::Matcher::AllOf(vec![mockito::Matcher::UrlEncoded(
106                "apikey".into(),
107                "test-key".into(),
108            )]))
109            .with_status(200)
110            .with_body(
111                serde_json::json!({
112                    "symbol": "AAPL",
113                    "historical": [
114                        {
115                            "date": "2024-02-09",
116                            "label": "February 09, 24",
117                            "adjDividend": 0.24,
118                            "dividend": 0.24,
119                            "recordDate": "2024-02-12",
120                            "paymentDate": "2024-02-15",
121                            "declarationDate": "2024-02-01"
122                        }
123                    ]
124                })
125                .to_string(),
126            )
127            .create_async()
128            .await;
129
130        let client = super::super::build_test_client(&server.url()).unwrap();
131        let path = "/api/v3/historical-price-full/stock_dividend/AAPL";
132        let resp: DividendHistoryResponse = client.get(path, &[]).await.unwrap();
133        assert_eq!(resp.symbol.as_deref(), Some("AAPL"));
134        assert_eq!(resp.historical.len(), 1);
135        assert!((resp.historical[0].adj_dividend.unwrap() - 0.24).abs() < 0.001);
136    }
137
138    #[tokio::test]
139    async fn test_historical_splits_mock() {
140        let mut server = mockito::Server::new_async().await;
141        let _mock = server
142            .mock("GET", "/api/v3/historical-price-full/stock_split/AAPL")
143            .match_query(mockito::Matcher::AllOf(vec![mockito::Matcher::UrlEncoded(
144                "apikey".into(),
145                "test-key".into(),
146            )]))
147            .with_status(200)
148            .with_body(
149                serde_json::json!({
150                    "symbol": "AAPL",
151                    "historical": [
152                        {
153                            "date": "2020-08-31",
154                            "label": "August 31, 20",
155                            "numerator": 4.0,
156                            "denominator": 1.0
157                        }
158                    ]
159                })
160                .to_string(),
161            )
162            .create_async()
163            .await;
164
165        let client = super::super::build_test_client(&server.url()).unwrap();
166        let path = "/api/v3/historical-price-full/stock_split/AAPL";
167        let resp: SplitHistoryResponse = client.get(path, &[]).await.unwrap();
168        assert_eq!(resp.symbol.as_deref(), Some("AAPL"));
169        assert_eq!(resp.historical.len(), 1);
170        assert!((resp.historical[0].numerator.unwrap() - 4.0).abs() < 0.001);
171    }
172}