use reqwest::{Client, header};
use std::error::Error;
use std::time::Duration as StdDuration;
use tracing::error;
use crate::errors::CustomError;
use async_trait::async_trait;
use crate::types::{IntradayTrading, VpsForeignTradingData};
#[derive(thiserror::Error, Debug)]
pub enum AppError {
#[error("HTTP error: {0}")]
HttpError(#[from] reqwest::Error),
}
impl From<AppError> for CustomError {
fn from(err: AppError) -> Self {
CustomError {
message: err.to_string(),
}
}
}
#[async_trait]
pub trait VpsServiceTrait: Send + Sync {
async fn fetch_stock_trade_list_by_ticker_code(
&self,
ticker: String,
) -> Result<Vec<IntradayTrading>, reqwest::Error>;
async fn fetch_latest_stock_trade_item_by_ticker_code(
&self,
ticker: String,
) -> Option<IntradayTrading>;
async fn fetch_latest_total_volume(&self, ticker: String) -> Result<i64, reqwest::Error>;
async fn fetch_foreign_trading_data(
&self,
ticker: String,
) -> Result<VpsForeignTradingData, CustomError>;
}
pub struct VpsService {
base_url: String,
client: Client,
}
impl VpsService {
pub fn new() -> Self {
let client = Client::builder()
.timeout(StdDuration::from_secs(30))
.build()
.expect("Failed to create HTTP client");
Self {
base_url: "https://bgapidatafeed.vps.com.vn".to_string(),
client,
}
}
}
#[async_trait]
impl VpsServiceTrait for VpsService {
async fn fetch_stock_trade_list_by_ticker_code(
&self,
ticker: String,
) -> Result<Vec<IntradayTrading>, reqwest::Error> {
let url = format!("{}/getliststocktrade/{}", self.base_url, ticker);
let client = self.client.clone();
let response = client
.get(&url)
.header(header::CONTENT_TYPE, "application/json; charset=utf-8")
.send()
.await?;
let result = response.json::<Option<Vec<IntradayTrading>>>().await;
match result {
Ok(list_option) => {
let stock_trade_list = list_option.unwrap_or_default();
Ok(stock_trade_list)
}
Err(e) => {
if e.is_decode() {
Ok(Vec::new())
} else {
Err(e)
}
}
}
}
async fn fetch_foreign_trading_data(
&self,
ticker: String,
) -> Result<VpsForeignTradingData, CustomError> {
let url = format!("{}/getliststockdata/{}", self.base_url, ticker);
let client = self.client.clone();
let response = client
.get(&url)
.header(header::CONTENT_TYPE, "application/json; charset=utf-8")
.send()
.await
.map_err(|err| {
CustomError::from(AppError::from(err))
})?;
let data: Vec<VpsForeignTradingData> = response
.json()
.await
.map_err(|err| {
println!("Error {:?}", err.source());
CustomError::from(AppError::from(err))
})?;
let record = data.into_iter().next().ok_or_else(|| CustomError {
message: "No trading data found".to_string(),
})?;
Ok(record)
}
async fn fetch_latest_total_volume(&self, ticker: String) -> Result<i64, reqwest::Error> {
let stock_trade_list = self.fetch_stock_trade_list_by_ticker_code(ticker).await?;
if let Some(latest_trade) = stock_trade_list.last() {
Ok(latest_trade.total_vol)
} else {
Ok(0) }
}
async fn fetch_latest_stock_trade_item_by_ticker_code(
&self,
ticker: String,
) -> Option<IntradayTrading> {
let result: Option<IntradayTrading> = None;
let stock_trade_list = self.fetch_stock_trade_list_by_ticker_code(ticker).await;
match stock_trade_list {
Ok(list) => list.into_iter().last(),
Err(err) => {
error!(
"fetch_latest_stock_trade_item_by_ticker_code error: {:?}",
err
);
result
} }
}
}