shindo_coding_utils 0.2.6

A utils crates which will be used in various micro-services
Documentation
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),
}

// Implement conversion from AppError to CustomError.
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 {
        // Create a reqwest client with default configuration
        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) => {
                // An empty response body causes a decode error. We treat this as an empty list.
                if e.is_decode() {
                    Ok(Vec::new())
                } else {
                    Err(e)
                }
            }
        }
    }

    /// Fetch the ongoing trading data -- Realtime
    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();

        // Get the reqwest::Response. If sending fails, convert the error.
        let response = client
            .get(&url)
            .header(header::CONTENT_TYPE, "application/json; charset=utf-8")
            .send()
            .await
            .map_err(|err| {
                CustomError::from(AppError::from(err))
            })?;

        // Deserialize the response. Assuming the endpoint returns an array.
        let data: Vec<VpsForeignTradingData> = response
            .json()
            .await
            .map_err(|err| {
                println!("Error {:?}", err.source());
                CustomError::from(AppError::from(err))
            })?;

        // Find the first record; if missing return a custom error.
        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) // Return 0 if no trades are found
        }
    }

    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
            } // If there's an error, return None
        }
    }
}