1use chrono::{DateTime, Utc};
2
3use crate::{
4 client::CryptoBot,
5 error::{CryptoBotError, CryptoBotResult, ValidationErrorKind},
6 models::{APIEndpoint, APIMethod, AppStats, Currency, GetMeResponse, GetStatsParams, Method},
7};
8use async_trait::async_trait;
9
10use super::MiscAPI;
11
12pub struct GetMeBuilder<'a> {
13 client: &'a CryptoBot,
14}
15
16impl<'a> GetMeBuilder<'a> {
17 pub fn new(client: &'a CryptoBot) -> Self {
18 Self { client }
19 }
20
21 pub async fn execute(self) -> CryptoBotResult<GetMeResponse> {
23 self.client
24 .make_request(
25 &APIMethod {
26 endpoint: APIEndpoint::GetMe,
27 method: Method::GET,
28 },
29 None::<&()>,
30 )
31 .await
32 }
33}
34
35pub struct GetCurrenciesBuilder<'a> {
36 client: &'a CryptoBot,
37}
38
39impl<'a> GetCurrenciesBuilder<'a> {
40 pub fn new(client: &'a CryptoBot) -> Self {
41 Self { client }
42 }
43
44 pub async fn execute(self) -> CryptoBotResult<Vec<Currency>> {
46 self.client
47 .make_request(
48 &APIMethod {
49 endpoint: APIEndpoint::GetCurrencies,
50 method: Method::GET,
51 },
52 None::<&()>,
53 )
54 .await
55 }
56}
57
58pub struct GetStatsBuilder<'a> {
59 client: &'a CryptoBot,
60 params: GetStatsParams,
61}
62
63impl<'a> GetStatsBuilder<'a> {
64 pub fn new(client: &'a CryptoBot) -> Self {
65 Self {
66 client,
67 params: GetStatsParams::default(),
68 }
69 }
70
71 pub fn start_at(mut self, start_at: DateTime<Utc>) -> Self {
74 self.params.start_at = Some(start_at);
75 self
76 }
77
78 pub fn end_at(mut self, end_at: DateTime<Utc>) -> Self {
81 self.params.end_at = Some(end_at);
82 self
83 }
84
85 pub async fn execute(self) -> CryptoBotResult<AppStats> {
87 let now = Utc::now();
88
89 if let Some(start) = self.params.start_at {
90 if start > now {
91 return Err(CryptoBotError::ValidationError {
92 kind: ValidationErrorKind::Range,
93 message: "start_at cannot be in the future".to_string(),
94 field: Some("start_at".to_string()),
95 });
96 }
97 }
98
99 if let (Some(start), Some(end)) = (self.params.start_at, self.params.end_at) {
100 if end < start {
101 return Err(CryptoBotError::ValidationError {
102 kind: ValidationErrorKind::Range,
103 message: "end_at cannot be earlier than start_at".to_string(),
104 field: Some("end_at".to_string()),
105 });
106 }
107 }
108
109 self.client
110 .make_request(
111 &APIMethod {
112 endpoint: APIEndpoint::GetStats,
113 method: Method::GET,
114 },
115 Some(&self.params),
116 )
117 .await
118 }
119}
120
121#[async_trait]
122impl MiscAPI for CryptoBot {
123 fn get_me(&self) -> GetMeBuilder<'_> {
131 GetMeBuilder::new(self)
132 }
133
134 fn get_currencies(&self) -> GetCurrenciesBuilder<'_> {
142 GetCurrenciesBuilder::new(self)
143 }
144
145 fn get_stats(&self) -> GetStatsBuilder<'_> {
153 GetStatsBuilder::new(self)
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use chrono::{Duration, Utc};
160 use mockito::Mock;
161 use rust_decimal::Decimal;
162 use serde_json::json;
163
164 use crate::{
165 api::MiscAPI,
166 client::CryptoBot,
167 models::{CryptoCurrencyCode, CurrencyCode},
168 prelude::{CryptoBotError, ValidationErrorKind},
169 utils::test_utils::TestContext,
170 };
171
172 impl TestContext {
173 pub fn mock_get_me_response(&mut self) -> Mock {
174 self.server
175 .mock("GET", "/getMe")
176 .with_header("content-type", "application/json")
177 .with_header("Crypto-Pay-API-Token", "test_token")
178 .with_body(
179 json!({
180 "ok": true,
181 "result": {
182 "app_id": 28692,
183 "name": "Stated Seaslug App",
184 "payment_processing_bot_username": "CryptoTestnetBot"
185 }
186 })
187 .to_string(),
188 )
189 .create()
190 }
191
192 pub fn mock_currencies_response(&mut self) -> Mock {
193 println!("Setting up mock response");
194 self.server
195 .mock("GET", "/getCurrencies")
196 .with_header("content-type", "application/json")
197 .with_header("Crypto-Pay-API-Token", "test_token")
198 .with_body(
199 json!({
200 "ok": true,
201 "result": [
202 {
203 "is_blockchain": false,
204 "is_stablecoin": true,
205 "is_fiat": false,
206 "name": "Tether",
207 "code": "USDT",
208 "url": "https://tether.to/",
209 "decimals": 18
210 },
211 {
212 "is_blockchain": true,
213 "is_stablecoin": false,
214 "is_fiat": false,
215 "name": "Toncoin",
216 "code": "TON",
217 "url": "https://ton.org/",
218 "decimals": 9
219 },
220 ]
221 })
222 .to_string(),
223 )
224 .create()
225 }
226
227 pub fn mock_get_stats_response(&mut self) -> Mock {
228 self.server
229 .mock("GET", "/getStats")
230 .with_header("content-type", "application/json")
231 .with_header("Crypto-Pay-API-Token", "test_token")
232 .with_body(
233 json!({
234 "ok": true,
235 "result": {
236 "volume": 0,
237 "conversion": 0,
238 "unique_users_count": 0,
239 "created_invoice_count": 0,
240 "paid_invoice_count": 0,
241 "start_at": "2025-02-07T10:55:17.438Z",
242 "end_at": "2025-02-08T10:55:17.438Z"
243 }
244 })
245 .to_string(),
246 )
247 .create()
248 }
249 }
250
251 #[test]
252 fn test_get_me() {
253 let mut ctx = TestContext::new();
254 let _m = ctx.mock_get_me_response();
255
256 let client = CryptoBot::builder()
257 .api_token("test_token")
258 .base_url(ctx.server.url())
259 .build()
260 .unwrap();
261
262 let result = ctx.run(async { client.get_me().execute().await });
263
264 println!("Result: {:?}", result);
265
266 assert!(result.is_ok());
267 let me = result.unwrap();
268 assert_eq!(me.app_id, 28692);
269 assert_eq!(me.name, "Stated Seaslug App");
270 assert_eq!(me.payment_processing_bot_username, "CryptoTestnetBot");
271 assert_eq!(me.webhook_endpoint, None);
272 }
273
274 #[test]
275 fn test_get_currencies() {
276 let mut ctx = TestContext::new();
277 let _m = ctx.mock_currencies_response();
278
279 let client = CryptoBot::builder()
280 .api_token("test_token")
281 .base_url(ctx.server.url())
282 .build()
283 .unwrap();
284
285 let result = ctx.run(async { client.get_currencies().execute().await });
286
287 assert!(result.is_ok());
288 let currencies = result.unwrap();
289
290 assert_eq!(currencies.len(), 2); assert_eq!(currencies[0].code, CurrencyCode::Crypto(CryptoCurrencyCode::Usdt));
292 assert_eq!(currencies[1].code, CurrencyCode::Crypto(CryptoCurrencyCode::Ton));
293 }
294
295 #[test]
296 fn test_get_stats_without_params() {
297 let mut ctx = TestContext::new();
298 let _m = ctx.mock_get_stats_response();
299
300 let client = CryptoBot::builder()
301 .api_token("test_token")
302 .base_url(ctx.server.url())
303 .build()
304 .unwrap();
305
306 let result = ctx.run(async { client.get_stats().execute().await });
307
308 println!("result: {:?}", result);
309
310 assert!(result.is_ok());
311 let stats = result.unwrap();
312 assert_eq!(stats.volume, Decimal::from(0));
313 assert_eq!(stats.conversion, Decimal::from(0));
314 }
315
316 #[test]
317 fn test_get_stats_with_params() {
318 let mut ctx = TestContext::new();
319 let _m = ctx.mock_get_stats_response();
320
321 let client = CryptoBot::builder()
322 .api_token("test_token")
323 .base_url(ctx.server.url())
324 .build()
325 .unwrap();
326
327 let result = ctx.run(async {
328 client
329 .get_stats()
330 .start_at(Utc::now() - Duration::days(7))
331 .end_at(Utc::now())
332 .execute()
333 .await
334 });
335
336 assert!(result.is_ok());
337 let stats = result.unwrap();
338 assert_eq!(stats.volume, Decimal::from(0));
339 assert_eq!(stats.conversion, Decimal::from(0));
340 }
341
342 #[test]
343 fn test_get_stats_start_date_in_future_rejected() {
344 let ctx = TestContext::new();
345 let client = CryptoBot::builder()
346 .api_token("test_token")
347 .base_url(ctx.server.url())
348 .build()
349 .unwrap();
350
351 let future = Utc::now() + Duration::days(1);
352 let result = ctx.run(async { client.get_stats().start_at(future).execute().await });
353
354 assert!(matches!(
355 result,
356 Err(CryptoBotError::ValidationError {
357 field,
358 kind: ValidationErrorKind::Range,
359 ..
360 }) if field == Some("start_at".to_string())
361 ));
362 }
363
364 #[test]
365 fn test_get_stats_end_before_start_rejected() {
366 let ctx = TestContext::new();
367 let client = CryptoBot::builder()
368 .api_token("test_token")
369 .base_url(ctx.server.url())
370 .build()
371 .unwrap();
372
373 let start = Utc::now();
374 let end = start - Duration::hours(1);
375
376 let result = ctx.run(async { client.get_stats().start_at(start).end_at(end).execute().await });
377
378 assert!(matches!(
379 result,
380 Err(CryptoBotError::ValidationError {
381 field,
382 kind: ValidationErrorKind::Range,
383 ..
384 }) if field == Some("end_at".to_string())
385 ));
386 }
387}