use std::sync::Arc;
use std::time::Duration;
use reqwest::{Client, StatusCode};
use tracing::debug;
use super::models::{CompareFilter, DataRequest, DateRangeFilter, FinraError, RegShoDailyRow};
use crate::adapters::common::{keyless_http_client, status_error};
use crate::error::{FinanceError, Result};
use crate::rate_limiter::RateLimiter;
pub(super) const FINRA_BASE: &str = "https://api.finra.org/data/group";
const ROW_LIMIT: u32 = 1_000;
pub(super) struct FinraClient {
http: Client,
limiter: Arc<RateLimiter>,
base_url: String,
}
impl FinraClient {
pub(super) fn new(
timeout: Duration,
limiter: Arc<RateLimiter>,
base_url: impl Into<String>,
) -> Result<Self> {
Ok(Self {
http: keyless_http_client(timeout)?,
limiter,
base_url: base_url.into(),
})
}
pub(super) async fn reg_sho_daily(
&self,
symbol: &str,
start: String,
end: String,
) -> Result<Vec<RegShoDailyRow>> {
self.limiter.acquire().await;
let url = format!("{}/otcMarket/name/regShoDaily", self.base_url);
let body = DataRequest {
limit: ROW_LIMIT,
compare_filters: vec![CompareFilter {
field_name: "securitiesInformationProcessorSymbolIdentifier",
field_value: symbol,
compare_type: "EQUAL",
}],
date_range_filters: vec![DateRangeFilter {
field_name: "tradeReportDate",
start_date: start,
end_date: end,
}],
};
debug!("FINRA request: regShoDaily {symbol}");
let resp = self
.http
.post(&url)
.header(reqwest::header::ACCEPT, "application/json")
.json(&body)
.send()
.await?;
let status = resp.status();
if status == StatusCode::NO_CONTENT {
return Ok(Vec::new());
}
let bytes = resp.bytes().await?;
if !status.is_success() {
return Err(Self::map_error(status, &bytes, symbol));
}
serde_json::from_slice(&bytes).map_err(|e| FinanceError::ResponseStructureError {
field: "finra.regShoDaily".to_string(),
context: format!("unrecognised FINRA payload: {e}"),
})
}
fn map_error(status: StatusCode, body: &[u8], symbol: &str) -> FinanceError {
if status == StatusCode::TOO_MANY_REQUESTS {
return status_error("FINRA", status);
}
if let Ok(err) = serde_json::from_slice::<FinraError>(body)
&& let Some(message) = err.message
{
return FinanceError::ApiError(format!("FINRA ({symbol}): {message}"));
}
status_error("FINRA", status)
}
}