use chrono::{DateTime, Utc};
use crate::{
client::CryptoBot,
error::{CryptoBotError, CryptoBotResult, ValidationErrorKind},
models::{APIEndpoint, APIMethod, AppStats, Currency, GetMeResponse, GetStatsParams, Method},
};
use async_trait::async_trait;
use super::MiscAPI;
pub struct GetMeBuilder<'a> {
client: &'a CryptoBot,
}
impl<'a> GetMeBuilder<'a> {
pub fn new(client: &'a CryptoBot) -> Self {
Self { client }
}
pub async fn execute(self) -> CryptoBotResult<GetMeResponse> {
self.client
.make_request(
&APIMethod {
endpoint: APIEndpoint::GetMe,
method: Method::GET,
},
None::<&()>,
)
.await
}
}
pub struct GetCurrenciesBuilder<'a> {
client: &'a CryptoBot,
}
impl<'a> GetCurrenciesBuilder<'a> {
pub fn new(client: &'a CryptoBot) -> Self {
Self { client }
}
pub async fn execute(self) -> CryptoBotResult<Vec<Currency>> {
self.client
.make_request(
&APIMethod {
endpoint: APIEndpoint::GetCurrencies,
method: Method::GET,
},
None::<&()>,
)
.await
}
}
pub struct GetStatsBuilder<'a> {
client: &'a CryptoBot,
params: GetStatsParams,
}
impl<'a> GetStatsBuilder<'a> {
pub fn new(client: &'a CryptoBot) -> Self {
Self {
client,
params: GetStatsParams::default(),
}
}
pub fn start_at(mut self, start_at: DateTime<Utc>) -> Self {
self.params.start_at = Some(start_at);
self
}
pub fn end_at(mut self, end_at: DateTime<Utc>) -> Self {
self.params.end_at = Some(end_at);
self
}
pub async fn execute(self) -> CryptoBotResult<AppStats> {
let now = Utc::now();
if let Some(start) = self.params.start_at {
if start > now {
return Err(CryptoBotError::ValidationError {
kind: ValidationErrorKind::Range,
message: "start_at cannot be in the future".to_string(),
field: Some("start_at".to_string()),
});
}
}
if let (Some(start), Some(end)) = (self.params.start_at, self.params.end_at) {
if end < start {
return Err(CryptoBotError::ValidationError {
kind: ValidationErrorKind::Range,
message: "end_at cannot be earlier than start_at".to_string(),
field: Some("end_at".to_string()),
});
}
}
self.client
.make_request(
&APIMethod {
endpoint: APIEndpoint::GetStats,
method: Method::GET,
},
Some(&self.params),
)
.await
}
}
#[async_trait]
impl MiscAPI for CryptoBot {
fn get_me(&self) -> GetMeBuilder<'_> {
GetMeBuilder::new(self)
}
fn get_currencies(&self) -> GetCurrenciesBuilder<'_> {
GetCurrenciesBuilder::new(self)
}
fn get_stats(&self) -> GetStatsBuilder<'_> {
GetStatsBuilder::new(self)
}
}
#[cfg(test)]
mod tests {
use chrono::{Duration, Utc};
use mockito::Mock;
use rust_decimal::Decimal;
use serde_json::json;
use crate::{
api::MiscAPI,
client::CryptoBot,
models::{CryptoCurrencyCode, CurrencyCode},
prelude::{CryptoBotError, ValidationErrorKind},
utils::test_utils::TestContext,
};
impl TestContext {
pub fn mock_get_me_response(&mut self) -> Mock {
self.server
.mock("GET", "/getMe")
.with_header("content-type", "application/json")
.with_header("Crypto-Pay-API-Token", "test_token")
.with_body(
json!({
"ok": true,
"result": {
"app_id": 28692,
"name": "Stated Seaslug App",
"payment_processing_bot_username": "CryptoTestnetBot"
}
})
.to_string(),
)
.create()
}
pub fn mock_currencies_response(&mut self) -> Mock {
println!("Setting up mock response");
self.server
.mock("GET", "/getCurrencies")
.with_header("content-type", "application/json")
.with_header("Crypto-Pay-API-Token", "test_token")
.with_body(
json!({
"ok": true,
"result": [
{
"is_blockchain": false,
"is_stablecoin": true,
"is_fiat": false,
"name": "Tether",
"code": "USDT",
"url": "https://tether.to/",
"decimals": 18
},
{
"is_blockchain": true,
"is_stablecoin": false,
"is_fiat": false,
"name": "Toncoin",
"code": "TON",
"url": "https://ton.org/",
"decimals": 9
},
]
})
.to_string(),
)
.create()
}
pub fn mock_get_stats_response(&mut self) -> Mock {
self.server
.mock("GET", "/getStats")
.with_header("content-type", "application/json")
.with_header("Crypto-Pay-API-Token", "test_token")
.with_body(
json!({
"ok": true,
"result": {
"volume": 0,
"conversion": 0,
"unique_users_count": 0,
"created_invoice_count": 0,
"paid_invoice_count": 0,
"start_at": "2025-02-07T10:55:17.438Z",
"end_at": "2025-02-08T10:55:17.438Z"
}
})
.to_string(),
)
.create()
}
}
#[test]
fn test_get_me() {
let mut ctx = TestContext::new();
let _m = ctx.mock_get_me_response();
let client = CryptoBot::builder()
.api_token("test_token")
.base_url(ctx.server.url())
.build()
.unwrap();
let result = ctx.run(async { client.get_me().execute().await });
println!("Result: {:?}", result);
assert!(result.is_ok());
let me = result.unwrap();
assert_eq!(me.app_id, 28692);
assert_eq!(me.name, "Stated Seaslug App");
assert_eq!(me.payment_processing_bot_username, "CryptoTestnetBot");
assert_eq!(me.webhook_endpoint, None);
}
#[test]
fn test_get_currencies() {
let mut ctx = TestContext::new();
let _m = ctx.mock_currencies_response();
let client = CryptoBot::builder()
.api_token("test_token")
.base_url(ctx.server.url())
.build()
.unwrap();
let result = ctx.run(async { client.get_currencies().execute().await });
assert!(result.is_ok());
let currencies = result.unwrap();
assert_eq!(currencies.len(), 2); assert_eq!(currencies[0].code, CurrencyCode::Crypto(CryptoCurrencyCode::Usdt));
assert_eq!(currencies[1].code, CurrencyCode::Crypto(CryptoCurrencyCode::Ton));
}
#[test]
fn test_get_stats_without_params() {
let mut ctx = TestContext::new();
let _m = ctx.mock_get_stats_response();
let client = CryptoBot::builder()
.api_token("test_token")
.base_url(ctx.server.url())
.build()
.unwrap();
let result = ctx.run(async { client.get_stats().execute().await });
println!("result: {:?}", result);
assert!(result.is_ok());
let stats = result.unwrap();
assert_eq!(stats.volume, Decimal::from(0));
assert_eq!(stats.conversion, Decimal::from(0));
}
#[test]
fn test_get_stats_with_params() {
let mut ctx = TestContext::new();
let _m = ctx.mock_get_stats_response();
let client = CryptoBot::builder()
.api_token("test_token")
.base_url(ctx.server.url())
.build()
.unwrap();
let result = ctx.run(async {
client
.get_stats()
.start_at(Utc::now() - Duration::days(7))
.end_at(Utc::now())
.execute()
.await
});
assert!(result.is_ok());
let stats = result.unwrap();
assert_eq!(stats.volume, Decimal::from(0));
assert_eq!(stats.conversion, Decimal::from(0));
}
#[test]
fn test_get_stats_start_date_in_future_rejected() {
let ctx = TestContext::new();
let client = CryptoBot::builder()
.api_token("test_token")
.base_url(ctx.server.url())
.build()
.unwrap();
let future = Utc::now() + Duration::days(1);
let result = ctx.run(async { client.get_stats().start_at(future).execute().await });
assert!(matches!(
result,
Err(CryptoBotError::ValidationError {
field,
kind: ValidationErrorKind::Range,
..
}) if field == Some("start_at".to_string())
));
}
#[test]
fn test_get_stats_end_before_start_rejected() {
let ctx = TestContext::new();
let client = CryptoBot::builder()
.api_token("test_token")
.base_url(ctx.server.url())
.build()
.unwrap();
let start = Utc::now();
let end = start - Duration::hours(1);
let result = ctx.run(async { client.get_stats().start_at(start).end_at(end).execute().await });
assert!(matches!(
result,
Err(CryptoBotError::ValidationError {
field,
kind: ValidationErrorKind::Range,
..
}) if field == Some("end_at".to_string())
));
}
}