use serde::{Deserialize, Serialize};
use crate::adapters::common::encode_path_segment;
use crate::error::Result;
use crate::adapters::fmp::build_client;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct DividendHistoryDTO {
pub date: Option<String>,
pub label: Option<String>,
#[serde(rename = "adjDividend")]
pub adj_dividend: Option<f64>,
pub dividend: Option<f64>,
#[serde(rename = "recordDate")]
pub record_date: Option<String>,
#[serde(rename = "paymentDate")]
pub payment_date: Option<String>,
#[serde(rename = "declarationDate")]
pub declaration_date: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct DividendHistoryResponseDTO {
pub symbol: Option<String>,
pub historical: Vec<DividendHistoryDTO>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct SplitHistoryDTO {
pub date: Option<String>,
pub label: Option<String>,
pub numerator: Option<f64>,
pub denominator: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct SplitHistoryResponseDTO {
pub symbol: Option<String>,
pub historical: Vec<SplitHistoryDTO>,
}
fn dividends_splits_to_events(
divs: DividendHistoryResponseDTO,
splits: SplitHistoryResponseDTO,
) -> crate::models::chart::events::ChartEvents {
use crate::models::chart::events::{ChartEvents, DividendEvent, SplitEvent};
let mut chart_events = ChartEvents::default();
chart_events.dividends = divs
.historical
.into_iter()
.filter_map(|d| {
let ts = chrono::NaiveDate::parse_from_str(d.date.as_deref()?, "%Y-%m-%d")
.ok()?
.and_hms_opt(0, 0, 0)?
.and_utc()
.timestamp();
Some((
ts.to_string(),
DividendEvent {
date: ts,
amount: d.adj_dividend.or(d.dividend).unwrap_or(0.0),
},
))
})
.collect();
chart_events.splits = splits
.historical
.into_iter()
.filter_map(|s| {
let ts = chrono::NaiveDate::parse_from_str(s.date.as_deref()?, "%Y-%m-%d")
.ok()?
.and_hms_opt(0, 0, 0)?
.and_utc()
.timestamp();
let numerator = s.numerator.unwrap_or(1.0);
let denominator = s.denominator.unwrap_or(1.0);
Some((
ts.to_string(),
SplitEvent {
date: ts,
numerator,
denominator,
split_ratio: format!("{}:{}", numerator, denominator),
},
))
})
.collect();
chart_events
}
pub async fn fetch_canonical_events(
symbol: &str,
) -> Result<crate::models::chart::events::ChartEvents> {
let divs = historical_dividends(symbol).await?;
let splits = historical_splits(symbol).await?;
Ok(dividends_splits_to_events(divs, splits))
}
pub async fn historical_dividends(symbol: &str) -> Result<DividendHistoryResponseDTO> {
let client = build_client()?;
let path = format!(
"/api/v3/historical-price-full/stock_dividend/{}",
encode_path_segment(symbol)
);
client.get(&path, &[]).await
}
pub async fn historical_splits(symbol: &str) -> Result<SplitHistoryResponseDTO> {
let client = build_client()?;
let path = format!(
"/api/v3/historical-price-full/stock_split/{}",
encode_path_segment(symbol)
);
client.get(&path, &[]).await
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_historical_dividends_mock() {
let mut server = mockito::Server::new_async().await;
let _mock = server
.mock("GET", "/api/v3/historical-price-full/stock_dividend/AAPL")
.match_query(mockito::Matcher::AllOf(vec![mockito::Matcher::UrlEncoded(
"apikey".into(),
"test-key".into(),
)]))
.with_status(200)
.with_body(
serde_json::json!({
"symbol": "AAPL",
"historical": [
{
"date": "2024-02-09",
"label": "February 09, 24",
"adjDividend": 0.24,
"dividend": 0.24,
"recordDate": "2024-02-12",
"paymentDate": "2024-02-15",
"declarationDate": "2024-02-01"
}
]
})
.to_string(),
)
.create_async()
.await;
let client = crate::adapters::fmp::build_test_client(&server.url()).unwrap();
let path = "/api/v3/historical-price-full/stock_dividend/AAPL";
let resp: DividendHistoryResponseDTO = client.get(path, &[]).await.unwrap();
assert_eq!(resp.symbol.as_deref(), Some("AAPL"));
assert_eq!(resp.historical.len(), 1);
assert!((resp.historical[0].adj_dividend.unwrap() - 0.24).abs() < 0.001);
}
#[tokio::test]
async fn test_historical_splits_mock() {
let mut server = mockito::Server::new_async().await;
let _mock = server
.mock("GET", "/api/v3/historical-price-full/stock_split/AAPL")
.match_query(mockito::Matcher::AllOf(vec![mockito::Matcher::UrlEncoded(
"apikey".into(),
"test-key".into(),
)]))
.with_status(200)
.with_body(
serde_json::json!({
"symbol": "AAPL",
"historical": [
{
"date": "2020-08-31",
"label": "August 31, 20",
"numerator": 4.0,
"denominator": 1.0
}
]
})
.to_string(),
)
.create_async()
.await;
let client = crate::adapters::fmp::build_test_client(&server.url()).unwrap();
let path = "/api/v3/historical-price-full/stock_split/AAPL";
let resp: SplitHistoryResponseDTO = client.get(path, &[]).await.unwrap();
assert_eq!(resp.symbol.as_deref(), Some("AAPL"));
assert_eq!(resp.historical.len(), 1);
assert!((resp.historical[0].numerator.unwrap() - 4.0).abs() < 0.001);
}
}